Compare commits

...

183 Commits

Author SHA1 Message Date
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
83e966d208 Merge pull request #40 from bcmi-labs/arduino-cli
Switched to the official `arduino-cli`.
2019-07-25 09:57:13 +02:00
Luca Cipriani
5cc75118cd Merge pull request #30 from bcmi-labs/PROEDITOR-19
PROEDITOR-19: Open new sketches in new windows
2019-07-25 09:21:15 +02:00
Akos Kitta
19f058cb7f Run the Board.List requests sequentially.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-25 08:50:05 +02:00
Akos Kitta
7494beca33 Switched to the official arduino-cli.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-25 07:58:00 +02:00
Sven Efftinge
835107b5a6 [styling] use arduino theme background color from start 2019-07-24 15:00:01 +00:00
jbicker
4ced8237f7 Merge branch 'sketch-loader-bug-fix' 2019-07-24 16:50:52 +02:00
Jan Bicker
40eb74aeff Merge pull request #39 from bcmi-labs/sketch-loader-bug-fix-win
[win] Open sketch.
2019-07-24 07:48:50 -07:00
Jan Bicker
457b283ca5 Use theia main menu factory for electron again. 2019-07-24 07:47:02 -07:00
Akos Kitta
c0e279f3e8 [win] Open sketch.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-23 21:35:06 +02:00
jbicker
75f7d3ca7c Fixed sketch service 2019-07-23 16:42:25 +02:00
Jan Bicker
cd8c138e1e Merge pull request #38 from bcmi-labs/status-bar-contribution
Added status bar contribution for line number and selected board.
2019-07-23 16:19:38 +02:00
jbicker
c0dea5c6b7 Added status bar contribution for line number and selected board.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-23 15:39:38 +02:00
Akos Kitta
c45e85eb89 PROEDITOR-19: Open new sketches in new windows
- From now on, each new sketch has its own window.
 - Fixed the `Download (of)? X completed` logging during index update.
 - Validate the file before opening the sketch in new window.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-23 08:25:49 +02:00
Akos Kitta
87cf5c6fd7 Merge pull request #34 from bcmi-labs/PROEDITOR-7
PROEDITOR-7: Cloned the Library Manager layout.
2019-07-22 16:46:18 +02:00
jbicker
0bfb4ba6ab Added noselect class to boards toolbar item
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-22 16:08:29 +02:00
Akos Kitta
8d838fcce4 PROEDITOR-7: Cloned the Library Manager layout.
To match with the official Arduino editor's UI.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-22 15:52:08 +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
Luca Cipriani
cf44fe2ad0 Merge pull request #36 from bcmi-labs/board-select-dialog-ui-improvements
Improved layout of "Board Select" and "File Navigator" dialogs
2019-07-22 14:10:34 +02:00
jbicker
a6e1c4a93e Improved layout of "Board Select" and "File Navigator" dialogs
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-22 14:09:55 +02:00
Luca Cipriani
269f08b74d Merge pull request #37 from bcmi-labs/board-selector-toolbar-item
Board selector toolbar item
2019-07-22 14:03:13 +02:00
jbicker
4d2bd87f74 Implemented custom dropdown for board selection
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-22 11:06:54 +02:00
jbicker
c2fbccc9e8 App doesn't show "open..." anymore if there are no sketches in default sketch folder.
Opens file navigator directly instead.

Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-18 13:49:28 +02:00
jbicker
6e0a0a19c5 Fixed Open Button
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-18 11:35:54 +02:00
jbicker
c88d8dd17b Small ui fix. Adapted css for browser menu active items.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-18 10:08:49 +02:00
Jan Bicker
7dfb28975e Merge pull request #35 from bcmi-labs/ui-improvements
Ui improvements
2019-07-18 09:59:10 +02:00
jbicker
2d4c710b6a Syntax coloring
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-17 18:40:48 +02:00
jbicker
dfc2ef967a Colors adapted
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-15 15:56:45 +02:00
jbicker
e2d3c7c1bd Made buttons bigger
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-15 15:56:45 +02:00
Luca Cipriani
5a97a71d9b Merge pull request #32 from bcmi-labs/bring-back-menu
PROEDITOR-22: Brought back the menu.
2019-07-15 15:51:39 +02:00
jbicker
5e728523ef Changed layout of top panel in browser.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-15 13:12:33 +02:00
jbicker
2a0273c771 Brought back the menu.
With a restricted set of items.

Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-15 12:53:17 +02:00
Luca Cipriani
35086ca1a1 Merge pull request #31 from bcmi-labs/open-boards-dialog
PROEDITOR-9: Open boards dialog
2019-07-15 12:44:16 +02:00
jbicker
23446284b7 Toggle the selected board in dropdown
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-10 17:48:20 +02:00
jbicker
89fb2fddbd Cache attached boards
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-10 11:50:44 +02:00
jbicker
a039597d40 Select Board Dialog Style and Layout
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-09 18:31:08 +02:00
jbicker
4429094139 Styling of select board and port dialog
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-08 13:55:31 +02:00
jbicker
769689ff6d Added dialog implementation
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-08 11:58:59 +02:00
jbicker
4c66dec36e Added board select toolbar item
fill context menu with connected boards

Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-26 14:08:45 +02:00
jbicker
ff336dccc5 Enhanced boards toolbar item.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-26 09:03:26 +02:00
Jan Bicker
75ef8ea987 Merge pull request #27 from bcmi-labs/electron-4x-menu
Several UI enhancements
2019-06-26 09:02:14 +02:00
jbicker
1b90b7af29 Updated theia
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-26 08:43:38 +02:00
jbicker
c7c5ab57c2 Bugfix and yarn.lock update
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-25 17:37:58 +02:00
Akos Kitta
b7e17b5828 Updated to next (26c3273a) Theia.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-06-25 08:47:58 +02:00
jbicker
6195da3858 Added boards toolbar item.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-24 18:00:17 +02:00
jbicker
c54ae96a3c Overwritten EditorWidgetFactory, made editor widget not closable.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-24 15:09:04 +02:00
jbicker
54d90d5413 Added save button and turned auto save on by default.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-21 13:11:16 +02:00
jbicker
f4f7a9998e Small fixes
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-20 17:28:53 +02:00
jbicker
4096afde96 Open the sketch in a new window.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-20 09:35:56 +02:00
jbicker
92afa48c05 Implemented possibility to open sketches via file navigator.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-19 11:30:26 +02:00
jbicker
9d3cbf2ea0 Use service to load sketches
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-19 09:48:28 +02:00
jbicker
0c937212e2 Added Open Toolbutton
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-17 18:26:24 +02:00
Jan Bicker
7760915014 Merge pull request #23 from bcmi-labs/ui-changes
Ui changes
2019-06-17 09:36:26 +02:00
jbicker
f9746b350d Further customization of the statusbar
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-14 18:32:32 +02:00
jbicker
2066f20d78 Added the tooltip to the right of the toolbar
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-14 17:37:46 +02:00
jbicker
f5560626e5 Removed/hidden elements from bottom widget bar (Log output)
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-14 16:08:19 +02:00
jbicker
1f1861e1d8 Adapted design of toolbar.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-14 15:39:03 +02:00
jbicker
6a956afbcd Added toolbar to top panel.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-13 17:02:07 +02:00
jbicker
4c3becd3e8 Sidebar with Filenavigator, Librarylist and Boardlist does not get rendered initially anymore.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-13 10:10:29 +02:00
jbicker
088951ae01 More color adapted
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-13 08:30:59 +02:00
jbicker
b7cf9e6755 Removed menu, changed colors
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-13 07:55:58 +02:00
Akos Kitta
1d6ba97d68 Merge pull request #22 from bcmi-labs/build-fix2
Yet another attempt to fix the build.
2019-05-10 11:08:58 +02:00
Akos Kitta
94b9fbe29a Yet another attempt to fix the build.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-05-10 10:23:40 +02:00
Christian Weichel
d6fb7e2466 Update known-issues.txt 2019-05-09 20:23:05 +02:00
Christian Weichel
7da8851fad Added refresh attached boards command 2019-05-09 20:22:17 +02:00
Sven Efftinge
97135bd08e More styling impr. and less UI contributions 2019-05-09 19:32:01 +02:00
Christian Weichel
20db72fec7 Update known-issues.txt 2019-05-09 11:40:24 +02:00
Akos Kitta
d670e66f0e Merge pull request #18 from bcmi-labs/doc
Updated with the application download link.
2019-05-09 11:04:21 +02:00
Akos Kitta
93daed6a69 Updated with the application download link.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-05-09 11:02:56 +02:00
Christian Weichel
f592ac69f0 Updated known issues 2019-05-09 10:49:38 +02:00
161 changed files with 24235 additions and 13424 deletions

4
.gitignore vendored
View File

@@ -3,9 +3,11 @@ node_modules/
.node_modules/ .node_modules/
lib/ lib/
build/ build/
downloads/
!electron/build/ !electron/build/
src-gen/ src-gen/
arduino-ide-*/webpack.config.js browser-app/webpack.config.js
electron-app/webpack.config.js
.DS_Store .DS_Store
/workspace/static /workspace/static
# switching from `electron` to `browser` in dev mode. # switching from `electron` to `browser` in dev mode.

View File

@@ -8,7 +8,7 @@ ports:
tasks: tasks:
- init: > - init: >
yarn && yarn &&
yarn --cwd ./arduino-ide-browser start yarn --cwd ./browser-app start
github: github:
prebuilds: prebuilds:

13
.vscode/launch.json vendored
View File

@@ -4,11 +4,18 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Electron Packager",
"program": "${workspaceRoot}/electron/packager/index.js",
"cwd": "${workspaceFolder}/electron/packager"
},
{ {
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "Launch Backend", "name": "Launch Backend",
"program": "${workspaceRoot}/arduino-ide-browser/src-gen/backend/main.js", "program": "${workspaceRoot}/browser-app/src-gen/backend/main.js",
"args": [ "args": [
"--hostname=0.0.0.0", "--hostname=0.0.0.0",
"--port=3000", "--port=3000",
@@ -20,8 +27,8 @@
}, },
"sourceMaps": true, "sourceMaps": true,
"outFiles": [ "outFiles": [
"${workspaceRoot}/arduino-ide-browser/src-gen/backend/*.js", "${workspaceRoot}/browser-app/src-gen/backend/*.js",
"${workspaceRoot}/arduino-ide-browser/lib/**/*.js", "${workspaceRoot}/browser-app/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/*/lib/**/*.js" "${workspaceRoot}/arduino-ide-extension/*/lib/**/*.js"
], ],
"smartStep": true, "smartStep": true,

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

@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}

16
.vscode/tasks.json vendored
View File

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

View File

@@ -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) ## 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 git clone https://github.com/bcmi-labs/arduino-editor
cd arduino-editor cd arduino-editor
yarn yarn
yarn rebuild:electron 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 If you want to switch back to the browser-based example, execute the following in the repository root
@@ -23,13 +26,22 @@ yarn rebuild:browser
``` ```
Then you can start the browser example again: Then you can start the browser example again:
``` ```
yarn --cwd arduino-ide-browser start yarn --cwd browser-app start
``` ```
## Arduino-PoC Electron Application ## Arduino Pro IDE Electron Application
- The project is built on [Azure DevOps](https://dev.azure.com/typefox/Arduino). The project is built on [Azure DevOps](https://dev.azure.com/typefox/Arduino).
- The configured pipelines are available [here](https://dev.azure.com/typefox/Arduino/_build?definitionId=4).
- Currently, we build for Windows, macOS, and Linux. If you want to download the Arduino-PoC Electron application, follow the steps from below. Currently, we provide the Arduino Pro IDE for the following platforms:
- Windows,
- macOS, and
- Linux.
You can download the latest release applications fom [here](https://github.com/bcmi-labs/arduino-editor/releases/latest).
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_01.gif)
![](static/download_02.gif) ![](static/download_02.gif)
- Click [here](./electron/README.md) 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,38 +0,0 @@
{
"private": true,
"name": "arduino-ide-browser",
"version": "0.0.1",
"license": "MIT",
"dependencies": {
"@theia/core": "next",
"@theia/editor": "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"
},
"scripts": {
"prepare": "theia build --mode development",
"start": "theia start --root-dir=../workspace",
"watch": "theia build --watch --mode development"
},
"theia": {
"frontend": {
"config": {
"applicationName": "Arduino-PoC",
"defaultTheme": "arduino-theme"
}
}
}
}

View File

@@ -8,6 +8,113 @@
"name": "Ino", "name": "Ino",
"scopeName": "source.ino", "scopeName": "source.ino",
"patterns": [ "patterns": [
{
"include": "#special_block"
},
{
"include": "#strings"
},
{
"match": "\\b(friend|explicit|virtual|override|final|noexcept)\\b",
"name": "storage.modifier.cpp"
},
{
"match": "\\b(private:|protected:|public:)",
"name": "storage.modifier.cpp"
},
{
"match": "\\b(catch|operator|try|throw|using)\\b",
"name": "keyword.control.cpp"
},
{
"match": "\\bdelete\\b(\\s*\\[\\])?|\\bnew\\b(?!])",
"name": "keyword.control.cpp"
},
{
"match": "\\b(f|m)[A-Z]\\w*\\b",
"name": "variable.other.readwrite.member.cpp"
},
{
"match": "\\bthis\\b",
"name": "variable.language.this.cpp"
},
{
"match": "\\bnullptr\\b",
"name": "constant.language.cpp"
},
{
"match": "\\btemplate\\b\\s*",
"name": "storage.type.template.cpp"
},
{
"match": "\\b(const_cast|dynamic_cast|reinterpret_cast|static_cast)\\b\\s*",
"name": "keyword.operator.cast.cpp"
},
{
"match": "::",
"name": "punctuation.separator.namespace.access.cpp"
},
{
"match": "\\b(and|and_eq|bitand|bitor|compl|not|not_eq|or|or_eq|typeid|xor|xor_eq|alignof|alignas)\\b",
"name": "keyword.operator.cpp"
},
{
"match": "\\b(class|decltype|wchar_t|char16_t|char32_t)\\b",
"name": "storage.type.cpp"
},
{
"match": "\\b(constexpr|export|mutable|typename|thread_local)\\b",
"name": "storage.modifier.cpp"
},
{
"begin": "(?x)\n(?:\n ^ | # beginning of line\n (?:(?<!else|new|=)) # or word + space before name\n)\n((?:[A-Za-z_][A-Za-z0-9_]*::)*+~[A-Za-z_][A-Za-z0-9_]*) # actual name\n\\s*(\\() # opening bracket",
"beginCaptures": {
"1": {
"name": "entity.name.function.cpp"
},
"2": {
"name": "punctuation.definition.parameters.begin.c"
}
},
"end": "\\)",
"endCaptures": {
"0": {
"name": "punctuation.definition.parameters.end.c"
}
},
"name": "meta.function.destructor.cpp",
"patterns": [
{
"include": "$base"
}
]
},
{
"begin": "(?x)\n(?:\n ^ | # beginning of line\n (?:(?<!else|new|=)) # or word + space before name\n)\n((?:[A-Za-z_][A-Za-z0-9_]*::)*+~[A-Za-z_][A-Za-z0-9_]*) # actual name\n\\s*(\\() # opening bracket",
"beginCaptures": {
"1": {
"name": "entity.name.function.cpp"
},
"2": {
"name": "punctuation.definition.parameters.begin.c"
}
},
"end": "\\)",
"endCaptures": {
"0": {
"name": "punctuation.definition.parameters.end.c"
}
},
"name": "meta.function.destructor.prototype.cpp",
"patterns": [
{
"include": "$base"
}
]
},
{
"include": "source.c"
},
{ {
"include": "#preprocessor-rule-enabled" "include": "#preprocessor-rule-enabled"
}, },
@@ -321,6 +428,19 @@
"match": "((\\.)|(->))\\s*(([a-zA-Z_][a-zA-Z_0-9]*)\\b(?!\\s*\\())?" "match": "((\\.)|(->))\\s*(([a-zA-Z_][a-zA-Z_0-9]*)\\b(?!\\s*\\())?"
}, },
"block": { "block": {
"begin": "\\{",
"beginCaptures": {
"0": {
"name": "punctuation.section.block.begin.bracket.curly.c"
}
},
"end": "\\}",
"endCaptures": {
"0": {
"name": "punctuation.section.block.end.bracket.curly.c"
}
},
"name": "meta.block.cpp",
"patterns": [ "patterns": [
{ {
"begin": "{", "begin": "{",
@@ -341,6 +461,9 @@
"include": "#block_innards" "include": "#block_innards"
} }
] ]
},
{
"include": "$base"
} }
] ]
}, },
@@ -706,6 +829,69 @@
"include": "#line_continuation_character" "include": "#line_continuation_character"
} }
] ]
},
{
"begin": "(u|u8|U|L)?\"",
"beginCaptures": {
"0": {
"name": "punctuation.definition.string.begin.cpp"
},
"1": {
"name": "meta.encoding.cpp"
}
},
"end": "\"",
"endCaptures": {
"0": {
"name": "punctuation.definition.string.end.cpp"
}
},
"name": "string.quoted.double.cpp",
"patterns": [
{
"match": "\\\\u\\h{4}|\\\\U\\h{8}",
"name": "constant.character.escape.cpp"
},
{
"match": "\\\\['\"?\\\\abfnrtv]",
"name": "constant.character.escape.cpp"
},
{
"match": "\\\\[0-7]{1,3}",
"name": "constant.character.escape.cpp"
},
{
"match": "\\\\x\\h+",
"name": "constant.character.escape.cpp"
},
{
"include": "source.c#string_placeholder"
}
]
},
{
"begin": "(u|u8|U|L)?R\"(?:([^ ()\\\\\\t]{0,16})|([^ ()\\\\\\t]*))\\(",
"beginCaptures": {
"0": {
"name": "punctuation.definition.string.begin.cpp"
},
"1": {
"name": "meta.encoding.cpp"
},
"3": {
"name": "invalid.illegal.delimiter-too-long.cpp"
}
},
"end": "\\)\\2(\\3)\"",
"endCaptures": {
"0": {
"name": "punctuation.definition.string.end.cpp"
},
"1": {
"name": "invalid.illegal.delimiter-too-long.cpp"
}
},
"name": "string.quoted.double.raw.cpp"
} }
] ]
}, },
@@ -1954,6 +2140,233 @@
"include": "#block_innards" "include": "#block_innards"
} }
] ]
},
"angle_brackets": {
"begin": "<",
"end": ">",
"name": "meta.angle-brackets.cpp",
"patterns": [
{
"include": "#angle_brackets"
},
{
"include": "$base"
}
]
},
"constructor": {
"patterns": [
{
"begin": "(?x)\n(?:^\\s*) # beginning of line\n((?!while|for|do|if|else|switch|catch|enumerate|r?iterate)[A-Za-z_][A-Za-z0-9_:]*) # actual name\n\\s*(\\() # opening bracket",
"beginCaptures": {
"1": {
"name": "entity.name.function.cpp"
},
"2": {
"name": "punctuation.definition.parameters.begin.c"
}
},
"end": "\\)",
"endCaptures": {
"0": {
"name": "punctuation.definition.parameters.end.c"
}
},
"name": "meta.function.constructor.cpp",
"patterns": [
{
"include": "$base"
}
]
},
{
"begin": "(?x)\n(:)\n(\n (?=\n \\s*[A-Za-z_][A-Za-z0-9_:]* # actual name\n \\s* (\\() # opening bracket\n )\n)",
"beginCaptures": {
"1": {
"name": "punctuation.definition.parameters.c"
}
},
"end": "(?=\\{)",
"name": "meta.function.constructor.initializer-list.cpp",
"patterns": [
{
"include": "$base"
}
]
}
]
},
"special_block": {
"patterns": [
{
"begin": "\\b(using)\\b\\s*(namespace)\\b\\s*((?:[_A-Za-z][_A-Za-z0-9]*\\b(::)?)*)",
"beginCaptures": {
"1": {
"name": "keyword.control.cpp"
},
"2": {
"name": "storage.type.cpp"
},
"3": {
"name": "entity.name.type.cpp"
}
},
"end": "(;)",
"name": "meta.using-namespace-declaration.cpp"
},
{
"begin": "\\b(namespace)\\b\\s*([_A-Za-z][_A-Za-z0-9]*\\b)?+",
"beginCaptures": {
"1": {
"name": "storage.type.cpp"
},
"2": {
"name": "entity.name.type.cpp"
}
},
"captures": {
"1": {
"name": "keyword.control.namespace.$2"
}
},
"end": "(?<=\\})|(?=(;|,|\\(|\\)|>|\\[|\\]|=))",
"name": "meta.namespace-block.cpp",
"patterns": [
{
"begin": "\\{",
"beginCaptures": {
"0": {
"name": "punctuation.definition.scope.cpp"
}
},
"end": "\\}",
"endCaptures": {
"0": {
"name": "punctuation.definition.scope.cpp"
}
},
"patterns": [
{
"include": "#special_block"
},
{
"include": "#constructor"
},
{
"include": "$base"
}
]
},
{
"include": "$base"
}
]
},
{
"begin": "\\b(class|struct)\\b\\s*([_A-Za-z][_A-Za-z0-9]*\\b)?+(\\s*:\\s*(public|protected|private)\\s*([_A-Za-z][_A-Za-z0-9]*\\b)((\\s*,\\s*(public|protected|private)\\s*[_A-Za-z][_A-Za-z0-9]*\\b)*))?",
"beginCaptures": {
"1": {
"name": "storage.type.cpp"
},
"2": {
"name": "entity.name.type.cpp"
},
"4": {
"name": "storage.type.modifier.cpp"
},
"5": {
"name": "entity.name.type.inherited.cpp"
},
"6": {
"patterns": [
{
"match": "(public|protected|private)",
"name": "storage.type.modifier.cpp"
},
{
"match": "[_A-Za-z][_A-Za-z0-9]*",
"name": "entity.name.type.inherited.cpp"
}
]
}
},
"end": "(?<=\\})|(?=(;|\\(|\\)|>|\\[|\\]|=))",
"name": "meta.class-struct-block.cpp",
"patterns": [
{
"include": "#angle_brackets"
},
{
"begin": "\\{",
"beginCaptures": {
"0": {
"name": "punctuation.section.block.begin.bracket.curly.cpp"
}
},
"end": "(\\})(\\s*\\n)?",
"endCaptures": {
"1": {
"name": "punctuation.section.block.end.bracket.curly.cpp"
},
"2": {
"name": "invalid.illegal.you-forgot-semicolon.cpp"
}
},
"patterns": [
{
"include": "#special_block"
},
{
"include": "#constructor"
},
{
"include": "$base"
}
]
},
{
"include": "$base"
}
]
},
{
"begin": "\\b(extern)(?=\\s*\")",
"beginCaptures": {
"1": {
"name": "storage.modifier.cpp"
}
},
"end": "(?<=\\})|(?=\\w)|(?=\\s*#\\s*endif\\b)",
"name": "meta.extern-block.cpp",
"patterns": [
{
"begin": "\\{",
"beginCaptures": {
"0": {
"name": "punctuation.section.block.begin.bracket.curly.c"
}
},
"end": "\\}|(?=\\s*#\\s*endif\\b)",
"endCaptures": {
"0": {
"name": "punctuation.section.block.end.bracket.curly.c"
}
},
"patterns": [
{
"include": "#special_block"
},
{
"include": "$base"
}
]
},
{
"include": "$base"
}
]
}
]
} }
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "arduino-ide-extension", "name": "arduino-ide-extension",
"version": "0.0.1", "version": "0.0.2",
"description": "An extension for Theia building the Arduino IDE", "description": "An extension for Theia building the Arduino IDE",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -8,29 +8,58 @@
}, },
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^0.4.0", "@grpc/grpc-js": "^0.4.0",
"@theia/application-package": "next",
"@theia/core": "next", "@theia/core": "next",
"@theia/editor": "next", "@theia/editor": "next",
"@theia/filesystem": "next", "@theia/filesystem": "next",
"@theia/git": "next",
"@theia/languages": "next", "@theia/languages": "next",
"@theia/markers": "next",
"@theia/monaco": "next", "@theia/monaco": "next",
"@theia/outline-view": "next",
"@theia/workspace": "next", "@theia/workspace": "next",
"p-queue": "^5.0.0" "@theia/navigator": "next",
"@theia/terminal": "next",
"@theia/search-in-workspace": "next",
"@theia/cpp": "next",
"@types/ps-tree": "^1.1.0",
"@types/which": "^1.3.1",
"@types/react-select": "^3.0.0",
"@types/google-protobuf": "^3.7.1",
"css-element-queries": "^1.2.0",
"react-select": "^3.0.4",
"p-queue": "^5.0.0",
"ps-tree": "^1.2.0",
"string-natural-compare": "^2.0.3",
"tree-kill": "^1.2.1",
"upath": "^1.1.2",
"which": "^1.3.1"
}, },
"scripts": { "scripts": {
"generate-protoc": "./scripts/generate-protoc.sh && node ./scripts/patch-grpc-js.js", "prepare": "yarn download-cli && yarn download-ls && yarn run clean && yarn run build",
"prepare": "yarn run clean && yarn run build",
"clean": "rimraf lib", "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", "lint": "tslint -c ./tslint.json --project ./tsconfig.json",
"build": "tsc && cp -rf src/node/cli-protocol lib/node && yarn lint", "build": "tsc && ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/ && yarn lint",
"watch": "tsc -w" "watch": "tsc -w"
}, },
"devDependencies": { "devDependencies": {
"@types/google-protobuf": "^3.2.7", "decompress": "^4.2.0",
"decompress-targz": "^4.1.1",
"decompress-unzip": "^4.0.1",
"download": "^7.1.0",
"grpc-tools": "^1.7.3", "grpc-tools": "^1.7.3",
"grpc_tools_node_protoc_ts": "^2.5.0", "grpc_tools_node_protoc_ts": "^2.5.0",
"moment": "^2.24.0",
"ncp": "^2.0.0",
"rimraf": "^2.6.1", "rimraf": "^2.6.1",
"shelljs": "^0.8.3",
"tslint": "^5.5.0", "tslint": "^5.5.0",
"typescript": "2.9.1" "typescript": "2.9.1",
"uuid": "^3.2.1",
"yargs": "^11.1.0"
}, },
"files": [ "files": [
"lib", "lib",
@@ -42,6 +71,10 @@
{ {
"backend": "lib/node/arduino-backend-module", "backend": "lib/node/arduino-backend-module",
"frontend": "lib/browser/arduino-frontend-module" "frontend": "lib/browser/arduino-frontend-module"
},
{
"frontend": "lib/browser/menu/browser-arduino-menu-module",
"frontendElectron": "lib/electron-browser/electron-arduino-menu-module"
} }
] ]
} }

