Compare commits

..

220 Commits

Author SHA1 Message Date
Luca Cipriani
41bf1ce6dc Merge pull request #97 from bcmi-labs/robust_workspace_init
More robust workspace initialization
2020-01-23 11:08:28 +00:00
Miro Spönemann
1aa944b25e More robust workspace initialization: guard against errors creating sketch dir 2020-01-20 16:48:24 +01:00
Miro Spönemann
b220ce4c5f Removed fs-extra dependency again, updated electron build resolution 2020-01-16 16:06:01 +01:00
Miro Spönemann
15c66442cc Use direct dependency instead of dep resolution for fs-extra 2020-01-16 15:31:30 +01:00
pirropirro
3a56c16ab2 Merge pull request #95 from bcmi-labs/pro-ide-142
Align language server spawning with arduino-cli
2020-01-16 11:36:28 +01:00
Miro Spönemann
cea62e315a Added unit test for getExecPath 2020-01-16 11:10:13 +01:00
Miro Spönemann
6618816330 Align language server spawning with arduino-cli 2020-01-16 09:39:43 +01:00
Miro Spönemann
2577451c15 Use latest version of fs-extra due to stack overflow bugs in earlier versions 2020-01-15 17:10:59 +01:00
pirropirro
a1ab42d282 Merge pull request #96 from bcmi-labs/pro-ide-21
Fallback rendering for Boards Manager
2020-01-14 16:27:03 +01:00
Miro Spönemann
8dbcb8bbb6 Fallback rendering in case a BoardPackage with no name or author is found (arduino/arduino-pro-ide#21) 2020-01-14 15:35:44 +01:00
pirropirro
1be76aa264 Merge pull request #93 from bcmi-labs/clangd_linux
Include clangd for Linux
2020-01-14 10:56:32 +01:00
Miro Spönemann
21cd580e41 Use zip target for Linux (see electron-userland/electron-builder#2868) 2020-01-13 09:44:06 +01:00
Miro Spönemann
8839e318f4 Merge pull request #92 from bcmi-labs/arduino-pro-ide_17
Updated keybindings
2020-01-13 09:14:24 +01:00
Miro Spönemann
bef9185c6c arduino/arduino-pro-ide#31: Include clangd for linux with 'bin' and 'lib' folders 2020-01-10 11:02:23 +01:00
Miro Spönemann
b8bd444def Updated keybindings (arduino/arduino-pro-ide#17), restore Output view on Verify / Upload 2020-01-09 16:22:03 +01:00
Miro Spönemann
b8fdb03433 Added script to configure node_gyp (nodejs/node-gyp#1933) 2020-01-08 13:09:22 +01:00
pirropirro
4290497edc Merge pull request #91 from bcmi-labs/fixed_cli_version
Use fixed version 0.7.1 of the cli to avoid future breakage
2020-01-07 11:02:05 +01:00
Miro Spönemann
7a89a8048f Use fixed version 0.7.1 of the cli to avoid future breakage 2020-01-07 10:49:36 +01:00
pirropirro
84be804df0 Update CLI config parsing (v0.7.0) 2020-01-07 10:44:54 +01:00
Miro Spönemann
f07441a7fc Updated clangd to version 9.0.0 2020-01-06 16:25:00 +01:00
Miro Spönemann
08f127711f bcmi-labs/arduino-language-server#17 (workaround for eclipse-theia/theia#6308) 2019-12-17 11:40:33 +01:00
Akos Kitta
d1ae15a838 Updated to the latest CLI config format.
See: arduino/arduino-cli@f2df284
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-12 15:11:02 +01:00
Akos Kitta
7ca8830a02 Bumped up from 0.0.3 to 0.0.4.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-11 16:23:33 +01:00
Akos Kitta
13b7a7e1d8 Ignore LS logs.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-11 16:23:06 +01:00
Akos Kitta
c5c9b8674b Merge pull request #85 from bcmi-labs/arduino/arduino-pro-ide#112
Various serial-monitor fixes for 0.0.4: arduino/arduino-pro-ide#112
2019-12-11 16:18:46 +01:00
Akos Kitta
6447191bf5 [win] Implemented naive reconnecting.
For some reason, port is reported to be busy on Windows.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-11 09:40:41 +01:00
Akos Kitta
e78ed85761 [win]: Launch config fixes for Windows.
It is a workaround for eclipse-theia/theia#3590

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 14:37:10 +01:00
Akos Kitta
9bc520ccf9 Removed unused chance.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:54:01 +01:00
Akos Kitta
cfdb00529c Fixed focus.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:06:16 +01:00
Akos Kitta
8ccea24452 Let the serial monitor connect to unknown boards.
Fixes arduino/arduino-pro-ide#127

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:06:16 +01:00
Akos Kitta
ad563d26ba Re-enabled the build on this branch.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:06:15 +01:00
Akos Kitta
f0a628534e Updated to 0.14.0-next.0159cd5b Theia.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:05:55 +01:00
Akos Kitta
1b95242ad1 forward messages only when connected.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:30 +01:00
Akos Kitta
729588770e fixed broken promise when reconnecting on baudRate changes.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:30 +01:00
Akos Kitta
6b2046e090 Do not disconnect from the widget, it is done automatically.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:30 +01:00
Akos Kitta
80673ad18f Styled the input.
Do not render the widget after a close request.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:30 +01:00
Akos Kitta
2f33038695 No disconnect/reconnect when DNDing the widget.
- Updated to next Theia,
 - Added elecron launch config,
 - Yet another syling for the input + selects,
 - Close monitor connection on widget close not detach.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:28 +01:00
Akos Kitta
6154d1e8d5 aligned input to the selects.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
557ec2ae42 Wait until the boards config has been reset
from the local storage, then start the monitor connection.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
8c49c04359 🤞 finalized the monitor UI
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
29ebf055e6 Do not re-render the entire widget.
Otherwise the selects do not work.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
71842abfa3 Got rid of the send button, use keybinding instead
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
ed660ccd64 fixed the input focus when the view is activated
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
6af22ec9b8 aligned select height with Theia styles
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
79f588d067 Finalized the styling for react-select
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
e3e4a96db3 do not show border on focus.
when the list is opened, we have the accent color anyway.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
4129544738 generalized the react-select component
so tha we can reuse it all over the application.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
a866bde4d1 fixed background style when holding down select
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
92b6208a76 aligned core/lib input style with react-select
made the UI slicker by removing any non- :hover, :select borders.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
12deceef19 got rid of the border when not focused/selected
it does not remove the border but uses the background of the container.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
f635751a8c fix the react-select styling
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
85bf50213d Removed more logic from the widget.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
5aeb2d388e Flip the auto install buttons.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
b6b4c75718 CI skip.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:16 +01:00
Akos Kitta
c4a8062df4 Updated to next Theia.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:02:14 +01:00
Akos Kitta
6e89e89738 Fixed layout initialization.
Fixes for eclipse-theia/theia#6688

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:01:54 +01:00
Akos Kitta
c7242ca34f slightly better reconnecting experience.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:01:54 +01:00
Akos Kitta
a4e5e65286 simplified monitor connection API.
we have one connenction per editor anyways.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:01:54 +01:00
Akos Kitta
80549db289 a few bugfixes. updated grpc dependencies.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:01:54 +01:00
Akos Kitta
eb7b3ad683 Reconnect on interrupted system call.
However, we have to figure out why does it happen at all.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:01:53 +01:00
Akos Kitta
9efcbcf2ae [rewrite-me]: initial serial monitor changes
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 12:01:53 +01:00
Akos Kitta
d22c0b9e55 Merge pull request #90 from bcmi-labs/jx/update-theia
Update Theia and adjust Electron package name
2019-12-10 11:59:45 +01:00
Jan Keromnes
5d2f09354d Adjust Electron package 'name' and 'appId'. 2019-12-10 11:03:01 +01:00
Jan Keromnes
fcd6c792e3 Updated to next (0.14.0-next.0159cd5b) Theia.
Co-authored-by: Jan Keromnes <jan.keromnes@typefox.io>
Co-authored-by: Akos Kitta <kittaakos@typefox.io>

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-12-10 11:02:49 +01:00
Akos Kitta
94233a1a19 Merge pull request #80 from bcmi-labs/0.3.0-rc
All in on PR for 0.0.3
2019-12-09 09:58:59 +01:00
Jan Keromnes
7fb32766ca [doc] Fix a few typos 2019-12-07 09:59:45 +01:00
Akos Kitta
85cf8757c4 Merge pull request #89 from bcmi-labs/jx/document-boards-service
[doc] Document Arduino Pro IDE extension and a few services
2019-12-07 09:50:19 +01:00
Jan Keromnes
41c56c1126 [doc] Document Arduino Pro IDE extension and a few services 2019-12-06 16:03:58 +00:00
Jan Keromnes
4c503c0c5e Merge pull request #86 from bcmi-labs/jx/support-electron-dev
Support running the Electron app in Gitpod for faster code reviews
2019-12-06 11:10:03 +01:00
Jan Keromnes
3f180b6059 [minor] Make the README.md GIFs a bit smaller 2019-12-06 11:07:03 +01:00
Jan Keromnes
6a8a76f720 [minor] Fix project name in README.md 2019-12-06 11:04:01 +01:00
Jan Keromnes
9a27252d91 Support running the Electron app in Gitpod for faster code reviews 2019-12-05 10:26:53 +00:00
Akos Kitta
4e683b237d Made the editors closeable in pro mode.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-26 10:33:59 +01:00
Akos Kitta
a2a9cbb02e Fixed a styling issue in the boards select dialog.
Closes: arduino/arduino-pro-ide#137
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-26 09:18:46 +01:00
Akos Kitta
dd10436051 Fixed the Platform ordering.
We have to pick the installed version first.
Otherwise we lose the FQBN of the boards.
FBQN is used to check if a board has the corresponding core installed.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-26 09:10:01 +01:00
Akos Kitta
e79d42d6bd Updated to next (0.13.0-next.145f9137) Theia.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-25 09:16:22 +01:00
Akos Kitta
a9c9dcde7b Fixed a selection bug after installing a core/lib
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
62b18ccbed Fixed lib uninstall. Do not log it more than once.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
0a8b6bc41e Restart the LS on core install/remove.
Fixes bcmi-labs/arduino-language-server#9

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
b1388be5f9 Implemented uninstall.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
b4848f62fa Corrected the docs.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
f359843635 Fixed the version ordering for libs.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
6448b447b3 Resolved a React warning.
Removed the widget update due to an incorrect manual merge.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
c7bb3abf19 Updated doc, code style changes.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
c3e2aa4feb Generalized the list item renderers.
To support update/downgrade.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
63cd2701b4 Simplified code when deceding which CLI to use.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
35ac73181b Enabled New Folder in classic mode.
-  Made sure the `Explorer` is visible before raising the input dialog.
 - Removed unused module.

Fixes arduino/arduino-pro-ide#84

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
840cde872c Made the editor mode dynamic.Signed-off-by: Akos Kitta <kittaakos@typefox.io> 2019-11-22 17:09:45 +01:00
Akos Kitta
c2008460b0 Fixed storeLayout when toggling the editor mode.
Workaround for eclipse-theia/theia#6579.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
435fdcdf7f Adjusted the version numbers.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
7e6343e60e Update the ListWidget on list container render.
To recalculate the desired size of the scroll-bar thumb.

arduino/arduino-pro-ide#101

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
fdda4a72d0 Initial support for updating/downgrading cores.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
7077303a36 Hid the Explorer. Disabled new folder in workspace
when not in `pro-mode`.

Closes arduino/arduino-pro-ide#84.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
acd9bf1354 Fixed the editor restoration when toggling mode.
- Do not reset the layout. It messes up the editor's restoration.
 - Close all pro-mode only views when leaving pro-mode.
 - Initialize all views `onStart`. It is a NOOP in normal mode.

Closes arduino/arduino-pro-ide#101

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
d92fc25769 Fixed endless loop in the sketch service.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
f9a98d708e Removed an unused field.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
df33c5689f Gracefully handle disconnected frontends.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
2dc73eb3b5 Updated to next (0.13.0-next.83c59134) Theia.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
f6444b2570 Update the ListWidget on list container render.
To recalculate the desired size of the scroll-bar thumb.

arduino/arduino-pro-ide#101

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Akos Kitta
186180800f Increased the z-index of the scroll-bar thumb
to make it visible even with the customized list item `background-color`

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-11-22 17:09:45 +01:00
Jan Keromnes
20bc3c6f13 Merge pull request #81 from bcmi-labs/choose-cli
Only pick `arduino-cli` from the PATH if it's more recent
2019-11-21 14:53:23 +01:00
Jan Keromnes
125ee70fa3 Sort package.json dependencies alphabetically 2019-11-21 10:37:14 +00:00
Jan Keromnes
3cfb1450c0 Only pick arduino-cli from the PATH if it's more recent 2019-11-21 10:37:14 +00:00
Akos Kitta
9643dd397f [doc]: Updated the CI section.
Documented when do we build the electron-based applications.
2019-11-21 11:16:52 +01:00
Akos Kitta
7c1ebf273c Merge pull request #75 from bcmi-labs/25-10-release
Various bug fixes, plus one or two enhancements.
2019-10-28 12:41:29 -04:00
Akos Kitta
05850b5f27 Ensure default data and sketch dir existence.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 15:13:53 +02:00
Akos Kitta
21dedd4b09 DROPME: disabled the uglification in the bundle.js
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 14:10:06 +02:00
Akos Kitta
90d7d88162 Updated the application name, version to 0.0.2.
Increased the heap size for the packager.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
3efb5a4e08 Switched from execFile to spawn.
So that we can add guards against whitespaces in the path.
Also cleaned up the code a bit.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
4353bfb5b9 Updated port discovery to support unknown boards
From now on, we do not retrieve the ports from the attached boards.
A board can be unknown but the port is still relevant.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
8971dc4c5f Implemented naive reconnecting.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
2be54944bf Fixed the checkmark position in the dropdown.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
2f84b5c6b7 Show the CLI version from the about dialog.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
cebe15ef69 Removed duplicate log level from the log.
Print CLI version at start-up.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
1dda5dd95b Exposed version from the CLI.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
de1caf1451 PROEDITOR-46: Added a core auto-installer.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
fb6785c5d3 PROEDITOR-53: Changed the way we set the workspace
Got rid of the `sketch` search parameter from the URL.

Rules:
 - Get the desired workspace location from the
  - `Path` defined as the `window.location.hash` of the URL,
  - most recent workspaces,
  - most recent sketches from the default sketch folder.
 - Validate the location.
 - If no valid location was found, create a new sketch in the default sketch folder.

Note: when validating the location of the workspace root, the root must always exist. However, when in pro-mode, the desired workspace root must
not be a sketch directory with the `.ino` file, but can be any existing location.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:24 +02:00
Akos Kitta
de1f341d19 Merge pull request #74 from bcmi-labs/remove-tab-context-menu-no-pro-mode
Removed the tab context menu when not in pro-mode.
2019-10-23 10:30:30 +02:00
Akos Kitta
817a28291b Removed the tab context menu when not in pro-mode.
When we are not in `pro-mode`, it does not make much sense to show the
context menu. It does not work anyways.

See: https://arduino.slack.com/archives/CJJHJCJSJ/p1569502896082800?thread_ts=1569489282.073200&cid=CJJHJCJSJ

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-23 09:57:56 +02:00
Akos Kitta
06ef598806 Merge pull request #72 from bcmi-labs/azure-service-connection-update
Switched to `typefox-service-account1`.
2019-10-17 10:00:27 +02:00
Akos Kitta
cc6a0ae212 Fixed various electron packager issues
- We have to reuse the `yarn.lock` file for the final app, not just
for the extensions.
 - Simplified the clean-up phase.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-17 08:45:26 +02:00
Akos Kitta
65a58ce2be Switched to typefox-service-account1.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-16 13:56:40 +02:00
Akos Kitta
6331b7ddfd Merge pull request #65 from bcmi-labs/increase_maxBuffer
Increase maxBuffer for spawned daemon
2019-10-07 16:57:25 +02:00
Miro Spönemann
f3667f0270 Merge pull request #67 from bcmi-labs/restartLSOnBoardChange
Restart the language server when the board is changed
2019-10-04 11:54:13 +02:00
Miro Spönemann
aa4f216544 Restart the language server when the board is changed 2019-10-02 13:16:10 +02:00
Akos Kitta
065f9f042b Merge pull request #66 from bcmi-labs/various-bug-fixes
Various bug fixes
2019-09-26 11:59:23 +02:00
Akos Kitta
765fcdfba7 Fixed the styles for the boards list dropdown.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-26 10:15:57 +02:00
Akos Kitta
476e658fea Cleaned up the menu contributions.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-26 10:15:30 +02:00
Akos Kitta
a96ed31a45 Fixed the daemon log.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-25 16:34:42 +02:00
Miro Spönemann
ed4f23a32a Merge pull request #64 from bcmi-labs/msp_sendBoardConfig
Added JSON-RPC message to send the board configuration to the language server
2019-09-25 09:12:59 +02:00
Akos Kitta
4949df7395 Fixed missing New Sketch menu in electron.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-24 16:01:19 +02:00
Akos Kitta
55923be7fd PROEDITOR-48: Open last sketch at start-up
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-24 14:23:16 +02:00
Martino Facchin
2ef0d1d0db Increase maxBuffer for spawned daemon
When installed cores are A LOT, core listing needs a lot of space :)
2019-09-23 17:06:10 +02:00
Akos Kitta
7244694bd3 PROEDITOR-47: FIxed the visibility of close button
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-20 16:09:58 +02:00
Akos Kitta
d9e71c7e0d PROEDITOR-51: Fixed the styling for toggeled icons
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-20 15:39:15 +02:00
Akos Kitta
daedae1ba7 PROEDITOR-50: Customized the editor for built-ins
- From now on, sources from the `dataDir` opened in a read-only editor.
 - Disabled the tab-bar decoration for built-ins.
 - No problem markers for the built-ins.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-20 15:10:38 +02:00
Akos Kitta
ac4e877a10 Open More info in the default browser.
Instead of the Electron application.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-18 15:37:58 +02:00
Miro Spönemann
09243ff74d Added JSON-RPC message to send the board configuration to the language server 2019-09-18 09:46:07 +02:00
Miro Spönemann
5496edbb42 Merge pull request #62 from bcmi-labs/setup_cpp
Configure cpp extension to use embedded clangd
2019-09-16 13:09:23 +02:00
Miro Spönemann
62eff29172 Configure cpp extension to use embedded clangd 2019-09-16 08:42:30 +02:00
jbicker
2220e66f4b Adapted Dark, Light and Arduino themes
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-11 11:55:29 +02:00
jbicker
c98ec29810 Merge branch 'add-cpp-extension' 2019-09-09 17:33:46 +02:00
jbicker
768958dfd5 Updated Yarn.lock
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-09 17:25:07 +02:00
Jan Bicker
8e747e19a6 CPP extension added. Theia updated.
Signed-off-by: Jan Bicker <jan.bicker@typefox.io>
2019-09-09 17:10:18 +02:00
Miro Spönemann
b1c69aef9f Merge pull request #59 from bcmi-labs/ino-language
Added language support for ino files
2019-09-09 10:28:40 +02:00
Miro Spönemann
ec6b5ed3f3 Added command line argument for specifying cli path 2019-09-06 14:15:59 +02:00
Miro Spönemann
60bf58ac0f Added download script for arduino-language-server 2019-09-06 14:15:59 +02:00
Miro Spönemann
c2675efea4 Fixed z-index of main toolbar 2019-09-06 14:15:59 +02:00
Miro Spönemann
7d04c7efb8 Added language contribution for ino files 2019-09-06 14:14:35 +02:00
Jan Bicker
fb542e2e40 Merge pull request #56 from bcmi-labs/advanced-mode-in-top-panel
Advanced mode in top panel
2019-09-06 13:08:18 +02:00
Jan Bicker
3e0842e93a Merge pull request #54 from bcmi-labs/monitor
Serial Monitor
2019-09-06 13:06:56 +02:00
Jan Bicker
90add23dae Merge pull request #61 from bcmi-labs/file-menu-fix
Fixed file menu
2019-09-06 13:05:18 +02:00
jbicker
6ff5405337 show advanced mode button in top panel
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-04 09:07:57 +02:00
jbicker
c564572718 Fixed file menu, New Sketch and New File items were not shown in main menu
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:44:50 +02:00
jbicker
79731304c1 Reestablish monitor connection after reloading the window, reconnect after selecting another board
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
2046c0bdee Continue monitoring with last connection if respective board is reconnected
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
3eebd580d8 Fixed wrong toppanel layout in electron
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
d8454456a9 Set focus on send field initially
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
dac9c6437e Preserve baud rate and line ending; Use google protobuf Struct type now for setting configs
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
7f33b62e0b Preserve Autoscroll and Timestamp states
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
459e55a69a Improved serial monitor dropdowns
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
f76f4543e9 Minor code improvements
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
9b255ac072 Timestamp formatted
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
592086466c Clear send field after sending the value
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
c81ee1ede2 Arduino Toolbar Tooltips don't get clipped anymore.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
6d2816a7f3 Added icon, changed appearance of active toolbar item
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
dd69092afd Added additional toolbar to the right of the toppanel.
Added Toolbar button for toggling serial monitor and tooltips for serial monitor toolbar items.

Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
76d0f5a464 Implemented the Widget
Re-introduced bottom panel tabs

Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:40:19 +02:00
Jan Bicker
206b65f138 Merge pull request #58 from bcmi-labs/use-sketch-as-ws
Use sketch folder as workspace
2019-09-03 16:32:08 +02:00
Akos Kitta
6d590cd111 Fixed the packager for the new folder structure.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-02 14:57:28 +02:00
Akos Kitta
e8e3c3dc4c Use the latest CLI, as stated in the doc.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-02 13:45:36 +02:00
Akos Kitta
e6e042c856 Moved System declaration before its use-site.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-30 10:41:05 +02:00
Akos Kitta
9298a8cc55 Log clean-up.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-30 10:26:13 +02:00
Akos Kitta
98764b56aa Switched to the JSON log format for the daemon.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-30 10:11:43 +02:00
Akos Kitta
9f7aec4091 Adapted to latest CLI.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-29 09:52:56 +02:00
Akos Kitta
e636e06a7e Download today's CLI.
The issues was an interim CI problem.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-29 09:21:51 +02:00
Akos Kitta
c6311ecb1d Adapted the CLI download script.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-29 09:17:44 +02:00
Akos Kitta
cd94608aee Renamed the applications.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-29 08:05:47 +02:00
Jan Bicker
b82d5e4f0b Use sketch directory as url param when a new created sketch gets opened
Signed-off-by: Jan Bicker <jan.bicker@typefox.io>
2019-08-28 08:13:11 +00:00
Akos Kitta
9ae721292d Fixed the FS path issue on Windows.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-27 18:27:12 +02:00
jbicker
41c603937c Setting download dir to {dataDir}/staging; Create data and sketch folders if they don't exist.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-08-26 16:53:59 +02:00
jbicker
d5589c435f Get the default sketchbook path from backend
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-08-26 15:59:18 +02:00
jbicker
d809daa20a Use sketch folder as workspace
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-08-26 13:20:01 +02:00
Akos Kitta
f9641a3d76 Initial support of the default paths from the CLI.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-26 12:42:27 +02:00
Sven Efftinge
59553bf81f Merge pull request #55 from bcmi-labs/se/better-output
Better output format
2019-08-16 14:00:44 +02:00
Sven Efftinge
037efbaba2 Better output format 2019-08-15 11:50:50 +00:00
Luca Cipriani
a936e4c505 Merge pull request #49 from bcmi-labs/boards-manager
generalized the boards and the libraries views.
2019-08-14 17:34:55 +02:00
Akos Kitta
7c2a295631 Fixed bug when checking if a core is installed
`startsWith` was incorrect: `arduino:samd` Vs. `arduino:samd_beta`

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-12 11:40:39 +02:00
Akos Kitta
c5796677f8 Ignore whitespaces when searhing for libs, cores.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-12 11:39:02 +02:00
Akos Kitta
b6306c330f Update/download the library_index.json at start
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 19:42:24 +02:00
Akos Kitta
692c3f6e3f Implemented serial-monitoring for the backend.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 19:42:24 +02:00
Akos Kitta
8d79bb3ffb restored the the search limit for the lib manager.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 19:40:25 +02:00
Akos Kitta
ec50ea673c do not await for attached boards.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 19:40:25 +02:00
Akos Kitta
502e9042ad fine tuned selectedPort update
based on the detached boards.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 19:40:25 +02:00
Akos Kitta
66f429c478 workaround for non-unique names.
Fine tuned the port unnselection when attached boards change.

This should make sure we do not have to `await` for the attached boards
from the backend.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 19:40:25 +02:00
Akos Kitta
0dc45daf01 aligned list view styles.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 19:40:25 +02:00
Akos Kitta
3fcf5a6ac9 generalized the boards and the libraries views.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 19:40:25 +02:00
Akos Kitta
b24d440e22 Merge pull request #53 from bcmi-labs/PROEDITOR-27
PROEDITOR-27: Try to preserve the window.
2019-08-05 13:47:53 +02:00
Akos Kitta
af9b9fbeab Yet another attempt to fix timeout issues on Azure.
```
info There appears to be trouble with your network connection. Retrying...
error An unexpected error occurred: "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.2.tgz: ESOCKETTIMEDOUT".
info If you think this is a bug, please open a bug report with the information provided in "D:\\a\\1\\s\\yarn-error.log".
```

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 13:17:59 +02:00
Stefania
37db6c4b43 Merge pull request #51 from bcmi-labs/search-in-workspace-and-git
Added "Search In Workspace" and "SCM/Git" Extensions.
2019-08-05 11:12:46 +02:00
Jan Bicker
9d5ad9b003 Added "Search In Workspace" and "SCM/Git" Extensions.
Signed-off-by: Jan Bicker <jan.bicker@typefox.io>
2019-08-05 09:58:52 +02:00
Akos Kitta
ded838b4e8 Fixed the preloadTemplate for electron.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 09:57:07 +02:00
Akos Kitta
82df8a6add PROEDITOR-27: Try to preserve the window.
When opening sketches. Rules:
 - If `sketch` is missing from the URL, we reuse the same window.
 - NOOP, if we try to open the currently opened sketch.
 - Otherwise, use the existing logic: open sketch in a new window.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 09:57:05 +02:00
Luca Cipriani
2914379586 Merge pull request #46 from bcmi-labs/poll-boards
Implemented the board discovery with polling.
2019-08-02 17:08:08 +02:00
Akos Kitta
6b25659fa6 Merge pull request #48 from bcmi-labs/monitoring-api
Updated the TS/JS API with the monitoring.
2019-07-30 13:04:46 +02:00
Akos Kitta
27dc6f9816 Updated the TS/JS API with the monitoring.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-30 10:57:23 +02:00
Akos Kitta
b78ddbeb64 Implemented the board discovery with polling.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-30 10:32:10 +02:00
jbicker
db78c8ac2d Minor css adjustments
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-26 12:00:26 +02:00
Luca Cipriani
95c5536060 Merge pull request #45 from bcmi-labs/new-file-menu-item
Added 'New File' Item in file menu
2019-07-26 11:37:48 +02:00
Akos Kitta
0aa34b1169 Added the Prerequisites section to the README.
Closes #44.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-26 11:25:40 +02:00
jbicker
b7d951b809 Added 'New File' Item in file menu
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-26 11:00:18 +02:00
Luca Cipriani
e11d9e0c78 Merge pull request #43 from bcmi-labs/toggle-pro-mode
One can toggle between pro and simple mode
2019-07-25 18:29:17 +02:00
jbicker
436e660d47 Avoid duplicate react keys in board select dialog
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-25 18:04:59 +02:00
jbicker
23a967bd4c One can toggle between pro and simple mode
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-25 17:38:16 +02:00
Jan Bicker
17fab651e5 Merge pull request #41 from bcmi-labs/made-board-selector-async
Made boards selector async
2019-07-25 08:04:02 -07:00
Luca Cipriani
df7225c32b Merge pull request #29 from bcmi-labs/PROEDITOR-20
PROEDITOR-20: Restored the dirt flag in editor tab
2019-07-25 16:52:50 +02:00
Akos Kitta
69f63668b2 Merge pull request #42 from bcmi-labs/terminate-daemon
Fixed the daemon process termination.
2019-07-25 16:44:04 +02:00
Akos Kitta
d29ed24e49 Fixed the daemon process termination.
From now on, we spawn a detached process that
will periodically check whether the parent Theia
the process is alive, if no, terminates the daemon.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-25 16:11:40 +02:00
jbicker
a5294417c3 Made boards selector async
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-25 11:20:56 +02:00
Akos Kitta
d6637c44e5 PROEDITOR-20: Restored the dirt flag in editor tab
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-22 14:19:49 +02:00
161 changed files with 11008 additions and 3992 deletions

10
.gitignore vendored
View File

@@ -2,12 +2,16 @@ node_modules/
# .node_modules is a hack for the electron builder.
.node_modules/
lib/
build/
downloads/
build/
!electron/build/
src-gen/
arduino-ide-*/webpack.config.js
.DS_Store
browser-app/webpack.config.js
electron-app/webpack.config.js
/workspace/static
.DS_Store
# switching from `electron` to `browser` in dev mode.
.browser_modules
# LS logs
inols*.log
yarn-error.log

View File

@@ -2,13 +2,17 @@ image:
file: Dockerfile
ports:
- port: 3000
onOpen: open-browser
- port: 3000
onOpen: open-preview
- port: 5900
onOpen: ignore
- port: 6080
onOpen: ignore
tasks:
- init: >
yarn &&
yarn --cwd ./arduino-ide-browser start
yarn --cwd ./browser-app start
github:
prebuilds:

145
.vscode/launch.json vendored
View File

@@ -1,38 +1,111 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Node.js Program",
"program": "${file}"
},
{
"type": "node",
"request": "launch",
"name": "Launch Backend",
"program": "${workspaceRoot}/arduino-ide-browser/src-gen/backend/main.js",
"args": [
"--hostname=0.0.0.0",
"--port=3000",
"--no-cluster",
"--no-app-auto-install"
],
"env": {
"NODE_ENV": "development"
},
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/arduino-ide-browser/src-gen/backend/*.js",
"${workspaceRoot}/arduino-ide-browser/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/*/lib/**/*.js"
],
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart",
"outputCapture": "std"
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Attach by Process ID",
"processId": "${command:PickProcess}"
},
{
"type": "node",
"request": "launch",
"name": "App (Electron)",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd",
"env": {
"NODE_ENV": "development",
"NODE_PRESERVE_SYMLINKS": "1"
}
]
}
},
"program": "${workspaceRoot}/electron-app/src-gen/frontend/electron-main.js",
"protocol": "inspector",
"args": [
"--log-level=debug",
"--hostname=localhost",
"--no-cluster",
"--remote-debugging-port=9222",
"--no-app-auto-install"
],
"env": {
"NODE_ENV": "development"
},
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/electron-app/src-gen/backend/*.js",
"${workspaceRoot}/electron-app/src-gen/frontend/*.js",
"${workspaceRoot}/electron-app/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js"
],
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart",
"outputCapture": "std"
},
{
"type": "node",
"request": "launch",
"name": "App (Browser)",
"program": "${workspaceRoot}/browser-app/src-gen/backend/main.js",
"args": [
"--hostname=0.0.0.0",
"--port=3000",
"--no-cluster",
"--no-app-auto-install"
],
"windows": {
"env": {
"NODE_ENV": "development",
"NODE_PRESERVE_SYMLINKS": "1"
}
},
"env": {
"NODE_ENV": "development"
},
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/browser-app/src-gen/backend/*.js",
"${workspaceRoot}/browser-app/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js"
],
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart",
"outputCapture": "std"
},
{
"type": "node",
"request": "launch",
"name": "App (Browser - Debug CLI daemon)",
"program": "${workspaceRoot}/browser-app/src-gen/backend/main.js",
"args": [
"--hostname=0.0.0.0",
"--port=3000",
"--no-cluster",
"--no-app-auto-install",
"--debug-cli=true"
],
"env": {
"NODE_ENV": "development"
},
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/browser-app/src-gen/backend/*.js",
"${workspaceRoot}/browser-app/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js"
],
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart",
"outputCapture": "std"
},
{
"type": "node",
"request": "launch",
"name": "Packager",
"program": "${workspaceRoot}/electron/packager/index.js",
"cwd": "${workspaceFolder}/electron/packager"
}
]
}

21
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,21 @@
{
"tslint.enable": true,
"tslint.configFile": "./tslint.json",
"editor.formatOnSave": true,
"files.exclude": {
"**/lib": false
},
"editor.insertSpaces": true,
"editor.detectIndentation": false,
"[typescript]": {
"editor.tabSize": 4
},
"[json]": {
"editor.tabSize": 2
},
"[jsonc]": {
"editor.tabSize": 2
},
"files.insertFinalNewline": true,
"typescript.tsdk": "node_modules/typescript/lib"
}

16
.vscode/tasks.json vendored
View File

@@ -4,9 +4,9 @@
"version": "2.0.0",
"tasks": [
{
"label": "Arduino-PoC - Start Browser Example",
"label": "Arduino Editor - Start Browser Example",
"type": "shell",
"command": "yarn --cwd ./arduino-ide-browser start",
"command": "yarn --cwd ./browser-app start",
"group": "build",
"presentation": {
"reveal": "always",
@@ -15,7 +15,7 @@
}
},
{
"label": "Arduino-PoC - Watch Theia Extension",
"label": "Arduino Editor - Watch Theia Extension",
"type": "shell",
"command": "yarn --cwd ./arduino-ide-extension watch",
"group": "build",
@@ -26,9 +26,9 @@
}
},
{
"label": "Arduino-PoC - Watch Browser Example",
"label": "Arduino Editor - Watch Browser Example",
"type": "shell",
"command": "yarn --cwd ./arduino-ide-browser watch",
"command": "yarn --cwd ./browser-app watch",
"group": "build",
"presentation": {
"reveal": "always",
@@ -37,11 +37,11 @@
}
},
{
"label": "Arduino-PoC - Watch All",
"label": "Arduino Editor - Watch All",
"type": "shell",
"dependsOn": [
"Arduino-PoC - Watch Theia Extension",
"Arduino-PoC - Watch Browser Example"
"Arduino Editor - Watch Theia Extension",
"Arduino Editor - Watch Browser Example"
]
}
]