View File

@@ -0,0 +1,62 @@
// @ts-check
// 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
(() => {
const DEFAULT_VERSION = 'latest'; // require('moment')().format('YYYYMMDD');
const os = require('os');
const path = require('path');
const shell = require('shelljs');
const downloader = require('./downloader');
const yargs = require('yargs')
.option('cli-version', {
alias: 'cv',
default: DEFAULT_VERSION,
describe: `The version of the 'arduino-cli' to download with the YYYYMMDD format, or 'latest'. Defaults to ${DEFAULT_VERSION}.`
})
.option('force-download', {
alias: 'fd',
default: false,
describe: `If set, this script force downloads the 'arduino-cli' even if it already exists on the file system.`
})
.version(false).parse();
const version = yargs['cli-version'];
const force = yargs['force-download'];
const { platform, arch } = process;
const build = path.join(__dirname, '..', 'build');
const cli = path.join(build, `arduino-cli${os.platform() === 'win32' ? '.exe' : ''}`);
const suffix = (() => {
switch (platform) {
case 'darwin': return 'macOS_64bit.tar.gz';
case 'win32': return 'Windows_64bit.zip';
case 'linux': {
switch (arch) {
case 'arm64': return 'Linux_ARM64.tar.gz';
case 'x64': return 'Linux_64bit.tar.gz';
default: return undefined;
}
}
default: return undefined;
}
})();
if (!suffix) {
shell.echo(`The CLI is not available for ${platform} ${arch}.`);
shell.exit(1);
}
const url = `https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-${version}_${suffix}`;
downloader.download(url, cli, 'arduino-cli', force);
})();

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 = '8.0.1';
const os = require('os');
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'],
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 als = path.join(build, `arduino-language-server${os.platform() === 'win32' ? '.exe' : ''}`);
const clangd = path.join(build, `clangd${os.platform() === 'win32' ? '.exe' : ''}`);
let alsSuffix, clangdSuffix;
switch (platform) {
case 'darwin':
alsSuffix = 'Darwin_amd64.zip';
clangdSuffix = 'macos.zip';
break;
case 'win32':
alsSuffix = 'Windows_NT_amd64.zip';
clangdSuffix = 'windows.zip';
break;
case 'linux':
alsSuffix = 'Linux_amd64.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.download(alsUrl, als, 'arduino-language-server', force);
if (clangdSuffix) {
const clangdUrl = `https://downloads.arduino.cc/arduino-language-server/clangd/clangd_${clangdVersion}_${clangdSuffix}`;
downloader.download(clangdUrl, clangd, 'clangd', force);
}
})();

View File

@@ -0,0 +1,71 @@
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;
});
exports.download = async (url, targetFile, filePrefix, force) => {
const { platform, arch } = process;
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);
} else {
shell.echo(`Done: ${targetFile}`);
}
}

View File

@@ -1,43 +0,0 @@
#!/bin/bash
WORKDIR=/tmp/arduino-cli-protoc
echo "Working in $WORKDIR"
# this could be a Git submodule, but that feels to clunky for just building the protobuf stuff
mkdir -p $WORKDIR
pushd $WORKDIR
if [ ! -d arduino-cli ]; then
git clone https://github.com/typefox/arduino-cli
cd arduino-cli
git checkout daemon
cd -
mkdir -p go/src/github.com/arduino
ln -s $PWD/arduino-cli go/src/github.com/arduino
export GOPATH=$PWD/go
cd go/src/github.com/arduino/arduino-cli
GOOS=linux go build -o arduino-cli.linux
# GOOS=darwin go build -o arduino-cli.darwin
fi
popd
# make sure the output path exists
mkdir -p src/node/cli-protocol
export PATH=$PATH:$PWD/node_modules/.bin
# generate js codes via grpc-tools
grpc_tools_node_protoc \
--js_out=import_style=commonjs,binary:./src/node/cli-protocol \
--grpc_out=./src/node/cli-protocol \
--plugin=protoc-gen-grpc=`which grpc_tools_node_protoc_plugin` \
-I /usr/lib/protoc/include \
-I $WORKDIR/arduino-cli/rpc \
$WORKDIR/arduino-cli/rpc/*.proto
# generate d.ts codes
protoc \
--plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts \
--ts_out=./src/node/cli-protocol \
-I /usr/lib/protoc/include \
-I $WORKDIR/arduino-cli/rpc \
$WORKDIR/arduino-cli/rpc/*.proto

View File

@@ -0,0 +1,81 @@
// @ts-check
(async () => {
const DEFAULT_VERSION = 'nightly'; // '0.3.7-alpha.preview';
const os = require('os');
const path = require('path');
const { v4 } = require('uuid');
const shell = require('shelljs');
shell.env.PATH = `${shell.env.PATH}${path.delimiter}${path.join(__dirname, '..', 'node_modules', '.bin')}`;
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. Specify either an existing version or use 'nightly'. Defaults to ${DEFAULT_VERSION}.`
})
.version(false).parse();
const version = yargs['cli-version'];
if (version !== 'nightly') {
shell.echo(`Only 'nightly' version is supported.`);
shell.exit(1);
}
const repository = path.join(os.tmpdir(), `${v4()}-arduino-cli`);
if (shell.mkdir('-p', repository).code !== 0) {
shell.exit(1);
}
if (shell.exec(`git clone https://github.com/arduino/arduino-cli.git ${repository}`).code !== 0) {
shell.exit(1);
}
if (version !== 'nightly') {
if (shell.exec(`git -C ${repository} checkout tags/${version} -b ${version}`).code !== 0) {
shell.exit(1);
}
}
shell.echo('Generating TS/JS API from:');
if (shell.exec(`git -C ${repository} rev-parse --abbrev-ref HEAD`).code !== 0) {
shell.exit(1);
}
if (shell.exec(`git -C ${repository} rev-parse --short HEAD`).code !== 0) {
shell.exit(1);
}
const pluginExec = shell.which('grpc_tools_node_protoc_plugin');
if (!pluginExec || pluginExec.code !== 0) {
shell.exit(1);
}
const plugin = pluginExec.stdout.trim();
const rpc = path.join(repository, 'rpc');
const out = path.join(__dirname, '..', 'src', 'node', 'cli-protocol');
// Generate JS code from the `.proto` files.
if (shell.exec(`grpc_tools_node_protoc \
--js_out=import_style=commonjs,binary:${out} \
--grpc_out=${out} \
--plugin=protoc-gen-grpc=${plugin} \
-I ${rpc} \
${path.join(rpc, '/**/*.proto')}`).code !== 0) {
shell.exit(1);
}
// Generate the `.d.ts` files for JS.
if (shell.exec(`protoc \
--plugin=protoc-gen-ts=${path.resolve(__dirname, '..', 'node_modules', '.bin', 'protoc-gen-ts')} \
--ts_out=${out} \
-I ${rpc} \
${path.join(rpc, '/**/*.proto')}`).code !== 0) {
shell.exit(1);
}
const { patch } = require('./patch-grpc-js');
patch([out])
shell.echo('Done.');
})();

View File

@@ -1,25 +1,38 @@
// Use `@grpc/grpc-js` instead of `grpc` at runtime. // Use `@grpc/grpc-js` instead of `grpc` at runtime.
// https://github.com/grpc/grpc-node/issues/624 // https://github.com/grpc/grpc-node/issues/624
(() => { // https://github.com/grpc/grpc-node/issues/931
const fs = require('fs');
const path = require('path'); const fs = require('fs');
const roots = ['src']; // XXX: patch the `lib` instead? const path = require('path');
console.info("🔧 >>> Patching code. Switching from 'grpc' to '@grpc/grpc-js'...");
for (const root of roots) { module.exports.patch = function (roots = [path.join(__dirname, '..', 'src', 'node')]) {
const cliProtocolPath = path.resolve(__dirname, '..', root, 'node', 'cli-protocol'); console.info('🔧 <<< Patching code...');
for (const fileName of fs.readdirSync(cliProtocolPath)) { patch(roots);
const filePath = path.resolve(cliProtocolPath, fileName); console.info('👌 <<< Done. The code has been patched.');
let content = fs.readFileSync(filePath, { encoding: 'utf8' }); };
if (content.indexOf("require('grpc')") !== -1) {
console.info(`Updated require('grpc') to require('@grpc/grpc-js') in ${filePath}.`); function patch(paths) {
fs.writeFileSync(filePath, content.replace("require('grpc')", "require('@grpc/grpc-js')")); for (const p of paths) {
} const exist = fs.existsSync(p);
content = fs.readFileSync(filePath, { encoding: 'utf8' }); if (exist) {
if (content.indexOf('import * as grpc from "grpc"') !== -1) { const stat = fs.statSync(p);
console.info(`Updated import * as grpc from "grpc" to import * as grpc from "@grpc/grpc-js" in ${filePath}.`); if (stat.isDirectory()) {
fs.writeFileSync(filePath, content.replace('import * as grpc from "grpc"', 'import * as grpc from "@grpc/grpc-js"')); console.info(`🔧 >>> Scanning code in ${p}...`);
patch(fs.readdirSync(p).map(name => path.join(p, name)));
} else {
let content = fs.readFileSync(p, { encoding: 'utf8' });
if (content.indexOf("require('grpc')") !== -1) {
console.info(`Updated require('grpc') to require('@grpc/grpc-js') in ${p}.`);
fs.writeFileSync(p, content.replace("require('grpc')", "require('@grpc/grpc-js')"));
}
content = fs.readFileSync(p, { encoding: 'utf8' });
if (content.indexOf('import * as grpc from "grpc"') !== -1) {
console.info(`Updated import * as grpc from "grpc" to import * as grpc from "@grpc/grpc-js" in ${p}.`);
fs.writeFileSync(p, content.replace('import * as grpc from "grpc"', 'import * as grpc from "@grpc/grpc-js"'));
}
} }
} else {
console.warn(`${p} does not exist. Skipping.`);
} }
} }
console.info('👌 <<< Done. The code has been patched.'); }
})();

View File

@@ -12,10 +12,34 @@ export namespace ArduinoCommands {
label: 'Upload Sketch' label: 'Upload Sketch'
} }
export const SHOW_OPEN_CONTEXT_MENU: Command = {
id: 'arduino-show-open-context-menu',
label: 'Open Sketch'
}
export const OPEN_FILE_NAVIGATOR: Command = {
id: 'arduino-open-file-navigator'
}
export const OPEN_SKETCH: Command = {
id: 'arduino-open-file'
}
export const SAVE_SKETCH: Command = {
id: 'arduino-save-file'
}
export const NEW_SKETCH: Command = { export const NEW_SKETCH: Command = {
id: "arduino-new-sketch", id: "arduino-new-sketch",
label: 'New Sketch', label: 'New Sketch',
category: 'File' category: 'File'
} }
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,15 +0,0 @@
import { injectable } from "inversify";
import { MenuContribution, MenuModelRegistry } from "@theia/core";
import { CommonMenus } from "@theia/core/lib/browser";
import { ArduinoCommands } from "./arduino-commands";
@injectable()
export class ArduinoFileMenuContribution implements MenuContribution {
registerMenus(registry: MenuModelRegistry) {
registry.registerMenuAction([...CommonMenus.FILE, '0_new_sletch'], {
commandId: ArduinoCommands.NEW_SKETCH.id
})
}
}

View File

@@ -3,36 +3,83 @@ import { injectable, inject, postConstruct } from 'inversify';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { EditorWidget } from '@theia/editor/lib/browser/editor-widget'; import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
import { MessageService } from '@theia/core/lib/common/message-service'; import { MessageService } from '@theia/core/lib/common/message-service';
import { CommandContribution, CommandRegistry } from '@theia/core/lib/common/command'; import { CommandContribution, CommandRegistry, Command } from '@theia/core/lib/common/command';
import { DefaultFrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { BoardsService } from '../common/protocol/boards-service'; import { BoardsService } from '../common/protocol/boards-service';
import { ArduinoCommands } from './arduino-commands'; import { ArduinoCommands } from './arduino-commands';
import { ConnectedBoards } from './components/connected-boards';
import { CoreService } from '../common/protocol/core-service'; import { CoreService } from '../common/protocol/core-service';
import { WorkspaceServiceExt } from './workspace-service-ext'; import { WorkspaceServiceExt } from './workspace-service-ext';
import { ToolOutputServiceClient } from '../common/protocol/tool-output-service'; import { ToolOutputServiceClient } from '../common/protocol/tool-output-service';
import { ConfirmDialog } from '@theia/core/lib/browser';
import { QuickPickService } from '@theia/core/lib/common/quick-pick-service'; import { QuickPickService } from '@theia/core/lib/common/quick-pick-service';
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution'; import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
import { BoardsNotificationService } from './boards-notification-service'; import { BoardsServiceClientImpl } from './boards/boards-service-client-impl';
import { WorkspaceRootUriAwareCommandHandler } from '@theia/workspace/lib/browser/workspace-commands'; import { WorkspaceRootUriAwareCommandHandler, WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
import { SelectionService } from '@theia/core'; import { SelectionService, MenuContribution, MenuModelRegistry, MAIN_MENU_BAR, MenuPath } from '@theia/core';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import { ArduinoToolbar } from './toolbar/arduino-toolbar';
import { SketchFactory } from './sketch-factory'; import { EditorManager, EditorMainMenu } from '@theia/editor/lib/browser';
import {
ContextMenuRenderer,
OpenerService,
Widget,
StatusBar,
ShellLayoutRestorer,
StatusBarAlignment,
QuickOpenService
} from '@theia/core/lib/browser';
import { OpenFileDialogProps, FileDialogService } from '@theia/filesystem/lib/browser/file-dialog';
import { FileSystem, FileStat } from '@theia/filesystem/lib/common';
import { Sketch, SketchesService } from '../common/protocol/sketches-service';
import { WindowService } from '@theia/core/lib/browser/window/window-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 { BoardsConfigDialog } from './boards/boards-config-dialog';
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
import { BoardsConfig } from './boards/boards-config';
import { MonitorService } from '../common/protocol/monitor-service';
import { ConfigService } from '../common/protocol/config-service';
import { MonitorConnection } from './monitor/monitor-connection';
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
import { ArduinoWorkspaceService } from './arduino-workspace-service';
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'];
}
export namespace ArduinoAdvancedMode {
export const LS_ID = 'arduino-advanced-mode';
export const TOGGLED: boolean = (() => {
const advancedModeStr = window.localStorage.getItem(LS_ID);
return advancedModeStr === 'true';
})();
}
@injectable() @injectable()
export class ArduinoFrontendContribution extends DefaultFrontendApplicationContribution implements TabBarToolbarContribution, CommandContribution { export class ArduinoFrontendContribution implements TabBarToolbarContribution, CommandContribution, MenuContribution {
@inject(MessageService) @inject(MessageService)
protected readonly messageService: MessageService; protected readonly messageService: MessageService;
@inject(BoardsService) @inject(BoardsService)
protected readonly boardService: BoardsService; protected readonly boardsService: BoardsService;
@inject(CoreService) @inject(CoreService)
protected readonly coreService: CoreService; protected readonly coreService: CoreService;
@inject(MonitorService)
protected readonly monitorService: MonitorService;
@inject(WorkspaceServiceExt) @inject(WorkspaceServiceExt)
protected readonly workspaceServiceExt: WorkspaceServiceExt; protected readonly workspaceServiceExt: WorkspaceServiceExt;
@@ -45,72 +92,138 @@ export class ArduinoFrontendContribution extends DefaultFrontendApplicationContr
@inject(BoardsListWidgetFrontendContribution) @inject(BoardsListWidgetFrontendContribution)
protected readonly boardsListWidgetFrontendContribution: BoardsListWidgetFrontendContribution; protected readonly boardsListWidgetFrontendContribution: BoardsListWidgetFrontendContribution;
@inject(BoardsNotificationService) @inject(BoardsServiceClientImpl)
protected readonly boardsNotificationService: BoardsNotificationService; protected readonly boardsServiceClient: BoardsServiceClientImpl;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(SelectionService) @inject(SelectionService)
protected readonly selectionService: SelectionService; protected readonly selectionService: SelectionService;
@inject(SketchFactory) @inject(EditorManager)
protected readonly sketchFactory: SketchFactory; protected readonly editorManager: EditorManager;
@inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(FileDialogService)
protected readonly fileDialogService: FileDialogService;
@inject(FileSystem)
protected readonly fileSystem: FileSystem;
@inject(OpenerService)
protected readonly openerService: OpenerService;
@inject(WindowService)
protected readonly windowService: WindowService;
@inject(SketchesService)
protected readonly sketchService: SketchesService;
@inject(BoardsConfigDialog)
protected readonly boardsConfigDialog: BoardsConfigDialog;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(CommandRegistry)
protected readonly commands: CommandRegistry;
@inject(StatusBar)
protected readonly statusBar: StatusBar;
@inject(ShellLayoutRestorer)
protected readonly layoutRestorer: ShellLayoutRestorer;
@inject(QuickOpenService)
protected readonly quickOpenService: QuickOpenService;
@inject(ArduinoWorkspaceService)
protected readonly workspaceService: ArduinoWorkspaceService;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(MonitorConnection)
protected readonly monitorConnection: MonitorConnection;
protected boardsToolbarItem: BoardsToolBarItem | null;
protected wsSketchCount: number = 0;
@postConstruct() @postConstruct()
protected async init(): Promise<void> { protected async init(): Promise<void> {
// This is a hack. Otherwise, the backend services won't bind. // This is a hack. Otherwise, the backend services won't bind.
await this.workspaceServiceExt.roots(); 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));
} }
registerToolbarItems(registry: TabBarToolbarRegistry): void { registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({ registry.registerItem({
id: ArduinoCommands.VERIFY.id, id: ArduinoCommands.VERIFY.id,
command: ArduinoCommands.VERIFY.id, command: ArduinoCommands.VERIFY.id,
tooltip: 'Verify', tooltip: 'Verify'
group: 'arduino',
text: '$(check)'
}); });
registry.registerItem({ registry.registerItem({
id: ArduinoCommands.UPLOAD.id, id: ArduinoCommands.UPLOAD.id,
command: ArduinoCommands.UPLOAD.id, command: ArduinoCommands.UPLOAD.id,
tooltip: 'Upload', tooltip: 'Upload'
group: 'arduino',
text: '$(arrow-right)'
}); });
registry.registerItem({ registry.registerItem({
id: ConnectedBoards.TOOLBAR_ID, id: ArduinoCommands.SHOW_OPEN_CONTEXT_MENU.id,
render: () => <ConnectedBoards command: ArduinoCommands.SHOW_OPEN_CONTEXT_MENU.id,
boardsService={this.boardService} tooltip: 'Open'
boardsNotificationService={this.boardsNotificationService} });
quickPickService={this.quickPickService} registry.registerItem({
onNoBoardsInstalled={this.onNoBoardsInstalled.bind(this)} id: ArduinoCommands.SAVE_SKETCH.id,
onUnknownBoard={this.onUnknownBoard.bind(this)} />, command: ArduinoCommands.SAVE_SKETCH.id,
isVisible: widget => this.isArduinoEditor(widget) tooltip: 'Save'
}) });
registry.registerItem({
id: BoardsToolBarItem.TOOLBAR_ID,
render: () => <BoardsToolBarItem
key='boardsToolbarItem'
ref={ref => this.boardsToolbarItem = ref}
commands={this.commands}
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: (ArduinoAdvancedMode.TOGGLED ? '$(toggle-on)' : '$(toggle-off)'),
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'right'
});
} }
registerCommands(registry: CommandRegistry): void { registerCommands(registry: CommandRegistry): void {
registry.registerCommand(ArduinoCommands.VERIFY, { registry.registerCommand(ArduinoCommands.VERIFY, {
isVisible: widget => this.isArduinoEditor(widget), isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: widget => this.isArduinoEditor(widget), isEnabled: widget => true,
execute: async widget => { execute: async () => {
if (widget instanceof EditorWidget) { const widget = this.getCurrentWidget();
await widget.saveable.save();
}
const uri = this.toUri(widget);
if (uri) {
const result = await this.coreService.compile({ uri: uri.toString() });
console.log('compile result', result);
}
}
});
registry.registerCommand(ArduinoCommands.UPLOAD, {
isVisible: widget => this.isArduinoEditor(widget),
isEnabled: widget => this.isArduinoEditor(widget),
execute: async widget => {
if (widget instanceof EditorWidget) { if (widget instanceof EditorWidget) {
await widget.saveable.save(); await widget.saveable.save();
} }
@@ -121,47 +234,273 @@ export class ArduinoFrontendContribution extends DefaultFrontendApplicationContr
} }
try { 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.');
}
if (!boardsConfig.selectedBoard.fqbn) {
throw new Error(`No core is installed for ${boardsConfig.selectedBoard.name}. Please install the board.`);
}
await this.coreService.compile({ uri: uri.toString(), board: boardsConfig.selectedBoard });
} catch (e) { } catch (e) {
new ConfirmDialog({ title: "Error during upload", msg: e.toString(), ok: "Ok" }).open(); await this.messageService.error(e.toString());
} }
} }
}); });
registry.registerCommand(ArduinoCommands.UPLOAD, {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: widget => true,
execute: async () => {
const widget = this.getCurrentWidget();
if (widget instanceof EditorWidget) {
await widget.saveable.save();
}
const uri = this.toUri(widget);
if (!uri) {
return;
}
const connectionConfig = this.monitorConnection.connectionConfig;
await this.monitorConnection.disconnect();
try {
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.');
}
await this.coreService.upload({ uri: uri.toString(), board: boardsConfig.selectedBoard, port: selectedPort.address });
} catch (e) {
await this.messageService.error(e.toString());
} finally {
if (connectionConfig) {
await this.monitorConnection.connect(connectionConfig);
}
}
}
});
registry.registerCommand(ArduinoCommands.SHOW_OPEN_CONTEXT_MENU, {
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;
if (el) {
this.contextMenuRenderer.render(ArduinoToolbarContextMenu.OPEN_SKETCH_PATH, {
x: el.getBoundingClientRect().left,
y: el.getBoundingClientRect().top + el.offsetHeight
});
}
} else {
this.commands.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.workspaceService.open(new URI(sketch.uri));
}
})
registry.registerCommand(ArduinoCommands.SAVE_SKETCH, {
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, { registry.registerCommand(ArduinoCommands.NEW_SKETCH, new WorkspaceRootUriAwareCommandHandler(this.workspaceService, this.selectionService, {
execute: async uri => { execute: async uri => {
try { try {
await this.sketchFactory.createNewSketch(uri); // hack: sometimes we don't get the workspace root, but the currently active file: correct for that
if (uri.path.ext !== "") {
uri = uri.withPath(uri.path.dir.dir);
}
const sketch = await this.sketchService.createNewSketch(uri.toString());
this.workspaceService.open(new URI(sketch.uri));
} catch (e) { } catch (e) {
new ConfirmDialog({ title: "Cannot create new sketch", msg: e.toString(), ok: "Ok" }).open(); await this.messageService.error(e.toString());
} }
} }
})) }));
registry.registerCommand(ArduinoCommands.OPEN_BOARDS_DIALOG, {
isEnabled: () => true,
execute: async () => {
const boardsConfig = await this.boardsConfigDialog.open();
if (boardsConfig) {
this.boardsServiceClient.boardsConfig = boardsConfig;
}
}
})
registry.registerCommand(ArduinoCommands.TOGGLE_ADVANCED_MODE, {
execute: () => {
const oldModeState = ArduinoAdvancedMode.TOGGLED;
window.localStorage.setItem(ArduinoAdvancedMode.LS_ID, oldModeState ? 'false' : 'true');
registry.executeCommand('reset.layout');
},
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'right',
isToggled: () => ArduinoAdvancedMode.TOGGLED
})
} }
private async onNoBoardsInstalled() { registerMenus(registry: MenuModelRegistry) {
const action = await this.messageService.info("You have no boards installed. Use the boards mangager to install one.", "Open Boards Manager"); if (!ArduinoAdvancedMode.TOGGLED) {
if (!action) { // If are not in pro-mode, we have to disable the context menu for the tabs.
return; // 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
]) {
registry.unregisterMenuAction(command);
}
registry.unregisterMenuAction(FileSystemCommands.UPLOAD);
registry.unregisterMenuAction(FileDownloadCommands.DOWNLOAD);
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.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));
} }
this.boardsListWidgetFrontendContribution.openView({reveal: true}); registry.registerSubmenu(ArduinoMenus.SKETCH, 'Sketch');
registry.registerMenuAction(ArduinoMenus.SKETCH, {
commandId: ArduinoCommands.VERIFY.id,
label: 'Verify/Compile',
order: '1'
});
registry.registerMenuAction(ArduinoMenus.SKETCH, {
commandId: ArduinoCommands.UPLOAD.id,
label: 'Upload',
order: '2'
});
registry.registerMenuAction(ArduinoToolbarContextMenu.OPEN_GROUP, {
commandId: ArduinoCommands.OPEN_FILE_NAVIGATOR.id,
label: 'Open...'
});
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
});
} }
private async onUnknownBoard() { protected getMenuId(menuPath: string[]): string {
const action = await this.messageService.warn("There's a board connected for which you need to install software." + const index = menuPath.length - 1;
" If this were not a PoC we would offer you the right package now.", "Open Boards Manager"); const menuId = menuPath[index];
if (!action) { return menuId;
return;
}
this.boardsListWidgetFrontendContribution.openView({reveal: true});
} }
private isArduinoEditor(maybeEditorWidget: any): boolean { protected async registerSketchesInMenu(registry: MenuModelRegistry): Promise<void> {
if (maybeEditorWidget instanceof EditorWidget) { this.sketchService.getSketches().then(sketches => {
return maybeEditorWidget.editor.uri.toString().endsWith('.ino'); 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)
});
registry.registerMenuAction(ArduinoToolbarContextMenu.WS_SKETCHES_GROUP, {
commandId: command.id,
label: sketch.name
});
})
})
}
async openSketchFiles(uri: string): Promise<void> {
this.sketchService.getSketchFiles(uri).then(uris => {
for (const uri of uris) {
this.editorManager.open(new URI(uri));
}
});
}
/**
* Opens a file after prompting the `Open File` dialog. Resolves to `undefined`, if
* - the workspace root is not set,
* - the file to open does not exist, or
* - it was not a file, but a directory.
*
* Otherwise, resolves to the URI of the file.
*/
protected async doOpenFile(): Promise<URI | undefined> {
const props: OpenFileDialogProps = {
title: WorkspaceCommands.OPEN_FILE.dialogLabel,
canSelectFolders: false,
canSelectFiles: true
};
const [rootStat] = await this.workspaceService.roots;
const destinationFileUri = await this.fileDialogService.showOpenDialog(props, rootStat);
if (destinationFileUri) {
const destinationFile = await this.fileSystem.getFileStat(destinationFileUri.toString());
if (destinationFile && !destinationFile.isDirectory) {
const message = await this.validate(destinationFile);
if (!message) {
await this.workspaceService.open(destinationFileUri);
return destinationFileUri;
} else {
this.messageService.warn(message);
}
}
} }
return false; return undefined;
}
protected getCurrentWidget(): EditorWidget | undefined {
let widget = this.editorManager.currentEditor;
if (!widget) {
const visibleWidgets = this.editorManager.all.filter(w => w.isVisible);
if (visibleWidgets.length > 0) {
widget = visibleWidgets[0];
}
}
return widget;
}
/**
* Returns `undefined` if the `file` is valid. Otherwise, returns with the validation error message.
*/
protected validate(file: FileStat): MaybePromise<string | undefined> {
const uri = new URI(file.uri);
const path = uri.path;
const { name, ext, dir } = path;
if (ext !== '.ino') {
return "Only sketches with '.ino' extension can be opened.";
}
if (name !== dir.name) {
return `The file "${name}${ext}" needs to be inside a sketch folder named "${name}".`;
}
return undefined;
} }
private toUri(arg: any): URI | undefined { private toUri(arg: any): URI | undefined {

View File

@@ -5,14 +5,16 @@ import { CommandContribution } from '@theia/core/lib/common/command';
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution'; import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider'; import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application' import { FrontendApplicationContribution, FrontendApplication } from '@theia/core/lib/browser/frontend-application'
import { LanguageGrammarDefinitionContribution } from '@theia/monaco/lib/browser/textmate'; 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 { LibraryListWidget } from './library/library-list-widget';
import { ArduinoFrontendContribution } from './arduino-frontend-contribution'; import { ArduinoFrontendContribution, ArduinoAdvancedMode } from './arduino-frontend-contribution';
import { ArduinoLanguageGrammarContribution } from './language/arduino-language-grammar-contribution'; import { ArduinoLanguageGrammarContribution } from './language/arduino-language-grammar-contribution';
import { LibraryService, LibraryServicePath } from '../common/protocol/library-service'; 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 { LibraryListWidgetFrontendContribution } from './library/list-widget-frontend-contribution'; import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service';
import { CoreService, CoreServicePath } from '../common/protocol/core-service'; import { CoreService, CoreServicePath } from '../common/protocol/core-service';
import { BoardsListWidget } from './boards/boards-list-widget'; import { BoardsListWidget } from './boards/boards-list-widget';
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution'; import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
@@ -21,24 +23,72 @@ import { WorkspaceServiceExtImpl } from './workspace-service-ext-impl';
import { ToolOutputServiceClient } from '../common/protocol/tool-output-service'; import { ToolOutputServiceClient } from '../common/protocol/tool-output-service';
import { ToolOutputService } from '../common/protocol/tool-output-service'; import { ToolOutputService } from '../common/protocol/tool-output-service';
import { ToolOutputServiceClientImpl } from './tool-output/client-service-impl'; 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 { 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 { ThemeService } from '@theia/core/lib/browser/theming';
import { ArduinoTheme } from './arduino-theme'; import { ArduinoTheme } from './arduino-theme';
import { ArduinoFileMenuContribution } from './arduino-file-menu';
import { MenuContribution } from '@theia/core'; 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 { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { SilentProblemContribution } from './customization/silent-problem-contribution';
import { SilentNavigatorContribution } from './customization/silent-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 { EditorContribution } from '@theia/editor/lib/browser/editor-contribution';
import { ArduinoEditorContribution } from './customization/arduino-editor-contribution';
import { MonacoStatusBarContribution } from '@theia/monaco/lib/browser/monaco-status-bar-contribution';
import { ArduinoMonacoStatusBarContribution } from './customization/arduino-monaco-status-bar-contribution';
import { ApplicationShell } from '@theia/core/lib/browser';
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 { SilentScmContribution } from './customization/silent-scm-contribution';
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { SilentSearchInWorkspaceContribution } from './customization/silent-search-in-workspace-contribution';
import { LibraryListWidgetFrontendContribution } from './library/library-widget-frontend-contribution';
import { LibraryItemRenderer } from './library/library-item-renderer';
import { BoardItemRenderer } from './boards/boards-item-renderer';
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';
const ElementQueries = require('css-element-queries/src/ElementQueries');
export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => { export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => {
ElementQueries.listen();
ElementQueries.init();
// Commands and toolbar items // Commands and toolbar items
bind(ArduinoFrontendContribution).toSelf().inSingletonScope(); bind(ArduinoFrontendContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(ArduinoFrontendContribution); bind(CommandContribution).toService(ArduinoFrontendContribution);
bind(MenuContribution).toService(ArduinoFrontendContribution);
bind(TabBarToolbarContribution).toService(ArduinoFrontendContribution); bind(TabBarToolbarContribution).toService(ArduinoFrontendContribution);
bind(MenuContribution).to(ArduinoFileMenuContribution).inSingletonScope(); bind(FrontendApplicationContribution).toService(ArduinoFrontendContribution);
// `ino` TextMate grammar bind(ArduinoToolbarContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(ArduinoToolbarContribution);
// `ino` TextMate grammar and language client
bind(LanguageGrammarDefinitionContribution).to(ArduinoLanguageGrammarContribution).inSingletonScope(); bind(LanguageGrammarDefinitionContribution).to(ArduinoLanguageGrammarContribution).inSingletonScope();
bind(LanguageClientContribution).to(ArduinoLanguageClientContribution).inSingletonScope();
// Library service // Library service
bind(LibraryService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, LibraryServicePath)).inSingletonScope(); bind(LibraryService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, LibraryServicePath)).inSingletonScope();
@@ -51,13 +101,31 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
createWidget: () => context.container.get(LibraryListWidget) createWidget: () => context.container.get(LibraryListWidget)
})); }));
bind(FrontendApplicationContribution).toService(LibraryListWidgetFrontendContribution); bind(FrontendApplicationContribution).toService(LibraryListWidgetFrontendContribution);
bind(LibraryItemRenderer).toSelf().inSingletonScope();
// Boards Notification service for updating boards list // Sketch list service
// TODO (post-PoC): move this to boards service/backend bind(SketchesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, SketchesServicePath)).inSingletonScope();
bind(BoardsNotificationService).toSelf().inSingletonScope();
// Config service
bind(ConfigService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ConfigServicePath)).inSingletonScope();
// Boards service // 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(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 // Boards list widget
bind(BoardsListWidget).toSelf(); bind(BoardsListWidget).toSelf();
@@ -67,6 +135,14 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
createWidget: () => context.container.get(BoardsListWidget) createWidget: () => context.container.get(BoardsListWidget)
})); }));
bind(FrontendApplicationContribution).toService(BoardsListWidgetFrontendContribution); bind(FrontendApplicationContribution).toService(BoardsListWidgetFrontendContribution);
bind(BoardItemRenderer).toSelf().inSingletonScope();
// Board select dialog
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
bind(BoardsConfigDialog).toSelf().inSingletonScope();
bind(BoardsConfigDialogProps).toConstantValue({
title: 'Select Board'
})
// Core service // Core service
bind(CoreService) bind(CoreService)
@@ -88,12 +164,84 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
container.get(CoreService); container.get(CoreService);
container.get(LibraryService); container.get(LibraryService);
container.get(BoardsService); container.get(BoardsService);
container.get(SketchesService);
return workspaceServiceExt; return workspaceServiceExt;
}); });
rebind(WorkspaceService).to(AWorkspaceService).inSingletonScope(); // Serial Monitor
bind(SketchFactory).toSelf().inSingletonScope(); bind(MonitorModel).toSelf().inSingletonScope();
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).to(ArduinoWorkspaceService).inSingletonScope();
const themeService = ThemeService.get(); const themeService = ThemeService.get();
themeService.register(...ArduinoTheme.themes); themeService.register(...ArduinoTheme.themes);
// Customizing default Theia layout
if (!ArduinoAdvancedMode.TOGGLED) {
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(ArduinoEditorContribution).inSingletonScope();
unbind(MonacoStatusBarContribution);
bind(MonacoStatusBarContribution).to(ArduinoMonacoStatusBarContribution).inSingletonScope();
unbind(ApplicationShell);
bind(ApplicationShell).to(ArduinoApplicationShell).inSingletonScope();
unbind(ScmContribution);
bind(ScmContribution).to(SilentScmContribution).inSingletonScope();
unbind(SearchInWorkspaceFrontendContribution);
bind(SearchInWorkspaceFrontendContribution).to(SilentSearchInWorkspaceContribution).inSingletonScope();
} else {
// 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(ArduinoAdvancedMode.LS_ID);
}
unbind(FrontendApplication);
bind(FrontendApplication).to(ArduinoFrontendApplication).inSingletonScope();
// Monaco customizations
unbind(MonacoEditorProvider);
bind(ArduinoMonacoEditorProvider).toSelf().inSingletonScope();
bind(MonacoEditorProvider).toService(ArduinoMonacoEditorProvider);
// Decorator customizations
unbind(TabBarDecoratorService);
bind(ArduinoTabBarDecoratorService).toSelf().inSingletonScope();
bind(TabBarDecoratorService).toService(ArduinoTabBarDecoratorService);
// Problem markers
unbind(ProblemManager);
bind(ArduinoProblemManager).toSelf().inSingletonScope();
bind(ProblemManager).toService(ArduinoProblemManager);
// About dialog to show the CLI version
unbind(AboutDialog);
bind(ArduinoAboutDialog).toSelf().inSingletonScope();
bind(AboutDialog).toService(ArduinoAboutDialog);
}); });