View File

@@ -1,4 +1,4 @@
FROM gitpod/workspace-full
FROM gitpod/workspace-full-vnc
USER root
RUN apt-get update -q --fix-missing && \
@@ -7,7 +7,8 @@ RUN apt-get update -q --fix-missing && \
build-essential \
libssl-dev \
golang-go \
libxkbfile-dev
libxkbfile-dev \
libnss3-dev
RUN set -ex && \
tmpdir=$(mktemp -d) && \

View File

@@ -1,4 +1,4 @@
# Arduino IDE PoC
# Arduino Pro IDE
[![Build Status](https://dev.azure.com/typefox/Arduino/_apis/build/status/bcmi-labs.arduino-editor?branchName=master)](https://dev.azure.com/typefox/Arduino/_build/latest?definitionId=4&branchName=master)
@@ -9,12 +9,15 @@ It's built on top of a [fork of the arduino-cli](https://github.com/typefox/ardu
## How to try (offline)
### Prerequisites
You should be able to build Theia locally. The requirements are defined [here](https://github.com/theia-ide/theia/blob/master/doc/Developing.md#prerequisites).
```
git clone https://github.com/bcmi-labs/arduino-editor
cd arduino-editor
yarn
yarn rebuild:electron
yarn --cwd arduino-ide-electron start
yarn --cwd electron-app start
```
If you want to switch back to the browser-based example, execute the following in the repository root
@@ -23,13 +26,16 @@ yarn rebuild:browser
```
Then you can start the browser example again:
```
yarn --cwd arduino-ide-browser start
yarn --cwd browser-app start
```
## Arduino-PoC Electron Application
Click [here](./arduino-ide-extension/README.md) for more details on various IDE services, and the Arduino Pro IDE implementation in general.
## Arduino Pro IDE Electron Application
The project is built on [Azure DevOps](https://dev.azure.com/typefox/Arduino).
Currently, we provide the Arduino-PoC for the following platforms:
Currently, we provide the Arduino Pro IDE for the following platforms:
- Windows,
- macOS, and
- Linux.
@@ -38,7 +44,7 @@ You can download the latest release applications fom [here](https://github.com/b
If you want to get a nightly build, go to the [Azure DevOps page](https://dev.azure.com/typefox/Arduino/_build?definitionId=4),
and follow the steps from below.
![](static/download_01.gif)
![](static/download_02.gif)
<img width="500" src="static/download_01.gif">
<img width="500" src="static/download_02.gif">
Click [here](./electron/README.md) for more details on the CI/CD, the GitHub release, and the build process in general.
Click [here](./electron/README.md) for more details on the CI/CD, the GitHub release, and the build process in general.

View File

@@ -1,49 +0,0 @@
{
"private": true,
"name": "arduino-ide-electron",
"version": "0.0.1",
"license": "MIT",
"dependencies": {
"@theia/core": "next",
"@theia/editor": "next",
"@theia/electron": "next",
"@theia/file-search": "next",
"@theia/filesystem": "next",
"@theia/languages": "next",
"@theia/messages": "next",
"@theia/monaco": "next",
"@theia/navigator": "next",
"@theia/preferences": "next",
"@theia/process": "next",
"@theia/terminal": "next",
"@theia/workspace": "next",
"@theia/textmate-grammars": "next",
"arduino-ide-extension": "0.0.1"
},
"devDependencies": {
"@theia/cli": "next",
"electron": "^4.2.0"
},
"scripts": {
"prepare": "theia build --mode development",
"start": "theia start --root-dir=../workspace",
"watch": "theia build --watch --mode development"
},
"theia": {
"target": "electron",
"frontend": {
"config": {
"applicationName": "Arduino-PoC",
"defaultTheme": "arduino-theme",
"preferences": {
"editor.autoSave": "on"
}
}
}
},
"generator": {
"config": {
"preloadTemplate": "<div class='theia-preload' style='background-color: rgb(237, 241, 242);'></div>"
}
}
}

View File

@@ -0,0 +1,52 @@
## Arduino IDE Extension
Arduino Pro IDE is based on Theia, and most of its IDE features, UIs and customizations are implemented in this Theia extension.
### IDE Services
IDE services typically have a backend part in [src/node/](./src/node/) and a front-end part in [src/browser/](./src/browser/).
#### Boards Service
The Boards Service continuously checks the computer's ports, in order to detect when you connect or disconnect an Arduino board.
The Boards Manager lists all the known board types, and allows downloading new cores to get additional board types.
- [src/common/protocol/boards-service.ts](./src/common/protocol/boards-service.ts) implements the common classes and interfaces
- [src/node/boards-service-impl.ts](./src/node/boards-service-impl.ts) implements the service backend:
- discovering ports & boards
- searching for compatible board types
- installing new board types
- [src/browser/boards/boards-list-widget.ts](./src/browser/boards/boards-service-client-impl.ts) implements the Boards Manager front-end:
- browsing/searching available board types
- installing new board types
#### Core Service
The Core Service is responsible for building your sketches and uploading them to a board.
- [src/common/protocol/core-service.ts](./src/common/protocol/core-service.ts) implements the common classes and interfaces
- [src/node/core-service-impl.ts](./src/node/core-service-impl.ts) implements the service backend:
- compiling a sketch for a selected board type
- uploading a sketch to a connected board
#### Monitor Service
The Monitor Service allows getting information back from sketches running on your Arduino boards.
- [src/common/protocol/monitor-service.ts](./src/common/protocol/monitor-service.ts) implements the common classes and interfaces
- [src/node/monitor-service-impl.ts](./src/node/monitor-service-impl.ts) implements the service backend:
- connecting to / disconnecting from a board
- receiving and sending data
- [src/browser/monitor/monitor-widget.tsx](./src/browser/monitor/monitor-widget.tsx) implements the serial monitor front-end:
- viewing the output from a connected board
- entering data to send to the board
#### Config Service
The Config Service knows about your system, like for example the default sketch locations.
- [src/common/protocol/config-service.ts](./src/common/protocol/config-service.ts) implements the common classes and interfaces
- [src/node/config-service-impl.ts](./src/node/config-service-impl.ts) implements the service backend:
- getting the `arduino-cli` version and configuration
- checking whether a file is in a data or sketch directory

View File

@@ -1,49 +1,85 @@
{
"name": "arduino-ide-extension",
"version": "0.0.1",
"version": "0.0.4",
"description": "An extension for Theia building the Arduino IDE",
"license": "MIT",
"engines": {
"node": ">=10.10.0"
},
"dependencies": {
"@grpc/grpc-js": "^0.4.0",
"@grpc/grpc-js": "^0.6.12",
"@theia/application-package": "next",
"@theia/core": "next",
"@theia/cpp": "next",
"@theia/editor": "next",
"@theia/filesystem": "next",
"@theia/git": "next",
"@theia/languages": "next",
"@theia/markers": "next",
"@theia/monaco": "next",
"@theia/outline-view": "next",
"@theia/workspace": "next",
"@theia/navigator": "next",
"@theia/outline-view": "next",
"@theia/search-in-workspace": "next",
"@theia/terminal": "next",
"@theia/workspace": "next",
"@types/dateformat": "^3.0.1",
"@types/google-protobuf": "^3.7.1",
"@types/ps-tree": "^1.1.0",
"@types/react-select": "^3.0.0",
"@types/which": "^1.3.1",
"css-element-queries": "^1.2.0",
"dateformat": "^3.0.3",
"google-protobuf": "^3.11.0",
"p-queue": "^5.0.0",
"ps-tree": "^1.2.0",
"react-select": "^3.0.4",
"semver": "^6.3.0",
"string-natural-compare": "^2.0.3",
"tree-kill": "^1.2.1",
"upath": "^1.1.2",
"which": "^1.3.1"
},
"scripts": {
"prepare": "yarn download-cli && yarn run clean && yarn run build",
"prepare": "yarn download-cli && yarn download-ls && yarn run clean && yarn run build",
"clean": "rimraf lib",
"download-cli": "node ./scripts/download-cli.js",
"download-ls": "node ./scripts/download-ls.js",
"generate-protocol": "node ./scripts/generate-protocol.js",
"lint": "tslint -c ./tslint.json --project ./tsconfig.json",
"build": "tsc && ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/ && yarn lint",
"watch": "tsc -w"
"watch": "tsc -w",
"test": "mocha \"./test/**/*.test.ts\""
},
"mocha": {
"require": [
"ts-node/register",
"reflect-metadata/Reflect"
],
"reporter": "spec",
"colors": true,
"watch-extensions": "ts,tsx",
"timeout": 10000
},
"devDependencies": {
"@types/chai": "^4.2.7",
"@types/chai-string": "^1.4.2",
"@types/mocha": "^5.2.7",
"chai": "^4.2.0",
"chai-string": "^1.5.0",
"decompress": "^4.2.0",
"decompress-tarbz2": "^4.1.1",
"decompress-targz": "^4.1.1",
"decompress-unzip": "^4.0.1",
"download": "^7.1.0",
"grpc-tools": "^1.7.3",
"grpc_tools_node_protoc_ts": "^2.5.0",
"grpc-tools": "^1.8.0",
"grpc_tools_node_protoc_ts": "^2.5.8",
"mocha": "^7.0.0",
"moment": "^2.24.0",
"ncp": "^2.0.0",
"rimraf": "^2.6.1",
"shelljs": "^0.8.3",
"ts-node": "^8.6.2",
"tslint": "^5.5.0",
"typescript": "2.9.1",
"typescript": "3.5.3",
"uuid": "^3.2.1",
"yargs": "^11.1.0"
},
@@ -63,4 +99,4 @@
"frontendElectron": "lib/electron-browser/electron-arduino-menu-module"
}
]
}
}

View File

@@ -1,41 +1,26 @@
// @ts-check
// The links to the downloads as of today (11.08.) are the followings:
// - https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli-nightly-latest-${FILE_NAME}
// - https://downloads.arduino.cc/arduino-cli/arduino-cli-latest-${FILE_NAME}
// The links to the downloads as of today (02.09.) are the followings:
// In order to get the latest nightly build for your platform use the following links replacing <DATE> with the current date, using the format YYYYMMDD (i.e for 2019/Aug/06 use 20190806 )
// Linux 64 bit: https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-<DATE>_Linux_64bit.tar.gz
// Linux ARM 64 bit: https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-<DATE>_Linux_ARM64.tar.gz
// Windows 64 bit: https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-<DATE>_Windows_64bit.zip
// Mac OSX: https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-<DATE>_macOS_64bit.tar.gz
// [...]
// redirecting to latest generated builds by replacing latest with the latest available build date, using the format YYYYMMDD (i.e for 2019/Aug/06 latest is replaced with 20190806
(async () => {
(() => {
const DEFAULT_VERSION = 'nightly';
const DEFAULT_VERSION = '0.7.1'; // require('moment')().format('YYYYMMDD');
const os = require('os');
const fs = require('fs');
const path = require('path');
const shell = require('shelljs');
const download = require('download');
const decompress = require('decompress');
const unzip = require('decompress-unzip');
const untarbz = require('decompress-tarbz2');
process.on('unhandledRejection', (reason, _) => {
shell.echo(String(reason));
shell.exit(1);
throw reason;
});
process.on('uncaughtException', error => {
shell.echo(String(error));
shell.exit(1);
throw error;
});
const downloader = require('./downloader');
const yargs = require('yargs')
.option('cli-version', {
alias: 'cv',
default: DEFAULT_VERSION,
choices: [
// 'latest', // TODO: How do we get the source for `latest`. Currently, `latest` is the `0.3.7-alpha.preview`.
'nightly'
],
describe: `The version of the 'arduino-cli' to download. Defaults to ${DEFAULT_VERSION}.`
describe: `The version of the 'arduino-cli' to download, or 'nightly-latest'. Defaults to ${DEFAULT_VERSION}.`
})
.option('force-download', {
alias: 'fd',
@@ -49,32 +34,16 @@
const { platform, arch } = process;
const build = path.join(__dirname, '..', 'build');
const downloads = path.join(__dirname, '..', 'downloads');
const cli = path.join(build, `arduino-cli${os.platform() === 'win32' ? '.exe' : ''}`);
if (fs.existsSync(cli) && !force) {
shell.echo(`The 'arduino-cli' already exists at ${cli}. Skipping download.`);
shell.exit(0);
}
if (!fs.existsSync(build)) {
if (shell.mkdir('-p', build).code !== 0) {
shell.echo('Could not create new directory.');
shell.exit(1);
}
}
if (shell.rm('-rf', cli, downloads).code !== 0) {
shell.exit(1);
}
const cli = path.join(build, `arduino-cli${platform === 'win32' ? '.exe' : ''}`);
const suffix = (() => {
switch (platform) {
case 'darwin': return 'macosx.zip';
case 'win32': return 'windows.zip';
case 'darwin': return 'macOS_64bit.tar.gz';
case 'win32': return 'Windows_64bit.zip';
case 'linux': {
switch (arch) {
case 'arm64': return 'linuxarm.tar.bz2';
case 'x32': return 'linux32.tar.bz2';
case 'x64': return 'linux64.tar.bz2';
case 'arm64': return 'Linux_ARM64.tar.gz';
case 'x64': return 'Linux_64bit.tar.gz';
default: return undefined;
}
}
@@ -86,32 +55,7 @@
shell.exit(1);
}
const url = `https://downloads.arduino.cc/arduino-cli/${version === 'nightly' ? 'nightly/' : ''}arduino-cli-${version}-latest-${suffix}`;
shell.echo(`>>> Downloading 'arduino-cli' from '${url}'...`);
const data = await download(url);
shell.echo(`<<< Download succeeded.`);
shell.echo('>>> Decompressing CLI...');
const files = await decompress(data, downloads, {
plugins: [
unzip(),
untarbz()
]
});
shell.echo('<<< Decompressing succeeded.');
const url = `https://downloads.arduino.cc/arduino-cli${version.startsWith('nightly-') ? '/nightly' : ''}/arduino-cli_${version}_${suffix}`;
downloader.downloadUnzipFile(url, cli, 'arduino-cli', force);
if (files.length !== 1) {
shell.echo('Error ocurred when decompressing the CLI.');
shell.exit(1);
}
if (shell.mv('-f', path.join(downloads, files[0].path), cli).code !== 0) {
shell.echo(`Could not move file to ${cli}.`);
shell.exit(1);
}
if (!fs.existsSync(cli)) {
shell.echo(`Could not find CLI at ${cli}.`);
shell.exit(1);
} else {
shell.echo('Done.');
}
})();
})();

View File

@@ -0,0 +1,72 @@
// @ts-check
// The links to the downloads as of today (28.08.2019) are the following:
// - https://downloads.arduino.cc/arduino-language-server/nightly/arduino-language-server_${SUFFIX}
// - https://downloads.arduino.cc/arduino-language-server/clangd/clangd_${VERSION}_${SUFFIX}
(() => {
const DEFAULT_ALS_VERSION = 'nightly';
const DEFAULT_CLANGD_VERSION = '9.0.0';
const path = require('path');
const shell = require('shelljs');
const downloader = require('./downloader');
const yargs = require('yargs')
.option('ls-version', {
alias: 'lv',
default: DEFAULT_ALS_VERSION,
choices: ['nightly'],
describe: `The version of the 'arduino-language-server' to download. Defaults to ${DEFAULT_ALS_VERSION}.`
})
.option('clangd-version', {
alias: 'cv',
default: DEFAULT_CLANGD_VERSION,
choices: ['8.0.1', '9.0.0'],
describe: `The version of 'clangd' to download. Defaults to ${DEFAULT_CLANGD_VERSION}.`
})
.option('force-download', {
alias: 'fd',
default: false,
describe: `If set, this script force downloads the 'arduino-language-server' even if it already exists on the file system.`
})
.version(false).parse();
const alsVersion = yargs['ls-version'];
const clangdVersion = yargs['clangd-version']
const force = yargs['force-download'];
const { platform, arch } = process;
const build = path.join(__dirname, '..', 'build');
const alsTarget = path.join(build, `arduino-language-server${platform === 'win32' ? '.exe' : ''}`);
let clangdTarget, alsSuffix, clangdSuffix;
switch (platform) {
case 'darwin':
clangdTarget = path.join(build, 'bin', 'clangd')
alsSuffix = 'Darwin_amd64.zip';
clangdSuffix = 'macos.zip';
break;
case 'linux':
clangdTarget = path.join(build, 'bin', 'clangd')
alsSuffix = 'Linux_amd64.zip';
clangdSuffix = 'linux.zip'
break;
case 'win32':
clangdTarget = path.join(build, 'clangd.exe')
alsSuffix = 'Windows_NT_amd64.zip';
clangdSuffix = 'windows.zip';
break;
}
if (!alsSuffix) {
shell.echo(`The arduino-language-server is not available for ${platform} ${arch}.`);
shell.exit(1);
}
const alsUrl = `https://downloads.arduino.cc/arduino-language-server/${alsVersion === 'nightly' ? 'nightly/arduino-language-server' : 'arduino-language-server_' + alsVersion}_${alsSuffix}`;
downloader.downloadUnzipAll(alsUrl, build, alsTarget, force);
const clangdUrl = `https://downloads.arduino.cc/arduino-language-server/clangd/clangd_${clangdVersion}_${clangdSuffix}`;
downloader.downloadUnzipAll(clangdUrl, build, clangdTarget, force);
})();

View File

@@ -0,0 +1,116 @@
const fs = require('fs');
const path = require('path');
const shell = require('shelljs');
const download = require('download');
const decompress = require('decompress');
const unzip = require('decompress-unzip');
const untargz = require('decompress-targz');
process.on('unhandledRejection', (reason, _) => {
shell.echo(String(reason));
shell.exit(1);
throw reason;
});
process.on('uncaughtException', error => {
shell.echo(String(error));
shell.exit(1);
throw error;
});
/**
* @param url {string} Download URL
* @param targetFile {string} Path to the file to copy from the decompressed archive
* @param filePrefix {string} Prefix of the file name found in the archive
* @param force {boolean} Whether to download even if the target file exists
*/
exports.downloadUnzipFile = async (url, targetFile, filePrefix, force) => {
if (fs.existsSync(targetFile) && !force) {
shell.echo(`Skipping download because file already exists: ${targetFile}`);
return;
}
if (!fs.existsSync(path.dirname(targetFile))) {
if (shell.mkdir('-p', path.dirname(targetFile)).code !== 0) {
shell.echo('Could not create new directory.');
shell.exit(1);
}
}
const downloads = path.join(__dirname, '..', 'downloads');
if (shell.rm('-rf', targetFile, downloads).code !== 0) {
shell.exit(1);
}
shell.echo(`>>> Downloading from '${url}'...`);
const data = await download(url);
shell.echo(`<<< Download succeeded.`);
shell.echo('>>> Decompressing...');
const files = await decompress(data, downloads, {
plugins: [
unzip(),
untargz()
]
});
if (files.length === 0) {
shell.echo('Error ocurred while decompressing the archive.');
shell.exit(1);
}
const fileIndex = files.findIndex(f => f.path.startsWith(filePrefix));
if (fileIndex === -1) {
shell.echo(`The downloaded artifact does not contain any file with prefix ${filePrefix}.`);
shell.exit(1);
}
shell.echo('<<< Decompressing succeeded.');
if (shell.mv('-f', path.join(downloads, files[fileIndex].path), targetFile).code !== 0) {
shell.echo(`Could not move file to target path: ${targetFile}`);
shell.exit(1);
}
if (!fs.existsSync(targetFile)) {
shell.echo(`Could not find file: ${targetFile}`);
shell.exit(1);
}
shell.echo(`Done: ${targetFile}`);
}
/**
* @param url {string} Download URL
* @param targetDir {string} Directory into which to decompress the archive
* @param targetFile {string} Path to the main file expected after decompressing
* @param force {boolean} Whether to download even if the target file exists
*/
exports.downloadUnzipAll = async (url, targetDir, targetFile, force) => {
if (fs.existsSync(targetFile) && !force) {
shell.echo(`Skipping download because file already exists: ${targetFile}`);
return;
}
if (!fs.existsSync(targetDir)) {
if (shell.mkdir('-p', targetDir).code !== 0) {
shell.echo('Could not create new directory.');
shell.exit(1);
}
}
shell.echo(`>>> Downloading from '${url}'...`);
const data = await download(url);
shell.echo(`<<< Download succeeded.`);
shell.echo('>>> Decompressing...');
const files = await decompress(data, targetDir, {
plugins: [
unzip(),
untargz()
]
});
if (files.length === 0) {
shell.echo('Error ocurred while decompressing the archive.');
shell.exit(1);
}
shell.echo('<<< Decompressing succeeded.');
if (!fs.existsSync(targetFile)) {
shell.echo(`Could not find file: ${targetFile}`);
shell.exit(1);
}
shell.echo(`Done: ${targetFile}`);
}

View File

@@ -35,17 +35,11 @@ export namespace ArduinoCommands {
category: 'File'
}
export const REFRESH_BOARDS: Command = {
id: "arduino-refresh-attached-boards",
label: "Refresh attached boards"
}
export const SELECT_BOARD: Command = {
id: "arduino-select-board"
}
export const OPEN_BOARDS_DIALOG: Command = {
id: "arduino-open-boards-dialog"
}
export const TOGGLE_ADVANCED_MODE: Command = {
id: "arduino-toggle-advanced-mode"
}
}

View File

@@ -1,25 +0,0 @@
import { injectable, inject } from "inversify";
import { MenuContribution, MenuModelRegistry, MenuPath } from "@theia/core";
import { CommonMenus } from "@theia/core/lib/browser";
import { ArduinoCommands } from "./arduino-commands";
export namespace ArduinoToolbarContextMenu {
export const OPEN_SKETCH_PATH: MenuPath = ['arduino-open-sketch-context-menu'];
export const OPEN_GROUP: MenuPath = [...OPEN_SKETCH_PATH, '1_open'];
export const WS_SKETCHES_GROUP: MenuPath = [...OPEN_SKETCH_PATH, '2_sketches'];
export const EXAMPLE_SKETCHES_GROUP: MenuPath = [...OPEN_SKETCH_PATH, '3_examples'];
}
@injectable()
export class ArduinoToolbarMenuContribution implements MenuContribution {
constructor(
@inject(MenuModelRegistry) protected readonly menuRegistry: MenuModelRegistry) {
}
registerMenus(registry: MenuModelRegistry) {
registry.registerMenuAction([...CommonMenus.FILE, '0_new_sletch'], {
commandId: ArduinoCommands.NEW_SKETCH.id
})
}
}

View File

@@ -3,51 +3,67 @@ import { injectable, inject, postConstruct } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
import { MessageService } from '@theia/core/lib/common/message-service';
import { CommandContribution, CommandRegistry, Command } from '@theia/core/lib/common/command';
import { CommandContribution, CommandRegistry, Command, CommandHandler } from '@theia/core/lib/common/command';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { BoardsService, Board } from '../common/protocol/boards-service';
import { BoardsService } from '../common/protocol/boards-service';
import { ArduinoCommands } from './arduino-commands';
import { ConnectedBoards } from './components/connected-boards';
import { CoreService } from '../common/protocol/core-service';
import { WorkspaceServiceExt } from './workspace-service-ext';
import { ToolOutputServiceClient } from '../common/protocol/tool-output-service';
import { QuickPickService } from '@theia/core/lib/common/quick-pick-service';
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
import { BoardsNotificationService } from './boards-notification-service';
import { BoardsServiceClientImpl } from './boards/boards-service-client-impl';
import { WorkspaceRootUriAwareCommandHandler, WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
import { SelectionService, MenuContribution, MenuModelRegistry, MAIN_MENU_BAR } from '@theia/core';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { SketchFactory } from './sketch-factory';
import { SelectionService, MenuContribution, MenuModelRegistry, MAIN_MENU_BAR, MenuPath } from '@theia/core';
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
import { EditorManager, EditorMainMenu } from '@theia/editor/lib/browser';
import { ContextMenuRenderer, OpenerService, Widget, StatusBar } from '@theia/core/lib/browser';
import {
ContextMenuRenderer, Widget, StatusBar, StatusBarAlignment, FrontendApplicationContribution,
FrontendApplication, KeybindingContribution, KeybindingRegistry
} from '@theia/core/lib/browser';
import { OpenFileDialogProps, FileDialogService } from '@theia/filesystem/lib/browser/file-dialog';
import { FileSystem, FileStat } from '@theia/filesystem/lib/common';
import { ArduinoToolbarContextMenu } from './arduino-file-menu';
import { Sketch, SketchesService } from '../common/protocol/sketches-service';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { ToolOutputServiceClient } from '../common/protocol/tool-output-service';
import { CommonCommands, CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
import { FileSystemCommands } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution';
import { FileDownloadCommands } from '@theia/filesystem/lib/browser/download/file-download-command-contribution';
import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu';
import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
import { MaybePromise } from '@theia/core/lib/common/types';
import { SelectBoardDialog } from './boards/select-board-dialog';
import { BoardsConfigDialog } from './boards/boards-config-dialog';
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
import { BoardsConfig } from './boards/boards-config';
import { MonitorConnection } from './monitor/monitor-connection';
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
import { ArduinoWorkspaceService } from './arduino-workspace-service';
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
import { OutputContribution } from '@theia/output/lib/browser/output-contribution';
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { FileNavigatorCommands } from '@theia/navigator/lib/browser/navigator-contribution';
import { EditorMode } from './editor-mode';
export namespace ArduinoMenus {
export const SKETCH = [...MAIN_MENU_BAR, '3_sketch'];
export const TOOLS = [...MAIN_MENU_BAR, '4_tools'];
}
export namespace ArduinoToolbarContextMenu {
export const OPEN_SKETCH_PATH: MenuPath = ['arduino-open-sketch-context-menu'];
export const OPEN_GROUP: MenuPath = [...OPEN_SKETCH_PATH, '1_open'];
export const WS_SKETCHES_GROUP: MenuPath = [...OPEN_SKETCH_PATH, '2_sketches'];
export const EXAMPLE_SKETCHES_GROUP: MenuPath = [...OPEN_SKETCH_PATH, '3_examples'];
}
@injectable()
export class ArduinoFrontendContribution implements TabBarToolbarContribution, CommandContribution, MenuContribution {
export class ArduinoFrontendContribution implements FrontendApplicationContribution,
TabBarToolbarContribution, CommandContribution, MenuContribution, KeybindingContribution {
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(BoardsService)
protected readonly boardService: BoardsService;
protected readonly boardsService: BoardsService;
@inject(CoreService)
protected readonly coreService: CoreService;
@@ -58,21 +74,12 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
@inject(ToolOutputServiceClient)
protected readonly toolOutputServiceClient: ToolOutputServiceClient;
@inject(QuickPickService)
protected readonly quickPickService: QuickPickService;
@inject(BoardsListWidgetFrontendContribution)
protected readonly boardsListWidgetFrontendContribution: BoardsListWidgetFrontendContribution;
@inject(BoardsNotificationService)
protected readonly boardsNotificationService: BoardsNotificationService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
@inject(SelectionService)
protected readonly selectionService: SelectionService;
@inject(SketchFactory)
protected readonly sketchFactory: SketchFactory;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
@@ -85,86 +92,176 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
@inject(FileSystem)
protected readonly fileSystem: FileSystem;
@inject(OpenerService)
protected readonly openerService: OpenerService;
@inject(WindowService)
protected readonly windowService: WindowService;
@inject(SketchesService)
protected readonly sketches: SketchesService;
protected readonly sketchService: SketchesService;
@inject(SelectBoardDialog)
protected readonly selectBoardsDialog: SelectBoardDialog;
@inject(BoardsConfigDialog)
protected readonly boardsConfigDialog: BoardsConfigDialog;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(CommandRegistry)
protected readonly commands: CommandRegistry;
protected readonly commandRegistry: CommandRegistry;
@inject(StatusBar)
protected readonly statusBar: StatusBar;
protected boardsToolbarItem: BoardsToolBarItem | null;
protected wsSketchCount: number = 0;
@inject(ArduinoWorkspaceService)
protected readonly workspaceService: ArduinoWorkspaceService;
constructor(@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService) {
this.workspaceService.onWorkspaceChanged(() => {
if (this.workspaceService.workspace) {
this.registerSketchesInMenu(this.menuRegistry);
}
})
}
@inject(MonitorConnection)
protected readonly monitorConnection: MonitorConnection;
@inject(FileNavigatorContribution)
protected readonly fileNavigatorContributions: FileNavigatorContribution;
@inject(OutputContribution)
protected readonly outputContribution: OutputContribution;
@inject(OutlineViewContribution)
protected readonly outlineContribution: OutlineViewContribution;
@inject(ProblemContribution)
protected readonly problemContribution: ProblemContribution;
@inject(ScmContribution)
protected readonly scmContribution: ScmContribution;
@inject(SearchInWorkspaceFrontendContribution)
protected readonly siwContribution: SearchInWorkspaceFrontendContribution;
@inject(EditorMode)
protected readonly editorMode: EditorMode;
protected application: FrontendApplication;
protected wsSketchCount: number = 0; // TODO: this does not belong here, does it?
@postConstruct()
protected async init(): Promise<void> {
// This is a hack. Otherwise, the backend services won't bind.
await this.workspaceServiceExt.roots();
const updateStatusBar = (config: BoardsConfig.Config) => {
this.statusBar.setElement('arduino-selected-board', {
alignment: StatusBarAlignment.RIGHT,
text: BoardsConfig.Config.toString(config)
});
}
this.boardsServiceClient.onBoardsConfigChanged(updateStatusBar);
updateStatusBar(this.boardsServiceClient.boardsConfig);
this.registerSketchesInMenu(this.menuRegistry);
Promise.all([
this.boardsService.getAttachedBoards(),
this.boardsService.getAvailablePorts()
]).then(([{ boards }, { ports }]) => this.boardsServiceClient.tryReconnect(boards, ports));
}
onStart(app: FrontendApplication): void {
this.application = app;
// Initialize all `pro-mode` widgets. This is a NOOP if in normal mode.
for (const viewContribution of [
this.fileNavigatorContributions,
this.outputContribution,
this.outlineContribution,
this.problemContribution,
this.scmContribution,
this.siwContribution] as Array<FrontendApplicationContribution>) {
if (viewContribution.initializeLayout) {
viewContribution.initializeLayout(this.application);
}
}
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: ArduinoCommands.VERIFY.id,
command: ArduinoCommands.VERIFY.id,
tooltip: 'Verify',
text: '$(check)'
tooltip: 'Verify'
});
registry.registerItem({
id: ArduinoCommands.UPLOAD.id,
command: ArduinoCommands.UPLOAD.id,
tooltip: 'Upload',
text: '$(arrow-right)'
tooltip: 'Upload'
});
registry.registerItem({
id: ArduinoCommands.SHOW_OPEN_CONTEXT_MENU.id,
command: ArduinoCommands.SHOW_OPEN_CONTEXT_MENU.id,
tooltip: 'Open',
text: '$(arrow-up)'
tooltip: 'Open'
});
registry.registerItem({
id: ArduinoCommands.SAVE_SKETCH.id,
command: ArduinoCommands.SAVE_SKETCH.id,
tooltip: 'Save',
text: '$(arrow-down)'
tooltip: 'Save'
});
registry.registerItem({
id: ConnectedBoards.TOOLBAR_ID,
id: BoardsToolBarItem.TOOLBAR_ID,
render: () => <BoardsToolBarItem
key='boardsToolbarItem'
ref={ref => this.boardsToolbarItem = ref}
commands={this.commands}
statusBar={this.statusBar}
contextMenuRenderer={this.contextMenuRenderer}
boardsNotificationService={this.boardsNotificationService}
boardService={this.boardService} />,
isVisible: widget => this.isArduinoToolbar(widget)
})
commands={this.commandRegistry}
boardsServiceClient={this.boardsServiceClient}
boardService={this.boardsService} />,
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left'
});
registry.registerItem({
id: 'toggle-serial-monitor',
command: MonitorViewContribution.OPEN_SERIAL_MONITOR,
tooltip: 'Toggle Serial Monitor',
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'right'
});
registry.registerItem({
id: ArduinoCommands.TOGGLE_ADVANCED_MODE.id,
command: ArduinoCommands.TOGGLE_ADVANCED_MODE.id,
tooltip: 'Toggle Advanced Mode',
text: (this.editorMode.proMode ? '$(toggle-on)' : '$(toggle-off)'),
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'right'
});
}
registerCommands(registry: CommandRegistry): void {
// TODO: use proper API https://github.com/eclipse-theia/theia/pull/6599
const allHandlers: { [id: string]: CommandHandler[] } = (registry as any)._handlers;
// Make sure to reveal the `Explorer` before executing `New File` and `New Folder`.
for (const command of [WorkspaceCommands.NEW_FILE, WorkspaceCommands.NEW_FOLDER]) {
const { id } = command;
const handlers = allHandlers[id].slice();
registry.unregisterCommand(id);
registry.registerCommand(command);
for (const handler of handlers) {
const wrapper: CommandHandler = {
execute: (...args: any[]) => {
this.fileNavigatorContributions.openView({ reveal: true }).then(() => handler.execute(args));
},
isVisible: (...args: any[]) => {
return handler.isVisible!(args);
},
isEnabled: (args: any[]) => {
return handler.isEnabled!(args);
},
isToggled: (args: any[]) => {
return handler.isToggled!(args);
}
};
if (!handler.isEnabled) {
delete wrapper.isEnabled;
}
if (!handler.isToggled) {
delete wrapper.isToggled;
}
if (!handler.isVisible) {
delete wrapper.isVisible;
}
registry.registerHandler(id, wrapper);
}
}
registry.registerCommand(ArduinoCommands.VERIFY, {
isVisible: widget => this.isArduinoToolbar(widget),
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: widget => true,
execute: async () => {
const widget = this.getCurrentWidget();
@@ -178,14 +275,24 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
}
try {
await this.coreService.compile({ uri: uri.toString() });
const { boardsConfig } = this.boardsServiceClient;
if (!boardsConfig || !boardsConfig.selectedBoard) {
throw new Error('No boards selected. Please select a board.');
}
if (!boardsConfig.selectedBoard.fqbn) {
throw new Error(`No core is installed for ${boardsConfig.selectedBoard.name}. Please install the board.`);
}
// Reveal the Output view asynchronously (don't await it)
this.outputContribution.openView({ reveal: true });
await this.coreService.compile({ uri: uri.toString(), board: boardsConfig.selectedBoard });
} catch (e) {
await this.messageService.error(e.toString());
}
}
});
registry.registerCommand(ArduinoCommands.UPLOAD, {
isVisible: widget => this.isArduinoToolbar(widget),
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: widget => true,
execute: async () => {
const widget = this.getCurrentWidget();
@@ -198,16 +305,36 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
return;
}
const monitorConfig = this.monitorConnection.monitorConfig;
if (monitorConfig) {
await this.monitorConnection.disconnect();
}
try {
await this.coreService.upload({ uri: uri.toString() });
const { boardsConfig } = this.boardsServiceClient;
if (!boardsConfig || !boardsConfig.selectedBoard) {
throw new Error('No boards selected. Please select a board.');
}
const { selectedPort } = boardsConfig;
if (!selectedPort) {
throw new Error('No ports selected. Please select a port.');
}
// Reveal the Output view asynchronously (don't await it)
this.outputContribution.openView({ reveal: true });
await this.coreService.upload({ uri: uri.toString(), board: boardsConfig.selectedBoard, port: selectedPort.address });
} catch (e) {
await this.messageService.error(e.toString());
} finally {
if (monitorConfig) {
await this.monitorConnection.connect(monitorConfig);
}
}
}
});
registry.registerCommand(ArduinoCommands.SHOW_OPEN_CONTEXT_MENU, {
isVisible: widget => this.isArduinoToolbar(widget),
isEnabled: widget => this.isArduinoToolbar(widget),
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
execute: async (widget: Widget, target: EventTarget) => {
if (this.wsSketchCount) {
const el = (target as HTMLElement).parentElement;
@@ -218,27 +345,31 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
});
}
} else {
this.commands.executeCommand(ArduinoCommands.OPEN_FILE_NAVIGATOR.id);
this.commandRegistry.executeCommand(ArduinoCommands.OPEN_FILE_NAVIGATOR.id);
}
}
});
registry.registerCommand(ArduinoCommands.OPEN_FILE_NAVIGATOR, {
isEnabled: () => true,
execute: () => this.doOpenFile()
})
});
registry.registerCommand(ArduinoCommands.OPEN_SKETCH, {
isEnabled: () => true,
execute: async (sketch: Sketch) => {
this.openSketchFilesInNewWindow(sketch.uri);
this.workspaceService.open(new URI(sketch.uri));
}
})
});
registry.registerCommand(ArduinoCommands.SAVE_SKETCH, {
isEnabled: widget => this.isArduinoToolbar(widget),
isVisible: widget => this.isArduinoToolbar(widget),
isEnabled: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
execute: async (sketch: Sketch) => {
registry.executeCommand(CommonCommands.SAVE_ALL.id);
}
})
});
registry.registerCommand(ArduinoCommands.NEW_SKETCH, new WorkspaceRootUriAwareCommandHandler(this.workspaceService, this.selectionService, {
execute: async uri => {
try {
@@ -247,58 +378,61 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
uri = uri.withPath(uri.path.dir.dir);
}
await this.sketchFactory.createNewSketch(uri);
const sketch = await this.sketchService.createNewSketch(uri.toString());
this.workspaceService.open(new URI(sketch.uri));
} catch (e) {
await this.messageService.error(e.toString());
}
}
}));
registry.registerCommand(ArduinoCommands.REFRESH_BOARDS, {
isEnabled: () => true,
execute: () => this.boardsNotificationService.notifyBoardsInstalled()
});
registry.registerCommand(ArduinoCommands.SELECT_BOARD, {
isEnabled: () => true,
execute: async (board: Board) => {
this.selectBoard(board);
}
})
registry.registerCommand(ArduinoCommands.OPEN_BOARDS_DIALOG, {
isEnabled: () => true,
execute: async () => {
const boardAndPort = await this.selectBoardsDialog.open();
if (boardAndPort && boardAndPort.board) {
this.selectBoard(boardAndPort.board);
const boardsConfig = await this.boardsConfigDialog.open();
if (boardsConfig) {
this.boardsServiceClient.boardsConfig = boardsConfig;
}
}
})
});
registry.registerCommand(ArduinoCommands.TOGGLE_ADVANCED_MODE, {
execute: () => this.editorMode.toggle(),
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'right',
isToggled: () => this.editorMode.proMode
});
}
protected async selectBoard(board: Board) {
await this.boardService.selectBoard(board);
if (this.boardsToolbarItem) {
this.boardsToolbarItem.setSelectedBoard(board);
}
}
registerMenus(registry: MenuModelRegistry) {
registry.unregisterMenuAction(FileSystemCommands.UPLOAD);
registry.unregisterMenuAction(FileDownloadCommands.DOWNLOAD);
if (!this.editorMode.proMode) {
// If are not in pro-mode, we have to disable the context menu for the tabs.
// Such as `Close`, `Close All`, etc.
for (const command of [
CommonCommands.CLOSE_TAB,
CommonCommands.CLOSE_OTHER_TABS,
CommonCommands.CLOSE_RIGHT_TABS,
CommonCommands.CLOSE_ALL_TABS,
CommonCommands.COLLAPSE_PANEL,
CommonCommands.TOGGLE_MAXIMIZED,
FileNavigatorCommands.REVEAL_IN_NAVIGATOR
]) {
registry.unregisterMenuAction(command);
}
registry.unregisterMenuAction(WorkspaceCommands.NEW_FILE);
registry.unregisterMenuAction(WorkspaceCommands.NEW_FOLDER);
registry.unregisterMenuAction(WorkspaceCommands.OPEN_FOLDER);
registry.unregisterMenuAction(WorkspaceCommands.OPEN_WORKSPACE);
registry.unregisterMenuAction(WorkspaceCommands.OPEN_RECENT_WORKSPACE);
registry.unregisterMenuAction(WorkspaceCommands.SAVE_WORKSPACE_AS);
registry.unregisterMenuAction(WorkspaceCommands.CLOSE);
registry.unregisterMenuAction(FileSystemCommands.UPLOAD);
registry.unregisterMenuAction(FileDownloadCommands.DOWNLOAD);
registry.getMenu(MAIN_MENU_BAR).removeNode(this.getMenuId(MonacoMenus.SELECTION));
registry.getMenu(MAIN_MENU_BAR).removeNode(this.getMenuId(EditorMainMenu.GO));
registry.getMenu(MAIN_MENU_BAR).removeNode(this.getMenuId(TerminalMenus.TERMINAL));
registry.getMenu(MAIN_MENU_BAR).removeNode(this.getMenuId(CommonMenus.VIEW));
registry.getMenu(MAIN_MENU_BAR).removeNode(this.getMenuId(CommonMenus.HELP));
registry.unregisterMenuAction(WorkspaceCommands.OPEN_FOLDER);
registry.unregisterMenuAction(WorkspaceCommands.OPEN_WORKSPACE);
registry.unregisterMenuAction(WorkspaceCommands.OPEN_RECENT_WORKSPACE);
registry.unregisterMenuAction(WorkspaceCommands.SAVE_WORKSPACE_AS);
registry.unregisterMenuAction(WorkspaceCommands.CLOSE);
registry.getMenu(MAIN_MENU_BAR).removeNode(this.getMenuId(MonacoMenus.SELECTION));
registry.getMenu(MAIN_MENU_BAR).removeNode(this.getMenuId(EditorMainMenu.GO));
registry.getMenu(MAIN_MENU_BAR).removeNode(this.getMenuId(TerminalMenus.TERMINAL));
registry.getMenu(MAIN_MENU_BAR).removeNode(this.getMenuId(CommonMenus.VIEW));
}
registry.registerSubmenu(ArduinoMenus.SKETCH, 'Sketch');
registry.registerMenuAction(ArduinoMenus.SKETCH, {
@@ -317,6 +451,15 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
});
registry.registerSubmenu(ArduinoMenus.TOOLS, 'Tools');
registry.registerMenuAction(CommonMenus.HELP, {
commandId: ArduinoCommands.TOGGLE_ADVANCED_MODE.id,
label: 'Advanced Mode'
});
registry.registerMenuAction([...CommonMenus.FILE, '0_new_sketch'], {
commandId: ArduinoCommands.NEW_SKETCH.id
});
}
protected getMenuId(menuPath: string[]): string {
@@ -325,15 +468,26 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
return menuId;
}
protected registerSketchesInMenu(registry: MenuModelRegistry) {
this.getWorkspaceSketches().then(sketches => {
registerKeybindings(keybindings: KeybindingRegistry): void {
keybindings.registerKeybinding({
command: ArduinoCommands.VERIFY.id,
keybinding: 'ctrlcmd+alt+v'
});
keybindings.registerKeybinding({
command: ArduinoCommands.UPLOAD.id,
keybinding: 'ctrlcmd+alt+u'
});
}
protected async registerSketchesInMenu(registry: MenuModelRegistry): Promise<void> {
this.sketchService.getSketches().then(sketches => {
this.wsSketchCount = sketches.length;
sketches.forEach(sketch => {
const command: Command = {
id: 'openSketch' + sketch.name
}
this.commands.registerCommand(command, {
execute: () => this.commands.executeCommand(ArduinoCommands.OPEN_SKETCH.id, sketch)
this.commandRegistry.registerCommand(command, {
execute: () => this.commandRegistry.executeCommand(ArduinoCommands.OPEN_SKETCH.id, sketch)
});
registry.registerMenuAction(ArduinoToolbarContextMenu.WS_SKETCHES_GROUP, {
@@ -344,25 +498,12 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
})
}
protected async getWorkspaceSketches(): Promise<Sketch[]> {
const sketches = this.sketches.getSketches(this.workspaceService.workspace);
return sketches;
}
protected async openSketchFilesInNewWindow(uri: string) {
const location = new URL(window.location.href);
location.searchParams.set('sketch', uri);
this.windowService.openNewWindow(location.toString());
}
async openSketchFiles(uri: string) {
const fileStat = await this.fileSystem.getFileStat(uri);
if (fileStat) {
const uris = await this.sketches.getSketchFiles(fileStat);
async openSketchFiles(uri: string): Promise<void> {
this.sketchService.getSketchFiles(uri).then(uris => {
for (const uri of uris) {
this.editorManager.open(new URI(uri));
}
}
});
}
/**
@@ -386,7 +527,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
if (destinationFile && !destinationFile.isDirectory) {
const message = await this.validate(destinationFile);
if (!message) {
await this.openSketchFilesInNewWindow(destinationFileUri.toString());
this.workspaceService.open(destinationFileUri);
return destinationFileUri;
} else {
this.messageService.warn(message);
@@ -423,32 +564,6 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
return undefined;
}
// private async onNoBoardsInstalled() {
// const action = await this.messageService.info("You have no boards installed. Use the boards manager to install one.", "Open Boards Manager");
// if (!action) {
// return;
// }
// this.boardsListWidgetFrontendContribution.openView({ reveal: true });
// }
// private async onUnknownBoard() {
// const action = await this.messageService.warn("There's a board connected for which you need to install software." +
// " If this were not a PoC we would offer you the right package now.", "Open Boards Manager");
// if (!action) {
// return;
// }
// this.boardsListWidgetFrontendContribution.openView({ reveal: true });
// }
private isArduinoToolbar(maybeToolbarWidget: any): boolean {
if (maybeToolbarWidget instanceof ArduinoToolbar) {
return true;
}
return false;
}
private toUri(arg: any): URI | undefined {
if (arg instanceof URI) {
return arg;

View File

@@ -7,13 +7,14 @@ import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider';
import { FrontendApplicationContribution, FrontendApplication } from '@theia/core/lib/browser/frontend-application'
import { LanguageGrammarDefinitionContribution } from '@theia/monaco/lib/browser/textmate';
import { LanguageClientContribution } from '@theia/languages/lib/browser';
import { ArduinoLanguageClientContribution } from './language/arduino-language-client-contribution';
import { LibraryListWidget } from './library/library-list-widget';
import { ArduinoFrontendContribution } from './arduino-frontend-contribution';
import { ArduinoLanguageGrammarContribution } from './language/arduino-language-grammar-contribution';
import { LibraryService, LibraryServicePath } from '../common/protocol/library-service';
import { BoardsService, BoardsServicePath } from '../common/protocol/boards-service';
import { BoardsService, BoardsServicePath, BoardsServiceClient } from '../common/protocol/boards-service';
import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service';
import { LibraryListWidgetFrontendContribution } from './library/list-widget-frontend-contribution';
import { CoreService, CoreServicePath } from '../common/protocol/core-service';
import { BoardsListWidget } from './boards/boards-list-widget';
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
@@ -22,34 +23,55 @@ import { WorkspaceServiceExtImpl } from './workspace-service-ext-impl';
import { ToolOutputServiceClient } from '../common/protocol/tool-output-service';
import { ToolOutputService } from '../common/protocol/tool-output-service';
import { ToolOutputServiceClientImpl } from './tool-output/client-service-impl';
import { BoardsNotificationService } from './boards-notification-service';
import { BoardsServiceClientImpl } from './boards/boards-service-client-impl';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { AWorkspaceService } from './arduino-workspace-service';
import { ArduinoWorkspaceService } from './arduino-workspace-service';
import { ThemeService } from '@theia/core/lib/browser/theming';
import { ArduinoTheme } from './arduino-theme';
import { ArduinoToolbarMenuContribution } from './arduino-file-menu';
import { MenuContribution } from '@theia/core';
import { SketchFactory } from './sketch-factory';
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
import { SilentOutlineViewContribution } from './customization/silent-outline-contribution';
import { ArduinoOutlineViewContribution } from './customization/arduino-outline-contribution';
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { SilentProblemContribution } from './customization/silent-problem-contribution';
import { SilentNavigatorContribution } from './customization/silent-navigator-contribution';
import { ArduinoProblemContribution } from './customization/arduino-problem-contribution';
import { ArduinoNavigatorContribution } from './customization/arduino-navigator-contribution';
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
import { ArduinoToolbarContribution } from './toolbar/arduino-toolbar-contribution';
import { OutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution';
import { ArduinoOutputToolContribution } from './customization/silent-output-tool-contribution';
import { ArduinoOutputToolContribution } from './customization/arduino-output-tool-contribution';
import { EditorContribution } from '@theia/editor/lib/browser/editor-contribution';
import { CustomEditorContribution } from './customization/custom-editor-contribution';
import { ArduinoEditorContribution } from './customization/arduino-editor-contribution';
import { MonacoStatusBarContribution } from '@theia/monaco/lib/browser/monaco-status-bar-contribution';
import { SilentMonacoStatusBarContribution } from './customization/silent-monaco-status-bar-contribution';
import { ApplicationShell } from '@theia/core/lib/browser';
import { CustomApplicationShell } from './customization/custom-application-shell';
import { CustomFrontendApplication } from './customization/custom-frontend-application';
import { EditorWidgetFactory } from '@theia/editor/lib/browser/editor-widget-factory';
import { CustomEditorWidgetFactory } from './customization/custom-editor-widget-factory';
import { SelectBoardDialog, SelectBoardDialogProps } from './boards/select-board-dialog';
import { SelectBoardDialogWidget } from './boards/select-board-dialog-widget';
import { ArduinoMonacoStatusBarContribution } from './customization/arduino-monaco-status-bar-contribution';
import { ApplicationShell, ShellLayoutRestorer, KeybindingContribution } from '@theia/core/lib/browser';
import { MenuContribution } from '@theia/core/lib/common/menu';
import { ArduinoApplicationShell } from './customization/arduino-application-shell';
import { ArduinoFrontendApplication } from './customization/arduino-frontend-application';
import { BoardsConfigDialog, BoardsConfigDialogProps } from './boards/boards-config-dialog';
import { BoardsConfigDialogWidget } from './boards/boards-config-dialog-widget';
import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
import { ArduinoScmContribution } from './customization/arduino-scm-contribution';
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { ArduinoSearchInWorkspaceContribution } from './customization/arduino-search-in-workspace-contribution';
import { LibraryListWidgetFrontendContribution } from './library/library-widget-frontend-contribution';
import { MonitorServiceClientImpl } from './monitor/monitor-service-client-impl';
import { MonitorServicePath, MonitorService, MonitorServiceClient } from '../common/protocol/monitor-service';
import { ConfigService, ConfigServicePath } from '../common/protocol/config-service';
import { MonitorWidget } from './monitor/monitor-widget';
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
import { MonitorConnection } from './monitor/monitor-connection';
import { MonitorModel } from './monitor/monitor-model';
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
import { ArduinoMonacoEditorProvider } from './editor/arduino-monaco-editor-provider';
import { TabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
import { ArduinoTabBarDecoratorService } from './shell/arduino-tab-bar-decorator';
import { ProblemManager } from '@theia/markers/lib/browser';
import { ArduinoProblemManager } from './markers/arduino-problem-manager';
import { BoardsAutoInstaller } from './boards/boards-auto-installer';
import { AboutDialog } from '@theia/core/lib/browser/about-dialog';
import { ArduinoAboutDialog } from './customization/arduino-about-dialog';
import { ArduinoShellLayoutRestorer } from './shell/arduino-shell-layout-restorer';
import { EditorMode } from './editor-mode';
import { ListItemRenderer } from './components/component-list/list-item-renderer';
const ElementQueries = require('css-element-queries/src/ElementQueries');
export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => {
@@ -61,18 +83,21 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
bind(CommandContribution).toService(ArduinoFrontendContribution);
bind(MenuContribution).toService(ArduinoFrontendContribution);
bind(TabBarToolbarContribution).toService(ArduinoFrontendContribution);
bind(KeybindingContribution).toService(ArduinoFrontendContribution);
bind(FrontendApplicationContribution).toService(ArduinoFrontendContribution);
bind(MenuContribution).to(ArduinoToolbarMenuContribution).inSingletonScope();
bind(ArduinoToolbarContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(ArduinoToolbarContribution);
// `ino` TextMate grammar
// `ino` TextMate grammar and language client
bind(LanguageGrammarDefinitionContribution).to(ArduinoLanguageGrammarContribution).inSingletonScope();
bind(LanguageClientContribution).to(ArduinoLanguageClientContribution).inSingletonScope();
// Renderer for both the library and the core widgets.
bind(ListItemRenderer).toSelf().inSingletonScope();
// Library service
bind(LibraryService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, LibraryServicePath)).inSingletonScope();
// Library list widget
bind(LibraryListWidget).toSelf();
bindViewContribution(bind, LibraryListWidgetFrontendContribution);
@@ -85,12 +110,27 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
// Sketch list service
bind(SketchesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, SketchesServicePath)).inSingletonScope();
// Boards Notification service for updating boards list
// TODO (post-PoC): move this to boards service/backend
bind(BoardsNotificationService).toSelf().inSingletonScope();
// Config service
bind(ConfigService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ConfigServicePath)).inSingletonScope();
// Boards service
bind(BoardsService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, BoardsServicePath)).inSingletonScope();
bind(BoardsService).toDynamicValue(context => {
const connection = context.container.get(WebSocketConnectionProvider);
const client = context.container.get(BoardsServiceClientImpl);
return connection.createProxy(BoardsServicePath, client);
}).inSingletonScope();
// Boards service client to receive and delegate notifications from the backend.
bind(BoardsServiceClientImpl).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(BoardsServiceClientImpl);
bind(BoardsServiceClient).toDynamicValue(context => {
const client = context.container.get(BoardsServiceClientImpl);
WebSocketConnectionProvider.createProxy(context.container, BoardsServicePath, client);
return client;
}).inSingletonScope();
// boards auto-installer
bind(BoardsAutoInstaller).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(BoardsAutoInstaller);
// Boards list widget
bind(BoardsListWidget).toSelf();
@@ -102,9 +142,9 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
bind(FrontendApplicationContribution).toService(BoardsListWidgetFrontendContribution);
// Board select dialog
bind(SelectBoardDialogWidget).toSelf().inSingletonScope();
bind(SelectBoardDialog).toSelf().inSingletonScope();
bind(SelectBoardDialogProps).toConstantValue({
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
bind(BoardsConfigDialog).toSelf().inSingletonScope();
bind(BoardsConfigDialogProps).toConstantValue({
title: 'Select Board'
})
@@ -122,7 +162,7 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
}).inSingletonScope();
// The workspace service extension
bind(WorkspaceServiceExt).to(WorkspaceServiceExtImpl).inSingletonScope().onActivation(({ container }, workspaceServiceExt) => {
bind(WorkspaceServiceExt).to(WorkspaceServiceExtImpl).inSingletonScope().onActivation(({ container }, workspaceServiceExt: WorkspaceServiceExt) => {
WebSocketConnectionProvider.createProxy(container, WorkspaceServiceExtPath, workspaceServiceExt);
// Eagerly active the core, library, and boards services.
container.get(CoreService);
@@ -132,30 +172,68 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
return workspaceServiceExt;
});
bind(AWorkspaceService).toSelf().inSingletonScope();
rebind(WorkspaceService).to(AWorkspaceService).inSingletonScope();
bind(SketchFactory).toSelf().inSingletonScope();
// Serial Monitor
bind(MonitorModel).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(MonitorModel);
bind(MonitorWidget).toSelf();
bindViewContribution(bind, MonitorViewContribution);
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
bind(WidgetFactory).toDynamicValue(context => ({
id: MonitorWidget.ID,
createWidget: () => context.container.get(MonitorWidget)
}));
// Frontend binding for the monitor service
bind(MonitorService).toDynamicValue(context => {
const connection = context.container.get(WebSocketConnectionProvider);
const client = context.container.get(MonitorServiceClientImpl);
return connection.createProxy(MonitorServicePath, client);
}).inSingletonScope();
bind(MonitorConnection).toSelf().inSingletonScope();
// Monitor service client to receive and delegate notifications from the backend.
bind(MonitorServiceClientImpl).toSelf().inSingletonScope();
bind(MonitorServiceClient).toDynamicValue(context => {
const client = context.container.get(MonitorServiceClientImpl);
WebSocketConnectionProvider.createProxy(context.container, MonitorServicePath, client);
return client;
}).inSingletonScope();
bind(ArduinoWorkspaceService).toSelf().inSingletonScope();
rebind(WorkspaceService).toService(ArduinoWorkspaceService);
const themeService = ThemeService.get();
themeService.register(...ArduinoTheme.themes);
// customizing default theia
unbind(OutlineViewContribution);
bind(OutlineViewContribution).to(SilentOutlineViewContribution).inSingletonScope();
unbind(ProblemContribution);
bind(ProblemContribution).to(SilentProblemContribution).inSingletonScope();
unbind(FileNavigatorContribution);
bind(FileNavigatorContribution).to(SilentNavigatorContribution).inSingletonScope();
unbind(OutputToolbarContribution);
bind(OutputToolbarContribution).to(ArduinoOutputToolContribution).inSingletonScope();
unbind(EditorContribution);
bind(EditorContribution).to(CustomEditorContribution).inSingletonScope();
unbind(MonacoStatusBarContribution);
bind(MonacoStatusBarContribution).to(SilentMonacoStatusBarContribution).inSingletonScope();
unbind(ApplicationShell);
bind(ApplicationShell).to(CustomApplicationShell).inSingletonScope();
unbind(FrontendApplication);
bind(FrontendApplication).to(CustomFrontendApplication).inSingletonScope();
unbind(EditorWidgetFactory);
bind(EditorWidgetFactory).to(CustomEditorWidgetFactory).inSingletonScope();
// Customizing default Theia layout based on the editor mode: `pro-mode` or `classic`.
bind(EditorMode).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(EditorMode);
rebind(OutlineViewContribution).to(ArduinoOutlineViewContribution).inSingletonScope();
rebind(ProblemContribution).to(ArduinoProblemContribution).inSingletonScope();
rebind(FileNavigatorContribution).to(ArduinoNavigatorContribution).inSingletonScope();
rebind(OutputToolbarContribution).to(ArduinoOutputToolContribution).inSingletonScope();
rebind(EditorContribution).to(ArduinoEditorContribution).inSingletonScope();
rebind(MonacoStatusBarContribution).to(ArduinoMonacoStatusBarContribution).inSingletonScope();
rebind(ApplicationShell).to(ArduinoApplicationShell).inSingletonScope();
rebind(ScmContribution).to(ArduinoScmContribution).inSingletonScope();
rebind(SearchInWorkspaceFrontendContribution).to(ArduinoSearchInWorkspaceContribution).inSingletonScope();
rebind(FrontendApplication).to(ArduinoFrontendApplication).inSingletonScope();
// Monaco customizations
bind(ArduinoMonacoEditorProvider).toSelf().inSingletonScope();
rebind(MonacoEditorProvider).toService(ArduinoMonacoEditorProvider);
// Decorator customizations
bind(ArduinoTabBarDecoratorService).toSelf().inSingletonScope();
rebind(TabBarDecoratorService).toService(ArduinoTabBarDecoratorService);
// Problem markers
bind(ArduinoProblemManager).toSelf().inSingletonScope();
rebind(ProblemManager).toService(ArduinoProblemManager);
// About dialog to show the CLI version
bind(ArduinoAboutDialog).toSelf().inSingletonScope();
rebind(AboutDialog).toService(ArduinoAboutDialog);
// Customized layout restorer that can restore the state in async way: https://github.com/eclipse-theia/theia/issues/6579
bind(ArduinoShellLayoutRestorer).toSelf().inSingletonScope();
rebind(ShellLayoutRestorer).toService(ArduinoShellLayoutRestorer);
});

View File

@@ -9,6 +9,7 @@ const ARDUINO_JSON = MonacoThemeRegistry.SINGLETON.register(
export class ArduinoTheme {
static readonly arduino: Theme = {
type: 'light',
id: 'arduino-theme',
label: 'Arduino Light Theme',
description: 'Arduino Light Theme',
@@ -19,9 +20,10 @@ export class ArduinoTheme {
deactivate() {
ARDUINO_CSS.unuse();
}
}
};
static readonly themes: Theme[] = [
ArduinoTheme.arduino
]
}
];
}

View File

@@ -0,0 +1,68 @@
import { toUnix } from 'upath';
import URI from '@theia/core/lib/common/uri';
import { isWindows } from '@theia/core/lib/common/os';
import { notEmpty } from '@theia/core/lib/common/objects';
import { MaybePromise } from '@theia/core/lib/common/types';
/**
* Class for determining the default workspace location from the
* `location.hash`, the historical workspace locations, and recent sketch files.
*
* The following logic is used for determining the default workspace location:
* - `hash` points to an exists in location?
* - Yes
* - `validate location`. Is valid sketch location?
* - Yes
* - Done.
* - No
* - `try open recent workspace roots`, then `try open last modified sketches`, finally `create new sketch`.
* - No
* - `try open recent workspace roots`, then `try open last modified sketches`, finally `create new sketch`.
*/
namespace ArduinoWorkspaceRootResolver {
export interface InitOptions {
readonly isValid: (uri: string) => MaybePromise<boolean>;
}
export interface ResolveOptions {
readonly hash?: string
readonly recentWorkspaces: string[];
// Gathered from the default sketch folder. The default sketch folder is defined by the CLI.
readonly recentSketches: string[];
}
}
export class ArduinoWorkspaceRootResolver {
constructor(protected options: ArduinoWorkspaceRootResolver.InitOptions) {
}
async resolve(options: ArduinoWorkspaceRootResolver.ResolveOptions): Promise<{ uri: string } | undefined> {
const { hash, recentWorkspaces, recentSketches } = options;
for (const uri of [this.hashToUri(hash), ...recentWorkspaces, ...recentSketches].filter(notEmpty)) {
const valid = await this.isValid(uri);
if (valid) {
return { uri };
}
}
return undefined;
}
protected isValid(uri: string): MaybePromise<boolean> {
return this.options.isValid(uri);
}
// Note: here, the `hash` was defined as new `URI(yourValidFsPath).path` so we have to map it to a valid FS path first.
// This is important for Windows only and a NOOP on POSIX.
// Note: we set the `new URI(myValidUri).path.toString()` as the `hash`. See:
// - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L143 and
// - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L423
protected hashToUri(hash: string | undefined): string | undefined {
if (hash
&& hash.length > 1
&& hash.startsWith('#')) {
const path = hash.slice(1); // Trim the leading `#`.
return new URI(toUnix(path.slice(isWindows && hash.startsWith('/') ? 1 : 0))).withScheme('file').toString();
}
return undefined;
}
}

View File

@@ -1,50 +1,79 @@
import { WorkspaceService } from "@theia/workspace/lib/browser/workspace-service";
import { injectable, inject } from "inversify";
import { WorkspaceServer } from "@theia/workspace/lib/common";
import { FileSystem, FileStat } from "@theia/filesystem/lib/common";
import URI from "@theia/core/lib/common/uri";
import { SketchFactory } from "./sketch-factory";
import { injectable, inject } from 'inversify';
import { MessageService } from '@theia/core';
import { LabelProvider } from '@theia/core/lib/browser';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { ConfigService } from '../common/protocol/config-service';
import { SketchesService } from '../common/protocol/sketches-service';
import { ArduinoWorkspaceRootResolver } from './arduino-workspace-resolver';
import { EditorMode } from './editor-mode';
/**
* This is workaround to have custom frontend binding for the default workspace, although we
* already have a custom binding for the backend.
*/
@injectable()
export class AWorkspaceService extends WorkspaceService {
export class ArduinoWorkspaceService extends WorkspaceService {
@inject(WorkspaceServer)
protected readonly workspaceServer: WorkspaceServer;
@inject(SketchesService)
protected readonly sketchService: SketchesService;
@inject(FileSystem)
protected readonly fileSystem: FileSystem;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(SketchFactory)
protected readonly sketchFactory: SketchFactory;
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
protected async getDefaultWorkspacePath(): Promise<string | undefined> {
let result = await super.getDefaultWorkspacePath();
if (!result) {
const userHome = await this.fileSystem.getCurrentUserHome();
if (!userHome) {
return;
@inject(EditorMode)
protected readonly editorMode: EditorMode;
@inject(MessageService)
protected readonly messageService: MessageService;
private workspaceUri?: Promise<string | undefined>;
protected getDefaultWorkspaceUri(): Promise<string | undefined> {
if (this.workspaceUri) {
// Avoid creating a new sketch twice
return this.workspaceUri;
}
this.workspaceUri = (async () => {
try {
const hash = window.location.hash;
const [recentWorkspaces, recentSketches] = await Promise.all([
this.server.getRecentWorkspaces(),
this.sketchService.getSketches().then(sketches => sketches.map(s => s.uri))
]);
const toOpen = await new ArduinoWorkspaceRootResolver({
isValid: this.isValid.bind(this)
}).resolve({ hash, recentWorkspaces, recentSketches });
if (toOpen) {
const { uri } = toOpen;
await this.server.setMostRecentlyUsedWorkspace(uri);
return toOpen.uri;
}
const { sketchDirUri } = (await this.configService.getConfiguration());
this.logger.info(`No valid workspace URI found. Creating new sketch in ${sketchDirUri}`)
return (await this.sketchService.createNewSketch(sketchDirUri)).uri;
} catch (err) {
this.logger.fatal(`Failed to determine the sketch directory: ${err}`)
this.messageService.error(
'There was an error creating the sketch directory. ' +
'See the log for more details. ' +
'The application will probably not work as expected.')
return super.getDefaultWorkspaceUri();
}
// The backend has created this location if it was missing.
result = new URI(userHome.uri).resolve('Arduino-PoC').resolve('Sketches').toString();
}
const stat = await this.fileSystem.getFileStat(result);
if (!stat) {
// workspace does not exist yet, create it
await this.fileSystem.createFolder(result);
await this.sketchFactory.createNewSketch(new URI(result));
}
return result;
})();
return this.workspaceUri;
}
protected async setWorkspace(workspaceStat: FileStat | undefined): Promise<void> {
await super.setWorkspace(workspaceStat);
private async isValid(uri: string): Promise<boolean> {
const exists = await this.fileSystem.exists(uri);
if (!exists) {
return false;
}
// The workspace root location must exist. However, when opening a workspace root in pro-mode,
// the workspace root must not be a sketch folder. It can be the default sketch directory, or any other directories, for instance.
if (this.editorMode.proMode) {
return true;
}
const sketchFolder = await this.sketchService.isSketchFolder(uri);
return sketchFolder;
}
}
}

View File

@@ -1,19 +0,0 @@
import { EventEmitter } from "events";
import { injectable } from "inversify";
// TODO (post-PoC): move this to the backend / BoardsService
@injectable()
export class BoardsNotificationService {
protected readonly emitter = new EventEmitter();
public on(event: 'boards-installed', listener: (...args: any[]) => void): this {
this.emitter.on(event, listener);
return this;
}
public notifyBoardsInstalled() {
this.emitter.emit('boards-installed');
}
}

View File

@@ -0,0 +1,63 @@
import { injectable, inject } from 'inversify';
import { MessageService } from '@theia/core/lib/common/message-service';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { BoardsService, Board } from '../../common/protocol/boards-service';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
import { InstallationProgressDialog } from '../components/progress-dialog';
import { BoardsConfig } from './boards-config';
/**
* Listens on `BoardsConfig.Config` changes, if a board is selected which does not
* have the corresponding core installed, it proposes the user to install the core.
*/
@injectable()
export class BoardsAutoInstaller implements FrontendApplicationContribution {
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
@inject(BoardsListWidgetFrontendContribution)
protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution;
onStart(): void {
this.boardsServiceClient.onBoardsConfigChanged(this.ensureCoreExists.bind(this));
this.ensureCoreExists(this.boardsServiceClient.boardsConfig);
}
protected ensureCoreExists(config: BoardsConfig.Config): void {
const { selectedBoard } = config;
if (selectedBoard) {
this.boardsService.search({}).then(({ items }) => {
const candidates = items
.filter(item => item.boards.some(board => Board.sameAs(board, selectedBoard)))
.filter(({ installable, installedVersion }) => installable && !installedVersion);
for (const candidate of candidates) {
// tslint:disable-next-line:max-line-length
this.messageService.info(`The \`"${candidate.name}"\` core has to be installed for the currently selected \`"${selectedBoard.name}"\` board. Do you want to install it now?`, 'Install Manually', 'Yes').then(async answer => {
if (answer === 'Yes') {
const dialog = new InstallationProgressDialog(candidate.name, candidate.availableVersions[0]);
dialog.open();
try {
await this.boardsService.install({ item: candidate });
} finally {
dialog.close();
}
}
if (answer) {
this.boardsManagerFrontendContribution.openView({ reveal: true }).then(widget => widget.refresh(candidate.name.toLocaleLowerCase()));
}
});
}
})
}
}
}

View File

@@ -0,0 +1,55 @@
import * as React from 'react';
import { injectable, inject } from 'inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { ReactWidget, Message } from '@theia/core/lib/browser';
import { BoardsService } from '../../common/protocol/boards-service';
import { BoardsConfig } from './boards-config';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
@injectable()
export class BoardsConfigDialogWidget extends ReactWidget {
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
protected readonly onBoardConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
readonly onBoardConfigChanged = this.onBoardConfigChangedEmitter.event;
protected focusNode: HTMLElement | undefined;
constructor() {
super();
this.id = 'select-board-dialog';
}
protected fireConfigChanged = (config: BoardsConfig.Config) => {
this.onBoardConfigChangedEmitter.fire(config);
}
protected setFocusNode = (element: HTMLElement | undefined) => {
this.focusNode = element;
}
protected render(): React.ReactNode {
return <div className='selectBoardContainer'>
<BoardsConfig
boardsService={this.boardsService}
boardsServiceClient={this.boardsServiceClient}
onConfigChange={this.fireConfigChanged}
onFocusNodeSet={this.setFocusNode} />
</div>;
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
if (this.focusNode instanceof HTMLInputElement) {
this.focusNode.select();
}
(this.focusNode || this.node).focus();
}
}

View File

@@ -0,0 +1,113 @@
import { injectable, inject, postConstruct } from 'inversify';
import { Message } from '@phosphor/messaging';
import { AbstractDialog, DialogProps, Widget, DialogError } from '@theia/core/lib/browser';
import { BoardsService } from '../../common/protocol/boards-service';
import { BoardsConfig } from './boards-config';
import { BoardsConfigDialogWidget } from './boards-config-dialog-widget';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
@injectable()
export class BoardsConfigDialogProps extends DialogProps {
}
@injectable()
export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
@inject(BoardsConfigDialogWidget)
protected readonly widget: BoardsConfigDialogWidget;
@inject(BoardsService)
protected readonly boardService: BoardsService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
protected config: BoardsConfig.Config = {};
constructor(@inject(BoardsConfigDialogProps) protected readonly props: BoardsConfigDialogProps) {
super(props);
this.contentNode.classList.add('select-board-dialog');
this.contentNode.appendChild(this.createDescription());
this.appendCloseButton('CANCEL');
this.appendAcceptButton('OK');
}
@postConstruct()
protected init(): void {
this.toDispose.push(this.boardsServiceClient.onBoardsConfigChanged(config => {
this.config = config;
this.update();
}));
}
protected createDescription(): HTMLElement {
const head = document.createElement('div');
head.classList.add('head');
const title = document.createElement('div');
title.textContent = 'Select Other Board & Port';
title.classList.add('title');
head.appendChild(title);
const text = document.createElement('div');
text.classList.add('text');
head.appendChild(text);
for (const paragraph of [
'Select both a Board and a Port if you want to upload a sketch.',
'If you only select a Board you will be able just to compile, but not to upload your sketch.'
]) {
const p = document.createElement('p');
p.textContent = paragraph;
text.appendChild(p);
}
return head;
}
protected onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
this.toDisposeOnDetach.push(this.widget.onBoardConfigChanged(config => {
this.config = config;
this.update();
}));
super.onAfterAttach(msg);
this.update();
}
protected onUpdateRequest(msg: Message) {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected handleEnter(event: KeyboardEvent): boolean | void {
if (event.target instanceof HTMLTextAreaElement) {
return false;
}
}
protected isValid(value: BoardsConfig.Config): DialogError {
if (!value.selectedBoard) {
if (value.selectedPort) {
return 'Please pick a board connected to the port you have selected.';
}
return false;
}
return '';
}
get value(): BoardsConfig.Config {
return this.config;
}
}

View File

@@ -0,0 +1,295 @@
import * as React from 'react';
import { DisposableCollection } from '@theia/core';
import { BoardsService, Board, Port, AttachedSerialBoard, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
export namespace BoardsConfig {
export interface Config {
selectedBoard?: Board;
selectedPort?: Port;
}
export interface Props {
readonly boardsService: BoardsService;
readonly boardsServiceClient: BoardsServiceClientImpl;
readonly onConfigChange: (config: Config) => void;
readonly onFocusNodeSet: (element: HTMLElement | undefined) => void;
}
export interface State extends Config {
searchResults: Array<Board & { packageName: string }>;
knownPorts: Port[];
showAllPorts: boolean;
}
}
export abstract class Item<T> extends React.Component<{
item: T,
label: string,
selected: boolean,
onClick: (item: T) => void,
missing?: boolean,
detail?: string
}> {
render(): React.ReactNode {
const { selected, label, missing, detail } = this.props;
const classNames = ['item'];
if (selected) {
classNames.push('selected');
}
if (missing === true) {
classNames.push('missing')
}
return <div onClick={this.onClick} className={classNames.join(' ')} title={`${label}${!detail ? '' : detail}`}>
<div className='label'>
{label}
</div>
{!detail ? '' : <div className='detail'>{detail}</div>}
{!selected ? '' : <div className='selected-icon'><i className='fa fa-check' /></div>}
</div>;
}
protected onClick = () => {
this.props.onClick(this.props.item);
}
}
export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConfig.State> {
protected toDispose = new DisposableCollection();
constructor(props: BoardsConfig.Props) {
super(props);
const { boardsConfig } = props.boardsServiceClient;
this.state = {
searchResults: [],
knownPorts: [],
showAllPorts: false,
...boardsConfig
}
}
componentDidMount() {
this.updateBoards();
this.props.boardsService.getAvailablePorts().then(({ ports }) => this.updatePorts(ports));
const { boardsServiceClient: client } = this.props;
this.toDispose.pushAll([
client.onBoardsChanged(event => this.updatePorts(event.newState.ports, AttachedBoardsChangeEvent.diff(event).detached.ports)),
client.onBoardsConfigChanged(({ selectedBoard, selectedPort }) => {
this.setState({ selectedBoard, selectedPort }, () => this.fireConfigChanged());
})
]);
}
componentWillUnmount(): void {
this.toDispose.dispose();
}
protected fireConfigChanged() {
const { selectedBoard, selectedPort } = this.state;
this.props.onConfigChange({ selectedBoard, selectedPort });
}
protected updateBoards = (eventOrQuery: React.ChangeEvent<HTMLInputElement> | string = '') => {
const query = (typeof eventOrQuery === 'string'
? eventOrQuery
: eventOrQuery.target.value.toLowerCase()
).trim();
this.queryBoards({ query }).then(({ searchResults }) => this.setState({ searchResults }));
}
protected updatePorts = (ports: Port[] = [], removedPorts: Port[] = []) => {
this.queryPorts(Promise.resolve({ ports })).then(({ knownPorts }) => {
let { selectedPort } = this.state;
// If the currently selected port is not available anymore, unset the selected port.
if (removedPorts.some(port => Port.equals(port, selectedPort))) {
selectedPort = undefined;
}
this.setState({ knownPorts, selectedPort }, () => this.fireConfigChanged());
});
}
protected queryBoards = (options: { query?: string } = {}): Promise<{ searchResults: Array<Board & { packageName: string }> }> => {
const { boardsService } = this.props;
const query = (options.query || '').toLocaleLowerCase();
return new Promise<{ searchResults: Array<Board & { packageName: string }> }>(resolve => {
boardsService.search(options)
.then(({ items }) => items
.map(item => item.boards.map(board => ({ ...board, packageName: item.name })))
.reduce((acc, curr) => acc.concat(curr), [])
.filter(board => board.name.toLocaleLowerCase().indexOf(query) !== -1)
.sort(Board.compare))
.then(searchResults => resolve({ searchResults }));
});
}
protected get attachedBoards(): Promise<{ boards: Board[] }> {
return this.props.boardsService.getAttachedBoards();
}
protected get availablePorts(): Promise<{ ports: Port[] }> {
return this.props.boardsService.getAvailablePorts();
}
protected queryPorts = (availablePorts: Promise<{ ports: Port[] }> = this.availablePorts) => {
return new Promise<{ knownPorts: Port[] }>(resolve => {
availablePorts
.then(({ ports }) => ports
.sort(Port.compare))
.then(knownPorts => resolve({ knownPorts }));
});
}
protected toggleFilterPorts = () => {
this.setState({ showAllPorts: !this.state.showAllPorts });
}
protected selectPort = (selectedPort: Port | undefined) => {
this.setState({ selectedPort }, () => this.fireConfigChanged());
}
protected selectBoard = (selectedBoard: Board & { packageName: string } | undefined) => {
this.setState({ selectedBoard }, () => this.fireConfigChanged());
}
protected focusNodeSet = (element: HTMLElement | null) => {
this.props.onFocusNodeSet(element || undefined);
}
render(): React.ReactNode {
return <div className='body'>
{this.renderContainer('boards', this.renderBoards.bind(this))}
{this.renderContainer('ports', this.renderPorts.bind(this), this.renderPortsFooter.bind(this))}
</div>;
}
protected renderContainer(title: string, contentRenderer: () => React.ReactNode, footerRenderer?: () => React.ReactNode): React.ReactNode {
return <div className='container'>
<div className='content'>
<div className='title'>
{title}
</div>
{contentRenderer()}
<div className='footer'>
{(footerRenderer ? footerRenderer() : '')}
</div>
</div>
</div>;
}
protected renderBoards(): React.ReactNode {
const { selectedBoard, searchResults } = this.state;
// Board names are not unique. We show the corresponding core name as a detail.
// https://github.com/arduino/arduino-cli/pull/294#issuecomment-513764948
const distinctBoardNames = new Map<string, number>();
for (const { name } of searchResults) {
const counter = distinctBoardNames.get(name) || 0;
distinctBoardNames.set(name, counter + 1);
}
// Due to the non-unique board names, we have to check the package name as well.
const selected = (board: Board & { packageName: string }) => {
if (!!selectedBoard) {
if (Board.equals(board, selectedBoard)) {
if ('packageName' in selectedBoard) {
return board.packageName === (selectedBoard as any).packageName;
}
return true;
}
}
return false;
}
return <React.Fragment>
<div className='search'>
<input type='search' placeholder='SEARCH BOARD' onChange={this.updateBoards} ref={this.focusNodeSet} />
<i className='fa fa-search'></i>
</div>
<div className='boards list'>
{this.state.searchResults.map(board => <Item<Board & { packageName: string }>
key={`${board.name}-${board.packageName}`}
item={board}
label={board.name}
detail={(distinctBoardNames.get(board.name) || 0) > 1 ? ` - ${board.packageName}` : undefined}
selected={selected(board)}
onClick={this.selectBoard}
missing={!Board.installed(board)}
/>)}
</div>
</React.Fragment>;
}
protected renderPorts(): React.ReactNode {
const filter = this.state.showAllPorts ? () => true : Port.isBoardPort;
const ports = this.state.knownPorts.filter(filter);
return !ports.length ?
(
<div className='loading noselect'>
No ports discovered
</div>
) :
(
<div className='ports list'>
{ports.map(port => <Item<Port>
key={Port.toString(port)}
item={port}
label={Port.toString(port)}
selected={Port.equals(this.state.selectedPort, port)}
onClick={this.selectPort}
/>)}
</div>
);
}
protected renderPortsFooter(): React.ReactNode {
return <div className='noselect'>
<label
title='Shows all available ports when enabled'>
<input
type='checkbox'
defaultChecked={this.state.showAllPorts}
onChange={this.toggleFilterPorts}
/>
<span>Show all ports</span>
</label>
</div>;
}
}
export namespace BoardsConfig {
export namespace Config {
export function sameAs(config: Config, other: Config | AttachedSerialBoard): boolean {
const { selectedBoard, selectedPort } = config;
if (AttachedSerialBoard.is(other)) {
return !!selectedBoard
&& Board.equals(other, selectedBoard)
&& Port.sameAs(selectedPort, other.port);
}
return sameAs(config, other);
}
export function equals(left: Config, right: Config): boolean {
return left.selectedBoard === right.selectedBoard
&& left.selectedPort === right.selectedPort;
}
export function toString(config: Config, options: { default: string } = { default: '' }): string {
const { selectedBoard, selectedPort: port } = config;
if (!selectedBoard) {
return options.default;
}
const { name } = selectedBoard;
return `${name}${port ? ' at ' + Port.toString(port) : ''}`;
}
}
}

View File

@@ -0,0 +1,27 @@
import { inject, injectable } from 'inversify';
import { BoardPackage, BoardsService } from '../../common/protocol/boards-service';
import { ListWidget } from '../components/component-list/list-widget';
import { ListItemRenderer } from '../components/component-list/list-item-renderer';
@injectable()
export class BoardsListWidget extends ListWidget<BoardPackage> {
static WIDGET_ID = 'boards-list-widget';
static WIDGET_LABEL = 'Boards Manager';
constructor(
@inject(BoardsService) protected service: BoardsService,
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<BoardPackage>) {
super({
id: BoardsListWidget.WIDGET_ID,
label: BoardsListWidget.WIDGET_LABEL,
iconClass: 'fa fa-microchip',
searchable: service,
installable: service,
itemLabel: (item: BoardPackage) => item.name,
itemRenderer
});
}
}

View File

@@ -1,16 +0,0 @@
import { ListWidget } from './list-widget';
export class BoardsListWidget extends ListWidget {
static WIDGET_ID = 'boards-list-widget';
static WIDGET_LABEL = 'Boards Manager';
protected widgetProps(): ListWidget.Props {
return {
id: BoardsListWidget.WIDGET_ID,
title: BoardsListWidget.WIDGET_LABEL,
iconClass: 'fa fa-microchip'
}
}
}

View File

@@ -0,0 +1,178 @@
import { injectable, inject } from 'inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { ILogger } from '@theia/core/lib/common/logger';
import { MessageService } from '@theia/core/lib/common/message-service';
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { RecursiveRequired } from '../../common/types';
import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, AttachedSerialBoard, Board, Port, BoardUninstalledEvent } from '../../common/protocol/boards-service';
import { BoardsConfig } from './boards-config';
@injectable()
export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApplicationContribution {
@inject(ILogger)
protected logger: ILogger;
@inject(MessageService)
protected messageService: MessageService;
@inject(LocalStorageService)
protected storageService: LocalStorageService;
protected readonly onBoardInstalledEmitter = new Emitter<BoardInstalledEvent>();
protected readonly onBoardUninstalledEmitter = new Emitter<BoardUninstalledEvent>();
protected readonly onAttachedBoardsChangedEmitter = new Emitter<AttachedBoardsChangeEvent>();
protected readonly onSelectedBoardsConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
/**
* Used for the auto-reconnecting. Sometimes, the attached board gets disconnected after uploading something to it.
* It happens with certain boards on Windows. For example, the `MKR1000` boards is selected on post `COM5` on Windows,
* perform an upload, the board automatically disconnects and reconnects, but on another port, `COM10`.
* We have to listen on such changes and auto-reconnect the same board on another port.
* See: https://arduino.slack.com/archives/CJJHJCJSJ/p1568645417013000?thread_ts=1568640504.009400&cid=CJJHJCJSJ
*/
protected latestValidBoardsConfig: RecursiveRequired<BoardsConfig.Config> | undefined = undefined;
protected _boardsConfig: BoardsConfig.Config = {};
readonly onBoardsChanged = this.onAttachedBoardsChangedEmitter.event;
readonly onBoardInstalled = this.onBoardInstalledEmitter.event;
readonly onBoardUninstalled = this.onBoardUninstalledEmitter.event;
readonly onBoardsConfigChanged = this.onSelectedBoardsConfigChangedEmitter.event;
async onStart(): Promise<void> {
return this.loadState();
}
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
this.logger.info('Attached boards and available ports changed: ', JSON.stringify(event));
const { detached, attached } = AttachedBoardsChangeEvent.diff(event);
const { selectedPort, selectedBoard } = this.boardsConfig;
this.onAttachedBoardsChangedEmitter.fire(event);
// Dynamically unset the port if is not available anymore. A port can be "detached" when removing a board.
if (detached.ports.some(port => Port.equals(selectedPort, port))) {
this.boardsConfig = {
selectedBoard,
selectedPort: undefined
};
}
// Try to reconnect.
this.tryReconnect(attached.boards, attached.ports);
}
async tryReconnect(attachedBoards: Board[], availablePorts: Port[]): Promise<boolean> {
if (this.latestValidBoardsConfig && !this.canUploadTo(this.boardsConfig)) {
for (const board of attachedBoards.filter(AttachedSerialBoard.is)) {
if (this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn
&& this.latestValidBoardsConfig.selectedBoard.name === board.name
&& Port.sameAs(this.latestValidBoardsConfig.selectedPort, board.port)) {
this.boardsConfig = this.latestValidBoardsConfig;
return true;
}
}
// If we could not find an exact match, we compare the board FQBN-name pairs and ignore the port, as it might have changed.
// See documentation on `latestValidBoardsConfig`.
for (const board of attachedBoards.filter(AttachedSerialBoard.is)) {
if (this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn
&& this.latestValidBoardsConfig.selectedBoard.name === board.name) {
this.boardsConfig = {
...this.latestValidBoardsConfig,
selectedPort: availablePorts.find(port => Port.sameAs(port, board.port))
};
return true;
}
}
}
return false;
}
notifyBoardInstalled(event: BoardInstalledEvent): void {
this.logger.info('Board installed: ', JSON.stringify(event));
this.onBoardInstalledEmitter.fire(event);
}
notifyBoardUninstalled(event: BoardUninstalledEvent): void {
this.logger.info('Board uninstalled: ', JSON.stringify(event));
this.onBoardUninstalledEmitter.fire(event);
}
set boardsConfig(config: BoardsConfig.Config) {
this.logger.info('Board config changed: ', JSON.stringify(config));
this._boardsConfig = config;
if (this.canUploadTo(this._boardsConfig)) {
this.latestValidBoardsConfig = this._boardsConfig;
}
this.saveState().then(() => this.onSelectedBoardsConfigChangedEmitter.fire(this._boardsConfig));
}
get boardsConfig(): BoardsConfig.Config {
return this._boardsConfig;
}
/**
* `true` if the `config.selectedBoard` is defined; hence can compile against the board. Otherwise, `false`.
*/
canVerify(
config: BoardsConfig.Config | undefined = this.boardsConfig,
options: { silent: boolean } = { silent: true }): config is BoardsConfig.Config & { selectedBoard: Board } {
if (!config) {
return false;
}
if (!config.selectedBoard) {
if (!options.silent) {
this.messageService.warn('No boards selected.', { timeout: 3000 });
}
return false;
}
return true;
}
/**
* `true` if the `canVerify` and the `config.selectedPort` is also set with FQBN, hence can upload to board. Otherwise, `false`.
*/
canUploadTo(
config: BoardsConfig.Config | undefined = this.boardsConfig,
options: { silent: boolean } = { silent: true }): config is RecursiveRequired<BoardsConfig.Config> {
if (!this.canVerify(config, options)) {
return false;
}
const { name } = config.selectedBoard;
if (!config.selectedPort) {
if (!options.silent) {
this.messageService.warn(`No ports selected for board: '${name}'.`, { timeout: 3000 });
}
return false;
}
if (!config.selectedBoard.fqbn) {
if (!options.silent) {
this.messageService.warn(`The FQBN is not available for the selected board ${name}. Do you have the corresponding core installed?`, { timeout: 3000 });
}
return false;
}
return true;
}
protected saveState(): Promise<void> {
return this.storageService.setData('latest-valid-boards-config', this.latestValidBoardsConfig);
}
protected async loadState(): Promise<void> {
const storedValidBoardsConfig = await this.storageService.getData<RecursiveRequired<BoardsConfig.Config>>('latest-valid-boards-config');
if (storedValidBoardsConfig) {
this.latestValidBoardsConfig = storedValidBoardsConfig;
if (this.canUploadTo(this.latestValidBoardsConfig)) {
this.boardsConfig = this.latestValidBoardsConfig;
}
}
}
}

View File

@@ -1,61 +1,42 @@
import * as React from 'react';
import { BoardsService, Board, AttachedSerialBoard } from '../../common/protocol/boards-service';
import { ContextMenuRenderer, StatusBar, StatusBarAlignment } from '@theia/core/lib/browser';
import { BoardsNotificationService } from '../boards-notification-service';
import { Command, CommandRegistry } from '@theia/core';
import * as ReactDOM from 'react-dom';
import { CommandRegistry, DisposableCollection } from '@theia/core';
import { BoardsService, Board, AttachedSerialBoard, Port } from '../../common/protocol/boards-service';
import { ArduinoCommands } from '../arduino-commands';
import ReactDOM = require('react-dom');
import { BoardsServiceClientImpl } from './boards-service-client-impl';
import { BoardsConfig } from './boards-config';
export interface BoardsDropdownItem {
label: string;
commandExecutor: () => void;
isSelected: () => boolean;
}
export interface BoardsDropDownListCoord {
top: number;
left: number;
width: number;
paddingTop: number;
}
export namespace BoardsDropdownItemComponent {
export interface Props {
label: string;
onClick: () => void;
isSelected: boolean;
}
}
export class BoardsDropdownItemComponent extends React.Component<BoardsDropdownItemComponent.Props> {
render() {
return <div className={`arduino-boards-dropdown-item ${this.props.isSelected ? 'selected' : ''}`} onClick={this.props.onClick}>
<div>{this.props.label}</div>
{this.props.isSelected ? <span className='fa fa-check'></span> : ''}
</div>;
}
export interface BoardsDropDownListCoords {
readonly top: number;
readonly left: number;
readonly width: number;
readonly paddingTop: number;
}
export namespace BoardsDropDown {
export interface Props {
readonly coords: BoardsDropDownListCoord;
readonly isOpen: boolean;
readonly dropDownItems: BoardsDropdownItem[];
readonly openDialog: () => void;
readonly coords: BoardsDropDownListCoords | 'hidden';
readonly items: Item[];
readonly openBoardsConfig: () => void;
}
export interface Item {
readonly label: string;
readonly selected: boolean;
readonly onClick: () => void;
}
}
export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
protected dropdownId: string = 'boards-dropdown-container';
protected dropdownElement: HTMLElement;
constructor(props: BoardsDropDown.Props) {
super(props);
let list = document.getElementById(this.dropdownId);
let list = document.getElementById('boards-dropdown-container');
if (!list) {
list = document.createElement('div');
list.id = this.dropdownId;
list.id = 'boards-dropdown-container';
document.body.appendChild(list);
this.dropdownElement = list;
}
@@ -65,179 +46,157 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
return ReactDOM.createPortal(this.renderNode(), this.dropdownElement);
}
renderNode(): React.ReactNode {
if (this.props.isOpen) {
return <div className='arduino-boards-dropdown-list'
style={{
position: 'absolute',
top: this.props.coords.top,
left: this.props.coords.left,
width: this.props.coords.width,
paddingTop: this.props.coords.paddingTop
}}>
{
this.props.dropDownItems.map(item => {
return <React.Fragment key={item.label}>
<BoardsDropdownItemComponent isSelected={item.isSelected()} label={item.label} onClick={item.commandExecutor}></BoardsDropdownItemComponent>
</React.Fragment>;
})
}
<BoardsDropdownItemComponent isSelected={false} label={'Select Other Board & Port'} onClick={this.props.openDialog}></BoardsDropdownItemComponent>
</div>
} else {
protected renderNode(): React.ReactNode {
const { coords, items } = this.props;
if (coords === 'hidden') {
return '';
}
items.push({
label: 'Select Other Board & Port',
selected: false,
onClick: () => this.props.openBoardsConfig()
})
return <div className='arduino-boards-dropdown-list'
style={{
position: 'absolute',
...coords
}}>
{items.map(this.renderItem)}
</div>
}
protected renderItem(item: BoardsDropDown.Item): React.ReactNode {
const { label, selected, onClick } = item;
return <div key={label} className={`arduino-boards-dropdown-item ${selected ? 'selected' : ''}`} onClick={onClick}>
<div>
{label}
</div>
{selected ? <span className='fa fa-check'/> : ''}
</div>
}
}
export namespace BoardsToolBarItem {
export interface Props {
readonly contextMenuRenderer: ContextMenuRenderer;
readonly boardsNotificationService: BoardsNotificationService;
readonly boardService: BoardsService;
readonly boardsServiceClient: BoardsServiceClientImpl;
readonly commands: CommandRegistry;
readonly statusBar: StatusBar;
}
export interface State {
selectedBoard?: Board;
selectedIsAttached: boolean;
boardItems: BoardsDropdownItem[];
isOpen: boolean;
boardsConfig: BoardsConfig.Config;
attachedBoards: Board[];
availablePorts: Port[];
coords: BoardsDropDownListCoords | 'hidden';
}
}
export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props, BoardsToolBarItem.State> {
protected attachedBoards: Board[];
protected dropDownListCoord: BoardsDropDownListCoord;
static TOOLBAR_ID: 'boards-toolbar';
protected readonly toDispose: DisposableCollection = new DisposableCollection();
constructor(props: BoardsToolBarItem.Props) {
super(props);
this.state = {
selectedBoard: undefined,
selectedIsAttached: true,
boardItems: [],
isOpen: false
boardsConfig: this.props.boardsServiceClient.boardsConfig,
attachedBoards: [],
availablePorts: [],
coords: 'hidden'
};
document.addEventListener('click', () => {
this.setState({ isOpen: false });
this.setState({ coords: 'hidden' });
});
}
componentDidMount() {
this.setAttachedBoards();
}
setSelectedBoard(board: Board) {
if (this.attachedBoards && this.attachedBoards.length) {
this.setState({ selectedIsAttached: !!this.attachedBoards.find(attachedBoard => attachedBoard.name === board.name) });
}
this.setState({ selectedBoard: board });
}
protected async setAttachedBoards() {
const { boards } = await this.props.boardService.getAttachedBoards();
this.attachedBoards = boards;
if (this.attachedBoards.length) {
await this.createBoardDropdownItems();
await this.props.boardService.selectBoard(this.attachedBoards[0]);
this.setSelectedBoard(this.attachedBoards[0]);
}
}
protected createBoardDropdownItems() {
const boardItems: BoardsDropdownItem[] = [];
this.attachedBoards.forEach(board => {
const { commands } = this.props;
const port = this.getPort(board);
const command: Command = {
id: 'selectBoard' + port
}
commands.registerCommand(command, {
execute: () => {
commands.executeCommand(ArduinoCommands.SELECT_BOARD.id, board);
this.setState({ isOpen: false, selectedBoard: board });
}
});
boardItems.push({
commandExecutor: () => commands.executeCommand(command.id),
label: board.name + ' at ' + port,
isSelected: () => this.doIsSelectedBoard(board)
});
const { boardsServiceClient: client, boardService } = this.props;
this.toDispose.pushAll([
client.onBoardsConfigChanged(boardsConfig => this.setState({ boardsConfig })),
client.onBoardsChanged(({ newState }) => this.setState({ attachedBoards: newState.boards, availablePorts: newState.ports }))
]);
Promise.all([
boardService.getAttachedBoards(),
boardService.getAvailablePorts()
]).then(([{boards: attachedBoards}, { ports: availablePorts }]) => {
this.setState({ attachedBoards, availablePorts })
});
this.setState({ boardItems });
}
protected doIsSelectedBoard = (board: Board) => this.isSelectedBoard(board);
protected isSelectedBoard(board: Board): boolean {
return AttachedSerialBoard.is(board) &&
!!this.state.selectedBoard &&
AttachedSerialBoard.is(this.state.selectedBoard) &&
board.port === this.state.selectedBoard.port &&
board.fqbn === this.state.selectedBoard.fqbn;
componentWillUnmount(): void {
this.toDispose.dispose();
}
protected getPort(board: Board): string {
if (AttachedSerialBoard.is(board)) {
return board.port;
protected readonly show = (event: React.MouseEvent<HTMLElement>) => {
const { currentTarget: element } = event;
if (element instanceof HTMLElement) {
if (this.state.coords === 'hidden') {
const rect = element.getBoundingClientRect();
this.setState({
coords: {
top: rect.top,
left: rect.left,
width: rect.width,
paddingTop: rect.height
}
});
} else {
this.setState({ coords: 'hidden'});
}
}
return '';
}
protected readonly doShowSelectBoardsMenu = (event: React.MouseEvent<HTMLElement>) => {
this.showSelectBoardsMenu(event);
event.stopPropagation();
event.nativeEvent.stopImmediatePropagation();
};
protected showSelectBoardsMenu(event: React.MouseEvent<HTMLElement>) {
const el = (event.currentTarget as HTMLElement);
if (el) {
this.dropDownListCoord = {
top: el.getBoundingClientRect().top,
left: el.getBoundingClientRect().left,
paddingTop: el.getBoundingClientRect().height,
width: el.getBoundingClientRect().width
}
this.setState({ isOpen: !this.state.isOpen });
}
}
render(): React.ReactNode {
const selectedBoard = this.state.selectedBoard;
const port = selectedBoard ? this.getPort(selectedBoard) : undefined;
const boardTxt = selectedBoard && `${selectedBoard.name}${port ? ' at ' + port : ''}` || '';
this.props.statusBar.setElement('arduino-selected-board', {
alignment: StatusBarAlignment.RIGHT,
text: boardTxt
});
const { boardsConfig, coords, attachedBoards, availablePorts } = this.state;
const title = BoardsConfig.Config.toString(boardsConfig, { default: 'no board selected' });
const configuredBoard = attachedBoards
.filter(AttachedSerialBoard.is)
.filter(board => availablePorts.some(port => Port.sameAs(port, board.port)))
.filter(board => BoardsConfig.Config.sameAs(boardsConfig, board)).shift();
const items = attachedBoards.filter(AttachedSerialBoard.is).map(board => ({
label: `${board.name} at ${board.port}`,
selected: configuredBoard === board,
onClick: () => {
this.props.boardsServiceClient.boardsConfig = {
selectedBoard: board,
selectedPort: availablePorts.find(port => Port.sameAs(port, board.port))
}
}
}));
return <React.Fragment>
<div className='arduino-boards-toolbar-item-container'>
<div className='arduino-boards-toolbar-item' title={boardTxt}>
<div className='inner-container' onClick={this.doShowSelectBoardsMenu}>
<span className={!selectedBoard || !this.state.selectedIsAttached ? 'fa fa-times notAttached' : ''}></span>
<div className='arduino-boards-toolbar-item' title={title}>
<div className='inner-container' onClick={this.show}>
<span className={!configuredBoard ? 'fa fa-times notAttached' : ''}/>
<div className='label noWrapInfo'>
<div className='noWrapInfo noselect'>
{selectedBoard ? boardTxt : 'no board selected'}
{title}
</div>
</div>
<span className='fa fa-caret-down caret'></span>
<span className='fa fa-caret-down caret'/>
</div>
</div>
</div>
<BoardsDropDown
isOpen={this.state.isOpen}
coords={this.dropDownListCoord}
dropDownItems={this.state.boardItems}
openDialog={this.openDialog}>
coords={coords}
items={items}
openBoardsConfig={this.openDialog}>
</BoardsDropDown>
</React.Fragment>;
}
protected openDialog = () => {
this.props.commands.executeCommand(ArduinoCommands.OPEN_BOARDS_DIALOG.id);
this.setState({ isOpen: false });
this.setState({ coords: 'hidden' });
};
}
}

View File

@@ -1,22 +1,12 @@
import { injectable } from 'inversify';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { ListWidget } from './list-widget';
import { BoardsListWidget } from './boards-list-widget';
import { MenuModelRegistry } from '@theia/core';
import { BoardsListWidget } from './boards-list-widget';
import { ArduinoMenus } from '../arduino-frontend-contribution';
import { BoardPackage } from '../../common/protocol/boards-service';
import { ListWidgetFrontendContribution } from '../components/component-list/list-widget-frontend-contribution';
@injectable()
export abstract class ListWidgetFrontendContribution extends AbstractViewContribution<ListWidget> implements FrontendApplicationContribution {
async initializeLayout(): Promise<void> {
// await this.openView();
}
}
@injectable()
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution {
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardPackage> {
static readonly OPEN_MANAGER = `${BoardsListWidget.WIDGET_ID}:toggle`;

View File

@@ -1,88 +0,0 @@
import * as React from 'react';
import { inject, injectable, postConstruct } from 'inversify';
import { Message } from '@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { FilterableListContainer } from '../components/component-list/filterable-list-container';
import { BoardsService, Board, BoardPackage } from '../../common/protocol/boards-service';
import { BoardsNotificationService } from '../boards-notification-service';
import { LibraryService } from '../../common/protocol/library-service';
@injectable()
export abstract class ListWidget extends ReactWidget {
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(WindowService)
protected readonly windowService: WindowService;
@inject(BoardsNotificationService)
protected readonly boardsNotificationService: BoardsNotificationService;
constructor() {
super();
const { id, title, iconClass } = this.widgetProps();
this.id = id;
this.title.label = title;
this.title.caption = title;
this.title.iconClass = iconClass;
this.title.closable = true;
this.addClass(ListWidget.Styles.LIST_WIDGET_CLASS);
this.node.tabIndex = 0; // To be able to set the focus on the widget.
}
protected abstract widgetProps(): ListWidget.Props;
@postConstruct()
protected init(): void {
this.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.node.focus();
this.render();
}
protected onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.render();
}
render(): React.ReactNode {
const boardsServiceDelegate = this.boardsService;
const boardsService: BoardsService = {
getAttachedBoards: () => boardsServiceDelegate.getAttachedBoards(),
selectBoard: (board: Board) => boardsServiceDelegate.selectBoard(board),
getSelectBoard: () => boardsServiceDelegate.getSelectBoard(),
search: (options: { query?: string, props?: LibraryService.Search.Props }) => boardsServiceDelegate.search(options),
install: async (item: BoardPackage) => {
await boardsServiceDelegate.install(item);
this.boardsNotificationService.notifyBoardsInstalled();
}
}
return <FilterableListContainer
service={boardsService}
windowService={this.windowService}
/>;
}
}
export namespace ListWidget {
/**
* Props for customizing the abstract list widget.
*/
export interface Props {
readonly id: string;
readonly title: string;
readonly iconClass: string;
}
export namespace Styles {
export const LIST_WIDGET_CLASS = 'arduino-list-widget'
}
}

View File

@@ -1,305 +0,0 @@
import * as React from 'react';
import { ReactWidget } from '@theia/core/lib/browser';
import { injectable, inject } from 'inversify';
import { BoardsService, Board, BoardPackage, AttachedSerialBoard } from '../../common/protocol/boards-service';
import { BoardsNotificationService } from '../boards-notification-service';
import { Emitter, Event } from '@theia/core';
export interface BoardAndPortSelection {
board?: Board;
port?: string;
}
export namespace BoardAndPortSelectableItem {
export interface Props {
item: BoardAndPortSelection,
selected: boolean,
onSelect: (selection: BoardAndPortSelection) => void
}
}
export class BoardAndPortSelectableItem extends React.Component<BoardAndPortSelectableItem.Props> {
render(): React.ReactNode {
if (this.props.item.board || this.props.item.port) {
return <div onClick={this.select} className={`item ${this.props.selected ? 'selected' : ''}`}>
{this.props.item.board ? this.props.item.board.name : this.props.item.port}
{this.props.selected ? <i className='fa fa-check'></i> : ''}
</div>;
}
}
protected readonly select = (() => {
this.props.onSelect({ board: this.props.item.board, port: this.props.item.port })
}).bind(this);
}
export namespace BoardAndPortSelectionList {
export interface Props {
type: 'boards' | 'ports';
list: BoardAndPortSelection[];
onSelect: (selection: BoardAndPortSelection) => void;
}
export interface State {
selection: BoardAndPortSelection
}
}
export class BoardAndPortSelectionList extends React.Component<BoardAndPortSelectionList.Props, BoardAndPortSelectionList.State> {
constructor(props: BoardAndPortSelectionList.Props) {
super(props);
this.state = {
selection: {}
}
}
reset(): void {
this.setState({ selection: {} });
}
render(): React.ReactNode {
return <div className={`${this.props.type} list`}>
{this.props.list.map(item => <BoardAndPortSelectableItem
key={item.board ? item.board.name : item.port}
onSelect={this.doSelect}
item={item}
selected={this.isSelectedItem(item)}
/>)}
</div>
}
protected readonly doSelect = (boardAndPortSelection: BoardAndPortSelection) => {
this.setState({ selection: boardAndPortSelection });
this.props.onSelect(boardAndPortSelection);
}
protected readonly isSelectedItem = ((item: BoardAndPortSelection) => {
if (this.state.selection.board) {
return (this.state.selection.board === item.board);
} else if (this.state.selection.port) {
return (this.state.selection.port === item.port);
}
return false;
});
protected readonly isSelectedPort = ((port: string) => {
return (this.state.selection.port && this.state.selection.port === port) || false;
});
}
export namespace BoardAndPortSelectionComponent {
export interface Props {
boardsService: BoardsService;
onSelect: (selection: BoardAndPortSelection) => void;
}
export interface State {
boards: Board[];
ports: string[];
selection: BoardAndPortSelection;
}
}
export class BoardAndPortSelectionComponent extends React.Component<BoardAndPortSelectionComponent.Props, BoardAndPortSelectionComponent.State> {
protected allBoards: Board[] = [];
protected boardListComponent: BoardAndPortSelectionList | null;
protected portListComponent: BoardAndPortSelectionList | null;
constructor(props: BoardAndPortSelectionComponent.Props) {
super(props);
this.state = {
boards: [],
ports: [],
selection: {}
}
}
componentDidMount() {
this.searchAvailableBoards();
this.setPorts();
}
reset(): void {
if (this.boardListComponent) {
this.boardListComponent.reset();
}
if (this.portListComponent) {
this.portListComponent.reset();
}
this.setState({ selection: {} });
}
render(): React.ReactNode {
return <React.Fragment>
<div className='body'>
<div className='left container'>
<div className='content'>
<div className='title'>
BOARDS
</div>
<div className='search'>
<input type='search' placeholder='SEARCH BOARD' onChange={this.doFilter} />
<i className='fa fa-search'></i>
</div>
<BoardAndPortSelectionList
ref={ref => { this.boardListComponent = ref }}
type='boards'
onSelect={this.doSelect}
list={this.state.boards.map<BoardAndPortSelection>(board => ({ board }))} />
</div>
</div>
<div className='right container'>
<div className='content'>
<div className='title'>
PORTS
</div>
{
this.state.ports.length ?
<BoardAndPortSelectionList
ref={ref => { this.portListComponent = ref }}
type='ports'
onSelect={this.doSelect}
list={this.state.ports.map<BoardAndPortSelection>(port => ({ port }))} /> : 'loading ports...'
}
</div>
</div>
</div>
</React.Fragment>
}
protected sort(items: Board[]): Board[] {
return items.sort((a, b) => {
if (a.name < b.name) {
return -1;
} else if (a.name === b.name) {
return 0;
} else {
return 1;
}
});
}
protected readonly doSelect = (boardAndPortSelection: BoardAndPortSelection) => {
const selection = this.state.selection;
if (boardAndPortSelection.board) {
selection.board = boardAndPortSelection.board;
}
if (boardAndPortSelection.port) {
selection.port = boardAndPortSelection.port;
}
this.setState({ selection });
this.props.onSelect(this.state.selection);
}
protected readonly doFilter = (event: React.ChangeEvent<HTMLInputElement>) => {
const boards = this.allBoards.filter(board => board.name.toLowerCase().indexOf(event.target.value.toLowerCase()) >= 0);
this.setState({ boards })
}
protected async searchAvailableBoards() {
const boardPkg = await this.props.boardsService.search({});
const boards = [].concat.apply([], boardPkg.items.map<Board[]>(item => item.boards)) as Board[];
this.allBoards = this.sort(boards);
this.setState({ boards: this.allBoards });
}
protected async setPorts() {
const ports: string[] = [];
const { boards } = await this.props.boardsService.getAttachedBoards();
boards.forEach(board => {
if (AttachedSerialBoard.is(board)) {
ports.push(board.port);
}
});
this.setState({ ports });
}
}
@injectable()
export class SelectBoardDialogWidget extends ReactWidget {
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsNotificationService)
protected readonly boardsNotificationService: BoardsNotificationService;
protected readonly onChangedEmitter = new Emitter<BoardAndPortSelection>();
protected boardAndPortSelectionComponent: BoardAndPortSelectionComponent | null;
protected attachedBoards: Promise<{ boards: Board[] }>;
boardAndPort: BoardAndPortSelection = {};
constructor() {
super();
this.id = 'select-board-dialog';
this.toDispose.push(this.onChangedEmitter);
}
get onChanged(): Event<BoardAndPortSelection> {
return this.onChangedEmitter.event;
}
reset(): void {
if (this.boardAndPortSelectionComponent) {
this.boardAndPortSelectionComponent.reset();
}
this.boardAndPort = {};
}
setAttachedBoards(attachedBoards: Promise<{ boards: Board[] }>): void {
this.attachedBoards = attachedBoards;
}
protected fireChanged(boardAndPort: BoardAndPortSelection): void {
this.onChangedEmitter.fire(boardAndPort);
}
protected render(): React.ReactNode {
let content: React.ReactNode;
const boardsServiceDelegate = this.boardsService;
const attachedBoards = this.attachedBoards;
const boardsService: BoardsService = {
getAttachedBoards: () => attachedBoards,
selectBoard: (board: Board) => boardsServiceDelegate.selectBoard(board),
getSelectBoard: () => boardsServiceDelegate.getSelectBoard(),
search: (options: { query?: string }) => boardsServiceDelegate.search(options),
install: async (item: BoardPackage) => {
await boardsServiceDelegate.install(item);
this.boardsNotificationService.notifyBoardsInstalled();
}
}
content = <React.Fragment>
<div className='selectBoardContainer'>
<div className='head'>
<div className='title'>
Select Other Board &amp; Port
</div>
<div className='text'>
<p>Select both a BOARD and a PORT if you want to upload a sketch.</p>
<p>If you only select a BOARD you will be able just to compile,</p>
<p>but not to upload your sketch.</p>
</div>
</div>
<BoardAndPortSelectionComponent
ref={ref => this.boardAndPortSelectionComponent = ref}
boardsService={boardsService}
onSelect={this.onSelect} />
</div>
</React.Fragment>
return content;
}
protected readonly onSelect = (selection: BoardAndPortSelection) => { this.doOnSelect(selection) };
protected doOnSelect(selection: BoardAndPortSelection) {
this.boardAndPort = selection;
this.fireChanged(this.boardAndPort);
}
}

View File

@@ -1,113 +0,0 @@
import { AbstractDialog, DialogProps, Widget, Panel, DialogError } from '@theia/core/lib/browser';
import { injectable, inject } from 'inversify';
import { SelectBoardDialogWidget, BoardAndPortSelection } from './select-board-dialog-widget';
import { Message } from '@phosphor/messaging';
import { Disposable } from '@theia/core';
import { Board, BoardsService, AttachedSerialBoard } from '../../common/protocol/boards-service';
@injectable()
export class SelectBoardDialogProps extends DialogProps {
}
@injectable()
export class SelectBoardDialog extends AbstractDialog<BoardAndPortSelection> {
protected readonly dialogPanel: Panel;
protected attachedBoards: Board[];
constructor(
@inject(SelectBoardDialogProps) protected readonly props: SelectBoardDialogProps,
@inject(SelectBoardDialogWidget) protected readonly widget: SelectBoardDialogWidget,
@inject(BoardsService) protected readonly boardService: BoardsService
) {
super({ title: props.title });
this.dialogPanel = new Panel();
this.dialogPanel.addWidget(this.widget);
this.contentNode.classList.add('select-board-dialog');
this.toDispose.push(this.widget.onChanged(() => this.update()));
this.toDispose.push(this.dialogPanel);
this.attachedBoards = [];
this.init();
this.appendCloseButton('CANCEL');
this.appendAcceptButton('OK');
}
protected init() {
const boards = this.boardService.getAttachedBoards();
boards.then(b => this.attachedBoards = b.boards);
this.widget.setAttachedBoards(boards);
}
protected onAfterAttach(msg: Message): void {
Widget.attach(this.dialogPanel, this.contentNode);
this.toDisposeOnDetach.push(Disposable.create(() => {
Widget.detach(this.dialogPanel);
}))
super.onAfterAttach(msg);
this.update();
}
protected onUpdateRequest(msg: Message) {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
this.widget.activate();
}
protected handleEnter(event: KeyboardEvent): boolean | void {
if (event.target instanceof HTMLTextAreaElement) {
return false;
}
}
protected isValid(value: BoardAndPortSelection): DialogError {
if (!value.board) {
if (value.port) {
return 'Please pick the Board connected to the Port you have selected';
}
return false;
}
return '';
}
get value(): BoardAndPortSelection {
const boardAndPortSelection = this.widget.boardAndPort;
if (this.attachedBoards.length) {
boardAndPortSelection.board = this.attachedBoards.find(b => {
const isAttachedBoard = !!boardAndPortSelection.board &&
b.name === boardAndPortSelection.board.name &&
b.fqbn === boardAndPortSelection.board.fqbn;
if (boardAndPortSelection.port) {
return isAttachedBoard &&
AttachedSerialBoard.is(b) &&
b.port === boardAndPortSelection.port;
} else {
return isAttachedBoard;
}
})
|| boardAndPortSelection.board;
}
return boardAndPortSelection;
}
close(): void {
this.widget.reset();
super.close();
}
onAfterDetach(msg: Message) {
this.widget.reset();
super.onAfterDetach(msg);
}
}

View File

@@ -0,0 +1,62 @@
import * as React from 'react';
import Select from 'react-select';
import { Styles } from 'react-select/src/styles';
import { Props } from 'react-select/src/components';
import { ThemeConfig } from 'react-select/src/theme';
export class ArduinoSelect<T> extends Select<T> {
constructor(props: Readonly<Props<T>>) {
super(props);
}
render(): React.ReactNode {
const controlHeight = 27; // from `monitor.css` -> `.serial-monitor-container .head` (`height: 27px;`)
const styles: Styles = {
control: styles => ({
...styles,
minWidth: 120,
color: 'var(--theia-ui-font-color1)'
}),
dropdownIndicator: styles => ({
...styles,
padding: 0
}),
indicatorSeparator: () => ({
display: 'none'
}),
indicatorsContainer: () => ({
padding: '0px 5px'
}),
menu: styles => ({
...styles,
marginTop: 0
})
};
const theme: ThemeConfig = theme => ({
...theme,
borderRadius: 0,
spacing: {
controlHeight,
baseUnit: 2,
menuGutter: 4
}, colors: {
...theme.colors,
// `primary50`??? it's crazy but apparently, without this, we would get a light-blueish
// color when selecting an option in the select by clicking and then not releasing the button.
// https://react-select.com/styles#overriding-the-theme
primary50: 'var(--theia-accent-color4)',
}
});
const DropdownIndicator = () => <span className='fa fa-caret-down caret' />;
return <Select
{...this.props}
components={{ DropdownIndicator }}
theme={theme}
styles={styles}
classNamePrefix='arduino-select'
isSearchable={false}
/>
}
}

View File

@@ -1,78 +1,66 @@
import * as React from 'react';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { Installable } from '../../../common/protocol/installable';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { ListItemRenderer } from './list-item-renderer';
export class ComponentListItem extends React.Component<ComponentListItem.Props> {
export class ComponentListItem<T extends ArduinoComponent> extends React.Component<ComponentListItem.Props<T>, ComponentListItem.State> {
protected onClick = (event: React.SyntheticEvent<HTMLAnchorElement, Event>) => {
const { target } = event.nativeEvent;
if (target instanceof HTMLAnchorElement) {
this.props.windowService.openNewWindow(target.href);
event.nativeEvent.preventDefault();
constructor(props: ComponentListItem.Props<T>) {
super(props);
if (props.item.installable) {
const version = props.item.availableVersions.filter(version => version !== props.item.installedVersion)[0];
this.state = {
selectedVersion: version
};
}
}
protected async install(item: ArduinoComponent): Promise<void> {
await this.props.install(item);
protected async install(item: T): Promise<void> {
const toInstall = this.state.selectedVersion;
const version = this.props.item.availableVersions.filter(version => version !== this.state.selectedVersion)[0];
this.setState({
selectedVersion: version
});
try {
await this.props.install(item, toInstall);
} catch {
this.setState({
selectedVersion: toInstall
});
}
}
protected async uninstall(item: T): Promise<void> {
await this.props.uninstall(item);
}
protected onVersionChange(version: Installable.Version) {
this.setState({ selectedVersion: version });
}
render(): React.ReactNode {
const { item } = this.props;
const style = ComponentListItem.Styles;
const name = <span className={style.NAME_CLASS}>{item.name}</span>;
const author = <span className={style.AUTHOR_CLASS}>{item.author}</span>;
const installedVersion = !!item.installedVersion && <div className={style.VERSION_INFO_CLASS}>
<span className={style.VERSION_CLASS}>Version {item.installedVersion}</span>
<span className={style.INSTALLED_CLASS}>INSTALLED</span>
</div>;
const summary = <div className={style.SUMMARY_CLASS}>{item.summary}</div>;
const moreInfo = !!item.moreInfoLink && <a href={item.moreInfoLink} onClick={this.onClick}>More info</a>;
const install = this.props.install && item.installable && !item.installedVersion &&
<button className={style.INSTALL_BTN_CLASS} onClick={this.install.bind(this, item)}>INSTALL</button>;
return <div className={[style.LIST_ITEM_CLASS, style.NO_SELECT_CLASS].join(' ')}>
<div className={style.HEADER_CLASS}>
<span>{name} by {author}</span>
{installedVersion}
</div>
<div className={style.CONTENT_CLASS}>
{summary}
</div>
<div className={style.FOOTER_CLASS}>
{moreInfo}
{install}
</div>
</div>;
const { item, itemRenderer } = this.props;
return itemRenderer.renderItem(
Object.assign(this.state, { item }),
this.install.bind(this),
this.uninstall.bind(this),
this.onVersionChange.bind(this)
);
}
}
export namespace ComponentListItem {
export interface Props {
readonly item: ArduinoComponent;
readonly windowService: WindowService;
readonly install: (comp: ArduinoComponent) => Promise<void>;
export interface Props<T extends ArduinoComponent> {
readonly item: T;
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
readonly uninstall: (item: T) => Promise<void>;
readonly itemRenderer: ListItemRenderer<T>;
}
export namespace Styles {
export const LIST_ITEM_CLASS = 'component-list-item';
export const HEADER_CLASS = 'header';
export const VERSION_INFO_CLASS = 'version-info';
export const CONTENT_CLASS = 'content';
export const FOOTER_CLASS = 'footer';
export const INSTALLED_CLASS = 'installed';
export const NO_SELECT_CLASS = 'noselect';
export const NAME_CLASS = 'name';
export const AUTHOR_CLASS = 'author';
export const VERSION_CLASS = 'version';
export const SUMMARY_CLASS = 'summary';
export const DESCRIPTION_CLASS = 'description';
export const INSTALL_BTN_CLASS = 'install';
export interface State {
selectedVersion?: Installable.Version;
}
}

View File

@@ -1,16 +1,17 @@
import * as React from 'react';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { ComponentListItem } from './component-list-item';
import { Installable } from '../../../common/protocol/installable';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { ComponentListItem } from './component-list-item';
import { ListItemRenderer } from './list-item-renderer';
export class ComponentList extends React.Component<ComponentList.Props> {
export class ComponentList<T extends ArduinoComponent> extends React.Component<ComponentList.Props<T>> {
protected container?: HTMLElement;
render(): React.ReactNode {
return <div
className={'items-container'}
ref={element => this.container = element || undefined}>
ref={this.setRef}>
{this.props.items.map(item => this.createItem(item))}
</div>;
}
@@ -21,19 +22,30 @@ export class ComponentList extends React.Component<ComponentList.Props> {
}
}
protected createItem(item: ArduinoComponent): React.ReactNode {
return <ComponentListItem key={item.name} item={item} windowService={this.props.windowService} install={this.props.install} />
protected setRef = (element: HTMLElement | null) => {
this.container = element || undefined;
}
protected createItem(item: T): React.ReactNode {
return <ComponentListItem<T>
key={this.props.itemLabel(item)}
item={item}
itemRenderer={this.props.itemRenderer}
install={this.props.install}
uninstall={this.props.uninstall} />
}
}
export namespace ComponentList {
export interface Props {
readonly items: ArduinoComponent[];
readonly windowService: WindowService;
readonly install: (comp: ArduinoComponent) => Promise<void>;
readonly resolveContainer?: (element: HTMLElement) => void;
export interface Props<T extends ArduinoComponent> {
readonly items: T[];
readonly itemLabel: (item: T) => string;
readonly itemRenderer: ListItemRenderer<T>;
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
readonly uninstall: (item: T) => Promise<void>;
readonly resolveContainer: (element: HTMLElement) => void;
}
}

View File

@@ -1,24 +1,36 @@
import * as React from 'react';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { SearchBar } from './search-bar';
import { ComponentList } from './component-list';
import { LibraryService } from '../../../common/protocol/library-service';
import debounce = require('lodash.debounce');
import { Event } from '@theia/core/lib/common/event';
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
import { Searchable } from '../../../common/protocol/searchable';
import { Installable } from '../../../common/protocol/installable';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { InstallationProgressDialog } from '../installation-progress-dialog';
import { InstallationProgressDialog, UninstallationProgressDialog } from '../progress-dialog';
import { SearchBar } from './search-bar';
import { ListWidget } from './list-widget';
import { ComponentList } from './component-list';
import { ListItemRenderer } from './list-item-renderer';
export class FilterableListContainer extends React.Component<FilterableListContainer.Props, FilterableListContainer.State> {
export class FilterableListContainer<T extends ArduinoComponent> extends React.Component<FilterableListContainer.Props<T>, FilterableListContainer.State<T>> {
constructor(props: Readonly<FilterableListContainer.Props>) {
constructor(props: Readonly<FilterableListContainer.Props<T>>) {
super(props);
this.state = {
filterText: '',
items: []
};
this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
}
componentWillMount(): void {
componentDidMount(): void {
this.search = debounce(this.search, 500);
this.handleFilterTextChange('');
this.props.filterTextChangeEvent(this.handleFilterTextChange.bind(this));
}
componentDidUpdate(): void {
// See: arduino/arduino-pro-ide#101
// Resets the top of the perfect scroll-bar's thumb.
this.props.container.updateScrollBar();
}
render(): React.ReactNode {
@@ -42,36 +54,66 @@ export class FilterableListContainer extends React.Component<FilterableListConta
}
protected renderComponentList(): React.ReactNode {
return <ComponentList
const { itemLabel, resolveContainer, itemRenderer } = this.props;
return <ComponentList<T>
items={this.state.items}
itemLabel={itemLabel}
itemRenderer={itemRenderer}
install={this.install.bind(this)}
windowService={this.props.windowService}
resolveContainer={this.props.resolveContainer}
uninstall={this.uninstall.bind(this)}
resolveContainer={resolveContainer}
/>
}
private handleFilterTextChange(filterText: string): void {
const { props } = this.state;
this.props.service.search({ query: filterText, props }).then(result => {
protected handleFilterTextChange = (filterText: string) => {
this.setState({ filterText });
this.search(filterText);
}
protected search(query: string): void {
const { searchable } = this.props;
searchable.search({ query: query.trim() }).then(result => {
const { items } = result;
this.setState({
filterText,
items: this.sort(items)
});
});
}
protected sort(items: ArduinoComponent[]): ArduinoComponent[] {
return items.sort((left, right) => left.name.localeCompare(right.name));
protected sort(items: T[]): T[] {
const { itemLabel } = this.props;
return items.sort((left, right) => itemLabel(left).localeCompare(itemLabel(right)));
}
protected async install(comp: ArduinoComponent): Promise<void> {
const dialog = new InstallationProgressDialog(comp.name);
protected async install(item: T, version: Installable.Version): Promise<void> {
const { installable, searchable, itemLabel } = this.props;
const dialog = new InstallationProgressDialog(itemLabel(item), version);
dialog.open();
try {
await this.props.service.install(comp);
const { props } = this.state;
const { items } = await this.props.service.search({ query: this.state.filterText, props });
await installable.install({ item, version });
const { items } = await searchable.search({ query: this.state.filterText });
this.setState({ items: this.sort(items) });
} finally {
dialog.close();
}
}
protected async uninstall(item: T): Promise<void> {
const uninstall = await new ConfirmDialog({
title: 'Uninstall',
msg: `Do you want to uninstall ${item.name}?`,
ok: 'Yes',
cancel: 'No'
}).open();
if (!uninstall) {
return;
}
const { installable, searchable, itemLabel } = this.props;
const dialog = new UninstallationProgressDialog(itemLabel(item));
dialog.open();
try {
await installable.uninstall({ item });
const { items } = await searchable.search({ query: this.state.filterText });
this.setState({ items: this.sort(items) });
} finally {
dialog.close();
@@ -82,23 +124,20 @@ export class FilterableListContainer extends React.Component<FilterableListConta
export namespace FilterableListContainer {
export interface Props {
readonly service: ComponentSource;
readonly windowService: WindowService;
readonly resolveContainer?: (element: HTMLElement) => void;
readonly resolveFocus?: (element: HTMLElement | undefined) => void;
export interface Props<T extends ArduinoComponent> {
readonly container: ListWidget<T>;
readonly installable: Installable<T>;
readonly searchable: Searchable<T>;
readonly itemLabel: (item: T) => string;
readonly itemRenderer: ListItemRenderer<T>;
readonly resolveContainer: (element: HTMLElement) => void;
readonly resolveFocus: (element: HTMLElement | undefined) => void;
readonly filterTextChangeEvent: Event<string>;
}
export interface State {
export interface State<T> {
filterText: string;
items: ArduinoComponent[];
props?: LibraryService.Search.Props;
}
export interface ComponentSource {
search(req: { query: string, props?: LibraryService.Search.Props }): Promise<{ items: ArduinoComponent[] }>
install(board: ArduinoComponent): Promise<void>;
items: T[];
}
}

View File

@@ -0,0 +1,101 @@
import * as React from 'react';
import { inject, injectable } from 'inversify';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { Installable } from '../../../common/protocol/installable';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { ComponentListItem } from './component-list-item';
@injectable()
export class ListItemRenderer<T extends ArduinoComponent> {
@inject(WindowService)
protected windowService: WindowService;
protected onMoreInfoClick = (event: React.SyntheticEvent<HTMLAnchorElement, Event>) => {
const { target } = event.nativeEvent;
if (target instanceof HTMLAnchorElement) {
this.windowService.openNewWindow(target.href, { external: true });
event.nativeEvent.preventDefault();
}
}
renderItem(
input: ComponentListItem.State & { item: T },
install: (item: T) => Promise<void>,
uninstall: (item: T) => Promise<void>,
onVersionChange: (version: Installable.Version) => void
): React.ReactNode {
const { item } = input;
let nameAndAuthor: JSX.Element;
if (item.name && item.author) {
const name = <span className='name'>{item.name}</span>;
const author = <span className='author'>{item.author}</span>;
nameAndAuthor = <span>{name} by {author}</span>
} else if (item.name) {
nameAndAuthor = <span className='name'>{item.name}</span>;
} else if ((item as any).id) {
nameAndAuthor = <span className='name'>{(item as any).id}</span>;
} else {
nameAndAuthor = <span className='name'>Unknown</span>;
}
const onClickUninstall = () => uninstall(item);
const installedVersion = !!item.installedVersion && <div className='version-info'>
<span className='version'>Version {item.installedVersion}</span>
<span className='installed' onClick={onClickUninstall} />
</div>;
const summary = <div className='summary'>{item.summary}</div>;
const description = <div className='summary'>{item.description}</div>;
const moreInfo = !!item.moreInfoLink && <a href={item.moreInfoLink} onClick={this.onMoreInfoClick}>More info</a>;
const onClickInstall = () => install(item);
const installButton = item.installable &&
<button className='install' onClick={onClickInstall}>INSTALL</button>;
const onSelectChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const version = event.target.value;
if (version) {
onVersionChange(version);
}
}
const versions = (() => {
const { availableVersions } = item;
if (availableVersions.length === 0) {
return undefined;
} else if (availableVersions.length === 1) {
return <label>{availableVersions[0]}</label>
} else {
return <select
value={input.selectedVersion}
onChange={onSelectChange}>
{
item.availableVersions
.filter(version => version !== item.installedVersion) // Filter the version that is currently installed.
.map(version => <option value={version} key={version}>{version}</option>)
}
</select>;
}
})();
return <div className='component-list-item noselect'>
<div className='header'>
{nameAndAuthor}
{installedVersion}
</div>
<div className='content'>
{summary}
{description}
</div>
<div className='info'>
{moreInfo}
</div>
<div className='footer'>
{installButton}
{versions}
</div>
</div>;
}
}

View File

@@ -0,0 +1,13 @@
import { injectable } from 'inversify';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { ListWidget } from './list-widget';
@injectable()
export abstract class ListWidgetFrontendContribution<T extends ArduinoComponent> extends AbstractViewContribution<ListWidget<T>> implements FrontendApplicationContribution {
async initializeLayout(): Promise<void> {
}
}

View File

@@ -0,0 +1,97 @@
import * as React from 'react';
import { injectable, postConstruct } from 'inversify';
import { Message } from '@phosphor/messaging';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { Emitter } from '@theia/core/lib/common/event';
import { MaybePromise } from '@theia/core/lib/common/types';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { Installable } from '../../../common/protocol/installable';
import { Searchable } from '../../../common/protocol/searchable';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { FilterableListContainer } from './filterable-list-container';
import { ListItemRenderer } from './list-item-renderer';
@injectable()
export abstract class ListWidget<T extends ArduinoComponent> extends ReactWidget {
/**
* Do not touch or use it. It is for setting the focus on the `input` after the widget activation.
*/
protected focusNode: HTMLElement | undefined;
protected readonly deferredContainer = new Deferred<HTMLElement>();
protected readonly filterTextChangeEmitter = new Emitter<string>();
constructor(protected options: ListWidget.Options<T>) {
super();
const { id, label, iconClass } = options;
this.id = id;
this.title.label = label;
this.title.caption = label;
this.title.iconClass = iconClass
this.title.closable = true;
this.addClass('arduino-list-widget');
this.node.tabIndex = 0; // To be able to set the focus on the widget.
this.scrollOptions = {
suppressScrollX: true
}
this.toDispose.push(this.filterTextChangeEmitter);
}
@postConstruct()
protected init(): void {
this.update();
}
protected getScrollContainer(): MaybePromise<HTMLElement> {
return this.deferredContainer.promise;
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
(this.focusNode || this.node).focus();
}
protected onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.render();
}
protected onFocusResolved = (element: HTMLElement | undefined) => {
this.focusNode = element;
}
render(): React.ReactNode {
return <FilterableListContainer<T>
container={this}
resolveContainer={this.deferredContainer.resolve}
resolveFocus={this.onFocusResolved}
searchable={this.options.searchable}
installable={this.options.installable}
itemLabel={this.options.itemLabel}
itemRenderer={this.options.itemRenderer}
filterTextChangeEvent={this.filterTextChangeEmitter.event}/>;
}
refresh(filterText: string): void {
this.deferredContainer.promise.then(() => this.filterTextChangeEmitter.fire(filterText));
}
updateScrollBar(): void {
if (this.scrollBar) {
this.scrollBar.update();
}
}
}
export namespace ListWidget {
export interface Options<T extends ArduinoComponent> {
readonly id: string;
readonly label: string;
readonly iconClass: string;
readonly installable: Installable<T>;
readonly searchable: Searchable<T>;
readonly itemLabel: (item: T) => string;
readonly itemRenderer: ListItemRenderer<T>;
}
}

View File

@@ -1,148 +0,0 @@
import * as React from 'react';
import { BoardsService, Board } from '../../common/protocol/boards-service';
// import { SelectBoardDialog } from './select-board-dialog';
import { QuickPickService } from '@theia/core/lib/common/quick-pick-service';
import { BoardsNotificationService } from '../boards-notification-service';
import { ARDUINO_TOOLBAR_ITEM_CLASS } from '../toolbar/arduino-toolbar';
export class ConnectedBoards extends React.Component<ConnectedBoards.Props, ConnectedBoards.State> {
static TOOLBAR_ID: 'connected-boards-toolbar';
constructor(props: ConnectedBoards.Props) {
super(props);
this.state = { boardsLoading: false };
props.boardsNotificationService.on('boards-installed', () => this.onBoardsInstalled());
}
render(): React.ReactNode {
let content = [];
if (!!this.state.boards && this.state.boards.length > 0) {
content = this.state.boards.map((b, i) => <option value={i} key={i}>{b.name}</option>);
} else {
let label;
if (this.state.boardsLoading) {
label = "Loading ...";
} else {
label = "No board attached";
}
content = [ <option key="loading" value="0">{label}</option> ];
}
return <div key='arduino-connected-boards' className={`${ARDUINO_TOOLBAR_ITEM_CLASS} item ${ConnectedBoards.Styles.CONNECTED_BOARDS_CLASS}`}>
<select key='arduino-connected-boards-select' disabled={!this.state.boards}
onChange={this.onBoardSelect.bind(this)}
value={this.state.selection}>
<optgroup key='arduino-connected-boards-select-opt-group' label="Attached boards">
{ content }
</optgroup>
<optgroup label="_________" key='arduino-connected-boards-select-opt-group2'>
{ !!this.state.otherBoard && <option value="selected-other" key="selected-other">{this.state.otherBoard.name} (not attached)</option> }
<option value="select-other" key="select-other">Select other Board</option>
</optgroup>
</select>
</div>;
}
componentDidMount(): void {
this.reloadBoards();
}
protected onBoardsInstalled() {
if (!!this.findUnknownBoards()) {
this.reloadBoards();
}
}
protected findUnknownBoards(): Board[] {
if (!this.state || !this.state.boards) {
return [];
}
return this.state.boards.filter(b => !b.fqbn || b.name === "unknown");
}
protected async reloadBoards() {
const prevSelection = this.state.selection;
this.setState({ boardsLoading: true, boards: undefined, selection: "loading" });
const { boards } = await this.props.boardsService.getAttachedBoards()
this.setState({ boards, boardsLoading: false, selection: prevSelection });
if (boards) {
this.setState({ selection: "0" });
await this.props.boardsService.selectBoard(boards[0]);
const unknownBoards = this.findUnknownBoards();
if (unknownBoards && unknownBoards.length > 0) {
this.props.onUnknownBoard(unknownBoards[0]);
}
}
}
protected async onBoardSelect(evt: React.ChangeEvent<HTMLSelectElement>) {
const selection = evt.target.value;
if (selection === "select-other" || selection === "selected-other") {
let selectedBoard = this.state.otherBoard;
if (selection === "select-other" || !selectedBoard) {
selectedBoard = await this.selectedInstalledBoard();
}
if (!selectedBoard) {
return;
}
await this.props.boardsService.selectBoard(selectedBoard);
this.setState({otherBoard: selectedBoard, selection: "selected-other"});
return;
}
const selectedBoard = (this.state.boards || [])[parseInt(selection, 10)];
if (!selectedBoard) {
return;
}
await this.props.boardsService.selectBoard(selectedBoard);
this.setState({selection});
}
protected async selectedInstalledBoard(): Promise<Board | undefined> {
const {items} = await this.props.boardsService.search({});
const idx = new Map<string, Board>();
items.filter(pkg => !!pkg.installedVersion).forEach(pkg => pkg.boards.forEach(brd => idx.set(`${brd.name}`, brd) ));
if (idx.size === 0) {
this.props.onNoBoardsInstalled();
return;
}
const selection = await this.props.quickPickService.show(Array.from(idx.keys()));
if (!selection) {
return;
}
return idx.get(selection);
}
}
export namespace ConnectedBoards {
export interface Props {
readonly boardsService: BoardsService;
readonly boardsNotificationService: BoardsNotificationService;
readonly quickPickService: QuickPickService;
readonly onNoBoardsInstalled: () => void;
readonly onUnknownBoard: (board: Board) => void;
}
export interface State {
boardsLoading: boolean;
boards?: Board[];
otherBoard?: Board;
selection?: string;
}
export namespace Styles {
export const CONNECTED_BOARDS_CLASS = 'connected-boards';
}
}

View File

@@ -1,12 +0,0 @@
import { AbstractDialog } from "@theia/core/lib/browser";
export class InstallationProgressDialog extends AbstractDialog<string> {
readonly value: "does-not-matter";
constructor(componentName: string) {
super({ title: 'Installation in progress' });
this.contentNode.textContent = `Installing ${componentName}. Please wait.`;
}
}

View File

@@ -0,0 +1,23 @@
import { AbstractDialog } from '@theia/core/lib/browser';
export class InstallationProgressDialog extends AbstractDialog<undefined> {
readonly value = undefined;
constructor(componentName: string, version: string) {
super({ title: 'Installation in progress' });
this.contentNode.textContent = `Installing ${componentName} [${version}]. Please wait...`;
}
}
export class UninstallationProgressDialog extends AbstractDialog<undefined> {
readonly value = undefined;
constructor(componentName: string) {
super({ title: 'Uninstallation in progress' });
this.contentNode.textContent = `Uninstalling ${componentName}. Please wait...`;
}
}

View File

@@ -0,0 +1,25 @@
import { injectable, inject, postConstruct } from 'inversify';
import { AboutDialog, ABOUT_CONTENT_CLASS } from '@theia/core/lib/browser/about-dialog';
import { ConfigService } from '../../common/protocol/config-service';
@injectable()
export class ArduinoAboutDialog extends AboutDialog {
@inject(ConfigService)
protected readonly configService: ConfigService;
@postConstruct()
protected async init(): Promise<void> {
const [, version] = await Promise.all([super.init(), this.configService.getVersion()]);
if (version) {
const { firstChild } = this.contentNode;
if (firstChild instanceof HTMLElement && firstChild.classList.contains(ABOUT_CONTENT_CLASS)) {
const cliVersion = document.createElement('div');
cliVersion.textContent = version;
firstChild.appendChild(cliVersion);
// TODO: anchor to the commit in the `arduino-cli` repository.
}
}
}
}

View File

@@ -0,0 +1,46 @@
import { ApplicationShell, Widget, Saveable, FocusTracker, Message } from '@theia/core/lib/browser';
import { EditorWidget } from '@theia/editor/lib/browser';
import { injectable, inject } from 'inversify';
import { EditorMode } from '../editor-mode';
@injectable()
export class ArduinoApplicationShell extends ApplicationShell {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
protected refreshBottomPanelToggleButton() {
if (this.editorMode.proMode) {
super.refreshBottomPanelToggleButton();
}
}
protected async track(widget: Widget): Promise<void> {
if (this.editorMode.proMode) {
super.track(widget);
} else {
const tracker = (this as any).tracker as FocusTracker<Widget>;
tracker.add(widget);
this.disableClose(Saveable.apply(widget));
if (ApplicationShell.TrackableWidgetProvider.is(widget)) {
for (const toTrack of await widget.getTrackableWidgets()) {
tracker.add(toTrack);
this.disableClose(Saveable.apply(toTrack));
}
if (widget.onDidChangeTrackableWidgets) {
widget.onDidChangeTrackableWidgets(widgets => widgets.forEach(w => this.track(w)));
}
}
}
}
private disableClose(widget: Widget | undefined): void {
if (widget instanceof EditorWidget) {
const onCloseRequest = (_: Message) => {
// NOOP
};
(widget as any).onCloseRequest = onCloseRequest.bind(widget);
}
}
}

View File

@@ -1,8 +1,9 @@
import {EditorContribution} from '@theia/editor/lib/browser/editor-contribution';
import { EditorContribution } from '@theia/editor/lib/browser/editor-contribution';
import { TextEditor } from '@theia/editor/lib/browser';
import { StatusBarAlignment } from '@theia/core/lib/browser';
export class CustomEditorContribution extends EditorContribution {
export class ArduinoEditorContribution extends EditorContribution {
protected updateLanguageStatus(editor: TextEditor | undefined): void {
}
@@ -18,4 +19,5 @@ export class CustomEditorContribution extends EditorContribution {
priority: 100
});
}
}
}

View File

@@ -0,0 +1,42 @@
import { injectable, inject } from 'inversify';
import { FileSystem } from '@theia/filesystem/lib/common/filesystem';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { EditorMode } from '../editor-mode';
import { ArduinoFrontendContribution } from '../arduino-frontend-contribution';
@injectable()
export class ArduinoFrontendApplication extends FrontendApplication {
@inject(FileSystem)
protected readonly fileSystem: FileSystem;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(ArduinoFrontendContribution)
protected readonly frontendContribution: ArduinoFrontendContribution;
@inject(EditorMode)
protected readonly editorMode: EditorMode;
protected async initializeLayout(): Promise<void> {
return super.initializeLayout().then(() => {
// If not in PRO mode, we open the sketch file with all the related files.
// Otherwise, we reuse the workbench's restore functionality and we do not open anything at all.
// TODO: check `otherwise`. Also, what if we check for opened editors, instead of blindly opening them?
if (!this.editorMode.proMode) {
this.workspaceService.roots.then(roots => {
for (const root of roots) {
this.fileSystem.exists(root.uri).then(exists => {
if (exists) {
this.frontendContribution.openSketchFiles(root.uri);
}
});
}
});
}
});
}
}

View File

@@ -0,0 +1,11 @@
import { MonacoStatusBarContribution } from '@theia/monaco/lib/browser/monaco-status-bar-contribution';
export class ArduinoMonacoStatusBarContribution extends MonacoStatusBarContribution {
protected setConfigTabSizeWidget() {
}
protected setLineEndingWidget() {
}
}

View File

@@ -0,0 +1,18 @@
import { injectable, inject } from 'inversify';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
import { EditorMode } from '../editor-mode';
@injectable()
export class ArduinoNavigatorContribution extends FileNavigatorContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
async initializeLayout(app: FrontendApplication): Promise<void> {
if (this.editorMode.proMode) {
return super.initializeLayout(app);
}
}
}

View File

@@ -0,0 +1,19 @@
import { injectable, inject } from 'inversify';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
import { EditorMode } from '../editor-mode';
@injectable()
export class ArduinoOutlineViewContribution extends OutlineViewContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
async initializeLayout(app: FrontendApplication): Promise<void> {
if (this.editorMode.proMode) {
return super.initializeLayout(app);
}
}
}

View File

@@ -0,0 +1,18 @@
import { inject, injectable } from 'inversify';
import { TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { OutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution';
import { EditorMode } from '../editor-mode';
@injectable()
export class ArduinoOutputToolContribution extends OutputToolbarContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
async registerToolbarItems(toolbarRegistry: TabBarToolbarRegistry): Promise<void> {
if (this.editorMode.proMode) {
super.registerToolbarItems(toolbarRegistry);
}
}
}

View File

@@ -0,0 +1,35 @@
import { inject, injectable } from 'inversify';
import { KeybindingRegistry } from '@theia/core/lib/browser';
import { ProblemStat } from '@theia/markers/lib/browser/problem/problem-manager';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { EditorMode } from '../editor-mode';
@injectable()
export class ArduinoProblemContribution extends ProblemContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
async initializeLayout(app: FrontendApplication): Promise<void> {
if (this.editorMode.proMode) {
return super.initializeLayout(app);
}
}
protected setStatusBarElement(problemStat: ProblemStat): void {
if (this.editorMode.proMode) {
super.setStatusBarElement(problemStat);
}
}
registerKeybindings(keybindings: KeybindingRegistry): void {
if (this.toggleCommand) {
keybindings.registerKeybinding({
command: this.toggleCommand.id,
keybinding: 'ctrlcmd+alt+shift+m'
});
}
}
}

View File

@@ -0,0 +1,24 @@
import { inject, injectable } from 'inversify';
import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
import { StatusBarEntry } from '@theia/core/lib/browser/status-bar/status-bar';
import { EditorMode } from '../editor-mode';
@injectable()
export class ArduinoScmContribution extends ScmContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
async initializeLayout(): Promise<void> {
if (this.editorMode.proMode) {
return super.initializeLayout();
}
}
protected setStatusBarEntry(id: string, entry: StatusBarEntry): void {
if (this.editorMode.proMode) {
super.setStatusBarEntry(id, entry);
}
}
}

View File

@@ -0,0 +1,18 @@
import { inject, injectable } from 'inversify';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { EditorMode } from '../editor-mode';
@injectable()
export class ArduinoSearchInWorkspaceContribution extends SearchInWorkspaceFrontendContribution {
@inject(EditorMode)
protected readonly editorMode: EditorMode;
async initializeLayout(app: FrontendApplication): Promise<void> {
if (this.editorMode.proMode) {
return super.initializeLayout(app);
}
}
}

View File

@@ -1,7 +0,0 @@
import { ApplicationShell } from "@theia/core/lib/browser";
export class CustomApplicationShell extends ApplicationShell {
protected refreshBottomPanelToggleButton() {
}
}

View File

@@ -1,44 +0,0 @@
import { injectable } from "inversify";
import { CommonFrontendContribution, CommonMenus, CommonCommands } from "@theia/core/lib/browser";
import { MenuModelRegistry } from "@theia/core";
@injectable()
export class CustomCommonFrontendContribution extends CommonFrontendContribution {
registerMenus(registry: MenuModelRegistry): void {
registry.registerSubmenu(CommonMenus.FILE, 'File');
registry.registerSubmenu(CommonMenus.EDIT, 'Edit');
registry.registerSubmenu(CommonMenus.FILE_SETTINGS_SUBMENU, 'Settings');
registry.registerMenuAction(CommonMenus.EDIT_UNDO, {
commandId: CommonCommands.UNDO.id,
order: '0'
});
registry.registerMenuAction(CommonMenus.EDIT_UNDO, {
commandId: CommonCommands.REDO.id,
order: '1'
});
registry.registerMenuAction(CommonMenus.EDIT_FIND, {
commandId: CommonCommands.FIND.id,
order: '0'
});
registry.registerMenuAction(CommonMenus.EDIT_FIND, {
commandId: CommonCommands.REPLACE.id,
order: '1'
});
registry.registerMenuAction(CommonMenus.EDIT_CLIPBOARD, {
commandId: CommonCommands.CUT.id,
order: '0'
});
registry.registerMenuAction(CommonMenus.EDIT_CLIPBOARD, {
commandId: CommonCommands.COPY.id,
order: '1'
});
registry.registerMenuAction(CommonMenus.EDIT_CLIPBOARD, {
commandId: CommonCommands.PASTE.id,
order: '2'
});
}
}

View File

@@ -1,22 +0,0 @@
import { injectable } from "inversify";
import { EditorWidgetFactory } from "@theia/editor/lib/browser/editor-widget-factory";
import URI from "@theia/core/lib/common/uri";
import { EditorWidget } from "@theia/editor/lib/browser";
@injectable()
export class CustomEditorWidgetFactory extends EditorWidgetFactory {
protected async createEditor(uri: URI): Promise<EditorWidget> {
const icon = await this.labelProvider.getIcon(uri);
return this.editorProvider(uri).then(textEditor => {
const newEditor = new EditorWidget(textEditor, this.selectionService);
newEditor.id = this.id + ':' + uri.toString();
newEditor.title.closable = false;
newEditor.title.label = this.labelProvider.getName(uri);
newEditor.title.iconClass = icon + ' file-icon';
newEditor.title.caption = this.labelProvider.getLongName(uri);
return newEditor;
});
}
}

View File

@@ -1,10 +0,0 @@
import { injectable } from "inversify";
import { FileMenuContribution } from "@theia/workspace/lib/browser";
import { MenuModelRegistry } from "@theia/core";
@injectable()
export class CustomFileMenuContribution extends FileMenuContribution {
registerMenus(registry: MenuModelRegistry) {
}
}

View File

@@ -1,18 +0,0 @@
import { injectable, inject } from "inversify";
import { FrontendApplication } from "@theia/core/lib/browser";
import { ArduinoFrontendContribution } from "../arduino-frontend-contribution";
@injectable()
export class CustomFrontendApplication extends FrontendApplication {
@inject(ArduinoFrontendContribution)
protected readonly frontendContribution: ArduinoFrontendContribution;
protected async initializeLayout(): Promise<void> {
const location = new URL(window.location.href);
const sketchPath = location.searchParams.get('sketch');
if (sketchPath) {
this.frontendContribution.openSketchFiles(decodeURIComponent(sketchPath));
}
}
}

View File

@@ -1,10 +0,0 @@
import { injectable } from "inversify";
import { MonacoEditorMenuContribution } from "@theia/monaco/lib/browser/monaco-menu";
import { MenuModelRegistry } from "@theia/core";
@injectable()
export class CustomMonacoEditorMenuContribution extends MonacoEditorMenuContribution {
registerMenus(registry: MenuModelRegistry) {
}
}

View File

@@ -1,11 +0,0 @@
import {MonacoStatusBarContribution} from '@theia/monaco/lib/browser/monaco-status-bar-contribution';
export class SilentMonacoStatusBarContribution extends MonacoStatusBarContribution {
protected setConfigTabSizeWidget() {
}
protected setLineEndingWidget() {
}
}

View File

@@ -1,10 +0,0 @@
import { injectable } from "inversify";
import { FileNavigatorContribution } from "@theia/navigator/lib/browser/navigator-contribution";
import { FrontendApplication } from "@theia/core/lib/browser";
@injectable()
export class SilentNavigatorContribution extends FileNavigatorContribution {
async initializeLayout(app: FrontendApplication): Promise<void> {
// await this.openView();
}
}

View File

@@ -1,26 +0,0 @@
/********************************************************************************
* Copyright (C) 2017 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { injectable } from 'inversify';
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
@injectable()
export class SilentOutlineViewContribution extends OutlineViewContribution {
async initializeLayout(): Promise<void> {
// await this.openView();
}
}

View File

@@ -1,10 +0,0 @@
import { OutputToolbarContribution } from "@theia/output/lib/browser/output-toolbar-contribution";
import { TabBarToolbarRegistry } from "@theia/core/lib/browser/shell/tab-bar-toolbar";
import { injectable } from "inversify";
@injectable()
export class ArduinoOutputToolContribution extends OutputToolbarContribution {
async registerToolbarItems(toolbarRegistry: TabBarToolbarRegistry): Promise<void> {
// register nothing
}
}

View File

@@ -1,15 +0,0 @@
import { injectable } from 'inversify';
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { ProblemStat } from '@theia/markers/lib/browser/problem/problem-manager';
@injectable()
export class SilentProblemContribution extends ProblemContribution {
async initializeLayout(): Promise<void> {
// await this.openView();
}
protected setStatusBarElement(problemStat: ProblemStat) {
}
}

View File

@@ -0,0 +1,48 @@
import { injectable } from 'inversify';
import { ApplicationShell, FrontendApplicationContribution, FrontendApplication } from '@theia/core/lib/browser';
import { ArduinoShellLayoutRestorer } from './shell/arduino-shell-layout-restorer';
import { OutputWidget } from '@theia/output/lib/browser/output-widget';
import { EditorWidget } from '@theia/editor/lib/browser';
@injectable()
export class EditorMode implements FrontendApplicationContribution {
protected app: FrontendApplication;
onStart(app: FrontendApplication): void {
this.app = app;
if (this.proMode) {
// We use this CSS class on the body to modify the visibility of the close button for the editors and views.
document.body.classList.add(EditorMode.PRO_MODE_KEY);
}
}
get proMode(): boolean {
const value = window.localStorage.getItem(EditorMode.PRO_MODE_KEY);
return value === 'true';
}
async toggle(): Promise<void> {
const oldState = this.proMode;
const inAdvancedMode = !oldState;
window.localStorage.setItem(EditorMode.PRO_MODE_KEY, String(inAdvancedMode));
if (!inAdvancedMode) {
const { shell } = this.app;
// Close all widget that is neither editor nor `Output`.
for (const area of ['left', 'right', 'bottom', 'main'] as Array<ApplicationShell.Area>) {
shell.closeTabs(area, ({ owner }) => !(owner instanceof EditorWidget || owner instanceof OutputWidget));
}
}
// `storeLayout` has a sync API but the implementation is async, we store the layout manually before we reload the page.
// See: https://github.com/eclipse-theia/theia/issues/6579
// XXX: hack instead of injecting the `ArduinoShellLayoutRestorer` we have to retrieve it from the application to avoid DI cycle.
const layoutRestorer = (this.app as any).layoutRestorer as ArduinoShellLayoutRestorer
await layoutRestorer.storeLayoutAsync(this.app);
window.location.reload(true);
}
}
export namespace EditorMode {
export const PRO_MODE_KEY = 'arduino-advanced-mode';
}

View File

@@ -0,0 +1,34 @@
import { inject, injectable } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
import { ConfigService } from '../../common/protocol/config-service';
@injectable()
export class ArduinoMonacoEditorProvider extends MonacoEditorProvider {
@inject(ConfigService)
protected readonly configService: ConfigService;
protected dataDirUri: string | undefined;
protected async getModel(uri: URI, toDispose: DisposableCollection): Promise<MonacoEditorModel> {
// `createMonacoEditorOptions` is not `async` so we ask the `dataDirUri` here.
// https://github.com/eclipse-theia/theia/issues/6234
const { dataDirUri } = await this.configService.getConfiguration()
this.dataDirUri = dataDirUri;
return super.getModel(uri, toDispose);
}
protected createMonacoEditorOptions(model: MonacoEditorModel): MonacoEditor.IOptions {
const options = this.createOptions(this.preferencePrefixes, model.uri, model.languageId);
options.model = model.textEditorModel;
options.readOnly = model.readOnly;
if (this.dataDirUri) {
options.readOnly = new URI(this.dataDirUri).isEqualOrParent(new URI(model.uri));
}
return options;
}
}

View File

Before

Width:  |  Height:  |  Size: 962 B

After

Width:  |  Height:  |  Size: 962 B

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)-->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" id="Layer_1" width="198px" height="99px" x="0px" y="0px" enable-background="new 0 0 198 99" version="1.1" viewBox="0 0 198 99" inkscape:version="0.91 r13725" sodipodi:docname="buttons.svg" xml:space="preserve">
<metadata id="metadata327">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>
image/svg+xml
</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<defs id="defs325"/>
<sodipodi:namedview id="namedview323" bordercolor="#666666" borderopacity="1" gridtolerance="10" guidetolerance="10" objecttolerance="10" pagecolor="#ffffff" showgrid="false" showguides="true" inkscape:current-layer="Layer_1" inkscape:cx="45.252385" inkscape:cy="36.224987" inkscape:guide-bbox="true" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-height="1000" inkscape:window-maximized="1" inkscape:window-width="1215" inkscape:window-x="65" inkscape:window-y="24" inkscape:zoom="4"/>
<g id="g5" transform="translate(-0.12000084,0)">
<polyline id="polyline9" stroke-miterlimit="10" points="21.453,12.745 15.788,20.571 11.775,16.658 " style="fill:none;stroke:#000000;stroke-width:2.30489993;stroke-miterlimit:10"/>
</g>
<g id="g23" transform="translate(-0.26100159,0)">
<polygon id="polygon27" points="45.412,15.313 49.307,15.313 49.307,11.53 54.701,16.875 49.331,22.245 49.331,18.563 45.412,18.539 " style="fill:#000000"/>
</g>
<g id="g41" transform="translate(-0.54399872,0)">
<polygon id="polygon45" points="114.44,19.083 114.44,15.116 110.586,15.116 116.032,9.621 121.502,15.091 117.751,15.091 117.726,19.083 " style="fill:#000000"/>
<rect id="rect47" width="1" height="1" x="110.511" y="22.193001" style="fill:#000000"/>
<rect id="rect49" width="1" height="1" x="112.518" y="22.193001" style="fill:#000000"/>
<rect id="rect51" width="1" height="1" x="114.517" y="22.193001" style="fill:#000000"/>
<rect id="rect53" width="1" height="1" x="116.525" y="22.193001" style="fill:#000000"/>
<rect id="rect55" width="1" height="1" x="118.524" y="22.193001" style="fill:#000000"/>
<rect id="rect57" width="1" height="1" x="120.531" y="22.193001" style="fill:#000000"/>
</g>
<g id="g59" transform="translate(-0.68600464,0)">
<polygon id="polygon63" points="150.79,9.621 150.79,13.588 154.644,13.588 149.198,19.083 143.728,13.612 147.479,13.613 147.504,9.621 " style="fill:#000000"/>
<rect id="rect65" width="1" height="1" x="143.65199" y="22.193001" style="fill:#000000"/>
<rect id="rect67" width="1" height="1" x="145.66" y="22.193001" style="fill:#000000"/>
<rect id="rect69" width="1" height="1" x="147.659" y="22.193001" style="fill:#000000"/>
<rect id="rect71" width="1" height="1" x="149.666" y="22.193001" style="fill:#000000"/>
<rect id="rect73" width="1" height="1" x="151.666" y="22.193001" style="fill:#000000"/>
<rect id="rect75" width="1" height="1" x="153.673" y="22.193001" style="fill:#000000"/>
</g>
<g id="g149" transform="translate(-0.40299988,0)">
<path id="path151" d="M 87.445,22.097" style="fill:#000000" inkscape:connector-curvature="0"/>
<polygon id="polygon155" points="83.44,10.094 84.441,10.094 88.421,14.079 88.421,15.057 87.445,15.057 83.44,15.057 " style="fill:#000000"/>
<polygon id="polygon157" points="78.404,11.093 78.404,22.097 87.445,22.097 87.445,14.87 88.421,14.87 88.421,23.134 77.399,23.134 77.399,10.094 83.562,10.094 83.568,11.093 " style="fill:#000000"/>
<rect id="rect159" width="0.995" height="0.99599999" x="79.399002" y="12.111" style="fill:#000000"/>
<rect id="rect161" width="0.995" height="0.99599999" x="81.394997" y="12.111" style="fill:#000000"/>
<rect id="rect163" width="0.995" height="0.99599999" x="79.399002" y="14.103" style="fill:#000000"/>
<rect id="rect165" width="0.995" height="0.99599999" x="81.394997" y="14.103" style="fill:#000000"/>
<rect id="rect167" width="0.995" height="0.99599999" x="79.399002" y="16.115999" style="fill:#000000"/>
<rect id="rect169" width="0.995" height="0.99599999" x="81.394997" y="16.115999" style="fill:#000000"/>
<rect id="rect171" width="0.995" height="0.99599999" x="83.403" y="16.115999" style="fill:#000000"/>
<rect id="rect173" width="0.995" height="0.99599999" x="85.400002" y="16.115999" style="fill:#000000"/>
<rect id="rect175" width="0.995" height="0.99599999" x="79.399002" y="18.118" style="fill:#000000"/>
<rect id="rect177" width="0.995" height="0.99599999" x="81.394997" y="18.118" style="fill:#000000"/>
<rect id="rect179" width="0.995" height="0.99599999" x="79.399002" y="20.132" style="fill:#000000"/>
<rect id="rect181" width="0.995" height="0.99599999" x="81.394997" y="20.132" style="fill:#000000"/>
<rect id="rect183" width="0.995" height="0.99599999" x="83.403" y="18.118" style="fill:#000000"/>
<rect id="rect185" width="0.995" height="0.99599999" x="85.400002" y="18.118" style="fill:#000000"/>
<rect id="rect187" width="0.995" height="0.99599999" x="83.403" y="20.132" style="fill:#000000"/>
<rect id="rect189" width="0.995" height="0.99599999" x="85.400002" y="20.132" style="fill:#000000"/>
</g>
<g id="g275" transform="translate(-0.82800293,0)">
<rect id="rect279" width="0.99900001" height="0.99800003" x="187.819" y="16.101" style="fill:#000000"/>
<rect id="rect281" width="0.99900001" height="0.99800003" x="189.825" y="16.101" style="fill:#000000"/>
<rect id="rect283" width="0.99900001" height="0.99800003" x="174.83299" y="16.101" style="fill:#000000"/>
<circle id="circle285" cx="181.80299" cy="16.101" r="4.0900002" stroke-miterlimit="10" style="fill:none;stroke:#000000;stroke-width:1.95439994;stroke-miterlimit:10"/>
<rect id="rect287" width="2.6800001" height="4.4229999" x="175.895" y="18.427999" style="fill:#000000" transform="matrix(0.6915,0.7224,-0.7224,0.6915,69.5827,-121.6599)"/>
<rect id="rect289" width="2.0280001" height="1.9960001" x="180.789" y="15.103" style="fill:#000000"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -0,0 +1,53 @@
import { injectable, inject, postConstruct } from 'inversify';
import { BaseLanguageClientContribution } from '@theia/languages/lib/browser';
import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
import { BoardsConfig } from '../boards/boards-config';
import { Board, BoardPackage } from '../../common/protocol/boards-service';
@injectable()
export class ArduinoLanguageClientContribution extends BaseLanguageClientContribution {
readonly id = 'ino';
readonly name = 'Arduino';
protected get documentSelector(): string[] {
return ['ino'];
}
protected get globPatterns() {
return ['**/*.ino'];
}
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
protected boardConfig?: BoardsConfig.Config;
@postConstruct()
protected init() {
this.boardsServiceClient.onBoardsConfigChanged(this.selectBoard.bind(this));
const restartIfAffected = (pkg: BoardPackage) => {
if (!this.boardConfig) {
this.restart();
return;
}
const { selectedBoard } = this.boardConfig;
if (selectedBoard && pkg.boards.some(board => Board.sameAs(board, selectedBoard))) {
this.restart();
}
}
this.boardsServiceClient.onBoardInstalled(({ pkg }) => restartIfAffected(pkg));
this.boardsServiceClient.onBoardUninstalled(({ pkg }) => restartIfAffected(pkg));
}
selectBoard(config: BoardsConfig.Config): void {
this.boardConfig = config;
// Force a restart to send the new board config to the language server
this.restart();
}
protected getStartParameters(): BoardsConfig.Config | undefined {
return this.boardConfig;
}
}

View File

@@ -1,50 +0,0 @@
import * as React from 'react';
import { ComponentListItem } from '../components/component-list/component-list-item';
export class LibraryComponentListItem extends ComponentListItem {
render(): React.ReactNode {
const { item } = this.props;
const name = <span className={'name'}>{item.name}</span>;
const author = <span className={'author'}>by {item.author}</span>;
const installedVersion = !!item.installedVersion && <div className={'version-info'}>
<span className={'version'}>Version {item.installedVersion}</span>
<span className={'installed'}>INSTALLED</span>
</div>;
const summary = <div className={'summary'}>{item.summary}</div>;
const moreInfo = !!item.moreInfoLink && <a href={item.moreInfoLink} onClick={this.onClick}>More info</a>;
const install = this.props.install && item.installable && !item.installedVersion &&
<button className={'install'} onClick={this.install.bind(this, item)}>INSTALL</button>;
const versions = (() => {
const { availableVersions } = item;
if (availableVersions.length === 0) {
return undefined;
} else if (availableVersions.length === 1) {
return <label>{availableVersions[0]}</label>
} else {
return <select>{item.availableVersions.map(version => <option value={version} key={version}>{version}</option>)}</select>;
}
})();
return <div className={'component-list-item noselect'}>
<div className={'header'}>
<span>{name} {author}</span>
{installedVersion}
</div>
<div className={'content'}>
{summary}
</div>
<div className={'info'}>
{moreInfo}
</div>
<div className={'footer'}>
{install}
{versions}
</div>
</div>;
}
}

View File

@@ -1,17 +0,0 @@
import * as React from 'react';
import { ArduinoComponent } from '../../common/protocol/arduino-component';
import { ComponentList } from '../components/component-list/component-list';
import { LibraryComponentListItem } from './library-component-list-item';
export class LibraryComponentList extends ComponentList {
createItem(item: ArduinoComponent): React.ReactNode {
return <LibraryComponentListItem
key={item.name}
item={item}
windowService={this.props.windowService}
install={this.props.install}
/>
}
}

View File

@@ -1,110 +0,0 @@
import * as React from 'react';
import { FilterableListContainer } from '../components/component-list/filterable-list-container';
import { LibraryComponentList } from './library-component-list';
export class LibraryFilterableListContainer extends FilterableListContainer {
constructor(props: Readonly<FilterableListContainer.Props>) {
super(props);
this.state = {
filterText: '',
items: [],
props: {
topic: this.topics[0],
type: this.types[0]
}
};
}
protected renderSearchFilter(): React.ReactNode {
const types = this.types.map(type => <option value={type} key={type}>{type}</option>);
let type = this.types[0];
if (this.state.props) {
const currentType = this.types.find(t => t === this.state.props!.type) || this.types[0];
if (currentType) {
type = currentType;
}
}
const topics = this.topics.map(topic => <option value={topic} key={topic}>{topic}</option>);
let topic = this.topics[0];
if (this.state.props) {
const currentTopic = this.topics.find(t => t === this.state.props!.topic) || this.topics[0];
if (currentTopic) {
topic = currentTopic;
}
}
return <div className={'search-filters'}>
<div className={'filter'}>
<div className={'title'} style={{ minWidth: '32.088px' }}>Type</div> {/** TODO: do `minWidth` better! */}
<select
value={type}
onChange={this.onTypeChange}>
{types}
</select>
</div>
<div className={'filter'}>
<div className={'title'}>Topic</div>
<select
value={topic}
onChange={this.onTopicChange}>
{topics}
</select>
</div>
</div>
}
protected onTypeChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const type = event.target.value;
const props = { ...(this.state.props || {}), ...{ type } };
this.setState({
props
});
}
protected onTopicChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const topic = event.target.value;
const props = { ...(this.state.props || {}), ...{ topic } };
this.setState({
props
});
}
protected renderComponentList(): React.ReactNode {
return <LibraryComponentList
items={this.state.items}
install={this.install.bind(this)}
windowService={this.props.windowService}
resolveContainer={this.props.resolveContainer}
/>
}
private get topics(): string[] {
return [
'All',
'Communication',
'Data Processing',
'Data Storage',
'Device Control',
'Display',
'Other',
'Sensor',
'Signal Input/Output',
'Timing',
'Uncategorized'
];
}
private get types(): string[] {
return [
'All',
'Updatable',
'Installed',
'Arduino',
'Partner',
'Recommended',
'Contributed',
'Retired'
];
}
}

View File

@@ -0,0 +1,27 @@
import { inject, injectable } from 'inversify';
import { Library, LibraryService } from '../../common/protocol/library-service';
import { ListWidget } from '../components/component-list/list-widget';
import { ListItemRenderer } from '../components/component-list/list-item-renderer';
@injectable()
export class LibraryListWidget extends ListWidget<Library> {
static WIDGET_ID = 'library-list-widget';
static WIDGET_LABEL = 'Library Manager';
constructor(
@inject(LibraryService) protected service: LibraryService,
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<Library>) {
super({
id: LibraryListWidget.WIDGET_ID,
label: LibraryListWidget.WIDGET_LABEL,
iconClass: 'library-tab-icon',
searchable: service,
installable: service,
itemLabel: (item: Library) => item.name,
itemRenderer
});
}
}

View File

@@ -1,88 +0,0 @@
import * as React from 'react';
import { inject, injectable, postConstruct } from 'inversify';
import { Message } from '@phosphor/messaging';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { MaybePromise } from '@theia/core/lib/common/types';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { LibraryFilterableListContainer } from './library-filterable-list-container';
import { LibraryService } from '../../common/protocol/library-service';
@injectable()
export class LibraryListWidget extends ReactWidget {
static WIDGET_ID = 'library-list-widget';
static WIDGET_LABEL = 'Library Manager';
@inject(LibraryService)
protected readonly libraryService: LibraryService;
@inject(WindowService)
protected readonly windowService: WindowService;
/**
* Do not touch or use it. It is for setting the focus on the `input` after the widget activation.
*/
protected focusNode: HTMLElement | undefined;
protected readonly deferredContainer = new Deferred<HTMLElement>();
constructor() {
super();
this.id = LibraryListWidget.WIDGET_ID
this.title.label = LibraryListWidget.WIDGET_LABEL;
this.title.caption = LibraryListWidget.WIDGET_LABEL
this.title.iconClass = 'library-tab-icon';
this.title.closable = true;
this.addClass('arduino-list-widget');
this.node.tabIndex = 0; // To be able to set the focus on the widget.
this.scrollOptions = {
suppressScrollX: true
}
}
@postConstruct()
protected init(): void {
this.update();
}
protected getScrollContainer(): MaybePromise<HTMLElement> {
return this.deferredContainer.promise;
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
(this.focusNode || this.node).focus();
}
protected onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.render();
}
protected onFocusResolved = (element: HTMLElement | undefined) => {
this.focusNode = element;
}
render(): React.ReactNode {
return <LibraryFilterableListContainer
resolveContainer={this.deferredContainer.resolve}
resolveFocus={this.onFocusResolved}
service={this.libraryService}
windowService={this.windowService}
/>;
}
}
export namespace ListWidget {
/**
* Props for customizing the abstract list widget.
*/
export interface Props {
readonly id: string;
readonly title: string;
readonly iconClass: string;
}
}

View File

@@ -0,0 +1,35 @@
import { inject, injectable, postConstruct } from 'inversify';
import { Diagnostic } from 'vscode-languageserver-types';
import URI from '@theia/core/lib/common/uri';
import { ILogger } from '@theia/core';
import { Marker } from '@theia/markers/lib/common/marker';
import { ProblemManager } from '@theia/markers/lib/browser/problem/problem-manager';
import { ConfigService } from '../../common/protocol/config-service';
@injectable()
export class ArduinoProblemManager extends ProblemManager {
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(ILogger)
protected readonly logger: ILogger;
protected dataDirUri: URI | undefined;
@postConstruct()
protected init(): void {
super.init();
this.configService.getConfiguration()
.then(({ dataDirUri }) => this.dataDirUri = new URI(dataDirUri))
.catch(err => this.logger.error(`Failed to determine the data directory: ${err}`));
}
setMarkers(uri: URI, owner: string, data: Diagnostic[]): Marker<Diagnostic>[] {
if (this.dataDirUri && this.dataDirUri.isEqualOrParent(uri)) {
return [];
}
return super.setMarkers(uri, owner, data);
}
}

View File

@@ -1,11 +1,13 @@
import { injectable } from "inversify";
import { BrowserMenuBarContribution } from "@theia/core/lib/browser/menu/browser-menu-plugin";
import { FrontendApplication } from "@theia/core/lib/browser";
import { injectable } from 'inversify';
import { FrontendApplication } from '@theia/core/lib/browser';
import { BrowserMenuBarContribution } from '@theia/core/lib/browser/menu/browser-menu-plugin';
@injectable()
export class ArduinoMenuContribution extends BrowserMenuBarContribution {
onStart(app: FrontendApplication): void {
const menu = this.factory.createMenuBar();
app.shell.addWidget(menu, { area: 'top' });
}
}
}

View File

@@ -7,4 +7,4 @@ import '../../../src/browser/style/browser-menu.css'
export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind) => {
unbind(BrowserMenuBarContribution);
bind(BrowserMenuBarContribution).to(ArduinoMenuContribution).inSingletonScope();
})
});

View File

@@ -0,0 +1,265 @@
import { injectable, inject, postConstruct } from 'inversify';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { MessageService } from '@theia/core/lib/common/message-service';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { MonitorService, MonitorConfig, MonitorError, Status, MonitorReadEvent } from '../../common/protocol/monitor-service';
import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
import { Port, Board, BoardsService, AttachedSerialBoard, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service';
import { MonitorServiceClientImpl } from './monitor-service-client-impl';
import { BoardsConfig } from '../boards/boards-config';
import { MonitorModel } from './monitor-model';
@injectable()
export class MonitorConnection {
@inject(MonitorModel)
protected readonly monitorModel: MonitorModel;
@inject(MonitorService)
protected readonly monitorService: MonitorService;
@inject(MonitorServiceClientImpl)
protected readonly monitorServiceClient: MonitorServiceClientImpl;
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsServiceClientImpl)
protected boardsServiceClient: BoardsServiceClientImpl;
@inject(MessageService)
protected messageService: MessageService;
@inject(FrontendApplicationStateService)
protected readonly applicationState: FrontendApplicationStateService;
protected state: MonitorConnection.State | undefined;
/**
* Note: The idea is to toggle this property from the UI (`Monitor` view)
* and the boards config and the boards attachment/detachment logic can be at on place, here.
*/
protected _autoConnect: boolean = false;
protected readonly onConnectionChangedEmitter = new Emitter<MonitorConnection.State | undefined>();
/**
* This emitter forwards all read events **iff** the connection is established.
*/
protected readonly onReadEmitter = new Emitter<MonitorReadEvent>();
/**
* Array for storing previous monitor errors received from the server, and based on the number of elements in this array,
* we adjust the reconnection delay.
* Super naive way: we wait `array.length * 1000` ms. Once we hit 10 errors, we do not try to reconnect and clean the array.
*/
protected monitorErrors: MonitorError[] = [];
protected reconnectTimeout?: number;
@postConstruct()
protected init(): void {
// Forward the messages from the board **iff** connected.
this.monitorServiceClient.onRead(event => {
if (this.connected) {
this.onReadEmitter.fire(event);
}
});
this.monitorServiceClient.onError(async error => {
let shouldReconnect = false;
if (this.state) {
const { code, config } = error;
const { board, port } = config;
const options = { timeout: 3000 };
switch (code) {
case MonitorError.ErrorCodes.CLIENT_CANCEL: {
console.debug(`Connection was canceled by client: ${MonitorConnection.State.toString(this.state)}.`);
break;
}
case MonitorError.ErrorCodes.DEVICE_BUSY: {
this.messageService.warn(`Connection failed. Serial port is busy: ${Port.toString(port)}.`, options);
shouldReconnect = this.autoConnect;
this.monitorErrors.push(error);
break;
}
case MonitorError.ErrorCodes.DEVICE_NOT_CONFIGURED: {
this.messageService.info(`Disconnected ${Board.toString(board, { useFqbn: false })} from ${Port.toString(port)}.`, options);
break;
}
case undefined: {
this.messageService.error(`Unexpected error. Reconnecting ${Board.toString(board)} on port ${Port.toString(port)}.`, options);
console.error(JSON.stringify(error));
shouldReconnect = this.connected && this.autoConnect;
break;
}
}
const oldState = this.state;
this.state = undefined;
this.onConnectionChangedEmitter.fire(this.state);
if (shouldReconnect) {
if (this.monitorErrors.length >= 10) {
this.messageService.warn(`Failed to reconnect ${Board.toString(board, { useFqbn: false })} to the the serial-monitor after 10 consecutive attempts. The ${Port.toString(port)} serial port is busy. after 10 consecutive attempts.`);
this.monitorErrors.length = 0;
} else {
const attempts = (this.monitorErrors.length || 1);
if (this.reconnectTimeout !== undefined) {
// Clear the previous timer.
window.clearTimeout(this.reconnectTimeout);
}
const timeout = attempts * 1000;
this.messageService.warn(`Reconnecting ${Board.toString(board, { useFqbn: false })} to ${Port.toString(port)} in ${attempts} seconds...`, { timeout });
this.reconnectTimeout = window.setTimeout(() => this.connect(oldState.config), timeout);
}
}
}
});
this.boardsServiceClient.onBoardsConfigChanged(this.handleBoardConfigChange.bind(this));
this.boardsServiceClient.onBoardsChanged(event => {
if (this.autoConnect && this.connected) {
const { boardsConfig } = this.boardsServiceClient;
if (this.boardsServiceClient.canUploadTo(boardsConfig, { silent: false })) {
const { attached } = AttachedBoardsChangeEvent.diff(event);
if (attached.boards.some(board => AttachedSerialBoard.is(board) && BoardsConfig.Config.sameAs(boardsConfig, board))) {
const { selectedBoard: board, selectedPort: port } = boardsConfig;
const { baudRate } = this.monitorModel;
this.disconnect()
.then(() => this.connect({ board, port, baudRate }));
}
}
}
});
// Handles the `baudRate` changes by reconnecting if required.
this.monitorModel.onChange(({ property }) => {
if (property === 'baudRate' && this.autoConnect && this.connected) {
const { boardsConfig } = this.boardsServiceClient;
this.handleBoardConfigChange(boardsConfig);
}
});
}
get connected(): boolean {
return !!this.state;
}
get monitorConfig(): MonitorConfig | undefined {
return this.state ? this.state.config : undefined;
}
get autoConnect(): boolean {
return this._autoConnect;
}
set autoConnect(value: boolean) {
const oldValue = this._autoConnect;
this._autoConnect = value;
// When we enable the auto-connect, we have to connect
if (!oldValue && value) {
// We have to make sure the previous boards config has been restored.
// Otherwise, we might start the auto-connection without configured boards.
this.applicationState.reachedState('started_contributions').then(() => {
const { boardsConfig } = this.boardsServiceClient;
this.handleBoardConfigChange(boardsConfig);
});
} else if (oldValue && !value) {
if (this.reconnectTimeout !== undefined) {
window.clearTimeout(this.reconnectTimeout);
this.monitorErrors.length = 0;
}
}
}
async connect(config: MonitorConfig): Promise<Status> {
if (this.connected) {
const disconnectStatus = await this.disconnect();
if (!Status.isOK(disconnectStatus)) {
return disconnectStatus;
}
}
console.info(`>>> Creating serial monitor connection for ${Board.toString(config.board)} on port ${Port.toString(config.port)}...`);
const connectStatus = await this.monitorService.connect(config);
if (Status.isOK(connectStatus)) {
this.state = { config };
console.info(`<<< Serial monitor connection created for ${Board.toString(config.board, { useFqbn: false })} on port ${Port.toString(config.port)}.`);
}
this.onConnectionChangedEmitter.fire(this.state);
return Status.isOK(connectStatus);
}
async disconnect(): Promise<Status> {
if (!this.state) { // XXX: we user `this.state` instead of `this.connected` to make the type checker happy.
return Status.OK;
}
console.log('>>> Disposing existing monitor connection...');
const status = await this.monitorService.disconnect();
if (Status.isOK(status)) {
console.log(`<<< Disposed connection. Was: ${MonitorConnection.State.toString(this.state)}`);
} else {
console.warn(`<<< Could not dispose connection. Activate connection: ${MonitorConnection.State.toString(this.state)}`);
}
this.state = undefined;
this.onConnectionChangedEmitter.fire(this.state);
return status;
}
/**
* Sends the data to the connected serial monitor.
* The desired EOL is appended to `data`, you do not have to add it.
* It is a NOOP if connected.
*/
async send(data: string): Promise<Status> {
if (!this.connected) {
return Status.NOT_CONNECTED;
}
return new Promise<Status>(resolve => {
this.monitorService.send(data + this.monitorModel.lineEnding)
.then(() => resolve(Status.OK));
});
}
get onConnectionChanged(): Event<MonitorConnection.State | undefined> {
return this.onConnectionChangedEmitter.event;
}
get onRead(): Event<MonitorReadEvent> {
return this.onReadEmitter.event;
}
protected async handleBoardConfigChange(boardsConfig: BoardsConfig.Config): Promise<void> {
if (this.autoConnect) {
if (this.boardsServiceClient.canUploadTo(boardsConfig, { silent: false })) {
// Instead of calling `getAttachedBoards` and filtering for `AttachedSerialBoard` we have to check the available ports.
// The connected board might be unknown. See: https://github.com/arduino/arduino-pro-ide/issues/127#issuecomment-563251881
this.boardsService.getAvailablePorts().then(({ ports }) => {
if (ports.some(port => Port.equals(port, boardsConfig.selectedPort))) {
new Promise<void>(resolve => {
// First, disconnect if connected.
if (this.connected) {
this.disconnect().then(() => resolve());
return;
}
resolve();
}).then(() => {
// Then (re-)connect.
const { selectedBoard: board, selectedPort: port } = boardsConfig;
const { baudRate } = this.monitorModel;
this.connect({ board, port, baudRate });
});
}
});
}
}
}
}
export namespace MonitorConnection {
export interface State {
readonly config: MonitorConfig;
}
export namespace State {
export function toString(state: State): string {
const { config } = state;
const { board, port } = config;
return `${Board.toString(board)} ${Port.toString(port)}`;
}
}
}

View File

@@ -0,0 +1,119 @@
import { injectable, inject } from 'inversify';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { MonitorConfig } from '../../common/protocol/monitor-service';
import { FrontendApplicationContribution, LocalStorageService } from '@theia/core/lib/browser';
import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
@injectable()
export class MonitorModel implements FrontendApplicationContribution {
protected static STORAGE_ID = 'arduino-monitor-model';
@inject(LocalStorageService)
protected readonly localStorageService: LocalStorageService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
protected readonly onChangeEmitter: Emitter<MonitorModel.State.Change<keyof MonitorModel.State>>;
protected _autoscroll: boolean;
protected _timestamp: boolean;
protected _baudRate: MonitorConfig.BaudRate;
protected _lineEnding: MonitorModel.EOL;
constructor() {
this._autoscroll = true;
this._timestamp = false;
this._baudRate = MonitorConfig.BaudRate.DEFAULT;
this._lineEnding = MonitorModel.EOL.DEFAULT;
this.onChangeEmitter = new Emitter<MonitorModel.State.Change<keyof MonitorModel.State>>();
}
onStart(): void {
this.localStorageService.getData<MonitorModel.State>(MonitorModel.STORAGE_ID).then(state => {
if (state) {
this.restoreState(state);
}
});
}
get onChange(): Event<MonitorModel.State.Change<keyof MonitorModel.State>> {
return this.onChangeEmitter.event;
}
get autoscroll(): boolean {
return this._autoscroll;
}
toggleAutoscroll(): void {
this._autoscroll = !this._autoscroll;
this.storeState();
this.storeState().then(() => this.onChangeEmitter.fire({ property: 'autoscroll', value: this._autoscroll }));
}
get timestamp(): boolean {
return this._timestamp;
}
toggleTimestamp(): void {
this._timestamp = !this._timestamp;
this.storeState().then(() => this.onChangeEmitter.fire({ property: 'timestamp', value: this._timestamp }));
}
get baudRate(): MonitorConfig.BaudRate {
return this._baudRate;
}
set baudRate(baudRate: MonitorConfig.BaudRate) {
this._baudRate = baudRate;
this.storeState().then(() => this.onChangeEmitter.fire({ property: 'baudRate', value: this._baudRate }));
}
get lineEnding(): MonitorModel.EOL {
return this._lineEnding;
}
set lineEnding(lineEnding: MonitorModel.EOL) {
this._lineEnding = lineEnding;
this.storeState().then(() => this.onChangeEmitter.fire({ property: 'lineEnding', value: this._lineEnding }));
}
protected restoreState(state: MonitorModel.State): void {
this._autoscroll = state.autoscroll;
this._timestamp = state.timestamp;
this._baudRate = state.baudRate;
this._lineEnding = state.lineEnding;
}
protected async storeState(): Promise<void> {
return this.localStorageService.setData(MonitorModel.STORAGE_ID, {
autoscroll: this._autoscroll,
timestamp: this._timestamp,
baudRate: this._baudRate,
lineEnding: this._lineEnding
});
}
}
export namespace MonitorModel {
export interface State {
autoscroll: boolean;
timestamp: boolean;
baudRate: MonitorConfig.BaudRate;
lineEnding: EOL;
}
export namespace State {
export interface Change<K extends keyof State> {
readonly property: K;
readonly value: State[K];
}
}
export type EOL = '' | '\n' | '\r' | '\r\n';
export namespace EOL {
export const DEFAULT: EOL = '\n';
}
}

View File

@@ -0,0 +1,23 @@
import { injectable } from 'inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { MonitorServiceClient, MonitorReadEvent, MonitorError } from '../../common/protocol/monitor-service';
@injectable()
export class MonitorServiceClientImpl implements MonitorServiceClient {
protected readonly onReadEmitter = new Emitter<MonitorReadEvent>();
protected readonly onErrorEmitter = new Emitter<MonitorError>();
readonly onRead = this.onReadEmitter.event;
readonly onError = this.onErrorEmitter.event;
notifyRead(event: MonitorReadEvent): void {
this.onReadEmitter.fire(event);
const { data } = event;
console.debug(`Received data: ${data}`);
}
notifyError(error: MonitorError): void {
this.onErrorEmitter.fire(error);
}
}

View File

@@ -0,0 +1,128 @@
import * as React from 'react';
import { injectable, inject } from "inversify";
import { AbstractViewContribution } from "@theia/core/lib/browser";
import { MonitorWidget } from "./monitor-widget";
import { MenuModelRegistry, Command, CommandRegistry } from "@theia/core";
import { ArduinoMenus } from "../arduino-frontend-contribution";
import { TabBarToolbarContribution, TabBarToolbarRegistry } from "@theia/core/lib/browser/shell/tab-bar-toolbar";
import { MonitorModel } from './monitor-model';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
export namespace SerialMonitor {
export namespace Commands {
export const AUTOSCROLL: Command = {
id: 'serial-monitor-autoscroll',
label: 'Autoscroll'
}
export const TIMESTAMP: Command = {
id: 'serial-monitor-timestamp',
label: 'Timestamp'
}
export const CLEAR_OUTPUT: Command = {
id: 'serial-monitor-clear-output',
label: 'Clear Output',
iconClass: 'clear-all'
}
}
}
@injectable()
export class MonitorViewContribution extends AbstractViewContribution<MonitorWidget> implements TabBarToolbarContribution {
static readonly OPEN_SERIAL_MONITOR = MonitorWidget.ID + ':toggle';
@inject(MonitorModel) protected readonly model: MonitorModel;
constructor() {
super({
widgetId: MonitorWidget.ID,
widgetName: 'Serial Monitor',
defaultWidgetOptions: {
area: 'bottom'
},
toggleCommandId: MonitorViewContribution.OPEN_SERIAL_MONITOR,
toggleKeybinding: 'ctrlcmd+shift+m'
})
}
registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS, {
commandId: this.toggleCommand.id,
label: 'Serial Monitor'
});
}
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: 'monitor-autoscroll',
render: () => this.renderAutoScrollButton(),
isVisible: widget => widget instanceof MonitorWidget,
onDidChange: this.model.onChange as any // XXX: it's a hack. See: https://github.com/eclipse-theia/theia/pull/6696/
});
registry.registerItem({
id: 'monitor-timestamp',
render: () => this.renderTimestampButton(),
isVisible: widget => widget instanceof MonitorWidget,
onDidChange: this.model.onChange as any // XXX: it's a hack. See: https://github.com/eclipse-theia/theia/pull/6696/
});
registry.registerItem({
id: SerialMonitor.Commands.CLEAR_OUTPUT.id,
command: SerialMonitor.Commands.CLEAR_OUTPUT.id,
tooltip: 'Clear Output'
});
}
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(SerialMonitor.Commands.CLEAR_OUTPUT, {
isEnabled: widget => widget instanceof MonitorWidget,
isVisible: widget => widget instanceof MonitorWidget,
execute: widget => {
if (widget instanceof MonitorWidget) {
widget.clearConsole();
}
}
});
if (this.toggleCommand) {
commands.registerCommand(this.toggleCommand, {
execute: () => this.openView({
toggle: true,
activate: true
}),
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'right'
});
}
}
protected renderAutoScrollButton(): React.ReactNode {
return <React.Fragment key='autoscroll-toolbar-item'>
<div
title='Toggle Autoscroll'
className={`item enabled fa fa-angle-double-down arduino-monitor ${this.model.autoscroll ? 'toggled' : ''}`}
onClick={this.toggleAutoScroll}
></div>
</React.Fragment>;
}
protected readonly toggleAutoScroll = () => this.doToggleAutoScroll();
protected async doToggleAutoScroll(): Promise<void> {
this.model.toggleAutoscroll();
}
protected renderTimestampButton(): React.ReactNode {
return <React.Fragment key='line-ending-toolbar-item'>
<div
title='Toggle Timestamp'
className={`item enabled fa fa-clock-o arduino-monitor ${this.model.timestamp ? 'toggled' : ''}`}
onClick={this.toggleTimestamp}
></div>
</React.Fragment>;
}
protected readonly toggleTimestamp = () => this.doToggleTimestamp();
protected async doToggleTimestamp(): Promise<void> {
this.model.toggleTimestamp();
}
}

View File

@@ -0,0 +1,333 @@
import * as React from 'react';
import * as dateFormat from 'dateformat';
import { postConstruct, injectable, inject } from 'inversify';
import { OptionsType } from 'react-select/src/types';
import { isOSX } from '@theia/core/lib/common/os';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { Key, KeyCode } from '@theia/core/lib/browser/keys';
import { DisposableCollection } from '@theia/core/lib/common/disposable'
import { ReactWidget, Message, Widget, MessageLoop } from '@theia/core/lib/browser/widgets';
import { Board, Port } from '../../common/protocol/boards-service';
import { MonitorConfig } from '../../common/protocol/monitor-service';
import { ArduinoSelect } from '../components/arduino-select';
import { MonitorModel } from './monitor-model';
import { MonitorConnection } from './monitor-connection';
import { MonitorServiceClientImpl } from './monitor-service-client-impl';
@injectable()
export class MonitorWidget extends ReactWidget {
static readonly ID = 'serial-monitor';
@inject(MonitorModel)
protected readonly monitorModel: MonitorModel;
@inject(MonitorConnection)
protected readonly monitorConnection: MonitorConnection;
@inject(MonitorServiceClientImpl)
protected readonly monitorServiceClient: MonitorServiceClientImpl;
protected widgetHeight: number;
/**
* Do not touch or use it. It is for setting the focus on the `input` after the widget activation.
*/
protected focusNode: HTMLElement | undefined;
/**
* Guard against re-rendering the view after the close was requested.
* See: https://github.com/eclipse-theia/theia/issues/6704
*/
protected closing = false;
protected readonly clearOutputEmitter = new Emitter<void>();
constructor() {
super();
this.id = MonitorWidget.ID;
this.title.label = 'Serial Monitor';
this.title.iconClass = 'arduino-serial-monitor-tab-icon';
this.title.closable = true;
this.scrollOptions = undefined;
this.toDispose.push(this.clearOutputEmitter);
}
@postConstruct()
protected init(): void {
this.update();
this.toDispose.push(this.monitorConnection.onConnectionChanged(() => this.clearConsole()));
}
clearConsole(): void {
this.clearOutputEmitter.fire(undefined);
this.update();
}
dispose(): void {
super.dispose();
}
protected onAfterAttach(msg: Message): void {
super.onAfterAttach(msg);
this.monitorConnection.autoConnect = true;
}
onCloseRequest(msg: Message): void {
this.closing = true;
this.monitorConnection.autoConnect = false;
if (this.monitorConnection.connected) {
this.monitorConnection.disconnect();
}
super.onCloseRequest(msg);
}
protected onUpdateRequest(msg: Message): void {
// TODO: `this.isAttached`
// See: https://github.com/eclipse-theia/theia/issues/6704#issuecomment-562574713
if (!this.closing && this.isAttached) {
super.onUpdateRequest(msg);
}
}
protected onResize(msg: Widget.ResizeMessage): void {
super.onResize(msg);
this.widgetHeight = msg.height;
this.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
(this.focusNode || this.node).focus();
}
protected onFocusResolved = (element: HTMLElement | undefined) => {
this.focusNode = element;
requestAnimationFrame(() => MessageLoop.sendMessage(this, Widget.Msg.ActivateRequest));
}
protected get lineEndings(): OptionsType<SelectOption<MonitorModel.EOL>> {
return [
{
label: 'No Line Ending',
value: ''
},
{
label: 'New Line',
value: '\n'
},
{
label: 'Carriage Return',
value: '\r'
},
{
label: 'Both NL & CR',
value: '\r\n'
}
];
}
protected get baudRates(): OptionsType<SelectOption<MonitorConfig.BaudRate>> {
const baudRates: Array<MonitorConfig.BaudRate> = [300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200];
return baudRates.map(baudRate => ({ label: baudRate + ' baud', value: baudRate }));
}
protected render(): React.ReactNode {
const { baudRates, lineEndings } = this;
const lineEnding = lineEndings.find(item => item.value === this.monitorModel.lineEnding) || lineEndings[1]; // Defaults to `\n`.
const baudRate = baudRates.find(item => item.value === this.monitorModel.baudRate) || baudRates[4]; // Defaults to `9600`.
return <div className='serial-monitor'>
<div className='head'>
<div className='send'>
<SerialMonitorSendInput
monitorConfig={this.monitorConnection.monitorConfig}
resolveFocus={this.onFocusResolved}
onSend={this.onSend} />
</div>
<div className='config'>
<div className='select'>
<ArduinoSelect
maxMenuHeight={this.widgetHeight - 40}
options={lineEndings}
defaultValue={lineEnding}
onChange={this.onChangeLineEnding} />
</div>
<div className='select'>
<ArduinoSelect
className='select'
maxMenuHeight={this.widgetHeight - 40}
options={baudRates}
defaultValue={baudRate}
onChange={this.onChangeBaudRate} />
</div>
</div>
</div>
<div className='body'>
<SerialMonitorOutput
monitorModel={this.monitorModel}
monitorConnection={this.monitorConnection}
clearConsoleEvent={this.clearOutputEmitter.event} />
</div>
</div>;
}
protected readonly onSend = (value: string) => this.doSend(value);
protected async doSend(value: string): Promise<void> {
this.monitorConnection.send(value);
}
protected readonly onChangeLineEnding = (option: SelectOption<MonitorModel.EOL>) => {
this.monitorModel.lineEnding = option.value;
}
protected readonly onChangeBaudRate = async (option: SelectOption<MonitorConfig.BaudRate>) => {
this.monitorModel.baudRate = option.value;
}
}
export namespace SerialMonitorSendInput {
export interface Props {
readonly monitorConfig?: MonitorConfig;
readonly onSend: (text: string) => void;
readonly resolveFocus: (element: HTMLElement | undefined) => void;
}
export interface State {
text: string;
}
}
export class SerialMonitorSendInput extends React.Component<SerialMonitorSendInput.Props, SerialMonitorSendInput.State> {
constructor(props: Readonly<SerialMonitorSendInput.Props>) {
super(props);
this.state = { text: '' };
this.onChange = this.onChange.bind(this);
this.onSend = this.onSend.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
}
render(): React.ReactNode {
return <input
ref={this.setRef}
type='text'
className={this.props.monitorConfig ? '' : 'warning'}
placeholder={this.placeholder}
value={this.state.text}
onChange={this.onChange}
onKeyDown={this.onKeyDown} />
}
protected get placeholder(): string {
const { monitorConfig } = this.props;
if (!monitorConfig) {
return 'Not connected. Select a board and a port to connect automatically.'
}
const { board, port } = monitorConfig;
return `Message (${isOSX ? '⌘' : 'Ctrl'}+Enter to send message to '${Board.toString(board, { useFqbn: false })}' on '${Port.toString(port)}')`;
}
protected setRef = (element: HTMLElement | null) => {
if (this.props.resolveFocus) {
this.props.resolveFocus(element || undefined);
}
}
protected onChange(event: React.ChangeEvent<HTMLInputElement>): void {
this.setState({ text: event.target.value });
}
protected onSend(): void {
this.props.onSend(this.state.text);
this.setState({ text: '' });
}
protected onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
const keyCode = KeyCode.createKeyCode(event.nativeEvent);
if (keyCode) {
const { key, meta, ctrl } = keyCode;
if (key === Key.ENTER && ((isOSX && meta) || (!isOSX && ctrl))) {
this.onSend();
}
}
}
}
export namespace SerialMonitorOutput {
export interface Props {
readonly monitorModel: MonitorModel;
readonly monitorConnection: MonitorConnection;
readonly clearConsoleEvent: Event<void>;
}
export interface State {
content: string;
timestamp: boolean;
}
}
export class SerialMonitorOutput extends React.Component<SerialMonitorOutput.Props, SerialMonitorOutput.State> {
/**
* Do not touch it. It is used to be able to "follow" the serial monitor log.
*/
protected anchor: HTMLElement | null;
protected toDisposeBeforeUnmount = new DisposableCollection();
constructor(props: Readonly<SerialMonitorOutput.Props>) {
super(props);
this.state = { content: '', timestamp: this.props.monitorModel.timestamp };
}
render(): React.ReactNode {
return <React.Fragment>
<div style={({ whiteSpace: 'pre', fontFamily: 'monospace' })}>
{this.state.content}
</div>
<div style={{ float: 'left', clear: 'both' }} ref={element => { this.anchor = element; }} />
</React.Fragment>;
}
componentDidMount(): void {
this.scrollToBottom();
let chunk = '';
this.toDisposeBeforeUnmount.pushAll([
this.props.monitorConnection.onRead(({ data }) => {
chunk += data;
const eolIndex = chunk.indexOf('\n');
if (eolIndex !== -1) {
const line = chunk.substring(0, eolIndex + 1);
chunk = chunk.slice(eolIndex + 1);
const content = `${this.state.content}${this.state.timestamp ? `${dateFormat(new Date(), 'H:M:ss.l')} -> ` : ''}${line}`;
this.setState({ content });
}
}),
this.props.clearConsoleEvent(() => this.setState({ content: '' })),
this.props.monitorModel.onChange(({ property }) => {
if (property === 'timestamp') {
const { timestamp } = this.props.monitorModel;
this.setState({ timestamp });
}
})
]);
}
componentDidUpdate(): void {
this.scrollToBottom();
}
componentWillUnmount(): void {
// TODO: "Your preferred browser's local storage is almost full." Discard `content` before saving layout?
this.toDisposeBeforeUnmount.dispose();
}
protected scrollToBottom(): void {
if (this.props.monitorModel.autoscroll && this.anchor) {
this.anchor.scrollIntoView();
}
}
}
export interface SelectOption<T> {
readonly label: string;
readonly value: T;
}

View File

@@ -0,0 +1,24 @@
import { injectable } from 'inversify';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { ShellLayoutRestorer } from '@theia/core/lib/browser/shell/shell-layout-restorer';
@injectable()
export class ArduinoShellLayoutRestorer extends ShellLayoutRestorer {
// Workaround for https://github.com/eclipse-theia/theia/issues/6579.
async storeLayoutAsync(app: FrontendApplication): Promise<void> {
if (this.shouldStoreLayout) {
try {
this.logger.info('>>> Storing the layout...');
const layoutData = app.shell.getLayoutData();
const serializedLayoutData = this.deflate(layoutData);
await this.storageService.setData(this.storageKey, serializedLayoutData);
this.logger.info('<<< The layout has been successfully stored.');
} catch (error) {
await this.storageService.setData(this.storageKey, undefined);
this.logger.error('Error during serialization of layout data', error);
}
}
}
}

View File

@@ -0,0 +1,40 @@
import { inject, injectable, postConstruct } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { Title, Widget } from '@phosphor/widgets';
import { ILogger } from '@theia/core';
import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
import { TabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
import { ConfigService } from '../../common/protocol/config-service';
import { EditorWidget } from '@theia/editor/lib/browser';
@injectable()
export class ArduinoTabBarDecoratorService extends TabBarDecoratorService {
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(ILogger)
protected readonly logger: ILogger;
protected dataDirUri: URI | undefined;
@postConstruct()
protected init(): void {
super.init();
this.configService.getConfiguration()
.then(({ dataDirUri }) => this.dataDirUri = new URI(dataDirUri))
.catch(err => this.logger.error(`Failed to determine the data directory: ${err}`));
}
getDecorations(title: Title<Widget>): WidgetDecoration.Data[] {
if (title.owner instanceof EditorWidget) {
const editor = title.owner.editor;
if (this.dataDirUri && this.dataDirUri.isEqualOrParent(editor.uri)) {
return [];
}
}
return super.getDecorations(title);
}
}

View File

@@ -1,60 +0,0 @@
import { injectable, inject } from "inversify";
import URI from "@theia/core/lib/common/uri";
import { FileSystem } from "@theia/filesystem/lib/common";
import { WindowService } from "@theia/core/lib/browser/window/window-service";
@injectable()
export class SketchFactory {
@inject(FileSystem)
protected readonly fileSystem: FileSystem;
@inject(WindowService)
protected readonly windowService: WindowService;
public async createNewSketch(parent: URI): Promise<void> {
const monthNames = ["january", "february", "march", "april", "may", "june",
"july", "august", "september", "october", "november", "december"
];
const today = new Date();
const sketchBaseName = `sketch_${monthNames[today.getMonth()]}${today.getDay()}`;
let sketchName: string | undefined;
for (let i = 97; i < 97 + 26; i++) {
let sketchNameCandidate = `${sketchBaseName}${String.fromCharCode(i)}`;
if (await this.fileSystem.exists(parent.resolve(sketchNameCandidate).toString())) {
continue;
}
sketchName = sketchNameCandidate;
break;
}
if (!sketchName) {
throw new Error("Cannot create a unique sketch name");
}
try {
const sketchDir = parent.resolve(sketchName);
const sketchFile = sketchDir.resolve(`${sketchName}.ino`);
this.fileSystem.createFolder(sketchDir.toString());
this.fileSystem.createFile(sketchFile.toString(), { content: `
void setup() {
// put your setup code here, to run once:
}
void loop() {
// put your main code here, to run repeatedly:
}
` });
const location = new URL(window.location.href);
location.searchParams.set('sketch', sketchFile.toString());
this.windowService.openNewWindow(location.toString());
} catch (e) {
throw new Error("Cannot create new sketch: " + e);
}
}
}

View File

@@ -0,0 +1,45 @@
.arduino-select__control {
border: var(--theia-layout-color2) var(--theia-border-width) solid !important;
background: var(--theia-layout-color2) !important;
}
.arduino-select__control:hover {
border: var(--theia-layout-color2) var(--theia-border-width) solid !important;
}
.arduino-select__control--is-focused {
box-shadow: none !important;
}
.arduino-select__option--is-selected {
background-color: var(--theia-accent-color3) !important;
color: var(--theia-content-font-color0) !important;
border-color: var(--theia-accent-color3) !important;
}
.arduino-select__option--is-focused {
background-color: var(--theia-accent-color4) !important;
border-color: var(--theia-accent-color3) !important;
}
.arduino-select__menu {
background-color: var(--theia-layout-color2) !important;
border: 1px solid var(--theia-accent-color3) !important;
top: auto !important; /* to align the top of the menu with the bottom of the control */
box-shadow: none !important;
}
.arduino-select__control.arduino-select__control--menu-is-open {
border: 1px solid !important;
border-color: var(--theia-accent-color3) !important;
border-bottom-color: var(--theia-layout-color2) !important; /* if the bottom border color has the same color as the background of the control, we make the border "invisible" */
}
.arduino-select__value-container .arduino-select__single-value {
color: var(--theia-ui-font-color1) !important;
}
.arduino-select__menu-list {
padding-top: 0 !important;
padding-bottom: 0 !important;
}

View File

@@ -18,47 +18,57 @@ is not optimized for dense, information rich UIs.
:root {
/* Custom Theme Colors */
--theia-arduino-light: rgb(0, 102, 102);
--theia-arduino-light1: rgb(0, 153, 153);
--theia-arduino-light: rgb(0, 100, 104);
--theia-arduino-light1: rgb(23, 161, 165);
--theia-arduino-light2: rgb(218, 226, 228);
--theia-arduino-light3: rgb(237, 241, 242);
--theia-arduino-terminal: rgb(0, 0, 0);
/* Borders: Width and color (bright to dark) */
--theia-border-width: 1px;
--theia-panel-border-width: 2px;
--theia-border-color0: var(--md-grey-100);
--theia-border-color1: var(--md-grey-200);
--theia-border-color2: var(--md-grey-300);
--theia-border-color3: var(--md-grey-400);
/* UI fonts: Family, size and color (dark to bright)
---------------------------------------------------
The UI font CSS variables are used for the typography all of the Theia
user interface elements that are not directly user-generated content.
*/
--theia-ui-font-scale-factor: 1.2;
--theia-ui-font-size0: calc(var(--theia-ui-font-size1) / var(--theia-ui-font-scale-factor));
--theia-ui-font-size1: 13px;
/* Base font size */
--theia-ui-font-size1: 13px; /* Base font size */
--theia-ui-font-size2: calc(var(--theia-ui-font-size1) * var(--theia-ui-font-scale-factor));
--theia-ui-font-size3: calc(var(--theia-ui-font-size2) * var(--theia-ui-font-scale-factor));
--theia-ui-icon-font-size: 14px;
/* Ensures px perfect FontAwesome icons */
--theia-ui-icon-font-size: 14px; /* Ensures px perfect FontAwesome icons */
--theia-ui-font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
--theia-ui-font-color0: var(--md-grey-900);
--theia-ui-font-color1: var(--md-grey-700);
--theia-ui-font-color2: var(--md-grey-500);
--theia-ui-font-color3: var(--md-grey-300);
/* Special font colors */
--theia-ui-icon-font-color: #ffffff;
--theia-ui-bar-font-color0: var(--theia-ui-font-color0);
--theia-ui-bar-font-color1: var(--theia-inverse-ui-font-color0); /* var(--theia-ui-font-color1); */
/* Use the inverse UI colors against the brand/accent/warn/error colors. */
--theia-inverse-ui-font-color0: rgba(255, 255, 255, 1.0);
--theia-inverse-ui-font-color2: rgba(255, 255, 255, 0.7);
--theia-inverse-ui-font-color3: rgba(255, 255, 255, 0.5);
/* Content fonts: Family, size and color (dark to bright)
Content font variables are used for typography of user-generated content.
*/
--theia-content-font-size: 13px;
--theia-content-line-height: 1.5;
--theia-content-font-color0: black;
@@ -71,75 +81,101 @@ is not optimized for dense, information rich UIs.
--theia-code-font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback";
--theia-terminal-font-family: monospace;
--theia-ui-padding: 6px;
/* Tab Icon Colors */
--theia-tab-icon-color: var(--theia-ui-font-color1);
--theia-tab-font-color: #000;
/* Main layout colors (bright to dark)
------------------------------------ */
------------------------------------ */
--theia-layout-color0: #ffffff;
--theia-layout-color1: var(--theia-arduino-light1);
--theia-layout-color1: #f7f9f9;
--theia-layout-color2: #ececec;
--theia-layout-color3: var(--theia-arduino-light);
--theia-layout-color3: var(--theia-arduino-light2);
--theia-layout-color4: #dcdcdc;
/* Brand colors */
--theia-brand-color0: var(--md-blue-700);
--theia-brand-color1: var(--md-blue-500);
--theia-brand-color2: var(--md-blue-300);
--theia-brand-color3: var(--md-blue-100);
--theia-brand-color0: var(--theia-arduino-light);
--theia-brand-color1: var(--theia-arduino-light1);
--theia-brand-color2: var(--theia-arduino-light2);
--theia-brand-color3: var(--theia-arduino-light3);
/* Secondary Brand colors */
--theia-secondary-brand-color0: var(--md-grey-700);
--theia-secondary-brand-color1: #b5c8c9;
--theia-secondary-brand-color2: var(--md-grey-300);
--theia-secondary-brand-color3: var(--md-grey-100);
--theia-secondary-brand-color2: var(--theia-arduino-light2);
--theia-secondary-brand-color3: var(--theia-arduino-light3);
/* Accent colors (dark to bright): Use these to create contrast to layout colors. */
--theia-accent-color0: rgb(0, 102, 105);
--theia-accent-color1: rgb(0, 164, 167, 1.0);
--theia-accent-color2: rgb(0, 164, 167, 0.8);
--theia-accent-color3: rgb(0, 164, 167, 0.6);
--theia-accent-color4: rgb(0, 164, 167, 0.4);
--theia-accent-color0: var(--theia-arduino-light);
--theia-accent-color1: rgb(77, 183, 187, 1.0);
--theia-accent-color2: rgb(77, 183, 187, 0.8);
--theia-accent-color3: rgb(77, 183, 187, 0.6);
--theia-accent-color4: rgba(77, 183, 187, 0.4);
/* Accent colors with opacity */
--theia-transparent-accent-color0: hsla(210, 63%, 46%, 0.3);
--theia-transparent-accent-color1: hsla(207, 66%, 56%, 0.3);
--theia-transparent-accent-color2: hsla(207, 64%, 85%, 0.3);
--theia-transparent-accent-color3: hsla(205, 70%, 91%, 0.3);
/* State colors (warn, error, success, info)
------------------------------------------ */
--theia-warn-color0: var(--md-amber-500);
--theia-warn-color1: var(--md-amber-400);
--theia-warn-color2: var(--md-amber-300);
--theia-warn-color3: var(--md-amber-200);
--theia-warn-font-color0: var(--md-grey-900);
--theia-error-color0: var(--md-red-400);
--theia-error-color1: #da5b4a;
--theia-error-color2: var(--md-red-200);
--theia-error-color3: var(--md-red-100);
--theia-error-font-color0: var(--md-grey-300);
--theia-success-color0: var(--md-green-500);
--theia-success-color1: var(--md-green-300);
--theia-success-color2: var(--md-green-100);
--theia-success-color3: var(--md-green-50);
--theia-success-font-color0: var(--md-grey-300);
--theia-info-color0: var(--md-cyan-700);
--theia-info-color1: var(--md-cyan-500);
--theia-info-color2: var(--md-cyan-300);
--theia-info-color3: var(--md-cyan-100);
--theia-info-font-color0: var(--md-grey-300);
--theia-disabled-color0: var(--md-grey-500);
--theia-disabled-color1: var(--md-grey-300);
--theia-disabled-color2: var(--md-grey-200);
--theia-disabled-color3: var(--md-grey-50);
--theia-added-color0: rgba(0, 255, 0, 0.8);
--theia-removed-color0: rgba(230, 0, 0, 0.8);
--theia-modified-color0: rgba(0, 100, 150, 0.8);
/* Background for selected text */
--theia-selected-text-background: var(--theia-accent-color3);
/* Colors to highlight words in widgets like tree or editors */
--theia-word-highlight-color0: rgba(168, 172, 148, 0.7);
--theia-word-highlight-color1: rgba(253, 255, 0, 0.2);
--theia-word-highlight-match-color0: rgba(234, 92, 0, 0.33);
--theia-word-highlight-match-color1: rgba(234, 92, 0, 0.5);
--theia-word-highlight-replace-color0: rgba(155, 185, 85, 0.2);
/* Scroll-bars */
--theia-scrollbar-width: 10px;
--theia-scrollbar-rail-width: 10px;
--theia-scrollbar-thumb-color: hsla(0, 0%, 61%, .4);
@@ -147,7 +183,7 @@ is not optimized for dense, information rich UIs.
--theia-scrollbar-active-thumb-color: hsla(0, 0%, 39%, .4);
--theia-scrollbar-active-rail-color: transparent;
/* Menu */
--theia-menu-color0: var(--theia-layout-color3);
--theia-menu-color0: var(--theia-arduino-light);
--theia-menu-color1: var(--theia-layout-color0);
--theia-menu-color2: #dae3e3;
/* Statusbar */
@@ -155,9 +191,9 @@ is not optimized for dense, information rich UIs.
--theia-statusBar-font-color: var(--theia-inverse-ui-font-color0);
--theia-statusBar-font-size: 12px;
/* Buttons */
--theia-ui-button-color: var(--theia-arduino-light);
--theia-ui-button-color-hover: var(--theia-arduino-light1);
--theia-ui-button-font-color: var(--theia-inverse-ui-font-color0);
--theia-ui-button-color: var(--theia-accent-color1);
--theia-ui-button-color-hover: var(--theia-accent-color2);
--theia-ui-button-font-color: var(--theia-arduino-light);
--theia-ui-button-color-secondary: var(--theia-secondary-brand-color1);
--theia-ui-button-color-secondary-hover: var(--theia-menu-color2);
--theia-ui-button-font-color-secondary: var(--theia-inverse-ui-font-color0);
@@ -197,4 +233,8 @@ is not optimized for dense, information rich UIs.
--theia-ansi-magenta-background-color: darkmagenta;
--theia-ansi-cyan-background-color: darkcyan;
--theia-ansi-white-background-color: #BDBDBD;
/* Output */
--theia-output-font-color: var(--theia-ui-font-color3);
}

View File

@@ -7,11 +7,11 @@ div#select-board-dialog .selectBoardContainer .body {
overflow: hidden;
}
div#select-board-dialog .selectBoardContainer .head {
margin-bottom: 10px;
div.dialogContent.select-board-dialog > div.head {
padding-left: 21px;
}
div#select-board-dialog .selectBoardContainer .head .title {
div.dialogContent.select-board-dialog > div.head .title {
font-weight: 400;
letter-spacing: .02em;
font-size: 1.2em;
@@ -31,11 +31,11 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i{
color: var(--theia-arduino-light);
}
#select-board-dialog .selectBoardContainer .body .search input,
#select-board-dialog .selectBoardContainer .body .boards.list,
#select-board-dialog .selectBoardContainer .body .search,
#select-board-dialog .selectBoardContainer .body .ports.list {
background: white;
#select-board-dialog .selectBoardContainer .search,
#select-board-dialog .selectBoardContainer .search input,
#select-board-dialog .selectBoardContainer .list,
#select-board-dialog .selectBoardContainer .list {
background: var(--theia-layout-color0);
}
#select-board-dialog .selectBoardContainer .body .search input {
@@ -43,7 +43,7 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i{
width: 100%;
height: auto;
max-height: 37px;
padding: 10px 8px;
padding: 10px 5px 10px 10px;
margin: 0;
vertical-align: top;
display: flex;
@@ -56,6 +56,7 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i{
#select-board-dialog .selectBoardContainer .body .container {
flex: 1;
padding: 0px 10px 0px 0px;
}
#select-board-dialog .selectBoardContainer .body .left.container .content {
@@ -66,27 +67,61 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i{
margin: 0 0 0 5px;
}
#select-board-dialog .selectBoardContainer .body .container .content .title{
#select-board-dialog .selectBoardContainer .body .container .content .title {
color: #7f8c8d;
margin-bottom: 10px;
padding: 0px 0px 10px 0px;
text-transform: uppercase;
}
#select-board-dialog .selectBoardContainer .body .container .content .footer {
padding: 10px 5px 10px 0px;
}
#select-board-dialog .selectBoardContainer .body .container .content .loading {
font-size: var(--theia-ui-font-size1);
color: #7f8c8d;
padding: 10px 5px 10px 10px;
text-transform: uppercase;
/* The max, min-height comes from `.body .list` 265px + 47px top padding - 2 * 10px top padding */
max-height: 292px;
min-height: 292px;
}
#select-board-dialog .selectBoardContainer .body .list .item {
padding: 10px 5px 10px 20px;
padding: 10px 5px 10px 10px;
display: flex;
justify-content: space-between;
justify-content: end;
}
#select-board-dialog .selectBoardContainer .body .list .item .selected-icon {
margin-left: auto;
}
#select-board-dialog .selectBoardContainer .body .list .item .detail {
font-size: var(--theia-ui-font-size1);
color: var(--theia-disabled-color0);
width: 155px; /* used heuristics for the calculation */
white-space: pre;
overflow: hidden;
text-overflow: ellipsis;
}
#select-board-dialog .selectBoardContainer .body .list .item.missing {
color: var(--theia-disabled-color0);
}
#select-board-dialog .selectBoardContainer .body .list .item:hover {
background: var(--theia-ui-button-color-secondary-hover);
}
#select-board-dialog .selectBoardContainer .body .list {
max-height: 265px;
min-height: 265px;
overflow-y: auto;
}
#select-board-dialog .selectBoardContainer .body .boards.list {
min-height: 265px;
#select-board-dialog .selectBoardContainer .body .ports.list {
margin: 47px 0px 0px 0px /* 47 is 37 as input height for the `Boards`, plus 10 margin bottom. */
}
#select-board-dialog .selectBoardContainer .body .search {
@@ -112,8 +147,8 @@ button.theia-button.secondary {
button.theia-button.main {
color: #fff;
background-color: #00979c;
box-shadow: 0 4px #005c5f;
/* background-color: #00979c; */
box-shadow: 0 4px var(--theia-accent-color0);
}
.dialogControl {
@@ -129,7 +164,7 @@ button.theia-button.main {
align-items: baseline;
width: 100%;
}
.arduino-boards-toolbar-item-container .arduino-boards-toolbar-item .inner-container .notAttached {
width: 10px;
height: 10px;
@@ -149,7 +184,6 @@ button.theia-button.main {
align-items: center;
margin: 0 5px;
width: 100%;
font-weight: bold;
}
.arduino-boards-toolbar-item .caret {
@@ -158,7 +192,7 @@ button.theia-button.main {
}
.arduino-boards-toolbar-item {
background: white;
background: var(--theia-layout-color1);
height: 22px;
display: flex;
width: 100%;
@@ -166,9 +200,9 @@ button.theia-button.main {
}
.arduino-boards-dropdown-list {
background: #f7f7f7;
border: 3px solid var(--theia-border-color2);
margin: -3px;
z-index: 1;
}
.arduino-boards-dropdown-item {
@@ -176,13 +210,16 @@ button.theia-button.main {
display: flex;
padding: 10px;
cursor: pointer;
color: var(--theia-ui-font-color1);
background: var(--theia-layout-color1);
}
.arduino-boards-dropdown-item .fa-check {
color: var(--theia-accent-color2);
color: var(--theia-accent-color1);
align-self: center;
}
.arduino-boards-dropdown-item.selected,
.arduino-boards-dropdown-item:hover {
background: var(--theia-ui-button-color-secondary-hover);
}
background: var(--theia-layout-color3);
}

View File

@@ -3,7 +3,7 @@
flex-direction: column;
}
.p-TabBar-toolbar {
#theia-top-panel .p-TabBar-toolbar {
justify-content: flex-end;
margin: 0;
padding-left: 10px;

View File

@@ -0,0 +1,13 @@
/* Do not show the `close` icon for editor, but show the dirty state if not in pro-mode. */
body:not(.arduino-advanced-mode) .p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable:hover > .p-TabBar-tabCloseIcon,
body:not(.arduino-advanced-mode) .p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-current > .p-TabBar-tabCloseIcon {
background-image: none;
cursor: pointer;
}
body:not(.arduino-advanced-mode) .p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable.theia-mod-dirty:hover > .p-TabBar-tabCloseIcon,
body:not(.arduino-advanced-mode) .p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable.theia-mod-dirty > .p-TabBar-tabCloseIcon:hover {
background-size: 10px;
background-image: var(--theia-icon-circle);
cursor: pointer;
}

View File

@@ -1,3 +1,43 @@
@import './list-widget.css';
@import './board-select-dialog.css';
@import './main.css';
@import './editor.css';
@import './monitor.css';
@import './arduino-select.css';
input:focus {
outline-width: 1px;
outline-style: solid;
outline-offset: -1px;
opacity: 1 !important;
outline-color: var(--theia-accent-color3);
}
input.warning:focus {
outline-width: 1px;
outline-style: solid;
outline-offset: -1px;
opacity: 1 !important;
color: var(--theia-warn-font-color0);
background-color: var(--theia-warn-color0);
}
input.warning {
background-color: var(--theia-warn-color0);
}
input.warning::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
color: var(--theia-warn-font-color0);
background-color: var(--theia-warn-color0);
opacity: 1; /* Firefox */
}
input.warning:-ms-input-placeholder { /* Internet Explorer 10-11 */
color: var(--theia-warn-font-color0);
background-color: var(--theia-warn-color0);
}
input.warning::-ms-input-placeholder { /* Microsoft Edge */
color: var(--theia-warn-font-color0);
background-color: var(--theia-warn-color0);
}

View File

@@ -1,6 +1,6 @@
.library-tab-icon {
-webkit-mask: url('library-tab-icon.svg');
mask: url('library-tab-icon.svg');
-webkit-mask: url('../icons/library-tab-icon.svg');
mask: url('../icons/library-tab-icon.svg');
}
.arduino-list-widget {
@@ -9,7 +9,6 @@
.arduino-list-widget .search-bar {
margin: 0px 10px 10px 15px;
border-color: var(--theia-border-color3);
}
.arduino-list-widget .search-filters {
@@ -50,17 +49,25 @@
}
.filterable-list-container .items-container > div:nth-child(odd) {
background-color: var(--theia-layout-color2);
}
.filterable-list-container .items-container > div:nth-child(even) {
background-color: var(--theia-layout-color0);
}
.filterable-list-container .items-container > div:hover {
.filterable-list-container .items-container > div:nth-child(even) {
background-color: var(--theia-layout-color1);
}
.filterable-list-container .items-container > div:hover {
background-color: var(--theia-layout-color2);
}
/* Perfect scrollbar does not like if we explicitly set the `background-color` of the contained elements.
See above: `.filterable-list-container .items-container > div:nth-child(odd|event)`.
We have to increase `z-index` of the scroll-bar thumb. Otherwise, the thumb is not visible.
https://github.com/arduino/arduino-pro-ide/issues/82 */
.arduino-list-widget .ps__rail-y > .ps__thumb-y {
z-index: 1;
}
.component-list-item {
padding: 10px 10px 10px 15px;
font-size: var(--theia-ui-font-size1);
@@ -108,15 +115,23 @@
color: var(--theia-ui-font-color2);
}
.component-list-item .header .installed {
.component-list-item .header .installed:before {
margin-left: 4px;
display: inline-block;
justify-self: end;
background-color: var(--theia-accent-color2);
background-color: var(--theia-accent-color1);
padding: 2px 4px 2px 4px;
font-size: 10px;
font-weight: bold;
max-height: calc(1em + 4px);
color: var(--theia-inverse-ui-font-color0);
content: 'INSTALLED';
}
.component-list-item .header .installed:hover:before {
background-color: var(--theia-inverse-ui-font-color0);
color: var(--theia-accent-color1);
content: 'UNINSTALL';
}
.component-list-item[min-width~="170px"] .footer {

View File

@@ -7,61 +7,90 @@
}
#outputView {
color: var(--theia-ui-font-color3);
}
#arduino-verify.arduino-tool-icon:hover,
#arduino-save-file.arduino-tool-icon:hover,
#arduino-show-open-context-menu.arduino-tool-icon:hover,
#arduino-upload.arduino-tool-icon:hover {
background-position-y: 60px;
}
#arduino-verify.arduino-tool-icon,
#arduino-save-file.arduino-tool-icon,
#arduino-show-open-context-menu.arduino-tool-icon,
#arduino-upload.arduino-tool-icon {
height: 24px;
width: 24px;
}
#arduino-save-file {
background: url(../icons/buttons.svg);
background-size: 800%;
background-position-y: 28px;
background-position-x: 59px;
}
#arduino-verify {
background: url(../icons/buttons.svg);
background-size: 800%;
background-position-y: 28px;
background-position-x: 188px;
}
#arduino-upload {
background: url(../icons/buttons.svg);
background-size: 800%;
background-position-y: 28px;
background-position-x: 156px;
}
#arduino-show-open-context-menu {
background: url(../icons/buttons.svg);
background-size: 800%;
background-position-y: 28px;
background-position-x: 92px;
cursor: text;
}
.p-TabBar-toolbar .item.arduino-tool-item {
margin-left: 3px;
}
.p-TabBar-toolbar .item.arduino-tool-item > div {
display: flex;
justify-content: center;
align-items: center;
height: 24px;
width: 24px;
background: var(--theia-ui-button-color);
}
.p-TabBar-toolbar .item.arduino-tool-item > div:hover {
background: var(--theia-ui-button-color-hover);
}
.arduino-verify, .arduino-upload {
border-radius: 12px;
}
.arduino-tool-icon {
height: 24px;
width: 24px;
background: var(--theia-ui-button-font-color);
-webkit-mask: url(../icons/mask-buttons.svg);
mask: url(../icons/mask-buttons.svg);
-webkit-mask-size: 800%;
mask-size: 800%;
}
.arduino-save-file-icon {
-webkit-mask-position: 59px -4px;
mask-position: 59px -4px;
}
.arduino-verify-icon {
-webkit-mask-position: 188px -4px;
mask-position: 188px -4px;
}
.arduino-upload-icon {
-webkit-mask-position: 156px -4px;
mask-position: 156px -4px;
}
.arduino-show-open-context-menu-icon {
-webkit-mask-position: 92px -4px;
mask-position: 92px -4px;
}
.toggle-serial-monitor-icon {
-webkit-mask-position: 28px -4px;
mask-position: 28px -4px;
}
#arduino-toolbar-container {
display: flex;
width: 100%;
}
.p-TabBar-toolbar.theia-arduino-toolbar {
flex: 1;
z-index: 0;
}
#theia-top-panel .p-TabBar-toolbar.theia-arduino-toolbar.right {
justify-content: flex-start;
min-width: 190px;
}
#theia-top-panel .p-TabBar-toolbar.theia-arduino-toolbar.left {
min-width: 398px;
justify-content: flex-end;
}
.arduino-tool-item.item.connected-boards {
opacity: 1;
}
.arduino-tool-item.item.connected-boards select {
.arduino-tool-item.item.connected-boards select {
line-height: var(--theia-content-line-height);
font-size: var(--theia-ui-font-size1);
color: var(--theia-ui-font-color1);
@@ -75,10 +104,6 @@
padding-right: 15px;
}
.p-Widget.p-TabBar.theia-app-centers.theia-app-bottom .p-TabBar-content-container.ps {
display: none;
}
.arduino-toolbar-tooltip {
margin-left: 10px;
display: flex;
@@ -86,6 +111,24 @@
color: var(--theia-ui-font-color3);
}
.p-TabBar-toolbar .item > div.arduino-toggle-advanced-mode {
display: flex;
width: 24px;
height: 24px;
justify-content: center;
align-items: center;
}
.arduino-toggle-advanced-mode-icon {
mask: none;
-webkit-mask: none;
background: none;
display: flex;
justify-content: center;
align-items: center;
color: var(--theia-ui-button-font-color);
}
.monaco-editor .margin {
border-right: 2px solid var(--theia-border-color1);
box-sizing: border-box;

View File

@@ -0,0 +1,63 @@
.p-TabBar.theia-app-centers .p-TabBar-tabIcon.arduino-serial-monitor-tab-icon {
background: url(../icons/buttons.svg);
background-size: 800%;
background-position-y: 41px;
background-position-x: 19px;
}
.serial-monitor {
height: 100%;
display: flex;
flex-direction: column;
}
.serial-monitor .head {
display: flex;
padding: 5px;
background: var(--theia-layout-color0);
height: 27px;
}
.serial-monitor .head .send {
display: flex;
flex: 1;
margin-right: 2px;
}
.serial-monitor .head .send > input {
line-height: var(--theia-content-line-height);
width: 100%;
}
.serial-monitor .head .send > input:focus {
border-color: var(--theia-accent-color3);
}
.serial-monitor .head .config {
display: flex;
}
.serial-monitor .head .config .select {
margin-left: 3px;
}
.serial-monitor .body {
overflow: auto;
flex: 1;
padding: 5px;
}
.p-TabBar-toolbar .item.arduino-monitor {
width: 24px;
justify-content: center;
font-size: medium;
box-sizing: border-box;
}
.p-TabBar-toolbar .item.arduino-monitor.toggled {
background: var(--theia-secondary-brand-color1);
}
.p-TabBar-toolbar .item .clear-all {
background: var(--theia-icon-clear) no-repeat;
}

View File

@@ -13,11 +13,11 @@ export class ToolOutputServiceClientImpl implements ToolOutputServiceClient {
protected readonly outputContribution: OutputContribution;
onNewOutput(tool: string, chunk: string): void {
this.outputContribution.openView({ reveal: true }).then(() => {
this.outputContribution.openView({ activate: true }).then(() => {
const channel = this.outputChannelManager.getChannel(`Arduino: ${tool}`);
channel.setVisibility(true);
channel.append(chunk);
})
});
}
}
}

View File

@@ -1,26 +1,45 @@
import { FrontendApplicationContribution, FrontendApplication } from "@theia/core/lib/browser";
import { FrontendApplicationContribution, FrontendApplication, Widget, Message } from "@theia/core/lib/browser";
import { injectable, inject } from "inversify";
import { ArduinoToolbar } from "./arduino-toolbar";
import { TabBarToolbarRegistry } from "@theia/core/lib/browser/shell/tab-bar-toolbar";
import { CommandRegistry } from "@theia/core";
import { LabelParser } from "@theia/core/lib/browser/label-parser";
export class ArduinoToolbarContainer extends Widget {
protected toolbars: ArduinoToolbar[];
constructor(...toolbars: ArduinoToolbar[]) {
super();
this.id = 'arduino-toolbar-container';
this.toolbars = toolbars;
}
onAfterAttach(msg: Message) {
for (const toolbar of this.toolbars) {
Widget.attach(toolbar, this.node);
}
}
}
@injectable()
export class ArduinoToolbarContribution implements FrontendApplicationContribution {
protected toolbarWidget: ArduinoToolbar;
protected arduinoToolbarContainer: ArduinoToolbarContainer;
constructor(
@inject(TabBarToolbarRegistry) protected tabBarToolBarRegistry: TabBarToolbarRegistry,
@inject(CommandRegistry) protected commandRegistry: CommandRegistry,
@inject(LabelParser) protected labelParser: LabelParser) {
this.toolbarWidget = new ArduinoToolbar(tabBarToolBarRegistry, commandRegistry, labelParser);
const leftToolbarWidget = new ArduinoToolbar(tabBarToolBarRegistry, commandRegistry, labelParser, 'left');
const rightToolbarWidget = new ArduinoToolbar(tabBarToolBarRegistry, commandRegistry, labelParser, 'right');
this.arduinoToolbarContainer = new ArduinoToolbarContainer(leftToolbarWidget, rightToolbarWidget);
}
onStart(app: FrontendApplication) {
app.shell.addWidget(this.toolbarWidget, {
app.shell.addWidget(this.arduinoToolbarContainer, {
area: 'top'
})
});
}
}

View File

@@ -2,14 +2,16 @@ import * as React from 'react';
import { TabBarToolbar, TabBarToolbarRegistry, TabBarToolbarItem, ReactTabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { ReactWidget } from '@theia/core/lib/browser';
import { LabelParser } from '@theia/core/lib/browser/label-parser';
import { LabelParser, LabelIcon } from '@theia/core/lib/browser/label-parser';
export const ARDUINO_TOOLBAR_ITEM_CLASS = 'arduino-tool-item';
export namespace ArduinoToolbarComponent {
export interface Props {
side: 'left' | 'right',
items: (TabBarToolbarItem | ReactTabBarToolbarItem)[],
commands: CommandRegistry,
labelParser: LabelParser,
commandIsEnabled: (id: string) => boolean,
executeCommand: (e: React.MouseEvent<HTMLElement>) => void
}
@@ -26,14 +28,24 @@ export class ArduinoToolbarComponent extends React.Component<ArduinoToolbarCompo
protected renderItem = (item: TabBarToolbarItem) => {
let innerText = '';
let className = `arduino-tool-icon ${item.id}-icon`;
if (item.text) {
for (const labelPart of this.props.labelParser.parse(item.text)) {
if (typeof labelPart !== 'string' && LabelIcon.is(labelPart)) {
className += ` fa fa-${labelPart.name}`;
} else {
innerText = labelPart;
}
}
}
const command = this.props.commands.getCommand(item.command);
const cls = `${ARDUINO_TOOLBAR_ITEM_CLASS} ${TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM} ${command && this.props.commandIsEnabled(command.id) ? ' enabled' : ''}`
return <div key={item.id}
className={cls} >
return <div key={item.id} className={cls} >
<div className={item.id}>
<div
key={item.id + '-icon'}
id={item.id}
className={`${item.id} arduino-tool-icon`}
className={className}
onClick={this.props.executeCommand}
onMouseOver={() => this.setState({ tooltip: item.tooltip || '' })}
onMouseOut={() => this.setState({ tooltip: '' })}
@@ -41,13 +53,22 @@ export class ArduinoToolbarComponent extends React.Component<ArduinoToolbarCompo
{innerText}
</div>
</div>
</div>
}
render(): React.ReactNode {
return <React.Fragment>
<div key='arduino-toolbar-tooltip' className={'arduino-toolbar-tooltip'}>{this.state.tooltip}</div>
{[...this.props.items].map(item => TabBarToolbarItem.is(item) ? this.renderItem(item) : item.render())}
</React.Fragment>;
const tooltip = <div key='arduino-toolbar-tooltip' className={'arduino-toolbar-tooltip'}>{this.state.tooltip}</div>;
const items = [
<React.Fragment key={this.props.side + '-arduino-toolbar-tooltip'}>
{[...this.props.items].map(item => TabBarToolbarItem.is(item) ? this.renderItem(item) : item.render())}
</React.Fragment>
]
if (this.props.side === 'left') {
items.unshift(tooltip);
} else {
items.push(tooltip)
}
return items;
}
}
@@ -58,10 +79,11 @@ export class ArduinoToolbar extends ReactWidget {
constructor(
protected readonly tabBarToolbarRegistry: TabBarToolbarRegistry,
protected readonly commands: CommandRegistry,
protected readonly labelParser: LabelParser
protected readonly labelParser: LabelParser,
public readonly side: 'left' | 'right'
) {
super();
this.id = 'arduino-toolbar';
this.id = side + '-arduino-toolbar';
this.addClass(TabBarToolbar.Styles.TAB_BAR_TOOLBAR);
this.init();
this.tabBarToolbarRegistry.onDidChange(() => this.updateToolbar());
@@ -82,7 +104,7 @@ export class ArduinoToolbar extends ReactWidget {
}
protected init(): void {
this.node.classList.add('theia-arduino-toolbar');
this.node.classList.add('theia-arduino-toolbar', this.side);
this.update();
}
@@ -93,6 +115,9 @@ export class ArduinoToolbar extends ReactWidget {
protected render(): React.ReactNode {
return <ArduinoToolbarComponent
key='arduino-toolbar-component'
side={this.side}
labelParser={this.labelParser}
items={[...this.items.values()]}
commands={this.commands}
commandIsEnabled={this.doCommandIsEnabled}
@@ -107,3 +132,9 @@ export class ArduinoToolbar extends ReactWidget {
}
}
}
export namespace ArduinoToolbar {
export function is(maybeToolbarWidget: any): maybeToolbarWidget is ArduinoToolbar {
return maybeToolbarWidget instanceof ArduinoToolbar;
}
}

View File

@@ -1,3 +1,4 @@
import { Installable } from './installable';
export interface ArduinoComponent {
readonly name: string;
@@ -6,8 +7,8 @@ export interface ArduinoComponent {
readonly description: string;
readonly moreInfoLink?: string;
readonly availableVersions: string[];
readonly availableVersions: Installable.Version[];
readonly installable: boolean;
readonly installedVersion?: string;
readonly installedVersion?: Installable.Version;
}

Some files were not shown because too many files have changed in this diff Show More