View File

@@ -19,9 +19,10 @@ export class ArduinoTheme {
deactivate() { deactivate() {
ARDUINO_CSS.unuse(); ARDUINO_CSS.unuse();
} }
} };
static readonly themes: Theme[] = [ static readonly themes: Theme[] = [
ArduinoTheme.arduino 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.bind(this)(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,56 @@
import { WorkspaceService } from "@theia/workspace/lib/browser/workspace-service"; import { injectable, inject } from 'inversify';
import { injectable, inject } from "inversify"; import { LabelProvider } from '@theia/core/lib/browser';
import { WorkspaceServer } from "@theia/workspace/lib/common"; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { FileSystem, FileStat } from "@theia/filesystem/lib/common"; import { ConfigService } from '../common/protocol/config-service';
import URI from "@theia/core/lib/common/uri"; import { SketchesService } from '../common/protocol/sketches-service';
import { SketchFactory } from "./sketch-factory"; import { ArduinoWorkspaceRootResolver } from './arduino-workspace-resolver';
import { ArduinoAdvancedMode } from './arduino-frontend-contribution';
/**
* This is workaround to have custom frontend binding for the default workspace, although we
* already have a custom binding for the backend.
*/
@injectable() @injectable()
export class AWorkspaceService extends WorkspaceService { export class ArduinoWorkspaceService extends WorkspaceService {
@inject(WorkspaceServer) @inject(SketchesService)
protected readonly workspaceServer: WorkspaceServer; protected readonly sketchService: SketchesService;
@inject(FileSystem) @inject(ConfigService)
protected readonly fileSystem: FileSystem; protected readonly configService: ConfigService;
@inject(SketchFactory) @inject(LabelProvider)
protected readonly sketchFactory: SketchFactory; protected readonly labelProvider: LabelProvider;
protected async getDefaultWorkspacePath(): Promise<string | undefined> { async getDefaultWorkspacePath(): Promise<string | undefined> {
let result = await super.getDefaultWorkspacePath(); const [hash, recentWorkspaces, recentSketches] = await Promise.all([
if (!result) { window.location.hash,
const userHome = await this.fileSystem.getCurrentUserHome(); this.sketchService.getSketches().then(sketches => sketches.map(({ uri }) => uri)),
if (!userHome) { this.server.getRecentWorkspaces()
return; ]);
} const toOpen = await new ArduinoWorkspaceRootResolver({
isValid: this.isValid.bind(this)
// The backend has created this location if it was missing. }).resolve({
result = new URI(userHome.uri).resolve('Arduino-PoC').resolve('Sketches').toString(); hash,
recentWorkspaces,
recentSketches
});
if (toOpen) {
const { uri } = toOpen;
await this.server.setMostRecentlyUsedWorkspace(uri);
return toOpen.uri;
} }
return (await this.sketchService.createNewSketch()).uri;
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;
} }
protected async setWorkspace(workspaceStat: FileStat | undefined): Promise<void> { private async isValid(uri: string): Promise<boolean> {
await super.setWorkspace(workspaceStat); 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 (!ArduinoAdvancedMode.TOGGLED) {
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/installation-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?`, 'Yes', 'Install Manually').then(async answer => {
if (answer === 'Yes') {
const dialog = new InstallationProgressDialog(candidate.name);
dialog.open();
try {
await this.boardsService.install(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,54 @@
import * as React from 'react';
import { injectable } from 'inversify';
import { ListItemRenderer } from '../components/component-list/list-item-renderer';
import { BoardPackage } from '../../common/protocol/boards-service';
@injectable()
export class BoardItemRenderer extends ListItemRenderer<BoardPackage> {
renderItem(item: BoardPackage, install: (item: BoardPackage) => Promise<void>): React.ReactNode {
const name = <span className='name'>{item.name}</span>;
const author = <span className='author'>{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 description = <div className='summary'>{item.description}</div>;
const moreInfo = !!item.moreInfoLink && <a href={item.moreInfoLink} onClick={this.onClick}>More info</a>;
const installButton = item.installable && !item.installedVersion &&
<button className='install' onClick={install.bind(this, item)}>INSTALL</button>;
const versions = (() => {
const { availableVersions } = item;
if (!!item.installedVersion || 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} by {author}</span>
{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,27 @@
import { inject, injectable } from 'inversify';
import { BoardPackage, BoardsService } from '../../common/protocol/boards-service';
import { ListWidget } from '../components/component-list/list-widget';
import { BoardItemRenderer } from './boards-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(BoardItemRenderer) protected itemRenderer: BoardItemRenderer) {
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,122 @@
import { injectable, inject, postConstruct } from 'inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { ILogger } from '@theia/core/lib/common/logger';
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
import { RecursiveRequired } from '../../common/types';
import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, AttachedSerialBoard, Board, Port } from '../../common/protocol/boards-service';
import { BoardsConfig } from './boards-config';
@injectable()
export class BoardsServiceClientImpl implements BoardsServiceClient {
@inject(ILogger)
protected logger: ILogger;
@inject(LocalStorageService)
protected storageService: LocalStorageService;
protected readonly onBoardInstalledEmitter = new Emitter<BoardInstalledEvent>();
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 onBoardsConfigChanged = this.onSelectedBoardsConfigChangedEmitter.event;
@postConstruct()
protected init(): void {
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);
}
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;
}
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;
}
}
protected canVerify(config: BoardsConfig.Config | undefined): config is BoardsConfig.Config & { selectedBoard: Board } {
return !!config && !!config.selectedBoard;
}
protected canUploadTo(config: BoardsConfig.Config | undefined): config is RecursiveRequired<BoardsConfig.Config> {
return this.canVerify(config) && !!config.selectedPort && !!config.selectedBoard.fqbn;
}
}

View File

@@ -0,0 +1,202 @@
import * as React from 'react';
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 { BoardsServiceClientImpl } from './boards-service-client-impl';
import { BoardsConfig } from './boards-config';
export interface BoardsDropDownListCoords {
readonly top: number;
readonly left: number;
readonly width: number;
readonly paddingTop: number;
}
export namespace BoardsDropDown {
export interface Props {
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 dropdownElement: HTMLElement;
constructor(props: BoardsDropDown.Props) {
super(props);
let list = document.getElementById('boards-dropdown-container');
if (!list) {
list = document.createElement('div');
list.id = 'boards-dropdown-container';
document.body.appendChild(list);
this.dropdownElement = list;
}
}
render(): React.ReactNode {
return ReactDOM.createPortal(this.renderNode(), this.dropdownElement);
}
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 boardService: BoardsService;
readonly boardsServiceClient: BoardsServiceClientImpl;
readonly commands: CommandRegistry;
}
export interface State {
boardsConfig: BoardsConfig.Config;
attachedBoards: Board[];
availablePorts: Port[];
coords: BoardsDropDownListCoords | 'hidden';
}
}
export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props, BoardsToolBarItem.State> {
static TOOLBAR_ID: 'boards-toolbar';
protected readonly toDispose: DisposableCollection = new DisposableCollection();
constructor(props: BoardsToolBarItem.Props) {
super(props);
this.state = {
boardsConfig: this.props.boardsServiceClient.boardsConfig,
attachedBoards: [],
availablePorts: [],
coords: 'hidden'
};
document.addEventListener('click', () => {
this.setState({ coords: 'hidden' });
});
}
componentDidMount() {
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 })
});
}
componentWillUnmount(): void {
this.toDispose.dispose();
}
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'});
}
}
event.stopPropagation();
event.nativeEvent.stopImmediatePropagation();
};
render(): React.ReactNode {
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={title}>
<div className='inner-container' onClick={this.show}>
<span className={!configuredBoard ? 'fa fa-times notAttached' : ''}/>
<div className='label noWrapInfo'>
<div className='noWrapInfo noselect'>
{title}
</div>
</div>
<span className='fa fa-caret-down caret'/>
</div>
</div>
</div>
<BoardsDropDown
coords={coords}
items={items}
openBoardsConfig={this.openDialog}>
</BoardsDropDown>
</React.Fragment>;
}
protected openDialog = () => {
this.props.commands.executeCommand(ArduinoCommands.OPEN_BOARDS_DIALOG.id);
this.setState({ coords: 'hidden' });
};
}

View File

@@ -1,20 +1,14 @@
import { injectable } from 'inversify'; import { injectable } from 'inversify';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; import { MenuModelRegistry } from '@theia/core';
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { ListWidget } from './list-widget';
import { BoardsListWidget } from './boards-list-widget'; 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() @injectable()
export abstract class ListWidgetFrontendContribution extends AbstractViewContribution<ListWidget> implements FrontendApplicationContribution { export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardPackage> {
async initializeLayout(): Promise<void> { static readonly OPEN_MANAGER = `${BoardsListWidget.WIDGET_ID}:toggle`;
await this.openView();
}
}
@injectable()
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution {
constructor() { constructor() {
super({ super({
@@ -24,9 +18,18 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
area: 'left', area: 'left',
rank: 600 rank: 600
}, },
toggleCommandId: `${BoardsListWidget.WIDGET_ID}:toggle`, toggleCommandId: BoardsListWidgetFrontendContribution.OPEN_MANAGER,
toggleKeybinding: 'ctrlcmd+shift+b' toggleKeybinding: 'ctrlcmd+shift+b'
}); });
} }
registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS, {
commandId: this.toggleCommand.id,
label: 'Boards Manager...'
});
}
}
} }

View File

@@ -1,87 +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';
@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 }) => 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,78 +1,25 @@
import * as React from 'react'; import * as React from 'react';
import { WindowService } from '@theia/core/lib/browser/window/window-service'; import { ListItemRenderer } from './list-item-renderer';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
export class ComponentListItem extends React.Component<ComponentListItem.Props> { export class ComponentListItem<T> extends React.Component<ComponentListItem.Props<T>> {
private onClick = (event: React.SyntheticEvent<HTMLAnchorElement, Event>) => { protected async install(item: T): Promise<void> {
const { target } = event.nativeEvent;
if (target instanceof HTMLAnchorElement) {
this.props.windowService.openNewWindow(target.href);
event.nativeEvent.preventDefault();
}
}
private async install(item: ArduinoComponent) {
await this.props.install(item); await this.props.install(item);
} }
render(): React.ReactNode { render(): React.ReactNode {
const { item } = this.props; const { item, itemRenderer, install } = this.props;
return itemRenderer.renderItem(item, install.bind(this));
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>;
} }
} }
export namespace ComponentListItem { export namespace ComponentListItem {
export interface Props { export interface Props<T> {
readonly item: ArduinoComponent; readonly item: T;
readonly windowService: WindowService; readonly install: (item: T) => Promise<void>;
readonly install: (comp: ArduinoComponent) => 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';
} }
} }

View File

@@ -1,24 +1,47 @@
import * as React from 'react'; import * as React from 'react';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { ComponentListItem } from './component-list-item'; import { ComponentListItem } from './component-list-item';
import { ArduinoComponent } from '../../../common/protocol/arduino-component'; import { ListItemRenderer } from './list-item-renderer';
export class ComponentList extends React.Component<ComponentList.Props> { export class ComponentList<T> extends React.Component<ComponentList.Props<T>> {
protected container?: HTMLElement;
render(): React.ReactNode { render(): React.ReactNode {
return <div> return <div
{this.props.items.map((item, idx) => <ComponentListItem key={idx} item={item} windowService={this.props.windowService} install={this.props.install} />)} className={'items-container'}
ref={this.setRef}>
{this.props.items.map(item => this.createItem(item))}
</div>; </div>;
} }
componentDidMount(): void {
if (this.container && this.props.resolveContainer) {
this.props.resolveContainer(this.container);
}
}
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} />
}
} }
export namespace ComponentList { export namespace ComponentList {
export interface Props { export interface Props<T> {
readonly items: ArduinoComponent[]; readonly items: T[];
readonly windowService: WindowService; readonly itemLabel: (item: T) => string;
readonly install: (comp: ArduinoComponent) => Promise<void>; readonly itemRenderer: ListItemRenderer<T>;
readonly install: (item: T) => Promise<void>;
readonly resolveContainer: (element: HTMLElement) => void;
} }
} }

View File

@@ -1,64 +1,88 @@
import * as React from 'react'; import * as React from 'react';
import { WindowService } from '@theia/core/lib/browser/window/window-service'; import debounce = require('lodash.debounce');
import { ComponentList } from './component-list'; import { Event } from '@theia/core/lib/common/event';
import { SearchBar } from './search-bar'; import { Searchable } from '../../../common/protocol/searchable';
import { ArduinoComponent } from '../../../common/protocol/arduino-component'; import { Installable } from '../../../common/protocol/installable';
import { InstallationProgressDialog } from '../installation-progress-dialog'; import { InstallationProgressDialog } from '../installation-progress-dialog';
import { SearchBar } from './search-bar';
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 React.Component<FilterableListContainer.Props<T>, FilterableListContainer.State<T>> {
constructor(props: Readonly<FilterableListContainer.Props>) { constructor(props: Readonly<FilterableListContainer.Props<T>>) {
super(props); super(props);
this.state = { this.state = {
filterText: '', filterText: '',
items: [] items: []
}; };
this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
} }
componentWillMount(): void { componentWillMount(): void {
this.search = debounce(this.search, 500);
this.handleFilterTextChange(''); this.handleFilterTextChange('');
this.props.filterTextChangeEvent(this.handleFilterTextChange.bind(this));
} }
render(): React.ReactNode { render(): React.ReactNode {
return <div className={FilterableListContainer.Styles.FILTERABLE_LIST_CONTAINER_CLASS}> return <div className={'filterable-list-container'}>
<SearchBar {this.renderSearchFilter()}
filterText={this.state.filterText} {this.renderSearchBar()}
onFilterTextChanged={this.handleFilterTextChange} {this.renderComponentList()}
/>
<ComponentList
items={this.state.items}
install={this.install.bind(this)}
windowService={this.props.windowService}
/>
</div> </div>
} }
private handleFilterTextChange(filterText: string): void { protected renderSearchFilter(): React.ReactNode {
this.props.service.search({ query: filterText }).then(result => { return undefined;
}
protected renderSearchBar(): React.ReactNode {
return <SearchBar
resolveFocus={this.props.resolveFocus}
filterText={this.state.filterText}
onFilterTextChanged={this.handleFilterTextChange}
/>
}
protected renderComponentList(): React.ReactNode {
const { itemLabel, resolveContainer, itemRenderer } = this.props;
return <ComponentList<T>
items={this.state.items}
itemLabel={itemLabel}
itemRenderer={itemRenderer}
install={this.install.bind(this)}
resolveContainer={resolveContainer}
/>
}
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; const { items } = result;
this.setState({ this.setState({
filterText, items: this.sort(items)
items: items.sort((a, b) => {
if (a.name < b.name) {
return -1;
} else if (a.name === b.name) {
return 0;
} else {
return 1;
}
})
}); });
}); });
} }
protected async install(comp: ArduinoComponent): Promise<void> { protected sort(items: T[]): T[] {
const dialog = new InstallationProgressDialog(comp.name); const { itemLabel } = this.props;
return items.sort((left, right) => itemLabel(left).localeCompare(itemLabel(right)));
}
protected async install(item: T): Promise<void> {
const { installable, searchable, itemLabel } = this.props;
const dialog = new InstallationProgressDialog(itemLabel(item));
dialog.open(); dialog.open();
try { try {
await this.props.service.install(comp); await installable.install(item);
const { items } = await this.props.service.search({ query: this.state.filterText }); const { items } = await searchable.search({ query: this.state.filterText });
this.setState({ items }); this.setState({ items: this.sort(items) });
} finally { } finally {
dialog.close(); dialog.close();
} }
@@ -68,24 +92,19 @@ export class FilterableListContainer extends React.Component<FilterableListConta
export namespace FilterableListContainer { export namespace FilterableListContainer {
export interface Props { export interface Props<T> {
readonly service: ComponentSource; readonly installable: Installable<T>;
readonly windowService: WindowService; 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; filterText: string;
items: ArduinoComponent[]; items: T[];
}
export namespace Styles {
export const FILTERABLE_LIST_CONTAINER_CLASS = 'filterable-list-container';
}
export interface ComponentSource {
search(req: { query: string }): Promise<{ items: ArduinoComponent[] }>
install(board: ArduinoComponent): Promise<void>;
} }
} }

View File

@@ -0,0 +1,21 @@
import * as React from 'react';
import { inject, injectable } from 'inversify';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
@injectable()
export abstract class ListItemRenderer<T> {
@inject(WindowService)
protected windowService: WindowService;
protected onClick = (event: React.SyntheticEvent<HTMLAnchorElement, Event>) => {
const { target } = event.nativeEvent;
if (target instanceof HTMLAnchorElement) {
this.windowService.openNewWindow(target.href, { external: true });
event.nativeEvent.preventDefault();
}
}
abstract renderItem(item: T, install: (item: T) => Promise<void>): React.ReactNode;
}

View File

@@ -0,0 +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';
@injectable()
export abstract class ListWidgetFrontendContribution<T> extends AbstractViewContribution<ListWidget<T>> implements FrontendApplicationContribution {
async initializeLayout(): Promise<void> {
}
}

View File

@@ -0,0 +1,89 @@
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 { FilterableListContainer } from './filterable-list-container';
import { ListItemRenderer } from './list-item-renderer';
@injectable()
export abstract class ListWidget<T> 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>
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));
}
}
export namespace ListWidget {
export interface Options<T> {
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

@@ -9,15 +9,22 @@ export class SearchBar extends React.Component<SearchBar.Props> {
render(): React.ReactNode { render(): React.ReactNode {
return <input return <input
ref={this.setRef}
className={SearchBar.Styles.SEARCH_BAR_CLASS} className={SearchBar.Styles.SEARCH_BAR_CLASS}
type='text' type='text'
placeholder='Search' placeholder='Filter your search...'
size={1} size={1}
value={this.props.filterText} value={this.props.filterText}
onChange={this.handleFilterTextChange} onChange={this.handleFilterTextChange}
/>; />;
} }
private setRef = (element: HTMLElement | null) => {
if (this.props.resolveFocus) {
this.props.resolveFocus(element || undefined);
}
}
private handleFilterTextChange(event: React.ChangeEvent<HTMLInputElement>): void { private handleFilterTextChange(event: React.ChangeEvent<HTMLInputElement>): void {
this.props.onFilterTextChanged(event.target.value); this.props.onFilterTextChanged(event.target.value);
} }
@@ -29,6 +36,7 @@ export namespace SearchBar {
export interface Props { export interface Props {
filterText: string; filterText: string;
onFilterTextChanged(filterText: string): void; onFilterTextChanged(filterText: string): void;
readonly resolveFocus?: (element: HTMLElement | undefined) => void;
} }
export namespace Styles { export namespace Styles {

View File

@@ -1,147 +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';
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 className={ConnectedBoards.Styles.CONNECTED_BOARDS_CLASS}>
<select disabled={!this.state.boards}
onChange={this.onBoardSelect.bind(this)}
value={this.state.selection}>
<optgroup label="Attached boards">
{ content }
</optgroup>
<optgroup label="_________">
{ !!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 +1,12 @@
import { AbstractDialog } from "@theia/core/lib/browser"; import { AbstractDialog } from '@theia/core/lib/browser';
export class InstallationProgressDialog extends AbstractDialog<undefined> {
export class InstallationProgressDialog extends AbstractDialog<string> { readonly value = undefined;
readonly value: "does-not-matter";
constructor(componentName: string) { constructor(componentName: string) {
super({ title: 'Installation in progress' }); super({ title: 'Installation in progress' });
this.contentNode.textContent = `Installing ${componentName}. Please wait.`; this.contentNode.textContent = `Installing ${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,33 @@
import { ApplicationShell, Widget, Saveable, FocusTracker, Message } from '@theia/core/lib/browser';
import { EditorWidget } from '@theia/editor/lib/browser';
export class ArduinoApplicationShell extends ApplicationShell {
protected refreshBottomPanelToggleButton() {
}
protected async track(widget: Widget): Promise<void> {
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

@@ -0,0 +1,23 @@
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 ArduinoEditorContribution extends EditorContribution {
protected updateLanguageStatus(editor: TextEditor | undefined): void {
}
protected setCursorPositionStatus(editor: TextEditor | undefined): void {
if (!editor) {
this.statusBar.removeElement('editor-status-cursor-position');
return;
}
const { cursor } = editor;
this.statusBar.setElement('editor-status-cursor-position', {
text: `${cursor.line + 1}`,
alignment: StatusBarAlignment.LEFT,
priority: 100
});
}
}

View File

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

View File

@@ -0,0 +1,38 @@
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 { ArduinoFrontendContribution, ArduinoAdvancedMode } 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;
protected async initializeLayout(): Promise<void> {
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 (!ArduinoAdvancedMode.TOGGLED) {
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,11 @@
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> {
}
}

View File

@@ -0,0 +1,12 @@
import { injectable } from 'inversify';
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
import { FrontendApplication } from '@theia/core/lib/browser';
@injectable()
export class SilentOutlineViewContribution extends OutlineViewContribution {
async initializeLayout(app: FrontendApplication): Promise<void> {
}
}

View File

@@ -0,0 +1,11 @@
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> {
}
}

View File

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

View File

@@ -0,0 +1,14 @@
import { injectable } from 'inversify';
import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
import { StatusBarEntry } from '@theia/core/lib/browser';
@injectable()
export class SilentScmContribution extends ScmContribution {
async initializeLayout(): Promise<void> {
}
protected setStatusBarEntry(id: string, entry: StatusBarEntry): void {
}
}

View File

@@ -0,0 +1,11 @@
import { injectable } from 'inversify';
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { FrontendApplication } from '@theia/core/lib/browser';
@injectable()
export class SilentSearchInWorkspaceContribution extends SearchInWorkspaceFrontendContribution {
async initializeLayout(app: FrontendApplication): Promise<void> {
}
}

View File

@@ -2,493 +2,89 @@
"tokenColors": [ "tokenColors": [
{ {
"settings": { "settings": {
"background": "#FFFFFF", "foreground": "#434f54"
"foreground": "#000000"
} }
}, },
{ {
"name": "Source", "name": "Comments",
"scope": "source",
"settings": {
"background": "#FFFFFF",
"fontStyle": ""
}
},
{
"name": "Comment",
"scope": "comment", "scope": "comment",
"settings": { "settings": {
"fontStyle": "italic", "foreground": "#95a5a6cc"
"foreground": "#91a6a6"
} }
}, },
{ {
"name": "Docblock", "name": "Keywords Attributes",
"scope": "comment.block",
"settings": {
"fontStyle": "",
"foreground": "#91a6a6"
}
},
{
"name": "Docblock tag",
"scope": "keyword.other.phpdoc",
"settings": {
"fontStyle": "",
"foreground": "#91a6a6"
}
},
{
"name": "Core function",
"scope": "support.function",
"settings": {
"fontStyle": "",
"foreground": "#6f8100"
}
},
{
"name": "Keyword",
"scope": [ "scope": [
"keyword", "storage",
"storage" "support",
"string.quoted.single.c"
], ],
"settings": { "settings": {
"fontStyle": "bold", "foreground": "#00979D"
"foreground": "#000000"
} }
}, },
{ {
"name": "Keyword operator", "name": "literal",
"scope": "keyword.operator.assignment",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "keyword like new",
"scope": "keyword.other.special-method",
"settings": {
"fontStyle": ""
}
},
{
"name": "Function (definition)",
"scope": [ "scope": [
"meta.function.c",
"entity.name.function", "entity.name.function",
"keyword.other.name-of-parameter.objc", "meta.function-call.c"
"support.type.exception.python"
], ],
"settings": { "settings": {
"fontStyle": "bold", "foreground": "#D35400"
"foreground": "#000000"
} }
}, },
{ {
"name": "Class (definition)", "name": "punctuation",
"scope": "entity.name",
"settings": {
"fontStyle": "bold",
"foreground": "#445588"
}
},
{
"name": "Number",
"scope": "constant.numeric",
"settings": {
"fontStyle": "",
"foreground": "#00b0b3"
}
},
{
"name": "Variable",
"scope": [ "scope": [
"variable.language", "punctuation.section",
"variable.other" "meta.function-call.c",
"meta.block.c",
"meta.function.c",
"entity.name.function.preprocessor.c",
"meta.preprocessor.macro.c"
], ],
"settings": { "settings": {
"fontStyle": "", "foreground": "#434f54"
"foreground": "#00b0b3"
} }
}, },
{ {
"name": "Built-in constant", "name": "strings",
"scope": "constant.language",
"settings": {
"fontStyle": "bold",
"foreground": "#000000"
}
},
{
"name": "Variable",
"scope": "variable.other.constant.ruby",
"settings": {
"foreground": "#0F8787"
}
},
{
"name": "Built-in constant",
"scope": [ "scope": [
"constant.language", "string.quoted.double"
"support.function.construct"
], ],
"settings": { "settings": {
"fontStyle": "bold", "foreground": "#005C5F"
"foreground": "#000000"
} }
}, },
{ {
"name": "String", "name": "meta keywords",
"scope": "string",
"settings": {
"background": "#EB104512",
"foreground": "#006062"
}
},
{
"name": "Inherited class",
"scope": "entity.other.inherited-class",
"settings": {
"fontStyle": "",
"foreground": "#0F8787"
}
},
{
"name": "Inherited class seperator",
"scope": "punctuation.separator.inheritance",
"settings": {
"fontStyle": "bold",
"foreground": "#000000"
}
},
{
"name": "punctuation block",
"scope": "punctuation.separator.variable",
"settings": {
"fontStyle": "bold",
"foreground": "#000000"
}
},
{
"name": "array brackets",
"scope": "punctuation.section.array",
"settings": {
"fontStyle": "bold",
"foreground": "#000000"
}
},
{
"name": "hash separator",
"scope": "punctuation.separator.key-value",
"settings": {
"fontStyle": "bold",
"foreground": "#000000"
}
},
{
"name": "() brackets",
"scope": "punctuation.section.function",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "Method call",
"scope": "meta.function-call",
"settings": {
"fontStyle": "",
"foreground": "#108888"
}
},
{
"name": "Method call",
"scope": "meta.function-call.python",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "hash brackets",
"scope": "punctuation.section.scope",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "Special ruby method",
"scope": "keyword.other.special-method.ruby",
"settings": {
"fontStyle": "bold"
}
},
{
"name": "symbol",
"scope": "constant.other.symbol",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "Support class",
"scope": "support.class.ruby",
"settings": {
"fontStyle": "",
"foreground": "#008080"
}
},
{
"name": "String.regexp",
"scope": "string.regexp",
"settings": {
"foreground": "#009926"
}
},
{
"name": "String embedded source",
"scope": "string.quoted source",
"settings": {
"foreground": "#000000"
}
},
{
"name": "doctype punctation",
"scope": "meta.tag.sgml.html",
"settings": {
"fontStyle": "bold",
"foreground": "#999999"
}
},
{
"name": "doctype declaration",
"scope": "entity.name.tag.doctype",
"settings": {
"fontStyle": "bold",
"foreground": "#999999"
}
},
{
"name": "doctype string",
"scope": "string.quoted.double.doctype",
"settings": {
"fontStyle": "bold",
"foreground": "#999999"
}
},
{
"name": "html tag punctuation",
"scope": "punctuation.definition.tag",
"settings": {
"fontStyle": "",
"foreground": "#121289"
}
},
{
"name": "html tag punctuation",
"scope": "entity.name.tag",
"settings": {
"fontStyle": "",
"foreground": "#121289"
}
},
{
"name": "html attribute",
"scope": "entity.other.attribute-name",
"settings": {
"fontStyle": "",
"foreground": "#0A8585"
}
},
{
"name": "html attribute punctation",
"scope": "entity.other.attribute-name",
"settings": {
"fontStyle": "",
"foreground": "#0A8585"
}
},
{
"name": "erb tags",
"scope": "punctuation.section.embedded.ruby",
"settings": {
"fontStyle": "bold",
"foreground": "#999999"
}
},
{
"name": "ruby string interpolation",
"scope": "source.ruby.embedded.source punctuation.section.embedded.ruby",
"settings": {
"foreground": "#CF1040"
}
},
{
"name": "css brackets",
"scope": "punctuation.section.property-list.css",
"settings": {
"fontStyle": ""
}
},
{
"name": "css property",
"scope": "support.type.property-name.css",
"settings": {
"fontStyle": "bold"
}
},
{
"name": "css property punctuation",
"scope": "punctuation.separator.key-value.css",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "css property value",
"scope": [ "scope": [
"meta.property-value", "keyword.control",
"constant.other.color" "meta.preprocessor.c"
], ],
"settings": { "settings": {
"fontStyle": "", "foreground": "#728E00"
"foreground": "#00b0b3"
} }
}, },
{ {
"name": "css ending", "name": "numeric preprocessor",
"scope": "punctuation.terminator.rule.css",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "css font",
"scope": "support.constant.font-name",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "css keyword",
"scope": "keyword.other.unit",
"settings": {
"fontStyle": "",
"foreground": "#00b0b3"
}
},
{
"name": "pseudo class",
"scope": "entity.other.attribute-name.pseudo-class",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "css constant property value",
"scope": "support.constant.property-value",
"settings": {
"fontStyle": "bold",
"foreground": "#000000"
}
},
{
"name": "css attribute class",
"scope": "entity.other.attribute-name.class",
"settings": {
"fontStyle": "bold",
"foreground": "#445588"
}
},
{
"name": "css attribute id",
"scope": "entity.other.attribute-name.id",
"settings": {
"fontStyle": "bold",
"foreground": "#990000"
}
},
{
"name": "diff header from",
"scope": "meta.diff.header.from-file",
"settings": {
"background": "#FFDDDD",
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "diff header to",
"scope": "meta.diff.header.to-file",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "diff inserted",
"scope": "markup.inserted.diff",
"settings": {
"fontStyle": ""
}
},
{
"name": "diff deleted",
"scope": "markup.deleted.diff",
"settings": {
"background": "#FFDDDD",
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "Function Parameters",
"scope": "variable.parameter",
"settings": {
"fontStyle": "italic"
}
},
{
"name": "Library object",
"scope": [ "scope": [
"support.function", "meta.preprocessor.macro.c",
"support.variable", "constant.numeric.preprocessor.c",
"support.constant", "meta.preprocessor.c"
"support.class",
"support.type"
], ],
"settings": { "settings": {
"fontStyle": "bold", "foreground": "#434f54"
"foreground": "#4183c4"
}
},
{
"name": "Function Meta Call Arguments",
"scope": "meta.function-call.arguments",
"settings": {
"fontStyle": "italic"
}
},
{
"name": "Function Call",
"scope": "meta.function-call",
"settings": {
"fontStyle": ""
} }
} }
], ],
"colors": { "colors": {
"editor.background": "#FFFFFF", "editor.background": "#FFFFFF",
"editorCursor.foreground": "#000000", "editorCursor.foreground": "#434f54",
"editor.foreground": "#000000", "editor.foreground": "#434f54",
"editorWhitespace.foreground": "#BFBFBF", "editorWhitespace.foreground": "#BFBFBF",
"editor.lineHighlightBackground": "#00000010", "editor.lineHighlightBackground": "#434f5410",
"editor.selectionBackground": "#ffcb00" "editor.selectionBackground": "#ffcb00"
}, },
"name": "Arduino" "name": "Arduino"

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

@@ -0,0 +1,869 @@
<?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:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
width="198px"
height="99px"
viewBox="0 0 198 99"
enable-background="new 0 0 198 99"
xml:space="preserve"
inkscape:version="0.91 r13725"
sodipodi:docname="buttons.svg"><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></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs325" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1215"
inkscape:window-height="1000"
id="namedview323"
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:zoom="4"
inkscape:cx="45.252385"
inkscape:cy="36.224987"
inkscape:window-x="65"
inkscape:window-y="24"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" /><g
id="g5"
transform="translate(-0.12000084,0)"><circle
cx="16.620001"
cy="16.608999"
r="12"
id="circle7"
style="fill:#ffcc00" /><polyline
stroke-miterlimit="10"
points="21.453,12.745 15.788,20.571 11.775,16.658 "
id="polyline9"
style="fill:none;stroke:#655100;stroke-width:2.30489993;stroke-miterlimit:10" /></g><g
id="g11"
transform="translate(-0.12000084,0)"><circle
cx="16.620001"
cy="49.465"
r="12"
id="circle13"
style="fill:#ffffff" /><polyline
stroke-miterlimit="10"
points="21.453,45.601 15.788,53.427 11.775,49.514 "
id="polyline15"
style="fill:none;stroke:#006468;stroke-width:2.30489993;stroke-miterlimit:10" /></g><g
id="g17"
transform="translate(-0.12000084,0)"><circle
cx="16.620001"
cy="82.476997"
r="12"
id="circle19"
style="fill:#4db7bb" /><polyline
stroke-miterlimit="10"
points="21.453,78.612 15.788,86.438 11.775,82.525 "
id="polyline21"
style="fill:none;stroke:#006468;stroke-width:2.30489993;stroke-miterlimit:10" /></g><g
id="g23"
transform="translate(-0.26100159,0)"><circle
cx="49.761002"
cy="16.608999"
r="12"
id="circle25"
style="fill:#ffcc00" /><polygon
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 "
id="polygon27"
style="fill:#655100" /></g><g
id="g29"
transform="translate(-0.26100159,0)"><circle
cx="49.761002"
cy="49.465"
r="12"
id="circle31"
style="fill:#ffffff" /><polygon
points="45.412,48.169 49.307,48.169 49.307,44.386 54.701,49.731 49.331,55.101 49.331,51.419 45.412,51.394 "
id="polygon33"
style="fill:#006468" /></g><g
id="g35"
transform="translate(-0.26100159,0)"><circle
cx="49.761002"
cy="82.476997"
r="12"
id="circle37"
style="fill:#4db7bb" /><polygon
points="45.412,81.18 49.307,81.18 49.307,77.397 54.701,82.743 49.331,88.113 49.331,84.43 45.412,84.406 "
id="polygon39"
style="fill:#006468" /></g><g
id="g41"
transform="translate(-0.54399872,0)"><rect
x="105.544"
y="6.1090002"
width="21"
height="21"
id="rect43"
style="fill:#ffcc00" /><polygon
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 "
id="polygon45"
style="fill:#655100" /><rect
x="110.511"
y="22.193001"
width="1"
height="1"
id="rect47"
style="fill:#655100" /><rect
x="112.518"
y="22.193001"
width="1"
height="1"
id="rect49"
style="fill:#655100" /><rect
x="114.517"
y="22.193001"
width="1"
height="1"
id="rect51"
style="fill:#655100" /><rect
x="116.525"
y="22.193001"
width="1"
height="1"
id="rect53"
style="fill:#655100" /><rect
x="118.524"
y="22.193001"
width="1"
height="1"
id="rect55"
style="fill:#655100" /><rect
x="120.531"
y="22.193001"
width="1"
height="1"
id="rect57"
style="fill:#655100" /></g><g
id="g59"
transform="translate(-0.68600464,0)"><rect
x="138.686"
y="6.1090002"
width="21"
height="21"
id="rect61"
style="fill:#ffcc00" /><polygon
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 "
id="polygon63"
style="fill:#655100" /><rect
x="143.65199"
y="22.193001"
width="1"
height="1"
id="rect65"
style="fill:#655100" /><rect
x="145.66"
y="22.193001"
width="1"
height="1"
id="rect67"
style="fill:#655100" /><rect
x="147.659"
y="22.193001"
width="1"
height="1"
id="rect69"
style="fill:#655100" /><rect
x="149.666"
y="22.193001"
width="1"
height="1"
id="rect71"
style="fill:#655100" /><rect
x="151.666"
y="22.193001"
width="1"
height="1"
id="rect73"
style="fill:#655100" /><rect
x="153.673"
y="22.193001"
width="1"
height="1"
id="rect75"
style="fill:#655100" /></g><g
id="g77"
transform="translate(-0.54399872,0)"><rect
x="105.544"
y="38.965"
width="21"
height="21"
id="rect79"
style="fill:#ffffff" /><polygon
points="114.44,51.939 114.44,47.971 110.586,47.971 116.032,42.476 121.502,47.947 117.751,47.947 117.726,51.939 "
id="polygon81"
style="fill:#006468" /><rect
x="110.511"
y="55.049"
width="1"
height="1"
id="rect83"
style="fill:#006468" /><rect
x="112.518"
y="55.049"
width="1"
height="1"
id="rect85"
style="fill:#006468" /><rect
x="114.517"
y="55.049"
width="1"
height="1"
id="rect87"
style="fill:#006468" /><rect
x="116.525"
y="55.049"
width="1"
height="1"
id="rect89"
style="fill:#006468" /><rect
x="118.524"
y="55.049"
width="1"
height="1"
id="rect91"
style="fill:#006468" /><rect
x="120.531"
y="55.049"
width="1"
height="1"
id="rect93"
style="fill:#006468" /></g><g
id="g95"
transform="translate(-0.68600464,0)"><rect
x="138.686"
y="38.965"
width="21"
height="21"
id="rect97"
style="fill:#ffffff" /><polygon
points="150.79,42.476 150.79,46.444 154.644,46.444 149.198,51.939 143.728,46.468 147.479,46.468 147.504,42.477 "
id="polygon99"
style="fill:#006468" /><rect
x="143.65199"
y="55.049"
width="1"
height="1"
id="rect101"
style="fill:#006468" /><rect
x="145.66"
y="55.049"
width="1"
height="1"
id="rect103"
style="fill:#006468" /><rect
x="147.659"
y="55.049"
width="1"
height="1"
id="rect105"
style="fill:#006468" /><rect
x="149.666"
y="55.049"
width="1"
height="1"
id="rect107"
style="fill:#006468" /><rect
x="151.666"
y="55.049"
width="1"
height="1"
id="rect109"
style="fill:#006468" /><rect
x="153.673"
y="55.049"
width="1"
height="1"
id="rect111"
style="fill:#006468" /></g><g
id="g113"
transform="translate(-0.54399872,0)"><rect
x="105.544"
y="71.976997"
width="21"
height="21"
id="rect115"
style="fill:#4db7bb" /><polygon
points="114.44,84.95 114.44,80.983 110.586,80.983 116.032,75.488 121.502,80.959 117.751,80.958 117.726,84.95 "
id="polygon117"
style="fill:#006468" /><rect
x="110.511"
y="88.060997"
width="1"
height="1"
id="rect119"
style="fill:#006468" /><rect
x="112.518"
y="88.060997"
width="1"
height="1"
id="rect121"
style="fill:#006468" /><rect
x="114.517"
y="88.060997"
width="1"
height="1"
id="rect123"
style="fill:#006468" /><rect
x="116.525"
y="88.060997"
width="1"
height="1"
id="rect125"
style="fill:#006468" /><rect
x="118.524"
y="88.060997"
width="1"
height="1"
id="rect127"
style="fill:#006468" /><rect
x="120.531"
y="88.060997"
width="1"
height="1"
id="rect129"
style="fill:#006468" /></g><g
id="g131"
transform="translate(-0.68600464,0)"><rect
x="138.686"
y="71.976997"
width="21"
height="21"
id="rect133"
style="fill:#4db7bb" /><polygon
points="150.79,75.488 150.79,79.455 154.644,79.455 149.198,84.95 143.728,79.48 147.479,79.48 147.504,75.488 "
id="polygon135"
style="fill:#006468" /><rect
x="143.65199"
y="88.060997"
width="1"
height="1"
id="rect137"
style="fill:#006468" /><rect
x="145.66"
y="88.060997"
width="1"
height="1"
id="rect139"
style="fill:#006468" /><rect
x="147.659"
y="88.060997"
width="1"
height="1"
id="rect141"
style="fill:#006468" /><rect
x="149.666"
y="88.060997"
width="1"
height="1"
id="rect143"
style="fill:#006468" /><rect
x="151.666"
y="88.060997"
width="1"
height="1"
id="rect145"
style="fill:#006468" /><rect
x="153.673"
y="88.060997"
width="1"
height="1"
id="rect147"
style="fill:#006468" /></g><g
id="g149"
transform="translate(-0.40299988,0)"><path
d="M 87.445,22.097"
id="path151"
inkscape:connector-curvature="0"
style="fill:#655100" /><rect
x="72.403"
y="6.1090002"
width="21"
height="21"
id="rect153"
style="fill:#ffcc00" /><polygon
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 "
id="polygon155"
style="fill:#655100" /><polygon
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 "
id="polygon157"
style="fill:#655100" /><rect
x="79.399002"
y="12.111"
width="0.995"
height="0.99599999"
id="rect159"
style="fill:#655100" /><rect
x="81.394997"
y="12.111"
width="0.995"
height="0.99599999"
id="rect161"
style="fill:#655100" /><rect
x="79.399002"
y="14.103"
width="0.995"
height="0.99599999"
id="rect163"
style="fill:#655100" /><rect
x="81.394997"
y="14.103"
width="0.995"
height="0.99599999"
id="rect165"
style="fill:#655100" /><rect
x="79.399002"
y="16.115999"
width="0.995"
height="0.99599999"
id="rect167"
style="fill:#655100" /><rect
x="81.394997"
y="16.115999"
width="0.995"
height="0.99599999"
id="rect169"
style="fill:#655100" /><rect
x="83.403"
y="16.115999"
width="0.995"
height="0.99599999"
id="rect171"
style="fill:#655100" /><rect
x="85.400002"
y="16.115999"
width="0.995"
height="0.99599999"
id="rect173"
style="fill:#655100" /><rect
x="79.399002"
y="18.118"
width="0.995"
height="0.99599999"
id="rect175"
style="fill:#655100" /><rect
x="81.394997"
y="18.118"
width="0.995"
height="0.99599999"
id="rect177"
style="fill:#655100" /><rect
x="79.399002"
y="20.132"
width="0.995"
height="0.99599999"
id="rect179"
style="fill:#655100" /><rect
x="81.394997"
y="20.132"
width="0.995"
height="0.99599999"
id="rect181"
style="fill:#655100" /><rect
x="83.403"
y="18.118"
width="0.995"
height="0.99599999"
id="rect183"
style="fill:#655100" /><rect
x="85.400002"
y="18.118"
width="0.995"
height="0.99599999"
id="rect185"
style="fill:#655100" /><rect
x="83.403"
y="20.132"
width="0.995"
height="0.99599999"
id="rect187"
style="fill:#655100" /><rect
x="85.400002"
y="20.132"
width="0.995"
height="0.99599999"
id="rect189"
style="fill:#655100" /></g><g
id="g191"
transform="translate(-0.40299988,0)"><path
d="M 87.445,54.953"
id="path193"
inkscape:connector-curvature="0"
style="fill:#006468" /><rect
x="72.403"
y="38.965"
width="21"
height="21"
id="rect195"
style="fill:#ffffff" /><polygon
points="83.44,42.95 84.441,42.95 88.421,46.935 88.421,47.912 87.445,47.912 83.44,47.912 "
id="polygon197"
style="fill:#006468" /><polygon
points="78.404,43.949 78.404,54.953 87.445,54.953 87.445,47.726 88.421,47.726 88.421,55.99 77.399,55.99 77.399,42.95 83.586,42.95 83.599,43.951 "
id="polygon199"
style="fill:#006468" /><rect
x="79.399002"
y="44.966999"
width="0.995"
height="0.99599999"
id="rect201"
style="fill:#006468" /><rect
x="81.394997"
y="44.966999"
width="0.995"
height="0.99599999"
id="rect203"
style="fill:#006468" /><rect
x="79.399002"
y="46.959"
width="0.995"
height="0.99599999"
id="rect205"
style="fill:#006468" /><rect
x="81.394997"
y="46.959"
width="0.995"
height="0.99599999"
id="rect207"
style="fill:#006468" /><rect
x="79.399002"
y="48.972"
width="0.995"
height="0.99599999"
id="rect209"
style="fill:#006468" /><rect
x="81.394997"
y="48.972"
width="0.995"
height="0.99599999"
id="rect211"
style="fill:#006468" /><rect
x="83.403"
y="48.972"
width="0.995"
height="0.99599999"
id="rect213"
style="fill:#006468" /><rect
x="85.400002"
y="48.972"
width="0.995"
height="0.99599999"
id="rect215"
style="fill:#006468" /><rect
x="79.399002"
y="50.973999"
width="0.995"
height="0.99599999"
id="rect217"
style="fill:#006468" /><rect
x="81.394997"
y="50.973999"
width="0.995"
height="0.99599999"
id="rect219"
style="fill:#006468" /><rect
x="79.399002"
y="52.987"
width="0.995"
height="0.99599999"
id="rect221"
style="fill:#006468" /><rect
x="81.394997"
y="52.987"
width="0.995"
height="0.99599999"
id="rect223"
style="fill:#006468" /><rect
x="83.403"
y="50.973999"
width="0.995"
height="0.99599999"
id="rect225"
style="fill:#006468" /><rect
x="85.400002"
y="50.973999"
width="0.995"
height="0.99599999"
id="rect227"
style="fill:#006468" /><rect
x="83.403"
y="52.987"
width="0.995"
height="0.99599999"
id="rect229"
style="fill:#006468" /><rect
x="85.400002"
y="52.987"
width="0.995"
height="0.99599999"
id="rect231"
style="fill:#006468" /></g><g
id="g233"
transform="translate(-0.40299988,0)"><path
d="M 87.445,87.965"
id="path235"
inkscape:connector-curvature="0"
style="fill:#006468" /><rect
x="72.403"
y="71.976997"
width="21"
height="21"
id="rect237"
style="fill:#4db7bb" /><polygon
points="83.44,75.961 84.441,75.961 88.421,79.946 88.421,80.924 87.445,80.924 83.44,80.924 "
id="polygon239"
style="fill:#006468" /><polygon
points="78.404,76.961 78.404,87.965 87.445,87.965 87.445,80.738 88.421,80.738 88.421,89.002 77.399,89.002 77.399,75.961 83.586,75.961 83.599,76.963 "
id="polygon241"
style="fill:#006468" /><rect
x="79.399002"
y="77.977997"
width="0.995"
height="0.99599999"
id="rect243"
style="fill:#006468" /><rect
x="81.394997"
y="77.977997"
width="0.995"
height="0.99599999"
id="rect245"
style="fill:#006468" /><rect
x="79.399002"
y="79.970001"
width="0.995"
height="0.99599999"
id="rect247"
style="fill:#006468" /><rect
x="81.394997"
y="79.970001"
width="0.995"
height="0.99599999"
id="rect249"
style="fill:#006468" /><rect
x="79.399002"
y="81.984001"
width="0.995"
height="0.99599999"
id="rect251"
style="fill:#006468" /><rect
x="81.394997"
y="81.984001"
width="0.995"
height="0.99599999"
id="rect253"
style="fill:#006468" /><rect
x="83.403"
y="81.984001"
width="0.995"
height="0.99599999"
id="rect255"
style="fill:#006468" /><rect
x="85.400002"
y="81.984001"
width="0.995"
height="0.99599999"
id="rect257"
style="fill:#006468" /><rect
x="79.399002"
y="83.986"
width="0.995"
height="0.99599999"
id="rect259"
style="fill:#006468" /><rect
x="81.394997"
y="83.986"
width="0.995"
height="0.99599999"
id="rect261"
style="fill:#006468" /><rect
x="79.399002"
y="85.999001"
width="0.995"
height="0.99599999"
id="rect263"
style="fill:#006468" /><rect
x="81.394997"
y="85.999001"
width="0.995"
height="0.99599999"
id="rect265"
style="fill:#006468" /><rect
x="83.403"
y="83.986"
width="0.995"
height="0.99599999"
id="rect267"
style="fill:#006468" /><rect
x="85.400002"
y="83.986"
width="0.995"
height="0.99599999"
id="rect269"
style="fill:#006468" /><rect
x="83.403"
y="85.999001"
width="0.995"
height="0.99599999"
id="rect271"
style="fill:#006468" /><rect
x="85.400002"
y="85.999001"
width="0.995"
height="0.99599999"
id="rect273"
style="fill:#006468" /></g><g
id="g275"
transform="translate(-0.82800293,0)"><rect
x="171.828"
y="6.1090002"
width="21"
height="21"
id="rect277"
style="fill:#ffcc00" /><rect
x="187.819"
y="16.101"
width="0.99900001"
height="0.99800003"
id="rect279"
style="fill:#655100" /><rect
x="189.825"
y="16.101"
width="0.99900001"
height="0.99800003"
id="rect281"
style="fill:#655100" /><rect
x="174.83299"
y="16.101"
width="0.99900001"
height="0.99800003"
id="rect283"
style="fill:#655100" /><circle
stroke-miterlimit="10"
cx="181.80299"
cy="16.101"
r="4.0900002"
id="circle285"
style="fill:none;stroke:#655100;stroke-width:1.95439994;stroke-miterlimit:10" /><rect
x="175.895"
y="18.427999"
transform="matrix(0.6915,0.7224,-0.7224,0.6915,69.5827,-121.6599)"
width="2.6800001"
height="4.4229999"
id="rect287"
style="fill:#655100" /><rect
x="180.789"
y="15.103"
width="2.0280001"
height="1.9960001"
id="rect289"
style="fill:#655100" /></g><g
id="g291"
transform="translate(-0.82800293,0)"><rect
x="171.828"
y="38.965"
width="21"
height="21"
id="rect293"
style="fill:#ffffff" /><rect
x="187.819"
y="48.957001"
width="0.99900001"
height="0.99800003"
id="rect295"
style="fill:#006468" /><rect
x="189.825"
y="48.957001"
width="0.99900001"
height="0.99800003"
id="rect297"
style="fill:#006468" /><rect
x="174.83299"
y="48.957001"
width="0.99900001"
height="0.99800003"
id="rect299"
style="fill:#006468" /><circle
stroke-miterlimit="10"
cx="181.80299"
cy="48.957001"
r="4.0900002"
id="circle301"
style="fill:none;stroke:#006468;stroke-width:1.95439994;stroke-miterlimit:10" /><rect
x="175.895"
y="51.284"
transform="matrix(0.6915,0.7224,-0.7224,0.6915,93.3163,-111.5246)"
width="2.6800001"
height="4.4229999"
id="rect303"
style="fill:#006468" /><rect
x="180.789"
y="47.959"
width="2.0280001"
height="1.9960001"
id="rect305"
style="fill:#006468" /></g><g
id="g307"
transform="translate(-0.82800293,0)"><rect
x="171.828"
y="71.976997"
width="21"
height="21"
id="rect309"
style="fill:#4db7bb" /><rect
x="187.819"
y="81.969002"
width="0.99900001"
height="0.99800003"
id="rect311"
style="fill:#006468" /><rect
x="189.825"
y="81.969002"
width="0.99900001"
height="0.99800003"
id="rect313"
style="fill:#006468" /><rect
x="174.83299"
y="81.969002"
width="0.99900001"
height="0.99800003"
id="rect315"
style="fill:#006468" /><circle
stroke-miterlimit="10"
cx="181.80299"
cy="81.969002"
r="4.0900002"
id="circle317"
style="fill:none;stroke:#006468;stroke-width:1.95439994;stroke-miterlimit:10" /><rect
x="175.895"
y="84.295998"
transform="matrix(0.6915,0.7224,-0.7224,0.6915,117.1625,-101.3412)"
width="2.6800001"
height="4.4229999"
id="rect319"
style="fill:#006468" /><rect
x="180.789"
y="80.970001"
width="2.0280001"
height="1.9960001"
id="rect321"
style="fill:#006468" /></g></svg>

After

Width:  |  Height:  |  Size: 23 KiB

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,40 @@
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';
@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));
}
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

@@ -0,0 +1,52 @@
import * as React from 'react';
import { injectable } from 'inversify';
import { Library } from '../../common/protocol/library-service';
import { ListItemRenderer } from '../components/component-list/list-item-renderer';
@injectable()
export class LibraryItemRenderer extends ListItemRenderer<Library> {
renderItem(item: Library, install: (item: Library) => Promise<void>): React.ReactNode {
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 installButton = item.installable && !item.installedVersion &&
<button className='install' onClick={install.bind(this, item)}>INSTALL</button>;
const versions = (() => {
const { availableVersions } = item;
if (!!item.installedVersion || 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'>
{installButton}
{versions}
</div>
</div>;
}
}

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 { LibraryItemRenderer } from './library-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(LibraryItemRenderer) protected itemRenderer: LibraryItemRenderer) {
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,16 +0,0 @@
import { ListWidget } from './list-widget';
export class LibraryListWidget extends ListWidget {
static WIDGET_ID = 'library-list-widget';
static WIDGET_LABEL = 'Library Manager';
protected widgetProps(): ListWidget.Props {
return {
id: LibraryListWidget.WIDGET_ID,
title: LibraryListWidget.WIDGET_LABEL,
iconClass: 'library-tab-icon'
}
}
}

View File

@@ -1,20 +1,12 @@
import { injectable } from 'inversify'; import { injectable } from 'inversify';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution'; import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { ListWidget } from './list-widget'; import { MenuModelRegistry } from '@theia/core';
import { LibraryListWidget } from './library-list-widget'; import { LibraryListWidget } from './library-list-widget';
import { ArduinoMenus } from '../arduino-frontend-contribution';
@injectable() @injectable()
export abstract class ListWidgetFrontendContribution extends AbstractViewContribution<ListWidget> implements FrontendApplicationContribution { export class LibraryListWidgetFrontendContribution extends AbstractViewContribution<LibraryListWidget> implements FrontendApplicationContribution {
async initializeLayout(): Promise<void> {
await this.openView();
}
}
@injectable()
export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendContribution {
constructor() { constructor() {
super({ super({
@@ -29,4 +21,17 @@ export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendCon
}); });
} }
initializeLayout(): void {
// NOOP
}
registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.SKETCH, {
commandId: this.toggleCommand.id,
label: 'Manage Libraries...'
});
}
}
} }

View File

@@ -1,72 +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 { LibraryService } from '../../common/protocol/library-service';
@injectable()
export abstract class ListWidget extends ReactWidget {
@inject(LibraryService)
protected readonly libraryService: LibraryService;
@inject(WindowService)
protected readonly windowService: WindowService;
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 {
return <FilterableListContainer
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;
}
export namespace Styles {
export const LIST_WIDGET_CLASS = 'arduino-list-widget'
}
}

View File

@@ -0,0 +1,28 @@
import { inject, injectable, postConstruct } from 'inversify';
import { Diagnostic } from 'vscode-languageserver-types';
import URI from '@theia/core/lib/common/uri';
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;
protected dataDirUri: URI | undefined;
@postConstruct()
protected init(): void {
super.init();
this.configService.getConfiguration().then(({ dataDirUri }) => this.dataDirUri = new URI(dataDirUri));
}
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

@@ -0,0 +1,13 @@
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

@@ -0,0 +1,10 @@
import { BrowserMenuBarContribution } from '@theia/core/lib/browser/menu/browser-menu-plugin';
import { ArduinoMenuContribution } from './arduino-menu-contribution';
import { ContainerModule, interfaces } from 'inversify';
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,53 @@
import { injectable, inject } from "inversify";
import { MonitorService, ConnectionConfig } from "../../common/protocol/monitor-service";
import { Emitter, Event } from "@theia/core";
@injectable()
export class MonitorConnection {
@inject(MonitorService)
protected readonly monitorService: MonitorService;
connectionId: string | undefined;
protected _connectionConfig: ConnectionConfig | undefined;
protected readonly onConnectionChangedEmitter = new Emitter<string | undefined>();
readonly onConnectionChanged: Event<string | undefined> = this.onConnectionChangedEmitter.event;
get connectionConfig(): ConnectionConfig | undefined {
return this._connectionConfig;
}
async connect(config: ConnectionConfig): Promise<string | undefined> {
if (this.connectionId) {
await this.disconnect();
}
const { connectionId } = await this.monitorService.connect(config);
this.connectionId = connectionId;
this._connectionConfig = config;
this.onConnectionChangedEmitter.fire(this.connectionId);
return connectionId;
}
async disconnect(): Promise<boolean> {
let result = true;
const connections = await this.monitorService.getConnectionIds();
if (this.connectionId && connections.findIndex(id => id === this.connectionId) >= 0) {
console.log('>>> Disposing existing monitor connection before establishing a new one...');
result = await this.monitorService.disconnect(this.connectionId);
if (!result) {
// TODO: better!!!
console.error(`Could not close connection: ${this.connectionId}. Check the backend logs.`);
} else {
console.log(`<<< Disposed ${this.connectionId} connection.`);
this.connectionId = undefined;
this._connectionConfig = undefined;
this.onConnectionChangedEmitter.fire(this.connectionId);
}
}
return result;
}
}

View File

@@ -0,0 +1,58 @@
import { injectable } from "inversify";
import { Emitter } from "@theia/core";
export namespace MonitorModel {
export interface Data {
autoscroll: boolean,
timestamp: boolean,
baudRate: number,
lineEnding: string
}
}
@injectable()
export class MonitorModel {
protected readonly onChangeEmitter = new Emitter<void>();
readonly onChange = this.onChangeEmitter.event;
protected _autoscroll: boolean = true;
protected _timestamp: boolean = false;
baudRate: number;
lineEnding: string = '\n';
get autoscroll(): boolean {
return this._autoscroll;
}
get timestamp(): boolean {
return this._timestamp;
}
toggleAutoscroll(): void {
this._autoscroll = !this._autoscroll;
this.onChangeEmitter.fire(undefined);
}
toggleTimestamp(): void {
this._timestamp = !this._timestamp;
this.onChangeEmitter.fire(undefined);
}
restore(model: MonitorModel.Data) {
this._autoscroll = model.autoscroll;
this._timestamp = model.timestamp;
this.baudRate = model.baudRate;
this.lineEnding = model.lineEnding;
}
store(): MonitorModel.Data {
return {
autoscroll: this._autoscroll,
timestamp: this._timestamp,
baudRate: this.baudRate,
lineEnding: this.lineEnding
}
}
}

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 { connectionId, data } = event;
console.log(`Received data from ${connectionId}: ${data}`);
}
notifyError(error: MonitorError): void {
this.onErrorEmitter.fire(error);
}
}

View File

@@ -0,0 +1,127 @@
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: 'ctrl+shift+m'
})
}
registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS, {
commandId: this.toggleCommand.id,
label: 'Serial Monitor'
});
}
}
async registerToolbarItems(registry: TabBarToolbarRegistry) {
registry.registerItem({
id: 'monitor-autoscroll',
render: () => this.renderAutoScrollButton(),
isVisible: widget => widget instanceof MonitorWidget,
onDidChange: this.model.onChange
});
registry.registerItem({
id: 'monitor-timestamp',
render: () => this.renderTimestampButton(),
isVisible: widget => widget instanceof MonitorWidget,
onDidChange: this.model.onChange
});
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.clear();
}
}
});
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() {
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() {
this.model.toggleTimestamp();
}
}

View File

@@ -0,0 +1,398 @@
import { ReactWidget, Message, Widget, StatefulWidget } from "@theia/core/lib/browser";
import { postConstruct, injectable, inject } from "inversify";
import * as React from 'react';
import Select, { components } from 'react-select';
import { Styles } from "react-select/src/styles";
import { ThemeConfig } from "react-select/src/theme";
import { OptionsType } from "react-select/src/types";
import { MonitorServiceClientImpl } from "./monitor-service-client-impl";
import { MessageService } from "@theia/core";
import { ConnectionConfig, MonitorService } from "../../common/protocol/monitor-service";
import { MonitorConnection } from "./monitor-connection";
import { BoardsServiceClientImpl } from "../boards/boards-service-client-impl";
import { AttachedSerialBoard, BoardsService, Board } from "../../common/protocol/boards-service";
import { BoardsConfig } from "../boards/boards-config";
import { MonitorModel } from "./monitor-model";
export namespace SerialMonitorSendField {
export interface Props {
onSend: (text: string) => void
}
export interface State {
value: string;
}
}
export class SerialMonitorSendField extends React.Component<SerialMonitorSendField.Props, SerialMonitorSendField.State> {
protected inputField: HTMLInputElement | null;
constructor(props: SerialMonitorSendField.Props) {
super(props);
this.state = { value: '' };
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
componentDidMount() {
if (this.inputField) {
this.inputField.focus();
}
}
render() {
return <React.Fragment>
<form onSubmit={this.handleSubmit}>
<input
tabIndex={-1}
ref={ref => this.inputField = ref}
type='text' id='serial-monitor-send'
autoComplete='off'
value={this.state.value}
onChange={this.handleChange} />
<input className="btn" type="submit" value="Submit" />
</form>
</React.Fragment>
}
protected handleChange(event: React.ChangeEvent<HTMLInputElement>) {
this.setState({ value: event.target.value });
}
protected handleSubmit(event: React.FormEvent<HTMLFormElement>) {
this.props.onSend(this.state.value);
this.setState({ value: '' });
event.preventDefault();
}
}
export namespace SerialMonitorOutput {
export interface Props {
lines: string[];
model: MonitorModel;
}
}
export class SerialMonitorOutput extends React.Component<SerialMonitorOutput.Props> {
protected theEnd: HTMLDivElement | null;
render() {
let result = '';
const style: React.CSSProperties = {
whiteSpace: 'pre',
fontFamily: 'monospace',
};
for (const text of this.props.lines) {
result += text;
}
return <React.Fragment>
<div style={style}>{result}</div>
<div style={{ float: "left", clear: "both" }}
ref={(el) => { this.theEnd = el; }}>
</div>
</React.Fragment>;
}
protected scrollToBottom() {
if (this.theEnd) {
this.theEnd.scrollIntoView();
}
}
componentDidMount() {
if (this.props.model.autoscroll) {
this.scrollToBottom();
}
}
componentDidUpdate() {
if (this.props.model.autoscroll) {
this.scrollToBottom();
}
}
}
export interface SelectOption {
label: string;
value: string | number;
}
@injectable()
export class MonitorWidget extends ReactWidget implements StatefulWidget {
static readonly ID = 'serial-monitor';
protected lines: string[];
protected tempData: string;
protected widgetHeight: number;
protected continuePreviousConnection: boolean;
constructor(
@inject(MonitorServiceClientImpl) protected readonly serviceClient: MonitorServiceClientImpl,
@inject(MonitorConnection) protected readonly connection: MonitorConnection,
@inject(MonitorService) protected readonly monitorService: MonitorService,
@inject(BoardsServiceClientImpl) protected readonly boardsServiceClient: BoardsServiceClientImpl,
@inject(MessageService) protected readonly messageService: MessageService,
@inject(BoardsService) protected readonly boardsService: BoardsService,
@inject(MonitorModel) protected readonly model: MonitorModel
) {
super();
this.id = MonitorWidget.ID;
this.title.label = 'Serial Monitor';
this.title.iconClass = 'arduino-serial-monitor-tab-icon';
this.lines = [];
this.tempData = '';
this.scrollOptions = undefined;
this.toDisposeOnDetach.push(serviceClient.onRead(({ data, connectionId }) => {
this.tempData += data;
if (this.tempData.endsWith('\n')) {
if (this.model.timestamp) {
const nu = new Date();
const h = (100 + nu.getHours()).toString().substr(1)
const min = (100 + nu.getMinutes()).toString().substr(1)
const sec = (100 + nu.getSeconds()).toString().substr(1)
const ms = (1000 + nu.getMilliseconds()).toString().substr(1);
this.tempData = `${h}:${min}:${sec}.${ms} -> ` + this.tempData;
}
this.lines.push(this.tempData);
this.tempData = '';
this.update();
}
}));
// TODO onError
}
@postConstruct()
protected init(): void {
this.update();
}
clear(): void {
this.lines = [];
this.update();
}
storeState(): MonitorModel.Data {
return this.model.store();
}
restoreState(oldState: MonitorModel.Data): void {
this.model.restore(oldState);
}
protected onAfterAttach(msg: Message) {
super.onAfterAttach(msg);
this.clear();
this.connect();
this.toDisposeOnDetach.push(
this.boardsServiceClient.onBoardsChanged(async states => {
const currentConnectionConfig = this.connection.connectionConfig;
const connectedBoard = states.newState.boards
.filter(AttachedSerialBoard.is)
.find(board => {
const potentiallyConnected = currentConnectionConfig && currentConnectionConfig.board;
if (AttachedSerialBoard.is(potentiallyConnected)) {
return Board.equals(board, potentiallyConnected) && board.port === potentiallyConnected.port;
}
return false;
});
if (connectedBoard && currentConnectionConfig) {
this.continuePreviousConnection = true;
this.connection.connect(currentConnectionConfig);
}
})
);
this.toDisposeOnDetach.push(
this.boardsServiceClient.onBoardsConfigChanged(async boardConfig => {
this.connect();
})
)
this.toDisposeOnDetach.push(this.connection.onConnectionChanged(() => {
if (!this.continuePreviousConnection) {
this.clear();
} else {
this.continuePreviousConnection = false;
}
}));
}
protected onBeforeDetach(msg: Message) {
super.onBeforeDetach(msg);
this.connection.disconnect();
}
protected onResize(msg: Widget.ResizeMessage) {
super.onResize(msg);
this.widgetHeight = msg.height;
this.update();
}
protected async connect() {
const config = await this.getConnectionConfig();
if (config) {
this.connection.connect(config);
}
}
protected async getConnectionConfig(): Promise<ConnectionConfig | undefined> {
const baudRate = this.model.baudRate;
const { boardsConfig } = this.boardsServiceClient;
const { selectedBoard, selectedPort } = boardsConfig;
if (!selectedBoard) {
this.messageService.warn('No boards selected.');
return;
}
const { name } = selectedBoard;
if (!selectedPort) {
this.messageService.warn(`No ports selected for board: '${name}'.`);
return;
}
const attachedBoards = await this.boardsService.getAttachedBoards();
const connectedBoard = attachedBoards.boards.filter(AttachedSerialBoard.is).find(board => BoardsConfig.Config.sameAs(boardsConfig, board));
if (!connectedBoard) {
return;
}
return {
baudRate,
board: selectedBoard,
port: selectedPort.address
}
}
protected getLineEndings(): OptionsType<SelectOption> {
return [
{
label: 'No Line Ending',
value: ''
},
{
label: 'Newline',
value: '\n'
},
{
label: 'Carriage Return',
value: '\r'
},
{
label: 'Both NL & CR',
value: '\r\n'
}
]
}
protected getBaudRates(): OptionsType<SelectOption> {
const baudRates = [300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200];
return baudRates.map<SelectOption>(baudRate => ({ label: baudRate + ' baud', value: baudRate }))
}
protected render(): React.ReactNode {
const le = this.getLineEndings();
const br = this.getBaudRates();
const leVal = this.model.lineEnding && le.find(val => val.value === this.model.lineEnding);
const brVal = this.model.baudRate && br.find(val => val.value === this.model.baudRate);
return <React.Fragment>
<div className='serial-monitor-container'>
<div className='head'>
<div className='send'>
<SerialMonitorSendField onSend={this.onSend} />
</div>
<div className='config'>
{this.renderSelectField('arduino-serial-monitor-line-endings', le, leVal || le[1], this.onChangeLineEnding)}
{this.renderSelectField('arduino-serial-monitor-baud-rates', br, brVal || br[4], this.onChangeBaudRate)}
</div>
</div>
<div id='serial-monitor-output-container'>
<SerialMonitorOutput model={this.model} lines={this.lines} />
</div>
</div>
</React.Fragment>;
}
protected readonly onSend = (value: string) => this.doSend(value);
protected async doSend(value: string) {
const { connectionId } = this.connection;
if (connectionId) {
this.monitorService.send(connectionId, value + this.model.lineEnding);
}
}
protected readonly onChangeLineEnding = (le: SelectOption) => {
this.model.lineEnding = typeof le.value === 'string' ? le.value : '\n';
}
protected readonly onChangeBaudRate = async (br: SelectOption) => {
await this.connection.disconnect();
this.model.baudRate = typeof br.value === 'number' ? br.value : 9600;
this.clear();
const config = await this.getConnectionConfig();
if (config) {
await this.connection.connect(config);
}
}
protected renderSelectField(id: string, options: OptionsType<SelectOption>, defaultVal: SelectOption, onChange: (v: SelectOption) => void): React.ReactNode {
const height = 25;
const selectStyles: Styles = {
control: (provided, state) => ({
...provided,
width: 200,
border: 'none'
}),
dropdownIndicator: (p, s) => ({
...p,
padding: 0
}),
indicatorSeparator: (p, s) => ({
display: 'none'
}),
indicatorsContainer: (p, s) => ({
padding: '0 5px'
}),
menu: (p, s) => ({
...p,
marginTop: 0
})
};
const theme: ThemeConfig = theme => ({
...theme,
borderRadius: 0,
spacing: {
controlHeight: height,
baseUnit: 2,
menuGutter: 4
}
});
const DropdownIndicator = (
props: React.Props<typeof components.DropdownIndicator>
) => {
return (
<span className='fa fa-caret-down caret'></span>
);
};
return <Select
options={options}
defaultValue={defaultVal}
onChange={onChange}
components={{ DropdownIndicator }}
theme={theme}
styles={selectStyles}
maxMenuHeight={this.widgetHeight - 40}
classNamePrefix='sms'
className='serial-monitor-select'
id={id}
/>
}
}

View File

@@ -0,0 +1,32 @@
import { inject, injectable, postConstruct } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { Title, Widget } from '@phosphor/widgets';
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;
protected dataDirUri: URI | undefined;
@postConstruct()
protected init(): void {
super.init();
this.configService.getConfiguration().then(({ dataDirUri }) => this.dataDirUri = new URI(dataDirUri));
}
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,57 +0,0 @@
import { injectable, inject } from "inversify";
import URI from "@theia/core/lib/common/uri";
import { OpenerService } from "@theia/core/lib/browser";
import { FileSystem } from "@theia/filesystem/lib/common";
@injectable()
export class SketchFactory {
@inject(FileSystem)
protected readonly fileSystem: FileSystem;
@inject(OpenerService)
protected readonly openerService: OpenerService;
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() {
}
void loop() {
}
` });
const opener = await this.openerService.getOpener(sketchFile)
opener.open(sketchFile, { reveal: true });
} catch (e) {
throw new Error("Cannot create new sketch: " + e);
}
}
}

View File

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

View File

@@ -0,0 +1,225 @@
div#select-board-dialog {
margin: 5px 20px 50px 20px;
}
div#select-board-dialog .selectBoardContainer .body {
display: flex;
overflow: hidden;
}
div.dialogContent.select-board-dialog > div.head {
padding-left: 21px;
}
div.dialogContent.select-board-dialog > div.head .title {
font-weight: 400;
letter-spacing: .02em;
font-size: 1.2em;
color: #00979d;
margin: 17px 0;
}
div#select-board-dialog .selectBoardContainer .head .text {
margin-bottom: 21px;
}
div#select-board-dialog .selectBoardContainer .body .list .item.selected {
background: var(--theia-ui-button-color-secondary-hover);
}
div#select-board-dialog .selectBoardContainer .body .list .item.selected i{
color: var(--theia-arduino-light);
}
#select-board-dialog .selectBoardContainer .search,
#select-board-dialog .selectBoardContainer .search input,
#select-board-dialog .selectBoardContainer .list,
#select-board-dialog .selectBoardContainer .list {
background: white; /* TODO find a theia color instead! */
}
#select-board-dialog .selectBoardContainer .body .search input {
border: none;
width: 100%;
height: auto;
max-height: 37px;
padding: 10px 5px 10px 10px;
margin: 0;
vertical-align: top;
display: flex;
color: var(--theia-content-font-color0);
}
#select-board-dialog .selectBoardContainer .body .search input:focus {
box-shadow: none;
}
#select-board-dialog .selectBoardContainer .body .container {
flex: 1;
padding: 0px 10px 0px 0px;
}
#select-board-dialog .selectBoardContainer .body .left.container .content {
margin: 0 5px 0 0;
}
#select-board-dialog .selectBoardContainer .body .right.container .content {
margin: 0 0 0 5px;
}
#select-board-dialog .selectBoardContainer .body .container .content .title {
color: #7f8c8d;
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 10px;
display: flex;
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 .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 {
margin-bottom: 10px;
display: flex;
align-items: center;
padding-right: 5px;
}
.p-Widget.dialogOverlay .dialogContent.select-board-dialog {
width: 740px;
}
button.theia-button {
height: 31px;
}
button.theia-button.secondary {
background-color: #b5c8c9;
color: #000;
box-shadow: 0 4px #95a5a6;
}
button.theia-button.main {
color: #fff;
/* background-color: #00979c; */
box-shadow: 0 4px var(--theia-accent-color0);
}
.dialogControl {
margin: 0 20px 30px 0;
}
.arduino-boards-toolbar-item-container {
margin-left: 3px;
}
.arduino-boards-toolbar-item-container .arduino-boards-toolbar-item .inner-container {
display: flex;
align-items: baseline;
width: 100%;
}
.arduino-boards-toolbar-item-container .arduino-boards-toolbar-item .inner-container .notAttached {
width: 10px;
height: 10px;
color: red;
margin: 0 5px;
}
.arduino-boards-toolbar-item-container {
display: flex;
align-items: center;
width: 220px;
}
.arduino-boards-toolbar-item .label {
height: 100%;
display: flex;
align-items: center;
margin: 0 5px;
width: 100%;
}
.arduino-boards-toolbar-item .caret {
width: 10px;
margin-right: 5px;
}
.arduino-boards-toolbar-item {
background: var(--theia-layout-color1);
height: 22px;
display: flex;
width: 100%;
overflow: hidden;
}
.arduino-boards-dropdown-list {
border: 3px solid var(--theia-border-color2);
margin: -3px;
z-index: 1;
}
.arduino-boards-dropdown-item {
font-size: var(--theia-ui-font-size1);
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-color1);
align-self: center;
}
.arduino-boards-dropdown-item.selected,
.arduino-boards-dropdown-item:hover {
background: var(--theia-layout-color3);
}

View File

@@ -0,0 +1,15 @@
#theia-top-panel {
min-height: 64px;
flex-direction: column;
}
#theia-top-panel .p-TabBar-toolbar {
justify-content: flex-end;
margin: 0;
padding-left: 10px;
width: 100%;
}
.p-MenuBar-item.p-mod-active {
color: var(--theia-ui-bar-font-color0);
}

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,2 +1,5 @@
@import './list-widget.css'; @import './list-widget.css';
@import './select-board-dialog.css'; @import './board-select-dialog.css';
@import './main.css';
@import './editor.css';
@import './monitor.css';

View File

@@ -7,15 +7,58 @@
color: var(--theia-ui-font-color1); color: var(--theia-ui-font-color1);
} }
.arduino-list-widget .search-bar > input { .arduino-list-widget .search-bar {
margin: 0px 5px 0px 5px; margin: 0px 10px 10px 15px;
width: 95%; border-color: var(--theia-border-color3);
}
.arduino-list-widget .search-filters {
margin: 0px 10px 0px 15px;
border-color: var(--theia-border-color3);
}
.arduino-list-widget .search-bar:focus {
border-color: var(--theia-accent-color3);
}
.arduino-list-widget .filterable-list-container .search-filters .filter {
margin: 0px 0px 10px 0px;
display: flex;
}
.arduino-list-widget .filterable-list-container .search-filters .filter .title {
margin: 0px 10px 0px 0px;
align-self: center;
text-transform: uppercase;
font-size: var(--theia-ui-font-size0);
}
.arduino-list-widget .filterable-list-container .search-filters .filter > select {
width: 100%;
} }
.filterable-list-container { .filterable-list-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
height: 100%; /* This has top be 100% down to the `scrollContainer`. */
}
.filterable-list-container .items-container {
height: 100%; /* This has to be propagated down from the widget. */
position: relative; /* To fix the `top` of the vertical toolbar. */
}
.filterable-list-container .items-container > div:nth-child(odd) {
background-color: var(--theia-layout-color0);
}
.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);
} }
.component-list-item { .component-list-item {
@@ -57,6 +100,10 @@
color: var(--theia-ui-font-color2); color: var(--theia-ui-font-color2);
} }
.component-list-item:hover .header .author {
color: var(--theia-ui-font-color1);
}
.component-list-item .header .version { .component-list-item .header .version {
color: var(--theia-ui-font-color2); color: var(--theia-ui-font-color2);
} }
@@ -64,7 +111,7 @@
.component-list-item .header .installed { .component-list-item .header .installed {
margin-left: 4px; margin-left: 4px;
justify-self: end; justify-self: end;
background-color: var(--theia-accent-color2); background-color: var(--theia-accent-color1);
padding: 2px 4px 2px 4px; padding: 2px 4px 2px 4px;
font-size: 10px; font-size: 10px;
font-weight: bold; font-weight: bold;
@@ -72,18 +119,35 @@
color: var(--theia-inverse-ui-font-color0); color: var(--theia-inverse-ui-font-color0);
} }
.component-list-item .footer { .component-list-item[min-width~="170px"] .footer {
padding-top: 5px; padding: 5px 5px 0px 0px;
min-height: 26px; /* 21 + 5 from the footer margin top */
display: flex; display: flex;
flex-direction: row-reverse;
} }
.component-list-item .footer a { .component-list-item .footer {
flex-direction: column-reverse;
}
.component-list-item .footer > * {
display: none
}
.component-list-item:hover .footer > * {
display: block;
margin: 5px 0px 0px 10px;
}
.component-list-item:hover .footer > label {
display: block;
align-self: center;
margin: 5px 0px 0px 10px;
}
.component-list-item .info a {
color: var(--theia-brand-color1); color: var(--theia-brand-color1);
text-decoration: none; text-decoration: none;
font-weight: 600;
}
.component-list-item .footer .install {
margin-left: auto;
} }
.component-list-item a:hover { .component-list-item a:hover {
@@ -92,4 +156,4 @@
.component-list-item strong.installed { .component-list-item strong.installed {
color: rgb(0, 151, 157) color: rgb(0, 151, 157)
} }

View File

@@ -0,0 +1,145 @@
.p-TabBar[data-orientation='horizontal'].theia-app-bottom {
background: var(--theia-layout-color1);
}
.theia-output {
background: var(--theia-arduino-terminal);
}
#outputView {
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 {
line-height: var(--theia-content-line-height);
font-size: var(--theia-ui-font-size1);
color: var(--theia-ui-font-color1);
-webkit-appearance: none;
-moz-appearance: none;
background-image: linear-gradient(45deg, transparent 50%, var(--theia-ui-font-color1) 50%), linear-gradient(135deg, var(--theia-ui-font-color1) 50%, transparent 50%);
background-position: calc(100% - 6px) 8px, calc(100% - 2px) 8px, 100% 0;
background-size: 4px 5px;
background-repeat: no-repeat;
padding-left: 3px;
padding-right: 15px;
}
.arduino-toolbar-tooltip {
margin-left: 10px;
display: flex;
align-items: center;
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;
}
.noWrapInfo {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.theia-sidepanel-toolbar .theia-sidepanel-title {
margin-left: 10px;
}

View File

@@ -0,0 +1,126 @@
.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-container {
height: 100%;
display: flex;
flex-direction: column;
}
.serial-monitor-container .head {
display: flex;
padding: 5px;
background: var(--theia-layout-color0);
height: 27px;
}
.serial-monitor-container .head .send {
display: flex;
flex:1;
}
.serial-monitor-container .head .send .btn {
display: flex;
padding: 0 5px;
align-items: center;
color: var(--theia-ui-font-color1);
opacity: 0.7;
}
.serial-monitor-container .head .send .btn:hover {
opacity: 1;
cursor: pointer;
}
.serial-monitor-container .head .send form {
flex: 1;
display: flex;
background: var(--theia-layout-color2);
}
.serial-monitor-container .head .send input#serial-monitor-send {
color: var(--theia-ui-font-color1);
flex: 1;
}
.serial-monitor-container .head .send input:focus {
outline: none;
}
.serial-monitor-container .head .config {
display: flex;
}
.serial-monitor-container .head .config .serial-monitor-select {
margin-left: 5px;
}
#serial-monitor-output-container {
overflow: auto;
flex: 1;
padding: 6px;
}
.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;
}
/* React Select Styles */
.serial-monitor-select .sms__control {
border: var(--theia-border-color1) var(--theia-border-width) solid;
background: var(--theia-layout-color2);
}
.serial-monitor-select .sms__control--is-focused {
border-color: var(--theia-border-color2) !important;
box-shadow: none !important;
}
.sms__control--is-focused:hover {
border-color: var(--theia-border-color2) !important;
}
.serial-monitor-select .sms__option--is-selected {
background-color: var(--theia-ui-button-color-secondary-hover);
color: var(--theia-content-font-color0);
}
.serial-monitor-select .sms__option--is-focused {
background-color: var(--theia-ui-button-color-secondary-hover);
}
.serial-monitor-select .sms__menu {
background-color: var(--theia-layout-color1);
border: 1px solid var(--theia-border-color2);
border-top: none;
box-shadow: none;
}
.serial-monitor-select .sms__control.sms__control--menu-is-open {
border: 1px solid;
border-color: var(--theia-border-color2) !important;
border-bottom: none;
}
.serial-monitor-select .sms__value-container .sms__single-value {
color: var(--theia-ui-font-color1);
}
.sms__menu-list {
padding-top: 0 !important;
padding-bottom: 0 !important;
}

View File

@@ -1,13 +0,0 @@
.select-board-dialog {
width: 600px;
}
.select-board-dialog input {
width: calc(100% - 8px);
margin-bottom: 5px;
}
.select-board-dialog select {
width: 100%;
}

View File

@@ -0,0 +1,45 @@
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 arduinoToolbarContainer: ArduinoToolbarContainer;
constructor(
@inject(TabBarToolbarRegistry) protected tabBarToolBarRegistry: TabBarToolbarRegistry,
@inject(CommandRegistry) protected commandRegistry: CommandRegistry,
@inject(LabelParser) protected labelParser: 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.arduinoToolbarContainer, {
area: 'top'
});
}
}

View File

@@ -0,0 +1,140 @@
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, 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
}
export interface State {
tooltip: string
}
}
export class ArduinoToolbarComponent extends React.Component<ArduinoToolbarComponent.Props, ArduinoToolbarComponent.State> {
constructor(props: ArduinoToolbarComponent.Props) {
super(props);
this.state = { tooltip: '' };
}
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} >
<div className={item.id}>
<div
key={item.id + '-icon'}
id={item.id}
className={className}
onClick={this.props.executeCommand}
onMouseOver={() => this.setState({ tooltip: item.tooltip || '' })}
onMouseOut={() => this.setState({ tooltip: '' })}
title={item.tooltip}>
{innerText}
</div>
</div>
</div>
}
render(): React.ReactNode {
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;
}
}
export class ArduinoToolbar extends ReactWidget {
protected items = new Map<string, TabBarToolbarItem | ReactTabBarToolbarItem>();
constructor(
protected readonly tabBarToolbarRegistry: TabBarToolbarRegistry,
protected readonly commands: CommandRegistry,
protected readonly labelParser: LabelParser,
public readonly side: 'left' | 'right'
) {
super();
this.id = side + '-arduino-toolbar';
this.addClass(TabBarToolbar.Styles.TAB_BAR_TOOLBAR);
this.init();
this.tabBarToolbarRegistry.onDidChange(() => this.updateToolbar());
}
protected updateItems(items: Array<TabBarToolbarItem | ReactTabBarToolbarItem>): void {
this.items.clear();
const revItems = items.reverse();
for (const item of revItems) {
this.items.set(item.id, item);
}
this.update();
}
protected updateToolbar(): void {
const items = this ? this.tabBarToolbarRegistry.visibleItems(this) : [];
this.updateItems(items);
}
protected init(): void {
this.node.classList.add('theia-arduino-toolbar', this.side);
this.update();
}
protected readonly doCommandIsEnabled = (id: string) => this.commandIsEnabled(id);
protected commandIsEnabled(command: string): boolean {
return this.commands.isEnabled(command, this);
}
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}
executeCommand={this.executeCommand}
/>
}
protected executeCommand = (e: React.MouseEvent<HTMLElement>) => {
const item = this.items.get(e.currentTarget.id);
if (TabBarToolbarItem.is(item)) {
this.commands.executeCommand(item.command, this, e.target);
}
}
}
export namespace ArduinoToolbar {
export function is(maybeToolbarWidget: any): maybeToolbarWidget is ArduinoToolbar {
return maybeToolbarWidget instanceof ArduinoToolbar;
}
}

View File

@@ -1,14 +1,169 @@
import { ArduinoComponent } from "./arduino-component"; import { isWindows, isOSX } from '@theia/core/lib/common/os';
import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
import { Searchable } from './searchable';
import { Installable } from './installable';
import { ArduinoComponent } from './arduino-component';
const naturalCompare: (left: string, right: string) => number = require('string-natural-compare').caseInsensitive;
export interface AttachedBoardsChangeEvent {
readonly oldState: Readonly<{ boards: Board[], ports: Port[] }>;
readonly newState: Readonly<{ boards: Board[], ports: Port[] }>;
}
export namespace AttachedBoardsChangeEvent {
export function diff(event: AttachedBoardsChangeEvent): Readonly<{
attached: {
boards: Board[],
ports: Port[]
},
detached: {
boards: Board[],
ports: Port[]
}
}> {
const diff = <T>(left: T[], right: T[]) => {
return left.filter(item => right.indexOf(item) === -1);
}
const { boards: newBoards } = event.newState;
const { boards: oldBoards } = event.oldState;
const { ports: newPorts } = event.newState;
const { ports: oldPorts } = event.oldState;
return {
detached: {
boards: diff(oldBoards, newBoards),
ports: diff(oldPorts, newPorts)
},
attached: {
boards: diff(newBoards, oldBoards),
ports: diff(newPorts, oldPorts)
}
};
}
}
export interface BoardInstalledEvent {
readonly pkg: Readonly<BoardPackage>;
}
export const BoardsServiceClient = Symbol('BoardsServiceClient');
export interface BoardsServiceClient {
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void;
notifyBoardInstalled(event: BoardInstalledEvent): void
}
export const BoardsServicePath = '/services/boards-service'; export const BoardsServicePath = '/services/boards-service';
export const BoardsService = Symbol('BoardsService'); export const BoardsService = Symbol('BoardsService');
export interface BoardsService { export interface BoardsService extends Installable<BoardPackage>, Searchable<BoardPackage>, JsonRpcServer<BoardsServiceClient> {
getAttachedBoards(): Promise<{ boards: Board[] }>; getAttachedBoards(): Promise<{ boards: Board[] }>;
selectBoard(board: Board): Promise<void>; getAvailablePorts(): Promise<{ ports: Port[] }>;
getSelectBoard(): Promise<Board | undefined>; }
export interface Port {
readonly address: string;
readonly protocol: Port.Protocol;
/**
* Optional label for the protocol. For example: `Serial Port (USB)`.
*/
readonly label?: string;
}
export namespace Port {
export type Protocol = 'serial' | 'network' | 'unknown';
export namespace Protocol {
export function toProtocol(protocol: string | undefined): Protocol {
if (protocol === 'serial') {
return 'serial';
} else if (protocol === 'network') {
return 'network';
} else {
return 'unknown';
}
}
}
export function toString(port: Port, options: { useLabel: boolean } = { useLabel: false }): string {
if (options.useLabel && port.label) {
return `${port.address} ${port.label}`
}
return port.address;
}
export function compare(left: Port, right: Port): number {
// Board ports have higher priorities, they come first.
if (isBoardPort(left) && !isBoardPort(right)) {
return -1;
}
if (!isBoardPort(left) && isBoardPort(right)) {
return 1;
}
let result = left.protocol.toLocaleLowerCase().localeCompare(right.protocol.toLocaleLowerCase());
if (result !== 0) {
return result;
}
result = naturalCompare(left.address, right.address);
if (result !== 0) {
return result;
}
return (left.label || '').localeCompare(right.label || '');
}
export function equals(left: Port | undefined, right: Port | undefined): boolean {
if (left && right) {
return left.address === right.address
&& left.protocol === right.protocol
&& (left.label || '') === (right.label || '');
}
return left === right;
}
// Based on: https://github.com/arduino/Arduino/blob/93581b03d723e55c60caedb4729ffc6ea808fe78/arduino-core/src/processing/app/SerialPortList.java#L48-L74
export function isBoardPort(port: Port): boolean {
const address = port.address.toLocaleLowerCase();
if (isWindows) {
// `COM1` seems to be the default serial port on Windows.
return address !== 'COM1'.toLocaleLowerCase();
}
// On macOS and Linux, the port should start with `/dev/`.
if (!address.startsWith('/dev/')) {
return false
}
if (isOSX) {
// Example: `/dev/cu.usbmodem14401`
if (/(tty|cu)\..*/.test(address.substring('/dev/'.length))) {
return [
'/dev/cu.MALS',
'/dev/cu.SOC',
'/dev/cu.Bluetooth-Incoming-Port'
].map(a => a.toLocaleLowerCase()).every(a => a !== address);
}
}
// Example: `/dev/ttyACM0`
if (/(ttyS|ttyUSB|ttyACM|ttyAMA|rfcomm|ttyO)[0-9]{1,3}/.test(address.substring('/dev/'.length))) {
// Default ports were `/dev/ttyS0` -> `/dev/ttyS31` on Ubuntu 16.04.2.
if (address.startsWith('/dev/ttyS')) {
const index = Number.parseInt(address.substring('/dev/ttyS'.length), 10);
if (!Number.isNaN(index) && 0 <= index && 31 >= index) {
return false;
}
}
return true;
}
return false;
}
export function sameAs(left: Port | undefined, right: string | undefined) {
if (left && right) {
if (left.protocol !== 'serial') {
console.log(`Unexpected protocol for port: ${JSON.stringify(left)}. Ignoring protocol, comparing addresses with ${right}.`);
}
return left.address === right;
}
return false;
}
search(options: { query?: string }): Promise<{ items: BoardPackage[] }>;
install(item: BoardPackage): Promise<void>;
} }
export interface BoardPackage extends ArduinoComponent { export interface BoardPackage extends ArduinoComponent {
@@ -21,33 +176,58 @@ export interface Board {
fqbn?: string fqbn?: string
} }
export namespace Board {
export function is(board: any): board is Board {
return !!board && 'name' in board;
}
export function equals(left: Board, right: Board): boolean {
return left.name === right.name && left.fqbn === right.fqbn;
}
export function sameAs(left: Board, right: string | Board): boolean {
// How to associate a selected board with one of the available cores: https://typefox.slack.com/archives/CJJHJCJSJ/p1571142327059200
// 1. How to use the FQBN if any and infer the package ID from it: https://typefox.slack.com/archives/CJJHJCJSJ/p1571147549069100
// 2. How to trim the `/Genuino` from the name: https://arduino.slack.com/archives/CJJHJCJSJ/p1571146951066800?thread_ts=1571142327.059200&cid=CJJHJCJSJ
const other = typeof right === 'string' ? { name: right } : right;
if (left.fqbn && other.fqbn) {
return left.fqbn === other.fqbn;
}
return left.name.replace('/Genuino', '') === other.name.replace('/Genuino', '');
}
export function compare(left: Board, right: Board): number {
let result = left.name.localeCompare(right.name);
if (result === 0) {
result = (left.fqbn || '').localeCompare(right.fqbn || '');
}
return result;
}
export function installed(board: Board): boolean {
return !!board.fqbn;
}
}
export interface AttachedSerialBoard extends Board { export interface AttachedSerialBoard extends Board {
port: string; port: string;
serialNumber: string;
productID: string;
vendorID: string;
} }
export namespace AttachedSerialBoard { export namespace AttachedSerialBoard {
export function is(b: Board): b is AttachedSerialBoard { export function is(b: Board | any): b is AttachedSerialBoard {
return 'port' in b return !!b && 'port' in b;
&& 'serialNumber' in b
&& 'productID' in b
&& 'vendorID' in b;
} }
} }
export interface AttachedNetworkBoard extends Board { export interface AttachedNetworkBoard extends Board {
info: string;
address: string; address: string;
port: number; port: string;
} }
export namespace AttachedNetworkBoard { export namespace AttachedNetworkBoard {
export function is(b: Board): b is AttachedNetworkBoard { export function is(b: Board): b is AttachedNetworkBoard {
return 'name' in b return 'address' in b && 'port' in b;
&& 'info' in b
&& 'address' in b
&& 'port' in b;
} }
} }

View File

@@ -0,0 +1,14 @@
export const ConfigServicePath = '/services/config-service';
export const ConfigService = Symbol('ConfigService');
export interface ConfigService {
getVersion(): Promise<string>;
getConfiguration(): Promise<Config>;
isInDataDir(uri: string): Promise<boolean>;
isInSketchDir(uri: string): Promise<boolean>;
}
export interface Config {
sketchDirUri: string;
dataDirUri: string;
}

View File

@@ -1,3 +1,5 @@
import { Board } from "./boards-service";
export const CoreServicePath = '/services/core-service'; export const CoreServicePath = '/services/core-service';
export const CoreService = Symbol('CoreService'); export const CoreService = Symbol('CoreService');
export interface CoreService { export interface CoreService {
@@ -10,12 +12,15 @@ export namespace CoreService {
export namespace Upload { export namespace Upload {
export interface Options { export interface Options {
readonly uri: string; readonly uri: string;
readonly board: Board;
readonly port: string;
} }
} }
export namespace Compile { export namespace Compile {
export interface Options { export interface Options {
readonly uri: string; readonly uri: string;
readonly board: Board;
} }
} }
} }

View File

@@ -0,0 +1,3 @@
export interface Installable<T> {
install(item: T): Promise<void>;
}

View File

@@ -1,9 +1,10 @@
import { ArduinoComponent } from "./arduino-component"; import { Searchable } from './searchable';
import { Installable } from './installable';
import { ArduinoComponent } from './arduino-component';
export const LibraryServicePath = '/services/library-service'; export const LibraryServicePath = '/services/library-service';
export const LibraryService = Symbol('LibraryService'); export const LibraryService = Symbol('LibraryService');
export interface LibraryService { export interface LibraryService extends Installable<Library>, Searchable<Library> {
search(options: { query?: string }): Promise<{ items: Library[] }>;
install(library: Library): Promise<void>; install(library: Library): Promise<void>;
} }

View File

@@ -0,0 +1,44 @@
import { JsonRpcServer } from '@theia/core';
import { Board } from './boards-service';
export interface MonitorError {
readonly message: string;
readonly code: number
}
export interface MonitorReadEvent {
readonly connectionId: string;
readonly data: string;
}
export const MonitorServiceClient = Symbol('MonitorServiceClient');
export interface MonitorServiceClient {
notifyRead(event: MonitorReadEvent): void;
notifyError(event: MonitorError): void;
}
export const MonitorServicePath = '/services/serial-monitor';
export const MonitorService = Symbol('MonitorService');
export interface MonitorService extends JsonRpcServer<MonitorServiceClient> {
connect(config: ConnectionConfig): Promise<{ connectionId: string }>;
disconnect(connectionId: string): Promise<boolean>;
send(connectionId: string, data: string | Uint8Array): Promise<void>;
getConnectionIds(): Promise<string[]>;
}
export interface ConnectionConfig {
readonly board: Board;
readonly port: string;
/**
* Defaults to [`SERIAL`](ConnectionType#SERIAL).
*/
readonly type?: ConnectionType;
/**
* Defaults to `9600`.
*/
readonly baudRate?: number;
}
export enum ConnectionType {
SERIAL = 0
}

View File

@@ -0,0 +1,11 @@
export interface Searchable<T> {
search(options: Searchable.Options): Promise<{ items: T[] }>;
}
export namespace Searchable {
export interface Options {
/**
* Defaults to empty an empty string.
*/
readonly query?: string;
}
}

View File

@@ -0,0 +1,21 @@
export const SketchesServicePath = '/services/sketches-service';
export const SketchesService = Symbol('SketchesService');
export interface SketchesService {
/**
* Returns with the direct sketch folders from the location of the `fileStat`.
* The sketches returns with inverse-chronological order, the first item is the most recent one.
*/
getSketches(uri?: string): Promise<Sketch[]>
getSketchFiles(uri: string): Promise<string[]>
/**
* Creates a new sketch folder in the `parentUri` location. If `parentUri` is not specified,
* it falls back to the default `sketchDirUri` from the CLI.
*/
createNewSketch(parentUri?: string): Promise<Sketch>
isSketchFolder(uri: string): Promise<boolean>
}
export interface Sketch {
readonly name: string;
readonly uri: string
}

View File

@@ -0,0 +1,3 @@
export type RecursiveRequired<T> = {
[P in keyof T]-?: RecursiveRequired<T[P]>;
};

View File

@@ -0,0 +1,12 @@
import { injectable } from 'inversify';
import { ElectronMenuContribution } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution';
@injectable()
export class ElectronArduinoMenuContribution extends ElectronMenuContribution {
protected hideTopPanel(): void {
// NOOP
// We reuse the `div` for the Arduino toolbar.
}
}

View File

@@ -0,0 +1,8 @@
import { ContainerModule } from 'inversify';
import { ElectronMenuContribution } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution'
import { ElectronArduinoMenuContribution } from './electron-arduino-menu-contribution';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ElectronArduinoMenuContribution).toSelf().inSingletonScope();
rebind(ElectronMenuContribution).to(ElectronArduinoMenuContribution);
});

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