Compare commits

..

121 Commits

Author SHA1 Message Date
github-actions[bot]
1104467329 Updated translation files (#1701)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-12-05 10:24:38 +01:00
Akos Kitta
5695fd8afb fix: filtered undesired contributions: RTOS view
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-12-01 11:43:25 +01:00
Akos Kitta
d0e383853f feat: patched the Theia debug functionality
Patch for:
 - eclipse-theia/theia#11871
 - eclipse-theia/theia#11879
 - eclipse-theia/theia#11880
 - eclipse-theia/theia#11885
 - eclipse-theia/theia#11886
 - eclipse-theia/theia#11916

Closes #1582

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-12-01 11:43:25 +01:00
Akos Kitta
3bc412b42f feat: Updated to cortex-debug@1.5.1
Closes #246

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-12-01 11:43:25 +01:00
Akos Kitta
f553d6919d feat: no ping timeout in dev mode
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-12-01 11:43:25 +01:00
Akos Kitta
d6a4b0f910 fix: update monitor settings only if it's changed
Closes #375

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-29 14:07:58 +01:00
Akos Kitta
c0488d1f64 fix: remote sketch creation if tree is not active
Closes #1715

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-29 11:19:13 +01:00
Akos Kitta
81195431b0 fix: double update of the zoom level on save
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-29 10:58:23 +01:00
Akos Kitta
87109e6559 chore: Switched to window.zoomLevel preference
Deprecated `arduino.window.zoomLevel`.

Closes #1657

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-29 10:58:23 +01:00
Akos Kitta
c0af1e62e8 fix: main sketch file editor focus on layout reset
Closes #643

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-29 10:51:19 +01:00
Akos Kitta
ac9cce16f7 chore: Updated to Theia 1.31.1 (#1662)
Closes #1655
Closes #1656

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-29 09:39:54 +01:00
Alberto Iannaccone
3ad660927f Fix keybindings to switch between tabs on MacOs (#1686) 2022-11-29 09:22:14 +01:00
Akos Kitta
8778d70ad7 fix: editor widget resolving when creating new tab
An already opened editor widget can resolve without waiting.

Closes #1718

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-29 09:20:16 +01:00
Alberto Iannaccone
fe3fbb189c 2.0.3 (#1687) 2022-11-17 15:03:22 +01:00
github-actions[bot]
23c7f5f848 Updated translation files (#1606)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-11-17 12:47:21 +01:00
Alberto Iannaccone
f1144efb93 Use 0.29.0 CLI in IDE2 (#1683) 2022-11-17 12:43:28 +01:00
per1234
9cec643cab Fix nightly build links in issue forms
The issue forms are configured to request the contributor to test using the nightly build of Arduino IDE before
submitting an issue in order to make sure the bug or feature request has not already been resolved.

Some time ago, the repository's readme contained a table of download links. The links in the issue forms pointed there.
That table was replaced with a link to the official "Software" page in order to reduce unnecessary verbosity and
maintenance burden of the project's documentation content.

The issue form links were not updated at that time. The resulting additional link in the chain made obtaining the
nightly build less convenient for the contributor to obtain.

The links are hereby updated to point directly to the list of nightly build download links on the arduino.cc "Software"
page.
2022-11-17 03:25:26 -08:00
Akos Kitta
1a7784a540 feat: progress for the remote sketch creation
Closes #1668

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-17 11:05:34 +01:00
Akos Kitta
d24a3911f8 fix: workaround for arduino/arduino-cli#1968
Do not try to parse the original `NotFound` error message, but look for
a sketch somewhere in the requested path.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-11 13:19:08 +01:00
Akos Kitta
3735553003 fix: flawed timing issue when opening editors
From now on, the editor widget open promise resolution does not rely on
internal Theia events but solely on @phosphor's `isAttached`/`isVisible`
properties.
The editor widget promise resolves with the next task after a navigation
frame so the browser can render the widget.

Closes #1612

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-11 13:19:08 +01:00
Akos Kitta
f6d112e1f6 fix: escaped regex chars in pattern
Closes #1600

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-10 16:33:42 +01:00
Akos Kitta
cc2d557706 fix: relaxed condition to check if resource exists
The original (`fs-extra`-based) implementation did not check if the
file is writable either.

Resources are not writable in mounted AppImages.

Closes #1586

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-10 11:27:44 +01:00
Akos Kitta
103acc4b7e fix: allow second instance on macOS
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-10 11:27:17 +01:00
Akos Kitta
c3dc7c6307 fix: avoid ENOTDIR when opening second instance.
If the resource is a file, do not try to `readdir`, but return undefined

Closes #1590

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-10 11:27:17 +01:00
Akos Kitta
7d6a2d5e33 feat: Create remote sketch
Closes #1580

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-10 11:12:20 +01:00
Akos Kitta
6984c52b92 fix: Handle gracefully when trying to detect invalid sketch name error and folder is missing on filesystem (#1616)
- feat: generalized Node.js error handling
    - Gracefully handle when the sketch folder has been deleted
 - feat: spare detecting invalid sketch name error
    - The invalid sketch name detection requires at least one extra FS access.
       Do not try to detect the invalid sketch name error, but use the original
       `NotFound` from the CLI.
 - fix: typo

Closes #1596

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-10 11:11:35 +01:00
Akos Kitta
3a70547770 fix: do not trim stdout of clang-format process
Otherwise, it always causes a _no newline at the end of file_ problem.

Closes #1487

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-08 09:39:41 +01:00
per1234
8a85b5c3d8 Migrate workflows from deprecated set-output commands
GitHub Actions provides the capability for workflow authors to use the capabilities of the GitHub Actions ToolKit
package directly in the `run` keys of workflows via "workflow commands". One such command is `set-output`, which allows
data to be passed out of a workflow step as an output.

It has been determined that this command has potential to be a security risk in some applications. For this reason,
GitHub has deprecated the command and a warning of this is shown in the workflow run summary page of any workflow using
it:

The `set-output` command is deprecated and will be disabled soon. Please upgrade to using Environment Files. For more
information see: https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/

The identical capability is now provided in a safer form via the GitHub Actions "environment files" system. Migrating
the use of the deprecated workflow commands to use the `GITHUB_OUTPUT` environment file instead fixes any potential
vulnerabilities in the workflows, resolves the warnings, and avoids the eventual complete breakage of the workflows that
would result from GitHub's planned removal of the `set-output` workflow command 2023-05-31.
2022-11-04 00:45:14 -07:00
dependabot[bot]
b998d35524 Bump actions/checkout from 2 to 3
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-03 16:39:12 -07:00
dependabot[bot]
ddec64c4a5 Bump svenstaro/upload-release-action from 2.2.0 to 2.3.0
Bumps [svenstaro/upload-release-action](https://github.com/svenstaro/upload-release-action) from 2.2.0 to 2.3.0.
- [Release notes](https://github.com/svenstaro/upload-release-action/releases)
- [Changelog](https://github.com/svenstaro/upload-release-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/svenstaro/upload-release-action/compare/2.2.0...2.3.0)

---
updated-dependencies:
- dependency-name: svenstaro/upload-release-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-03 16:09:57 -07:00
dependabot[bot]
8fed08003e Bump actions/upload-artifact from 2 to 3
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2 to 3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-03 16:09:37 -07:00
dependabot[bot]
8454c625f7 Bump geekyeggo/delete-artifact from 1 to 2
Bumps [geekyeggo/delete-artifact](https://github.com/geekyeggo/delete-artifact) from 1 to 2.
- [Release notes](https://github.com/geekyeggo/delete-artifact/releases)
- [Commits](https://github.com/geekyeggo/delete-artifact/compare/v1...v2)

---
updated-dependencies:
- dependency-name: geekyeggo/delete-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-03 16:04:18 -07:00
dependabot[bot]
60df322f09 Bump actions/setup-node from 1 to 3
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 1 to 3.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v1...v3)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-03 16:03:47 -07:00
dependabot[bot]
8bfb140e7c Bump actions/setup-python from 2 to 4
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 4.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v2...v4)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-03 14:51:19 -07:00
dependabot[bot]
260227e79a Bump peter-evans/create-pull-request from 3 to 4
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 3 to 4.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](https://github.com/peter-evans/create-pull-request/compare/v3...v4)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-03 14:30:50 -07:00
dependabot[bot]
cc310bf1a5 Bump actions/download-artifact from 2 to 3
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 2 to 3.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-03 14:25:30 -07:00
per1234
dbd52e2f34 Remove unused GitHub release download stats workflow
The "github-stats" GitHub Actions workflow periodically gathers GitHub release asset download statistics for Arduino CLI
and pushes the results to Datadog.

There are no known problems with this workflow. However, the companion "arduino-stats" workflow that did the same for
the downloads of Arduino IDE from downloads.arduino.cc was broken and thus removed from the repository.

The GitHub stats are not very valuable on their own as they only provide an unknown fraction of the total downloads of
Arduino IDE. They have also not ended up being used.

The workflow also uses deprecated Node.js 12 runtime, which currently results in warnings printed to the workflow run
summary page, but will eventually cause the complete breakage of the workflow.

Since it doesn't provide any value and represents a maintenance burden, the workflow is hereby removed from the
repository.
2022-11-03 14:13:40 -07:00
per1234
9cd03bec46 Remove broken download stats workflow
The "arduino-stats" GitHub Actions workflow was designed to periodically gather download statistics from Arduino CDN and
push results to Datadog.

The recorded stats from the identical system in the Arduino CLI repository showed a periodic decrease in total download
count. Since this is patently impossible, it is clear that something is wrong with the system and that the recorded data
is not trustworthy. An investigation into the problem
was never done.

On 2022-03-14, the runs of the "arduino-stats" GitHub Actions workflow began to fail. Because there had not been any
relevant change in the repository between the last successful run and the first failing run, it seems that some external
change caused the breakage.

The workflow also uses deprecated Node.js 12 runtime-based actions and set-output workflow command, which currently
results in warnings printed to the workflow run summary page, but will eventually cause the complete breakage of the
workflow.

Since the workflow was not ever working successfully and the lack of an investigation about that indicates that the
stats are not of immediate importance, the best course of action is to simply remove the broken infrastructure from the
repository rather than investing time into fixing something that isn't being used anyway.
2022-11-03 14:13:40 -07:00
dependabot[bot]
c29452a858 Bump carlosperate/download-file-action from 1 to 2
Bumps [carlosperate/download-file-action](https://github.com/carlosperate/download-file-action) from 1 to 2.
- [Release notes](https://github.com/carlosperate/download-file-action/releases)
- [Commits](https://github.com/carlosperate/download-file-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: carlosperate/download-file-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-03 14:05:51 -07:00
per1234
7d91f2d8cb Configure Dependabot to check for outdated actions used in workflows
Dependabot will periodically check the versions of all actions used in the repository's workflows. If any are found to
be outdated, it will submit a pull request to update them.

NOTE: Dependabot's PRs will occasionally propose to pin to the patch version of the action (e.g., updating
`uses: foo/bar@v1` to `uses: foo/bar@v2.3.4`). When the action author has provided a major version ref, use that instead
(e.g., `uses: foo/bar@v2`). Dependabot will automatically close its PR once the workflow has been updated.

More information:
https://docs.github.com/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
2022-11-03 13:56:30 -07:00
Akos Kitta
f6275f9f62 feat: build IDE2 on darwin arm64
- Use Node.js 16+,
 - All workflow files use `.yml` instead of `.yaml`,
 - Use Arduino LS `0.7.2`,
 - Updated `electron-builder` to `23.3.3`,
 - Removed unused `conf-node-gyp.sh`,
 - Removed unused `THEIA_ELECTRON_SKIP_REPLACE_FFMPEG`, and
 - Aligned `node-gyp@9.3.0`, `electron-rebuild@3.2.9` to Theia.

Co-authored-by: per1234 <accounts@perglass.com>
Co-authored-by: Akos Kitta <a.kitta@arduino.cc>

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-11-02 15:48:04 +01:00
per1234
0d0550974a Bump version metadata post release
On every startup, the Arduino IDE checks for new versions of the IDE. If a newer version is available, a
notification/dialog is shown offering an update.

"Newer" is determined by comparing the version of the user's IDE to the latest available version on the update channel.
This comparison is done according to the Semantic Versioning Specification ("SemVer").

In order to facilitate beta testing, builds are generated of the Arduino IDE at the current stage in development. These
builds are given an identifying version of the following form:

- <version>-snapshot-<short hash> - builds generated for every push and pull request that modifies relevant files
- <version>-nightly-<YYYYMMDD> - daily builds of the tip of the default branch

In order to cause these builds to be correctly considered "newer" than the release version, the version metadata must be
bumped immediately following each release.

This will also serve as the metadata bump for the next release in the event that release is a minor release. In case it
is instead a minor or major release, the version metadata will need to be updated once more before the release tag is
created.
2022-10-28 06:42:46 -07:00
Alberto Iannaccone
4e882d25d9 bump arduino-fwuploader to 2.2.2 (#1584) 2022-10-27 14:53:36 +02:00
github-actions[bot]
f93f78039b Updated translation files (#1496)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-10-27 12:40:56 +02:00
Akos Kitta
2b2463b834 fix: Prompt sketch move when opening an invalid outside from IDE2
Log IDE2 version on start.

Closes #964
Closes #1484

Co-authored-by: Alberto Iannaccone <a.iannaccone@arduino.cc>
Co-authored-by: Akos Kitta <a.kitta@arduino.cc>

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-10-26 18:53:00 +02:00
Nick B
0773c3915c Added an optional user modifiable default sketch file when creating a new project. (#1559)
* Added a modifiable default sketch for new project

* Removed unused file

* WiP : Now nothing's working... :(

* yarn i18n:generate for the settings

* Updated the desription for markdown description.

* Lintered the code

* Remove undesirable whitespaces

* Applied kittaakos suggestions

* Removed extra whitespaces

* Fixed default `.ino` for the missings empty lines.
2022-10-26 14:08:22 +02:00
Francesco Spissu
2f5afe0d9c Prevent layout shift on hover in libs/board manager (#1568) 2022-10-25 08:58:37 +02:00
Muhammad Zaheer
b8370686ec Coding style fix - newline added 2022-10-25 08:51:21 +02:00
Muhammad Zaheer
3b2d12eff9 Cleaner implementation of HistoryList
- The implementation has been taken from @kittaakos repo
d10de01736/arduino-ide-extension/src/browser/serial/monitor/serial-monitor-send-input.tsx
- The previous method has been modified to ensure the first element instead of an empty string is returned if the index is at the beginning of the list.
- The push method has been modified to check if the current command is same as the last command. If same then, it is not added to the list else it is added.
2022-10-25 08:51:21 +02:00
Muhammad Zaheer
cdaaa5584d Changed logic to avoid end value being shown twice 2022-10-25 08:51:21 +02:00
Muhammad Zaheer
3476de27f7 Added Message History to Serial Monitor 2022-10-25 08:51:21 +02:00
Akos Kitta
b55cfc2052 chore: Use 0.28.0 CLI in IDE2.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-10-24 16:08:57 +02:00
Akos Kitta
44751c370b Changed the daemon output from json to text
Closes #1544

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-10-24 14:14:33 +02:00
Alberto Iannaccone
32d904ca36 Let the user edit the font size settings with the keyboard (#1547)
* let the user edit the stepper input with keyboard

* consider exceptions and fix styling

* fix onBlur with empty strings

* always set the internal state value

* misc fixes

Co-authored-by: David Simpson <45690499+davegarthsimpson@users.noreply.github.com>
2022-10-21 17:36:19 +02:00
Muhammad Zaheer
5424dfcf70 Fix #1566 : Port submenu section heading show at top 2022-10-21 09:04:07 +02:00
per1234
b8bf1eefa2 Allow uploads without port selection
It is common for a "port" to be used in some way during the process of uploading to a board. However, the array of
Arduino boards is very diverse. Some of these do not produce a port and their upload method has no need for one.

For this reason, the IDE must allow the upload process to be initiated regardless of whether a port happens to be
selected. During the addition of support for user provided fields, an unwarranted assumption was made that all boards
require a port selection for upload and this resulted in a regression that broke uploading for these boards. This
regression was especially user unfriendly in that there was no response whatsoever from the IDE when the user attempted
to initiate an upload under these conditions.

The bug is hereby fixed. The upload process will always be initiated by the IDE regardless of whether a port is
selected. In cases where a port is required, the resulting error message returned by Arduino CLI or the upload tool will
communicate the problem to the user.
2022-10-20 08:31:52 -07:00
Francesco Spissu
93291b6811 Adjust library installation dialog buttons style (#1401)
Closes #1314.
2022-10-20 12:40:40 +02:00
Akos Kitta
87ebcbe77e Let CSS do the uppercase transformation.
Expose no implementation details to translation files.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-10-19 10:49:02 +02:00
Akos Kitta
99b10942bb Listen on the client's port change event
If the board select dialog is listening on the backend's event,
the frontend might miss the event when it comes up, although boards
are connected and ports are discovered.

Closes #573

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-10-17 10:31:19 +02:00
Alberto Iannaccone
960a2d0634 Fix boards listing (#1520)
* Fix boards listing

* use arduio-cli sorting fix

* re-use code to handle board list response

* change `handleListBoards` visibility to `private`

* pad menu items order with leading zeros to fix alphanumeric order
2022-10-17 10:03:41 +02:00
Francesco Spissu
e577de4e8e Put Arduino libs and platforms on top of the Library/Boards Manager (#1541) 2022-10-14 09:07:54 +02:00
Francesco Spissu
f3ef95cfe2 Retain installation interface using version menu (#1471) 2022-10-13 12:05:29 +02:00
dankeboy36
bc264d1adf Apply margin adjustments to the first hover row
Signed-off-by: dankeboy36 <dankeboy36@gmail.com>
2022-10-07 04:16:26 -07:00
dankeboy36
5444395f34 Better tooltips.
fixes #1503

Signed-off-by: dankeboy36 <dankeboy36@gmail.com>
2022-10-07 04:16:26 -07:00
Akos Kitta
2d2be1f6d0 Ensure exact match when installing Arduino_BuiltIn
on the first IDE2 startup.

Closes #1526

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-10-07 11:55:53 +02:00
r3inbowari
1e269ac83d Fix status bar clipped in minimal state (#1517) 2022-10-07 10:43:45 +02:00
Akos Kitta
0c49709f26 Link resolved for lib/boards manager.
Closes #1442

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-10-07 10:00:36 +02:00
Akos Kitta
019b2d5588 Avoid using reportResult if installing lib/core
Closes #1529

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-10-07 09:01:25 +02:00
Alberto Iannaccone
aa0807ca3f Limit interface scale (#1502)
* limit interface scale

* debounce interface scale updates

* limit font-size + refactor

* remove excessive settings duplicate

* remove useless async

* fix interface scale step

* change mainMenuManager visibility to private

* fix menu registration

* update menu actions when autoScaleInterface changes
2022-10-06 17:37:26 +02:00
Akos Kitta
61a11a0857 Removed real_name of the libraries.
It has been removed from the gRPC API: arduino/arduino-cli#1890

This PR switches from `real_name` to `name` in the UI, as the `name` is
the canonical form provided by the CLI.

Closes #1525

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-10-05 13:47:38 +02:00
Akos Kitta
0c20ae0e28 Various library/platform index update fixes
- IDE2 can start if the package index download fails. Closes #1084
 - Split the lib and platform index update. Closes #1156

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-10-05 09:17:37 +02:00
per1234
945a8f4841 Bump built-in example sketches version to 1.10.0
The Arduino IDE installation includes a collection of example sketches demonstrating fundamental concepts.

These examples are hosted in a dedicated repository, which is a dependency of this project. A new release has been made
in that `arduino/arduino-examples` repository.

This release updates the formatting of the examples to be compliant with the code style of the Arduino IDE 2.x
"Auto Format" feature.
2022-10-04 01:59:50 -07:00
per1234
ae76432944 Update library dependency installation dialog response indexes
Arduino libraries may specify dependencies on other libraries in their metadata. The installation of these dependencies
is offered by the Arduino IDE when the user installs the dependent library using the Library Manager widget.

The order of the buttons in the dialog that offers the dependencies installation was recently rearranged. The dialog
response interpretation code was not updated to reflect the changes to the button indexes at that time. This caused the
"CANCEL" button to trigger the behavior expected from the "INSTALL ALL" button, and vice versa.

The library dependencies installation dialog response interpretation code is hereby updated to use the new button
indexes.
2022-10-03 23:56:22 -07:00
per1234
40807db65e Bump arduino-serial-plotter-webapp dependency to 0.2.0 2022-10-03 23:33:17 -07:00
Akos Kitta
da22f1ed11 Refresh menus when opening example/recent fails.
Closes #53

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-10-04 08:10:27 +02:00
per1234
32b70efd5c Correct text of "INSTALLED" label in Library/Boards Manager
An "INSTALLED" label is shown on the items in the Library Manager and Boards Manager views that are currently installed
on the user's system.

During some work to add missing internationalization to the UI strings, this text was changed to "INSTALL". That text is
not appropriate for what this label is intended to communicate.

The regression is hereby corrected, while retaining the internationalization of the string.
2022-10-03 00:53:40 -07:00
Alberto Iannaccone
6f07717369 Dialog focus (#1472)
* focus on dialog controls when is open

* fix "Configure and Upload" label

* fix focus on user fields
2022-09-29 15:16:28 +02:00
r3inbowari
d6cb23f782 fix splitHandle above widget 2022-09-27 06:19:55 -07:00
r3inbowari
9ac2638335 Avoid intellisense widgets being covered by the bottom panel 2022-09-27 06:19:55 -07:00
Alberto Iannaccone
96cf09d594 Initialise the IDE updater even when 'checkForUpdates' preference is false (#1490) 2022-09-26 17:39:19 +02:00
per1234
8380c82028 Add readme for localization data
Arduino IDE has been translated to several languages.

The localization process follows the following steps:

1. An English language source string is defined in the Arduino IDE codebase
2. The source string is pushed to Transifex
3. Community translators localize the string
4. The localization data is pulled into the Arduino IDE repository
5. The localization data is incorporated into the Arduino IDE distribution

Experience with maintenance of Arduino's localized projects indicates that the data files generated at step (4) can
appear to be the appropriate place to make edits for casual contributors not familiar with the project's sophisticated
internationalization infrastructure.

Since those files are generated by automated systems, any edits made there would only be overwritten, so it is important
to clearly communicate the correct way to make enhancements or corrections to these strings. This is accomplished by a
local readme file most likely to be seen by those working in the folder containing these files, which supplements the
existing information about translation in the project's translation guide.
2022-09-26 05:07:34 -07:00
per1234
5eb2926407 Add a dedicated translator guide document
Translation of the strings of the Arduino IDE UI is a valuable contribution which helps to make Arduino accessible to
everyone around the world.

Localization of the Arduino-specific strings of the IDE is done in the "Arduino IDE 2.0" project on Transifex.
Previously, the "Translation" row in the contribution methods summary table in the contributor guide entry page simply
linked to that project.

Arduino IDE also uses localized strings from several other sources:

- VS Code language packs
- Arduino CLI

Users may notice unlocalized strings or errors or areas for improvement in the existing translations and wish to
contribute translations. For this reason, it is important to also provide instructions for contributing to those other
localization data sources. The contribution methods summary table can not effectively accommodate that additional
content so a dedicated document is added for the purpose. This will also allow linking directly to that document from
related documentation or conversations.
2022-09-26 05:07:34 -07:00
per1234
a4ab204400 Correct issue report guide link in issue template chooser
Contributor are presented with an issue template chooser page at the start of the issue creation process.

In addition to the issue report templates, some "contact links" provide information and links to other communication
channels. In order to encourage high quality issues, a link to the "issue report guide" is included on this page.

Previously that link pointed to an incorrect URL, resulting in a 404 error for those who visited it. The URL is hereby
corrected.
2022-09-26 05:04:43 -07:00
per1234
6416c431c6 Bump version metadata to produce correct tester and nightly build precedence
On every startup, the Arduino IDE checks for new versions of the IDE. If a newer version is available, a
notification/dialog is shown offering an update.

"Newer" is determined by comparing the version of the user's IDE to the latest available version on the update channel.
This comparison is done according to the Semantic Versioning Specification ("SemVer").

In order to facilitate beta testing, builds are generated of the Arduino IDE at the current stage in development. These
builds are given an identifying version of the following form:

- <version>-snapshot-<short hash> - builds generated for every push and pull request that modifies relevant files
- <version>-nightly-<YYYYMMDD> - daily builds of the tip of the default branch

The previous release procedure caused the <version> component of these to be the version of the most recent release.

During the pre-release phase of the project development, all releases had a pre-release suffix (e.g., 2.0.0-rc9.4).
Appending the "snapshot" or "nightly" suffix to that pre-release version caused these builds to have the correct
precedence (e.g., 2.0.0-rc9.2.snapshot-20cc34c > 2.0.0-rc9.2). This situation has changed now that the project is using
production release versions (e.g., 2.0.0-nightly-20220915 < 2.0.0). This caused users of "snapshot" or "nightly" builds
to be presented with a spurious update notification on startup.

The solution is to do a minor bump of the version metadata after creating the release tag. That was not done immediately
following the 2.0.0 release. The omission is hereby corrected.

This will provide the metadata bump traditionally done before the creation of the release tag in the event the version
number of the next release is 2.0.1. In case it is instead a minor or major release, the version metadata will need to
be updated once more before the release tag is created.
2022-09-26 05:04:43 -07:00
per1234
8f88aa69bf Adjust release procedure to produce correct tester and nightly build version precedence
On every startup, the Arduino IDE checks for new versions of the IDE. If a newer version is available, a
notification/dialog is shown offering an update.

"Newer" is determined by comparing the version of the user's IDE to the latest available version on the update channel.
This comparison is done according to the Semantic Versioning Specification ("SemVer").

In order to facilitate beta testing, builds are generated of the Arduino IDE at the current stage in development. These
builds are given an identifying version of the following form:

- <version>-snapshot-<short hash> - builds generated for every push and pull request that modifies relevant files
- <version>-nightly-<YYYYMMDD> - daily builds of the tip of the default branch

The previous release procedure caused the <version> component of these to be the version of the most recent release.

During the pre-release phase of the project development, all releases had a pre-release suffix (e.g., 2.0.0-rc9.4).
Appending the "snapshot" or "nightly" suffix to that pre-release version caused these builds to have the correct
precedence (e.g., 2.0.0-rc9.2.snapshot-20cc34c > 2.0.0-rc9.2). This situation has changed now that the project is using
production release versions (e.g., 2.0.0-nightly-20220915 < 2.0.0). This caused users of "snapshot" or "nightly" builds
to be presented with a spurious update notification on startup.

The solution is to add a step to the end of the release procedure to do a minor bump of the version metadata after
creating the release tag.

This means that the metadata bump traditionally done before the creation of the release tag will already have been done
in advance for patch releases. However, it will still need to be done for minor or major releases.

The release procedure documentation is hereby updated to produce correct tester and nightly build version precedence.

The metadata bump step is moved from before to after the tag creation step, replaced by a new step to verify the version
before the tag creation, updating it in the event it is not a patch release. Both those steps may require updating the
metadata. As an alternative to maintaining duplicate copies, each step links to a single copy of the fairly complex
instructions for doing so. The structure of the document is adjusted to accomodate this, by placing the steps of the
procedure under a "Steps" section and creating a new "Operations" section to contain any such shared content.
2022-09-26 05:04:43 -07:00
per1234
3c2b2a0734 Format release procedure document as ordered list
The release procedure is a set of steps which must be performed in a specific sequence. This fact is more effectively
communicated by formatting it as an ordered list.
2022-09-26 05:04:43 -07:00
per1234
39538f163f Move package metadata update step to dedicated section of release docs
Previously the instructions for updating the npm package metadata, submitting a PR for that, and merging the PR was in
the same section as the tag push instructions in the release procedure documentation.

These two operations are distinct from each other. Mashing them into a single step makes the release procedure document
difficult to read and the process more prone to error.

For this reason, a dedicated step is used for each of the two things.
2022-09-26 05:04:43 -07:00
Akos Kitta
9ef04bb8d6 Fixed missing translations
Aligned the languge pack versions.

Closes #1431

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-26 10:49:31 +02:00
Akos Kitta
707f3bef61 Listen on keyboard layout changes from the OS.
Closes #989

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-26 10:49:03 +02:00
Akos Kitta
878395221a Use the parent of the existing sketch if not temp.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-22 10:08:38 +02:00
Akos Kitta
6a35bbfa7e Made the file dialogs modal.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-22 10:08:38 +02:00
Francesco Spissu
42f6f43870 Avoid new line if 3rd party URLs text is too long (#1474)
Closes #1470.
2022-09-21 11:51:38 +02:00
Akos Kitta
6983c5bf7f Ensure directories.user exists.
Closes #1445

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-21 09:10:26 +02:00
Francesco Spissu
b3ab5cbd2a Fix input background in Firmware Updater dialog (#1465)
Closes #1441.
2022-09-20 14:47:30 +02:00
Alberto Iannaccone
8a5995920a fix board selection and workspace input dialogs width and height (#1406)
* fix board selection and workspace input dialogs width and height

* use same dialog for new file and rename

* fix board list getting small when filtering

* board select dialog: show variant text when no board is found

* fix addition boards url outline
2022-09-20 14:36:02 +02:00
github-actions[bot]
8de6cf84d9 Updated translation files (#1462)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-09-20 12:57:27 +02:00
Dwight
f5c36bb691 Serial Monitor autoscroll only makes bottom line partially visible #972 (#1446) 2022-09-20 12:26:57 +02:00
Francesco Spissu
364f8b8e51 Move primary buttons on the right of the dialogs (#1382)
Closes #1368.
2022-09-20 11:48:19 +02:00
Alberto Iannaccone
671d2eabd4 Show user fields dialog again if upload fails (#1415)
* Show user fields dialog again if upload fails

* move user fields logic into own contribution

* apply suggestions
2022-09-20 09:27:09 +02:00
Akos Kitta
9a65ef6ea8 Disabled the tokenizer after 500 chars.
Closes #1343.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-19 12:17:13 +02:00
Alberto Iannaccone
4e590ab618 use ipc message to focus on serial plotter window (#1410) 2022-09-15 16:07:13 +02:00
Dave Simpson
026e80e7fc fix #1383: missing port labels (#1412)
* fallback to port.address if addressLabel is false

* addressLabel if possible, reconcileAvailableBoards

* remove fallback
2022-09-15 15:29:28 +02:00
Akos Kitta
fdf6f0f9c8 Avoid deleting the workspace when it's still in use.
- From now on, NSFW service disposes after last reference
is removed. No more 10sec delay.
 - Moved the temp workspace deletion to a startup task.
 - Can set initial task for the window from electron-main.
 - Removed the `browser-app`.

Closes #39

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-15 15:24:25 +02:00
per1234
0151e4c224 Make instructions re: non-notarized PR tester build more user friendly
For security reasons, the macOS tester builds generated for PRs from forks are not notarized. Instructions are provided for
beta testing under these conditions.

Previously, the instructions for bypassing the macOS notarization requirement involved disabling macOS Gatekeeper
entirely using the spctl command. These instructions are hereby replaced by an alternative approach, where the
restriction is bypassed for the Arduino IDE application alone, via the macOS GUI.

The new approach is superior for the following reasons:

- More secure due to having limited scope
- More accessible due to the use of the macOS GUI
2022-09-15 05:53:09 -07:00
per1234
e8b0ea4f2d Reduce overlap between readme and development+contributor guides
The readme should provide a concise overview of the essential information about the project. Additional details are
provided in dedicated documents, so the readme only needs to provide links to that information.
2022-09-15 05:53:09 -07:00
per1234
7c1ca04c75 Add a project contributor guide
Documentation of how to contribute to the project gives everyone the opportunity to participate, while also reducing the
maintenance effort and increasing the quality of contributions.

This guide documents the various ways of contributing to the project.
2022-09-15 05:53:09 -07:00
per1234
0ba88d5ab6 Move beta testing information to a dedicated documentation file
Previously, the information about tester builds was mixed in with the documentation about building the project from
source.

The two subjects are of relevance to two distinct contribution options. Building from source will primarily be done by
developers working on the project code base. Tester builds will be used by beta testers and reviewers.

For this reason, it doesn't make sense to require beta testers to wade through a lot of development documentation not
directly related to their work in order to find the instructions for obtaining tester builds. Likewise, it doesn't make
sense to clutter up the development documentation with such information.

Moving the information about tester builds to a dedicated file makes it easier for the interested parties to find, and
also allows the creation of a comprehensive guide for beta testers without making a negative impact on the development
documentation content.
2022-09-15 05:53:09 -07:00
per1234
96e229d803 Move documentation assets to standard location 2022-09-15 05:53:09 -07:00
per1234
d07d83fdfe Move development documentation to a more suitable location
Previously, information about project development was stored in a file named BUILDING.md in the root of the repository.

That content is hereby moved to the file docs/development.md. This will provide the following benefits:

- Broaden the scope of the file to all information related to development rather than building only
- Store all documentation content under a single folder instead of randomly putting some in the root and others under
  docs
2022-09-15 05:53:09 -07:00
Akos Kitta
5f82577bc1 Can send message to the monitor with Enter.
Removed the required `Ctrl/Cmd` modifier.

Closes #572

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-14 17:18:31 +02:00
per1234
35fcfb89c1 Remove obsolete fragment identifier from IDE manual download link
The Arduino IDE has an auto update capability. A new version is checked for on every startup, and if available the user
is offered an update.

Although this update can usually be done automatically by the IDE, under some conditions this is not possible. For
example:

- The IDE package in use does not support auto update (i.e., Linux ZIP package)
- The file could not be downloaded due to a transient network service outage

In this case, the user is presented with a friendly dialog that explains the situation and links to the "Software" page
on arduino.cc, where the user can manually download and install the new version.

During the pre-release development phase of the project, the download links for Arduino IDE 2.x were on a sub-section of
the "Software" page. For this reason, the linked URL included the fragment identifier `#experimental-software` so that
the page would load scrolled down to the anchor at that section of the page.

With the 2.0.0 release, the Arduino IDE 2.x project has graduated to a production development phase. For this reason,
the download links have been moved to the top of the "Software" page and the now inaccurate `experimental-software`
anchor removed from the page.

The previous link with that fragment is still perfectly functional, but the fragment to a non-existent anchor serves no
purpose and also miscommunicates the project status to users who notice the URL that was loaded. For this reason, it is
hereby removed from the link.
2022-09-14 05:14:58 -07:00
Alberto Iannaccone
6e3fe08c4c update README.md 2022-09-13 23:58:23 -07:00
Alberto Iannaccone
7f06b148f4 Revert "change naming of nightly and snapshot builds (#1326)"
This reverts commit 5be1f9d7fe.
2022-09-13 23:58:23 -07:00
Alberto Iannaccone
bf303d1b2f 2.0.0 2022-09-13 23:58:23 -07:00
per1234
59ca91d805 Remove table of nightly build links from readme
During the early phase of development, the download links for the nightly build were only available from the table in the project readme.

Since that time, download links were also added to the "Software" page on arduino.cc, which is already linked to from
the readme. This means the nightly build link table is superfluous and only harms the readability of the readme.

The superfluous table is hereby removed from the readme.
2022-09-12 06:04:36 -07:00
github-actions[bot]
69bb0aa385 Updated translation files (#1421)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-09-12 09:53:06 +02:00
per1234
565970e779 Update release procedure documentation
An internal release procedure document was created separately from the documentation hosted in this repository. That
internal document became significantly more comprehensive and up to date than the unmaintained documentation in this
repository.

In order to avoid either the burden of maintaining two copies of the same information, or more likely the out of sync
state between the information in the two resulting from lack of such maintenance, a single document will be maintained
here in this repository.

The superior version of the information from the internal document is hereby migrated to the repository where it will be
maintained from here on.
2022-09-08 13:30:36 -07:00
per1234
fec3b1138b Move release procedure documentation to more appropriate location
Previously, the procedure for creating a new release of the project was included in the development documentation.

This information is distinct from the rest of the contents of that file in that it is not of any value or interest to
most contributors from the community since only project maintainers will ever create a release. This meant that it make
the document less readable and approachable without adding significant value in return.

The information is still essential to the project maintainers, so it must not be removed, but it can be moved to a
dedicated file under the existing `docs/internal/` folder that is specifically intended for storing such information.
2022-09-08 13:30:36 -07:00
Alberto Iannaccone
dcc0c0aa5d Prepare 2.0.0-rc9.4 (#1411) 2022-09-08 10:00:42 +02:00
Muhammad Zaheer
76673cb553 Time format in SerialMonitor changed.Fixes #580 2022-09-08 00:15:26 -07:00
per1234
8f95fd6ca6 Add instructions for accessing IDE's advanced settings
Although the Arduino IDE's primary preferences interface provides all required configuration capabilities, advanced
users may wish to fine tune the behavior of the application or temporarily enable additional log output to use for
troubleshooting problems with the IDE.

The IDE provides such settings in a separate interface.

Previously, the existence and access procedure for these settings was undocumented.

Since this is an advanced capability, the documentation is not appropriate for inclusion with the standard user
documentation on arduino.cc. A file here in the Arduino IDE is used instead. This file will serve as a container for all
such user-targeted documentation.
2022-09-07 00:16:14 -07:00
274 changed files with 14543 additions and 9894 deletions

View File

@@ -17,7 +17,6 @@ module.exports = {
'scripts/*',
'electron/*',
'electron-app/*',
'browser-app/*',
'plugins/*',
'arduino-ide-extension/src/node/cli-protocol',
],

View File

@@ -30,7 +30,7 @@ body:
description: |
Which version of the Arduino IDE are you using?
See **Help > About Arduino IDE** in the Arduino IDE menus (**Arduino IDE > About Arduino IDE** on macOS).
This should be the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds).
This should be the latest [nightly build](https://www.arduino.cc/en/software#nightly-builds).
validations:
required: true
- type: dropdown
@@ -68,7 +68,7 @@ body:
options:
- label: I searched for previous reports in [the issue tracker](https://github.com/arduino/arduino-ide/issues?q=)
required: true
- label: I verified the problem still occurs when using the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds)
- label: I verified the problem still occurs when using the latest [nightly build](https://www.arduino.cc/en/software#nightly-builds)
required: true
- label: My report contains all necessary details
required: true

View File

@@ -8,6 +8,12 @@ contact_links:
- name: Support request
url: https://forum.arduino.cc/
about: We can help you out on the Arduino Forum!
- name: Issue report guide
url: https://github.com/arduino/arduino-ide/blob/main/docs/contributor-guide/issues.md#issue-report-guide
about: Learn about submitting issue reports to this repository.
- name: Contributor guide
url: https://github.com/arduino/arduino-ide/blob/main/docs/CONTRIBUTING.md#contributor-guide
about: Learn about contributing to this project.
- name: Discuss development work on the project
url: https://groups.google.com/a/arduino.cc/g/developers
about: Arduino Developers Mailing List

View File

@@ -25,7 +25,7 @@ body:
description: |
Which version of the Arduino IDE are you using?
See **Help > About Arduino IDE** in the Arduino IDE menus (**Arduino IDE > About Arduino IDE** on macOS).
This should be the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds).
This should be the latest [nightly build](https://www.arduino.cc/en/software#nightly-builds).
validations:
required: true
- type: dropdown
@@ -63,7 +63,7 @@ body:
options:
- label: I searched for previous requests in [the issue tracker](https://github.com/arduino/arduino-ide/issues?q=)
required: true
- label: I verified the feature was still missing when using the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds)
- label: I verified the feature was still missing when using the latest [nightly build](https://www.arduino.cc/en/software#nightly-builds)
required: true
- label: My request contains all necessary details
required: true

15
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
# See: https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#about-the-dependabotyml-file
version: 2
updates:
# Configure check for outdated GitHub Actions actions in workflows.
# Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/assets/dependabot/README.md
# See: https://docs.github.com/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
- package-ecosystem: github-actions
directory: / # Check the repository's workflows under /.github/workflows/
assignees:
- per1234
schedule:
interval: daily
labels:
- "topic: infrastructure"

View File

@@ -1,131 +0,0 @@
import boto3
import semver
import os
import logging
import uuid
import time
# logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
log = logging.getLogger()
logging.getLogger("boto3").setLevel(logging.CRITICAL)
logging.getLogger("botocore").setLevel(logging.CRITICAL)
logging.getLogger("urllib3").setLevel(logging.CRITICAL)
def execute(client, statement, dest_s3_output_location):
log.info("execute query: {} dumping in {}".format(statement, dest_s3_output_location))
result = client.start_query_execution(
QueryString=statement,
ClientRequestToken=str(uuid.uuid4()),
ResultConfiguration={
"OutputLocation": dest_s3_output_location,
},
)
execution_id = result["QueryExecutionId"]
log.info("wait for query {} completion".format(execution_id))
wait_for_query_execution_completion(client, execution_id)
log.info("operation successful")
return execution_id
def wait_for_query_execution_completion(client, query_execution_id):
query_ended = False
while not query_ended:
query_execution = client.get_query_execution(QueryExecutionId=query_execution_id)
state = query_execution["QueryExecution"]["Status"]["State"]
if state == "SUCCEEDED":
query_ended = True
elif state in ["FAILED", "CANCELLED"]:
raise BaseException(
"query failed or canceled: {}".format(query_execution["QueryExecution"]["Status"]["StateChangeReason"])
)
else:
time.sleep(1)
def valid(key):
split = key.split("_")
if len(split) < 1:
return False
try:
semver.parse(split[0])
except ValueError:
return False
return True
def get_results(client, execution_id):
results_paginator = client.get_paginator("get_query_results")
results_iter = results_paginator.paginate(QueryExecutionId=execution_id, PaginationConfig={"PageSize": 1000})
res = {}
for results_page in results_iter:
for row in results_page["ResultSet"]["Rows"][1:]:
# Loop through the JSON objects
key = row["Data"][0]["VarCharValue"]
if valid(key):
res[key] = row["Data"][1]["VarCharValue"]
return res
def convert_data(data):
result = []
for key, value in data.items():
# 0.18.0_macOS_64bit.tar.gz
split_key = key.split("_")
if len(split_key) != 3:
continue
(version, os_version, arch) = split_key
arch_split = arch.split(".")
if len(arch_split) < 1:
continue
arch = arch_split[0]
if len(arch) > 10:
# This can't be an architecture really.
# It's an ugly solution but works for now so deal with it.
continue
repo = os.environ["GITHUB_REPOSITORY"].split("/")[1]
result.append(
{
"type": "gauge",
"name": "arduino.downloads.total",
"value": value,
"host": os.environ["GITHUB_REPOSITORY"],
"tags": [
f"version:{version}",
f"os:{os_version}",
f"arch:{arch}",
"cdn:downloads.arduino.cc",
f"project:{repo}",
],
}
)
return result
if __name__ == "__main__":
DEST_S3_OUTPUT = os.environ["AWS_ATHENA_OUTPUT_LOCATION"]
AWS_ATHENA_SOURCE_TABLE = os.environ["AWS_ATHENA_SOURCE_TABLE"]
session = boto3.session.Session(region_name="us-east-1")
athena_client = session.client("athena")
# Load all partitions before querying downloads
execute(athena_client, f"MSCK REPAIR TABLE {AWS_ATHENA_SOURCE_TABLE};", DEST_S3_OUTPUT)
query = f"""SELECT replace(json_extract_scalar(url_decode(url_decode(querystring)),
'$.data.url'), 'https://downloads.arduino.cc/arduino-ide/arduino-ide_', '')
AS flavor, count(json_extract(url_decode(url_decode(querystring)),'$')) AS gauge
FROM {AWS_ATHENA_SOURCE_TABLE}
WHERE json_extract_scalar(url_decode(url_decode(querystring)),'$.data.url')
LIKE 'https://downloads.arduino.cc/arduino-ide/arduino-ide_%'
AND json_extract_scalar(url_decode(url_decode(querystring)),'$.data.url')
NOT LIKE '%latest%' -- exclude latest redirect
group by 1 ;"""
exec_id = execute(athena_client, query, DEST_S3_OUTPUT)
results = get_results(athena_client, exec_id)
result_json = convert_data(results)
print(f"::set-output name=result::{result_json}")

View File

@@ -1,57 +0,0 @@
name: arduino-stats
on:
schedule:
# run every day at 07:00 AM, 03:00 PM and 11:00 PM
- cron: "0 7,15,23 * * *"
workflow_dispatch:
repository_dispatch:
jobs:
push-stats:
# This workflow is only of value to the arduino/arduino-ide repository and
# would always fail in forks
if: github.repository == 'arduino/arduino-ide'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Fetch downloads count form Arduino CDN using AWS Athena
id: fetch
env:
AWS_ACCESS_KEY_ID: ${{ secrets.STATS_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.STATS_AWS_SECRET_ACCESS_KEY }}
AWS_ATHENA_SOURCE_TABLE: ${{ secrets.STATS_AWS_ATHENA_SOURCE_TABLE }}
AWS_ATHENA_OUTPUT_LOCATION: ${{ secrets.STATS_AWS_ATHENA_OUTPUT_LOCATION }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
pip install boto3 semver
python .github/tools/fetch_athena_stats.py
- name: Send metrics
uses: masci/datadog@v1
with:
api-key: ${{ secrets.DD_API_KEY }}
# Metrics input expects YAML but JSON will work just right.
metrics: ${{steps.fetch.outputs.result}}
- name: Report failure
if: failure()
uses: masci/datadog@v1
with:
api-key: ${{ secrets.DD_API_KEY }}
events: |
- title: "Arduino IDE stats failing"
text: "Stats collection failed"
alert_type: "error"
host: ${{ github.repository }}
tags:
- "project:arduino-ide"
- "cdn:downloads.arduino.cc"
- "workflow:${{ github.workflow }}"

View File

@@ -55,16 +55,16 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install Node.js 14.x
uses: actions/setup-node@v1
- name: Install Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: '14.x'
node-version: '16.x'
registry-url: 'https://registry.npmjs.org'
- name: Install Python 3.x
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: '3.x'
@@ -109,7 +109,7 @@ jobs:
yarn --cwd ./electron/packager/ package
- name: Upload [GitHub Actions]
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: electron/build/dist/build-artifacts/
@@ -140,13 +140,13 @@ jobs:
steps:
- name: Download job transfer artifact
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: ${{ env.JOB_TRANSFER_ARTIFACT }}
- name: Upload tester build artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.artifact.name }}
path: ${{ env.JOB_TRANSFER_ARTIFACT }}/${{ matrix.artifact.path }}
@@ -158,7 +158,7 @@ jobs:
BODY: ${{ steps.changelog.outputs.BODY }}
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: 0 # To fetch all history for all branches and tags.
@@ -183,12 +183,12 @@ jobs:
OUTPUT_SAFE_BODY="${BODY//'%'/'%25'}"
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\n'/'%0A'}"
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\r'/'%0D'}"
echo "::set-output name=BODY::$OUTPUT_SAFE_BODY"
echo "BODY=$OUTPUT_SAFE_BODY" >> $GITHUB_OUTPUT
echo "$BODY" > CHANGELOG.txt
- name: Upload Changelog [GitHub Actions]
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: CHANGELOG.txt
@@ -199,7 +199,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Download [GitHub Actions]
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: ${{ env.JOB_TRANSFER_ARTIFACT }}
@@ -220,7 +220,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Download [GitHub Actions]
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: ${{ env.JOB_TRANSFER_ARTIFACT }}
@@ -228,10 +228,10 @@ jobs:
- name: Get Tag
id: tag_name
run: |
echo ::set-output name=TAG_NAME::${GITHUB_REF#refs/tags/}
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Publish Release [GitHub]
uses: svenstaro/upload-release-action@2.2.0
uses: svenstaro/upload-release-action@2.3.0
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
release_name: ${{ steps.tag_name.outputs.TAG_NAME }}
@@ -263,6 +263,6 @@ jobs:
steps:
- name: Remove unneeded job transfer artifact
uses: geekyeggo/delete-artifact@v1
uses: geekyeggo/delete-artifact@v2
with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }}

View File

@@ -108,7 +108,7 @@ jobs:
echo "Certificate expiration date: $EXPIRATION_DATE"
echo "Days remaining before expiration: $DAYS_BEFORE_EXPIRATION"
echo "::set-output name=days::$DAYS_BEFORE_EXPIRATION"
echo "days=$DAYS_BEFORE_EXPIRATION" >> $GITHUB_OUTPUT
- name: Check if expiration notification period has been reached
id: check-expiration

View File

@@ -27,12 +27,12 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install Node.js 14.x
uses: actions/setup-node@v2
- name: Install Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: '14.x'
node-version: '16.x'
registry-url: 'https://registry.npmjs.org'
- name: Install Go

View File

@@ -8,7 +8,7 @@ on:
env:
CHANGELOG_ARTIFACTS: changelog
# See: https://github.com/actions/setup-node/#readme
NODE_VERSION: 14.x
NODE_VERSION: 16.x
jobs:
create-changelog:
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
@@ -27,7 +27,7 @@ jobs:
- name: Get Tag
id: tag_name
run: |
echo ::set-output name=TAG_NAME::${GITHUB_REF#refs/tags/}
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Create full changelog
id: full-changelog

View File

@@ -1,96 +0,0 @@
name: github-stats
on:
schedule:
# run every 30 minutes
- cron: "*/30 * * * *"
workflow_dispatch:
repository_dispatch:
jobs:
push-stats:
# This workflow is only of value to the arduino/arduino-ide repository and
# would always fail in forks
if: github.repository == 'arduino/arduino-ide'
runs-on: ubuntu-latest
steps:
- name: Fetch downloads count
id: fetch
uses: actions/github-script@v4
with:
github-token: ${{github.token}}
script: |
let metrics = []
// Get a list of releases
const opts = github.repos.listReleases.endpoint.merge({
...context.repo
})
const releases = await github.paginate(opts)
// Get download stats for every release
for (const rel of releases) {
// Names for assets are like `arduino-ide_2.0.0-beta.12_Linux_64bit.zip`,
// we'll use this later to split the asset file name more easily
const baseName = `arduino-ide_${rel.name}_`
// Get a list of assets for this release
const opts = github.repos.listReleaseAssets.endpoint.merge({
...context.repo,
release_id: rel.id
})
const assets = await github.paginate(opts)
for (const asset of assets) {
// Ignore files that are not arduino-ide packages
if (!asset.name.startsWith(baseName)) {
continue
}
// Strip the base and remove file extension to get `Linux_32bit`
systemArch = asset.name.replace(baseName, "").split(".")[0].split("_")
// Add a metric object to the list of gathered metrics
metrics.push({
"type": "gauge",
"name": "arduino.downloads.total",
"value": asset.download_count,
"host": "${{ github.repository }}",
"tags": [
`version:${rel.name}`,
`os:${systemArch[0]}`,
`arch:${systemArch[1]}`,
"cdn:github.com",
"project:arduino-ide"
]
})
}
}
// The action will put whatever we return from this function in
// `outputs.result`, JSON encoded. So we just return the array
// of objects and GitHub will do the rest.
return metrics
- name: Send metrics
uses: masci/datadog@v1
with:
api-key: ${{ secrets.DD_API_KEY }}
# Metrics input expects YAML but JSON will work just right.
metrics: ${{steps.fetch.outputs.result}}
- name: Report failure
if: failure()
uses: masci/datadog@v1
with:
api-key: ${{ secrets.DD_API_KEY }}
events: |
- title: "Arduino IDE stats failing"
text: "Stats collection failed"
alert_type: "error"
host: ${{ github.repository }}
tags:
- "project:arduino-ide"
- "cdn:github.com"
- "workflow:${{ github.workflow }}"

View File

@@ -14,12 +14,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install Node.js 14.x
uses: actions/setup-node@v2
- name: Install Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: '14.x'
node-version: '16.x'
registry-url: 'https://registry.npmjs.org'
- name: Install Go

View File

@@ -14,12 +14,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install Node.js 14.x
uses: actions/setup-node@v2
- name: Install Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: '14.x'
node-version: '16.x'
registry-url: 'https://registry.npmjs.org'
- name: Install Go
@@ -45,7 +45,7 @@ jobs:
TRANSIFEX_API_KEY: ${{ secrets.TRANSIFEX_API_KEY }}
- name: Create Pull Request
uses: peter-evans/create-pull-request@v3
uses: peter-evans/create-pull-request@v4
with:
commit-message: Updated translation files
title: Update translation files

View File

@@ -27,11 +27,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Download JSON schema for labels configuration file
id: download-schema
uses: carlosperate/download-file-action@v1
uses: carlosperate/download-file-action@v2
with:
file-url: https://raw.githubusercontent.com/arduino/tooling-project-assets/main/workflow-templates/assets/sync-labels/arduino-tooling-gh-label-configuration-schema.json
location: ${{ runner.temp }}/label-configuration-schema
@@ -66,12 +66,12 @@ jobs:
steps:
- name: Download
uses: carlosperate/download-file-action@v1
uses: carlosperate/download-file-action@v2
with:
file-url: https://raw.githubusercontent.com/arduino/tooling-project-assets/main/workflow-templates/assets/sync-labels/${{ matrix.filename }}
- name: Pass configuration files to next job via workflow artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
path: |
*.yaml
@@ -103,19 +103,19 @@ jobs:
run: |
# Use of this flag in the github-label-sync command will cause it to only check the validity of the
# configuration.
echo "::set-output name=flag::--dry-run"
echo "flag=--dry-run" >> $GITHUB_OUTPUT
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Download configuration files artifact
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
name: ${{ env.CONFIGURATIONS_ARTIFACT }}
path: ${{ env.CONFIGURATIONS_FOLDER }}
- name: Remove unneeded artifact
uses: geekyeggo/delete-artifact@v1
uses: geekyeggo/delete-artifact@v2
with:
name: ${{ env.CONFIGURATIONS_ARTIFACT }}

View File

@@ -9,7 +9,7 @@ on:
env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: "1.17"
NODE_VERSION: 14.x
NODE_VERSION: 16.x
jobs:
pull-from-jsonbin:

4
.gitignore vendored
View File

@@ -7,7 +7,7 @@ build/
Examples/
!electron/build/
src-gen/
!webpack.config.js
webpack.config.js
gen-webpack.config.js
.DS_Store
# switching from `electron` to `browser` in dev mode.
@@ -15,8 +15,6 @@ gen-webpack.config.js
yarn*.log
# For the VS Code extensions used by Theia.
plugins
# the config files for the CLI
arduino-ide-extension/data/cli/config
# the tokens folder for the themes
scripts/themes/tokens
# environment variables

37
.vscode/launch.json vendored
View File

@@ -21,7 +21,8 @@
"--plugins=local-dir:../plugins",
"--hosted-plugin-inspect=9339",
"--content-trace",
"--open-devtools"
"--open-devtools",
"--no-ping-timeout",
],
"env": {
"NODE_ENV": "development"
@@ -56,7 +57,8 @@
"--remote-debugging-port=9222",
"--no-app-auto-install",
"--plugins=local-dir:../plugins",
"--hosted-plugin-inspect=9339"
"--hosted-plugin-inspect=9339",
"--no-ping-timeout",
],
"env": {
"NODE_ENV": "development"
@@ -80,37 +82,6 @@
"port": 9222,
"webRoot": "${workspaceFolder}/electron-app"
},
{
"type": "node",
"request": "launch",
"name": "App (Browser)",
"program": "${workspaceRoot}/browser-app/src-gen/backend/main.js",
"args": [
"--hostname=0.0.0.0",
"--port=3000",
"--no-cluster",
"--no-app-auto-install",
"--plugins=local-dir:plugins"
],
"windows": {
"env": {
"NODE_ENV": "development",
"NODE_PRESERVE_SYMLINKS": "1"
}
},
"env": {
"NODE_ENV": "development"
},
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/browser-app/src-gen/backend/*.js",
"${workspaceRoot}/browser-app/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js"
],
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart",
"outputCapture": "std"
},
{
"type": "node",
"request": "launch",

30
.vscode/tasks.json vendored
View File

@@ -12,17 +12,6 @@
"clear": false
}
},
{
"label": "Arduino IDE - Start Browser App",
"type": "shell",
"command": "yarn --cwd ./browser-app start",
"group": "build",
"presentation": {
"reveal": "always",
"panel": "new",
"clear": true
}
},
{
"label": "Arduino IDE - Watch IDE Extension",
"type": "shell",
@@ -34,17 +23,6 @@
"clear": false
}
},
{
"label": "Arduino IDE - Watch Browser App",
"type": "shell",
"command": "yarn --cwd ./browser-app watch",
"group": "build",
"presentation": {
"reveal": "always",
"panel": "new",
"clear": false
}
},
{
"label": "Arduino IDE - Watch Electron App",
"type": "shell",
@@ -56,14 +34,6 @@
"clear": false
}
},
{
"label": "Arduino IDE - Watch All [Browser]",
"type": "shell",
"dependsOn": [
"Arduino IDE - Watch IDE Extension",
"Arduino IDE - Watch Browser App"
]
},
{
"label": "Arduino IDE - Watch All [Electron]",
"type": "shell",

View File

@@ -1,152 +1,3 @@
# Development
This page includes technical documentation for developers who want to build the IDE locally and contribute to the project.
## Architecture overview
The IDE consists of three major parts:
- the _Electron main_ process,
- the _backend_, and
- the _frontend_.
The _Electron main_ process is responsible for:
- creating the application,
- managing the application lifecycle via listeners, and
- creating and managing the web pages for the app.
In Electron, the process that runs the main entry JavaScript file is called the main process. The _Electron main_ process can display a GUI by creating web pages. An Electron app always has exactly one main process.
By default, whenever the _Electron main_ process creates a web page, it will instantiate a new `BrowserWindow` instance. Since Electron uses Chromium for displaying web pages, Chromium's multi-process architecture is also used. Each web page in Electron runs in its own process, which is called the renderer process. Each `BrowserWindow` instance runs the web page in its own renderer process. When a `BrowserWindow` instance is destroyed, the corresponding renderer process is also terminated. The main process manages all web pages and their corresponding renderer processes. Each renderer process is isolated and only cares about the web page running in it.<sup>[[1]]</sup>
In normal browsers, web pages usually run in a sandboxed environment, and accessing native resources are disallowed. However, Electron has the power to use Node.js APIs in the web pages allowing lower-level OS interactions. Due to security reasons, accessing native resources is an undesired behavior in the IDE. So by convention, we do not use Node.js APIs. (Note: the Node.js integration is [not yet disabled](https://github.com/eclipse-theia/theia/issues/2018) although it is not used). In the IDE, only the _backend_ allows OS interaction.
The _backend_ process is responsible for:
- providing access to the filesystem,
- communicating with the [Arduino CLI](https://github.com/arduino/arduino-cli) via gRPC,
- running your terminal,
- exposing additional RESTful APIs,
- performing the Git commands in the local repositories,
- hosting and running any VS Code extensions, or
- executing VS Code tasks<sup>[[2]]</sup>.
The _Electron main_ process spawns the _backend_ process. There is always exactly one _backend_ process. However, due to performance considerations, the _backend_ spawns several sub-processes for the filesystem watching, Git repository discovery, etc. The communication between the _backend_ process and its sub-processes is established via IPC. Besides spawning sub-processes, the _backend_ will start an HTTP server on a random available port, and serves the web application as static content. When the sub-processes are up and running, and the HTTP server is also listening, the _backend_ process sends the HTTP server port to the _Electron main_ process via IPC. The _Electron main_ process will load the _backend_'s endpoint in the `BrowserWindow`.
The _frontend_ is running as an Electron renderer process and can invoke services implemented on the _backend_. The communication between the _backend_ and the _frontend_ is done via JSON-RPC over a websocket connection. This means, the services running in the _frontend_ are all proxies, and will ask the corresponding service implementation on the _backend_.
[1]: https://www.electronjs.org/docs/tutorial/application-architecture#differences-between-main-process-and-renderer-process
[2]: https://code.visualstudio.com/Docs/editor/tasks
## Build from source
If youre familiar with TypeScript, the [Theia IDE](https://theia-ide.org/), and if you want to contribute to the
project, you should be able to build the Arduino IDE locally.
Please refer to the [Theia IDE prerequisites](https://github.com/eclipse-theia/theia/blob/master/doc/Developing.md#prerequisites) documentation for the setup instructions.
> **Note**: Node.js 14 must be used instead of the version 12 recommended at the link above.
Once you have all the tools installed, you can build the editor following these steps
1. Install the dependencies and build
```sh
yarn
```
2. Rebuild the dependencies
```sh
yarn rebuild:browser
```
3. Rebuild the electron dependencies
```sh
yarn rebuild:electron
```
4. Start the application
```sh
yarn start
```
### Notes for Windows contributors
Windows requires the Microsoft Visual C++ (MSVC) compiler toolset to be installed on your development machine.
In case it's not already present, it can be downloaded from the "**Tools for Visual Studio 20XX**" section of the Visual Studio [downloads page](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022) via the "**Build Tools for Visual Studio 20XX**" (e.g., "**Build Tools for Visual Studio 2022**") download link.
Select "**Desktop development with C++**" from the "**Workloads**" tab during the installation procedure.
### CI
This project is built on [GitHub Actions](https://github.com/arduino/arduino-ide/actions).
- _Snapshot_ builds run when changes are pushed to the `main` branch, or when a PR is created against the `main` branch. For the sake of the review and verification process, the build artifacts for each operating system can be downloaded from the GitHub Actions page.
- _Nightly_ builds run every day at 03:00 GMT from the `main` branch.
- _Release_ builds run when a new tag is pushed to the remote. The tag must follow the [semver](https://semver.org/). For instance, `1.2.3` is a correct tag, but `v2.3.4` won't work. Steps to trigger a new release build:
- Create a local tag:
```sh
git tag -a 1.2.3 -m "Creating a new tag for the `1.2.3` release."
```
- Push it to the remote:
```sh
git push origin 1.2.3
```
## Notes for macOS contributors
Beginning in macOS 10.14.5, the software [must be notarized to run](https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution). The signing and notarization processes for the Arduino IDE are managed by our Continuous Integration (CI) workflows, implemented with GitHub Actions. On every push and pull request, the Arduino IDE is built and saved to a workflow artifact. These artifacts can be used by contributors and beta testers who don't want to set up a build system locally.
For security reasons, signing and notarization are disabled for workflow runs for pull requests from forks of this repository. This means that macOS will block you from running those artifacts.
Due to this limitation, Mac users have two options for testing contributions from forks:
### The Safe approach (recommended)
Follow [the instructions above](#build-from-source) to create the build environment locally, then build the code you want to test.
### The Risky approach
*Please note that this approach is risky as you are lowering the security on your system, therefore we strongly discourage you from following it.*
1. Use [this guide](https://help.apple.com/xcode/mac/10.2/index.html?localePath=en.lproj#/dev9b7736b0e), in order to disable Gatekeeper (at your own risk!).
1. Download the unsigned artifact provided by the CI workflow run related to the Pull Request at each push.
1. Re-enable Gatekeeper after tests are done, following the guide linked above.
### Creating a release
You will not need to create a new release yourself as the Arduino team takes care of this on a regular basis, but we are documenting the process here. Let's assume the current version is `0.1.3` and you want to release `0.2.0`.
- Make sure the `main` state represents what you want to release and you're on `main`.
- Prepare a release-candidate build on a branch:
```bash
git branch 0.2.0-rc \
&& git checkout 0.2.0-rc
```
- Bump up the version number. It must be a valid [semver](https://semver.org/) and must be greater than the current one:
```bash
yarn update:version 0.2.0
```
- This should generate multiple outgoing changes with the version update.
- Commit your changes and push to the remote:
```bash
git add . \
&& git commit -s -m "Updated versions to 0.2.0" \
&& git push
```
- Create the GH PR the workflow starts automatically.
- Once you're happy with the RC, merge the changes to the `main`.
- Create a tag and push it:
```bash
git tag -a 0.2.0 -m "0.2.0" \
&& git push origin 0.2.0
```
- The release build starts automatically and uploads the artifacts with the changelog to the [release page](https://github.com/arduino/arduino-ide/releases).
- If you do not want to release the `EXE` and `MSI` installers, wipe them manually.
- If you do not like the generated changelog, modify it and update the GH release.
## FAQ
* *Can I manually change the version of the [`arduino-cli`](https://github.com/arduino/arduino-cli/) used by the IDE?*
Yes. It is possible but not recommended. The CLI exposes a set of functionality via [gRPC](https://github.com/arduino/arduino-cli/tree/master/rpc) and the IDE uses this API to communicate with the CLI. Before we build a new version of IDE, we pin a specific version of CLI and use the corresponding `proto` files to generate TypeScript modules for gRPC. This means, a particular version of IDE is compliant only with the pinned version of CLI. Mismatching IDE and CLI versions might not be able to communicate with each other. This could cause unpredictable IDE behavior.
* *I have understood that not all versions of the CLI are compatible with my version of IDE but how can I manually update the `arduino-cli` inside the IDE?*
[Get](https://arduino.github.io/arduino-cli/installation) the desired version of `arduino-cli` for your platform and manually replace the one inside the IDE. The CLI can be found inside the IDE at:
- Windows: `C:\path\to\Arduino IDE\resources\app\node_modules\arduino-ide-extension\build\arduino-cli.exe`,
- macOS: `/path/to/Arduino IDE.app/Contents/Resources/app/node_modules/arduino-ide-extension/build/arduino-cli`, and
- Linux: `/path/to/Arduino IDE/resources/app/node_modules/arduino-ide-extension/build/arduino-cli`.
# Development Guide
This documentation has been moved [**here**](docs/development.md#development-guide).

View File

@@ -1,45 +1,18 @@
<img src="https://content.arduino.cc/website/Arduino_logo_teal.svg" height="100" align="right" />
# Arduino IDE 2.x (beta)
# Arduino IDE 2.x
[![Arduino IDE](https://github.com/arduino/arduino-ide/workflows/Arduino%20IDE/badge.svg)](https://github.com/arduino/arduino-ide/actions?query=workflow%3A%22Arduino+IDE%22)
This repository contains the source code of the Arduino IDE 2.x, which is currently in beta stage. If you're looking for the stable IDE, go to the repository of the 1.x version at https://github.com/arduino/Arduino.
This repository contains the source code of the Arduino IDE 2.x. If you're looking for the old IDE, go to the repository of the 1.x version at https://github.com/arduino/Arduino.
The Arduino IDE 2.x is a major rewrite, sharing no code with the IDE 1.x. It is based on the [Theia IDE](https://theia-ide.org/) framework and built with [Electron](https://www.electronjs.org/). The backend operations such as compilation and uploading are offloaded to an [arduino-cli](https://github.com/arduino/arduino-cli) instance running in daemon mode. This new IDE was developed with the goal of preserving the same interface and user experience of the previous major version in order to provide a frictionless upgrade.
> ⚠️ This is **beta** software. Help us test it!
![](static/screenshot.png)
## Download
You can download the latest version from the [software download page on the Arduino website](https://www.arduino.cc/en/software#experimental-software).
### Nightly builds
These builds are generated every day at 03:00 GMT from the `main` branch and
should be considered unstable:
| Platform | 32 bit | 64 bit |
| --------- | ------------------------ | ------------------------------------------------------------------------------------------------------ |
| Linux | | [Nightly Linux AppImage 64 bit]<br />[Nightly Linux ZIP file 64 bit] |
| Linux ARM | [🚧 Work in progress...] | [🚧 Work in progress...] |
| Windows | | [Nightly Windows 64 bit installer]<br />[Nightly Windows 64 bit MSI]<br />[Nightly Windows 64 bit ZIP] |
| macOS | | [Nightly macOS 64 bit] |
[🚧 work in progress...]: https://github.com/arduino/arduino-ide/issues/107
[nightly linux appimage 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Linux_64bit.AppImage
[nightly linux zip file 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Linux_64bit.zip
[nightly windows 64 bit installer]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.exe
[nightly windows 64 bit msi]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.msi
[nightly windows 64 bit zip]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.zip
[nightly macos 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_macOS_64bit.dmg
> These links return an HTTP `302: Found` response, 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`)
You can download the latest release version and nightly builds from the [software download page on the Arduino website](https://www.arduino.cc/en/software).
## Support
@@ -47,10 +20,9 @@ If you need assistance, see the [Help Center](https://support.arduino.cc/hc/en-u
## Bugs & Issues
If you want to report an issue, you can submit it to the [issue tracker](https://github.com/arduino/arduino-ide/issues) of this repository. A few rules apply:
If you want to report an issue, you can submit it to the [issue tracker](https://github.com/arduino/arduino-ide/issues) of this repository.
- Before posting, please check if the same problem has been already reported by someone else to avoid duplicates.
- Remember to include as much detail as you can about your hardware set-up, code and steps for reproducing the issue. Make sure you're using an original Arduino board.
See [**the issue report guide**](docs/contributor-guide/issues.md#issue-report-guide) for instructions.
### Security
@@ -62,16 +34,15 @@ e-mail contact: security@arduino.cc
## Contributions and development
Contributions are very welcome! You can browse the list of open issues to see what's needed and then you can submit your code using a Pull Request. Please provide detailed descriptions. We also appreciate any help in testing issues and patches contributed by other users.
Contributions are very welcome! There are several ways to participate in this project, including:
This repository contains the main code, but two more repositories are included during the build process:
- Fixing bugs
- Beta testing
- Translation
- [vscode-arduino-tools](https://github.com/arduino/vscode-arduino-tools): provides support for the language server and the debugger
- [arduino-language-server](https://github.com/arduino/arduino-language-server): provides the language server that parses Arduino code
See [**the contributor guide**](docs/CONTRIBUTING.md#contributor-guide) for more information.
See the [BUILDING.md](BUILDING.md) for a technical overview of the application and instructions for building the code.
You can help with the translation of the Arduino IDE to your language here: [Arduino IDE on Transifex](https://www.transifex.com/arduino-1/ide2/dashboard/).
See the [**development guide**](docs/development.md) for a technical overview of the application and instructions for building the code.
## Donations

View File

@@ -1,6 +1,6 @@
{
"name": "arduino-ide-extension",
"version": "2.0.0-rc9.3",
"version": "2.0.3",
"description": "An extension for Theia building the Arduino IDE",
"license": "AGPL-3.0-or-later",
"scripts": {
@@ -21,27 +21,31 @@
},
"dependencies": {
"@grpc/grpc-js": "^1.6.7",
"@theia/application-package": "1.25.0",
"@theia/core": "1.25.0",
"@theia/editor": "1.25.0",
"@theia/electron": "1.25.0",
"@theia/filesystem": "1.25.0",
"@theia/keymaps": "1.25.0",
"@theia/markers": "1.25.0",
"@theia/monaco": "1.25.0",
"@theia/navigator": "1.25.0",
"@theia/outline-view": "1.25.0",
"@theia/output": "1.25.0",
"@theia/preferences": "1.25.0",
"@theia/search-in-workspace": "1.25.0",
"@theia/terminal": "1.25.0",
"@theia/workspace": "1.25.0",
"@theia/application-package": "1.31.1",
"@theia/core": "1.31.1",
"@theia/debug": "1.31.1",
"@theia/editor": "1.31.1",
"@theia/electron": "1.31.1",
"@theia/filesystem": "1.31.1",
"@theia/keymaps": "1.31.1",
"@theia/markers": "1.31.1",
"@theia/messages": "1.31.1",
"@theia/monaco": "1.31.1",
"@theia/monaco-editor-core": "1.67.2",
"@theia/navigator": "1.31.1",
"@theia/outline-view": "1.31.1",
"@theia/output": "1.31.1",
"@theia/plugin-ext": "1.31.1",
"@theia/preferences": "1.31.1",
"@theia/scm": "1.31.1",
"@theia/search-in-workspace": "1.31.1",
"@theia/terminal": "1.31.1",
"@theia/typehierarchy": "1.31.1",
"@theia/workspace": "1.31.1",
"@tippyjs/react": "^4.2.5",
"@types/atob": "^2.1.2",
"@types/auth0-js": "^9.14.0",
"@types/btoa": "^1.2.3",
"@types/dateformat": "^3.0.1",
"@types/deep-equal": "^1.0.1",
"@types/deepmerge": "^2.2.0",
"@types/glob": "^7.2.0",
"@types/google-protobuf": "^3.7.2",
@@ -50,49 +54,50 @@
"@types/lodash.debounce": "^4.0.6",
"@types/ncp": "^2.0.4",
"@types/node-fetch": "^2.5.7",
"@types/p-queue": "^2.3.1",
"@types/ps-tree": "^1.1.0",
"@types/react-select": "^3.0.0",
"@types/react-tabs": "^2.3.2",
"@types/react-virtualized": "^9.21.21",
"@types/temp": "^0.8.34",
"@types/which": "^1.3.1",
"ajv": "^6.5.3",
"arduino-serial-plotter-webapp": "0.1.0",
"@vscode/debugprotocol": "^1.51.0",
"arduino-serial-plotter-webapp": "0.2.0",
"async-mutex": "^0.3.0",
"atob": "^2.1.2",
"auth0-js": "^9.14.0",
"btoa": "^1.2.1",
"classnames": "^2.3.1",
"dateformat": "^3.0.3",
"deep-equal": "^2.0.5",
"deepmerge": "2.0.1",
"electron-updater": "^4.6.5",
"fast-safe-stringify": "^2.1.1",
"glob": "^7.1.6",
"google-protobuf": "^3.20.1",
"hash.js": "^1.1.7",
"is-valid-path": "^0.1.1",
"js-yaml": "^3.13.1",
"just-diff": "^5.1.1",
"jwt-decode": "^3.1.2",
"keytar": "7.2.0",
"lodash.debounce": "^4.0.8",
"minimatch": "^3.1.2",
"ncp": "^2.0.0",
"node-fetch": "^2.6.1",
"open": "^8.0.6",
"p-queue": "^5.0.0",
"p-debounce": "^2.1.0",
"p-queue": "^2.4.2",
"ps-tree": "^1.2.0",
"query-string": "^7.0.1",
"react-disable": "^0.1.0",
"react-disable": "^0.1.1",
"react-markdown": "^8.0.0",
"react-select": "^3.0.4",
"react-perfect-scrollbar": "^1.5.8",
"react-select": "^5.6.0",
"react-tabs": "^3.1.2",
"react-virtualized": "^9.22.3",
"react-window": "^1.8.6",
"semver": "^7.3.2",
"string-natural-compare": "^2.0.3",
"temp": "^0.9.1",
"temp-dir": "^2.0.0",
"tree-kill": "^1.2.1",
"upath": "^1.1.2",
"url": "^0.11.0",
"which": "^1.3.1"
},
"devDependencies": {
@@ -101,11 +106,10 @@
"@types/chai-string": "^1.4.2",
"@types/mocha": "^5.2.7",
"@types/react-window": "^1.8.5",
"@types/sinon": "^10.0.6",
"@types/sinon-chai": "^3.2.6",
"chai": "^4.2.0",
"chai-string": "^1.5.0",
"decompress": "^4.2.0",
"decompress-tarbz2": "^4.1.1",
"decompress-targz": "^4.1.1",
"decompress-unzip": "^4.0.1",
"download": "^7.1.0",
@@ -115,9 +119,6 @@
"moment": "^2.24.0",
"protoc": "^1.0.4",
"shelljs": "^0.8.3",
"sinon": "^12.0.1",
"sinon-chai": "^3.7.0",
"typemoq": "^2.1.0",
"uuid": "^3.2.1",
"yargs": "^11.1.0"
},
@@ -147,11 +148,9 @@
"frontend": "lib/browser/arduino-ide-frontend-module"
},
{
"frontend": "lib/browser/theia/core/browser-menu-module",
"frontendElectron": "lib/electron-browser/theia/core/electron-menu-module"
},
{
"frontend": "lib/browser/theia/core/browser-window-module",
"frontendElectron": "lib/electron-browser/theia/core/electron-window-module"
},
{
@@ -160,16 +159,16 @@
],
"arduino": {
"cli": {
"version": "0.27.0-rc.1"
"version": "0.29.0"
},
"fwuploader": {
"version": "2.2.0"
"version": "2.2.2"
},
"clangd": {
"version": "14.0.0"
},
"languageServer": {
"version": "0.7.1"
"version": "0.7.2"
}
}
}

View File

@@ -42,6 +42,9 @@
const suffix = (() => {
switch (platform) {
case 'darwin':
if (arch === 'arm64') {
return 'macOS_ARM64.tar.gz';
}
return 'macOS_64bit.tar.gz';
case 'win32':
return 'Windows_64bit.zip';

View File

@@ -1,7 +1,7 @@
// @ts-check
// The version to use.
const version = '1.9.1';
const version = '1.10.0';
(async () => {
const os = require('os');

View File

@@ -76,6 +76,12 @@
lsSuffix = 'macOS_64bit.tar.gz';
clangdSuffix = 'macOS_64bit';
break;
case 'darwin-arm64':
clangdExecutablePath = path.join(build, 'clangd');
clangFormatExecutablePath = path.join(build, 'clang-format');
lsSuffix = 'macOS_ARM64.tar.gz';
clangdSuffix = 'macOS_ARM64';
break;
case 'linux-x64':
clangdExecutablePath = path.join(build, 'clangd');
clangFormatExecutablePath = path.join(build, 'clang-format');

View File

@@ -31,7 +31,7 @@ import { EditorCommands, EditorMainMenu } from '@theia/editor/lib/browser';
import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu';
import { FileNavigatorCommands } from '@theia/navigator/lib/browser/navigator-contribution';
import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
import { ArduinoPreferences } from './arduino-preferences';
import { ElectronWindowPreferences } from '@theia/core/lib/electron-browser/window/electron-window-preferences';
import { BoardsServiceProvider } from './boards/boards-service-provider';
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
import { ArduinoMenus } from './menu/arduino-menus';
@@ -58,8 +58,8 @@ export class ArduinoFrontendContribution
@inject(CommandRegistry)
private readonly commandRegistry: CommandRegistry;
@inject(ArduinoPreferences)
private readonly arduinoPreferences: ArduinoPreferences;
@inject(ElectronWindowPreferences)
private readonly electronWindowPreferences: ElectronWindowPreferences;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
@@ -78,10 +78,10 @@ export class ArduinoFrontendContribution
}
onStart(app: FrontendApplication): void {
this.arduinoPreferences.onPreferenceChanged((event) => {
this.electronWindowPreferences.onPreferenceChanged((event) => {
if (event.newValue !== event.oldValue) {
switch (event.preferenceName) {
case 'arduino.window.zoomLevel':
case 'window.zoomLevel':
if (typeof event.newValue === 'number') {
const webContents = remote.getCurrentWebContents();
webContents.setZoomLevel(event.newValue || 0);
@@ -91,11 +91,10 @@ export class ArduinoFrontendContribution
}
});
this.appStateService.reachedState('ready').then(() =>
this.arduinoPreferences.ready.then(() => {
this.electronWindowPreferences.ready.then(() => {
const webContents = remote.getCurrentWebContents();
const zoomLevel = this.arduinoPreferences.get(
'arduino.window.zoomLevel'
);
const zoomLevel =
this.electronWindowPreferences.get('window.zoomLevel');
webContents.setZoomLevel(zoomLevel);
})
);

View File

@@ -1,12 +1,9 @@
import '../../src/browser/style/index.css';
import { ContainerModule } from '@theia/core/shared/inversify';
import { Container, ContainerModule } from '@theia/core/shared/inversify';
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
import { CommandContribution } from '@theia/core/lib/common/command';
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import {
TabBarToolbarContribution,
TabBarToolbarFactory,
} 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 {
FrontendApplicationContribution,
@@ -53,8 +50,6 @@ import {
DockPanelRenderer as TheiaDockPanelRenderer,
TabBarRendererFactory,
ContextMenuRenderer,
createTreeContainer,
TreeWidget,
} from '@theia/core/lib/browser';
import { MenuContribution } from '@theia/core/lib/common/menu';
import {
@@ -86,10 +81,7 @@ import { BoardsAutoInstaller } from './boards/boards-auto-installer';
import { ShellLayoutRestorer } from './theia/core/shell-layout-restorer';
import { ListItemRenderer } from './widgets/component-list/list-item-renderer';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import {
MonacoThemeJson,
MonacoThemingService,
} from '@theia/monaco/lib/browser/monaco-theming-service';
import {
ArduinoDaemonPath,
ArduinoDaemon,
@@ -105,7 +97,8 @@ import {
} from '@theia/core/lib/browser/connection-status-service';
import { BoardsDataMenuUpdater } from './boards/boards-data-menu-updater';
import { BoardsDataStore } from './boards/boards-data-store';
import { ILogger } from '@theia/core';
import { ILogger } from '@theia/core/lib/common/logger';
import { bindContributionProvider } from '@theia/core/lib/common/contribution-provider';
import {
FileSystemExt,
FileSystemExtPath,
@@ -138,7 +131,6 @@ import { Settings } from './contributions/settings';
import { WorkspaceCommandContribution } from './theia/workspace/workspace-commands';
import { WorkspaceDeleteHandler as TheiaWorkspaceDeleteHandler } from '@theia/workspace/lib/browser/workspace-delete-handler';
import { WorkspaceDeleteHandler } from './theia/workspace/workspace-delete-handler';
import { TabBarToolbar } from './theia/core/tab-bar-toolbar';
import { EditorWidgetFactory as TheiaEditorWidgetFactory } from '@theia/editor/lib/browser/editor-widget-factory';
import { EditorWidgetFactory } from './theia/editor/editor-widget-factory';
import { BurnBootloader } from './contributions/burn-bootloader';
@@ -182,8 +174,6 @@ import { EditorCommandContribution } from './theia/editor/editor-command';
import { NavigatorTabBarDecorator as TheiaNavigatorTabBarDecorator } from '@theia/navigator/lib/browser/navigator-tab-bar-decorator';
import { NavigatorTabBarDecorator } from './theia/navigator/navigator-tab-bar-decorator';
import { Debug } from './contributions/debug';
import { DebugSessionManager } from './theia/debug/debug-session-manager';
import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
import { Sketchbook } from './contributions/sketchbook';
import { DebugFrontendApplicationContribution } from './theia/debug/debug-frontend-application-contribution';
import { DebugFrontendApplicationContribution as TheiaDebugFrontendApplicationContribution } from '@theia/debug/lib/browser/debug-frontend-application-contribution';
@@ -206,12 +196,8 @@ import { WorkspaceVariableContribution as TheiaWorkspaceVariableContribution } f
import { WorkspaceVariableContribution } from './theia/workspace/workspace-variable-contribution';
import { DebugConfigurationManager } from './theia/debug/debug-configuration-manager';
import { DebugConfigurationManager as TheiaDebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager';
import { SearchInWorkspaceWidget as TheiaSearchInWorkspaceWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-widget';
import { SearchInWorkspaceWidget } from './theia/search-in-workspace/search-in-workspace-widget';
import { SearchInWorkspaceFactory as TheiaSearchInWorkspaceFactory } from '@theia/search-in-workspace/lib/browser/search-in-workspace-factory';
import { SearchInWorkspaceFactory } from './theia/search-in-workspace/search-in-workspace-factory';
import { SearchInWorkspaceResultTreeWidget as TheiaSearchInWorkspaceResultTreeWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-result-tree-widget';
import { SearchInWorkspaceResultTreeWidget } from './theia/search-in-workspace/search-in-workspace-result-tree-widget';
import { MonacoEditorProvider } from './theia/monaco/monaco-editor-provider';
import {
MonacoEditorFactory,
@@ -246,7 +232,6 @@ import { UploadFirmware } from './contributions/upload-firmware';
import {
UploadFirmwareDialog,
UploadFirmwareDialogProps,
UploadFirmwareDialogWidget,
} from './dialogs/firmware-uploader/firmware-uploader-dialog';
import { UploadCertificate } from './contributions/upload-certificate';
@@ -263,7 +248,6 @@ import { PlotterFrontendContribution } from './serial/plotter/plotter-frontend-c
import {
UserFieldsDialog,
UserFieldsDialogProps,
UserFieldsDialogWidget,
} from './dialogs/user-fields/user-fields-dialog';
import { nls } from '@theia/core/lib/common';
import { IDEUpdaterCommands } from './ide-updater/ide-updater-commands';
@@ -276,7 +260,6 @@ import { IDEUpdaterClientImpl } from './ide-updater/ide-updater-client-impl';
import {
IDEUpdaterDialog,
IDEUpdaterDialogProps,
IDEUpdaterDialogWidget,
} from './dialogs/ide-updater/ide-updater-dialog';
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider';
import { MonitorModel } from './monitor-model';
@@ -308,7 +291,7 @@ import { CoreErrorHandler } from './contributions/core-error-handler';
import { CompilerErrors } from './contributions/compiler-errors';
import { WidgetManager } from './theia/core/widget-manager';
import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager';
import { StartupTasks } from './widgets/sketchbook/startup-task';
import { StartupTasks } from './contributions/startup-task';
import { IndexesUpdateProgress } from './contributions/indexes-update-progress';
import { Daemon } from './contributions/daemon';
import { FirstStartupInstaller } from './contributions/first-startup-installer';
@@ -318,10 +301,6 @@ import { SelectedBoard } from './contributions/selected-board';
import { CheckForIDEUpdates } from './contributions/check-for-ide-updates';
import { OpenBoardsConfig } from './contributions/open-boards-config';
import { SketchFilesTracker } from './contributions/sketch-files-tracker';
import { MonacoThemeServiceIsReady } from './utils/window';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { StatusBarImpl } from './theia/core/status-bar';
import { StatusBarImpl as TheiaStatusBarImpl } from '@theia/core/lib/browser';
import { EditorMenuContribution } from './theia/editor/editor-file';
import { EditorMenuContribution as TheiaEditorMenuContribution } from '@theia/editor/lib/browser/editor-menu';
import { PreferencesEditorWidget as TheiaPreferencesEditorWidget } from '@theia/preferences/lib/browser/views/preference-editor-widget';
@@ -334,32 +313,36 @@ import {
} from './widgets/component-list/filter-renderer';
import { CheckForUpdates } from './contributions/check-for-updates';
import { OutputEditorFactory } from './theia/output/output-editor-factory';
const registerArduinoThemes = () => {
const themes: MonacoThemeJson[] = [
{
id: 'arduino-theme',
label: 'Light (Arduino)',
uiTheme: 'vs',
json: require('../../src/browser/data/default.color-theme.json'),
},
{
id: 'arduino-theme-dark',
label: 'Dark (Arduino)',
uiTheme: 'vs-dark',
json: require('../../src/browser/data/dark.color-theme.json'),
},
];
themes.forEach((theme) => MonacoThemingService.register(theme));
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const global = window as any;
const ready = global[MonacoThemeServiceIsReady] as Deferred;
if (ready) {
ready.promise.then(registerArduinoThemes);
} else {
registerArduinoThemes();
}
import { StartupTaskProvider } from '../electron-common/startup-task';
import { DeleteSketch } from './contributions/delete-sketch';
import { UserFields } from './contributions/user-fields';
import { UpdateIndexes } from './contributions/update-indexes';
import { InterfaceScale } from './contributions/interface-scale';
import { OpenHandler } from '@theia/core/lib/browser/opener-service';
import { NewCloudSketch } from './contributions/new-cloud-sketch';
import { SketchbookCompositeWidget } from './widgets/sketchbook/sketchbook-composite-widget';
import { WindowTitleUpdater } from './theia/core/window-title-updater';
import { WindowTitleUpdater as TheiaWindowTitleUpdater } from '@theia/core/lib/browser/window/window-title-updater';
import { ThemeService } from './theia/core/theming';
import { ThemeService as TheiaThemeService } from '@theia/core/lib/browser/theming';
import { MonacoThemingService } from './theia/monaco/monaco-theming-service';
import { MonacoThemingService as TheiaMonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service';
import { TypeHierarchyServiceProvider } from './theia/typehierarchy/type-hierarchy-service';
import { TypeHierarchyServiceProvider as TheiaTypeHierarchyServiceProvider } from '@theia/typehierarchy/lib/browser/typehierarchy-service';
import { TypeHierarchyContribution } from './theia/typehierarchy/type-hierarchy-contribution';
import { TypeHierarchyContribution as TheiaTypeHierarchyContribution } from '@theia/typehierarchy/lib/browser/typehierarchy-contribution';
import { DefaultDebugSessionFactory } from './theia/debug/debug-session-contribution';
import { DebugSessionFactory } from '@theia/debug/lib/browser/debug-session-contribution';
import { DebugToolbar } from './theia/debug/debug-toolbar-widget';
import { DebugToolBar as TheiaDebugToolbar } from '@theia/debug/lib/browser/view/debug-toolbar-widget';
import { PluginMenuCommandAdapter } from './theia/plugin-ext/plugin-menu-command-adapter';
import { PluginMenuCommandAdapter as TheiaPluginMenuCommandAdapter } from '@theia/plugin-ext/lib/main/browser/menus/plugin-menu-command-adapter';
import { DebugSessionManager } from './theia/debug/debug-session-manager';
import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
import { DebugWidget } from '@theia/debug/lib/browser/view/debug-widget';
import { DebugViewModel } from '@theia/debug/lib/browser/view/debug-view-model';
import { DebugSessionWidget } from '@theia/debug/lib/browser/view/debug-session-widget';
import { DebugConfigurationWidget } from '@theia/debug/lib/browser/view/debug-configuration-widget';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Commands and toolbar items
@@ -398,6 +381,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(FrontendApplicationContribution).toService(
LibraryListWidgetFrontendContribution
);
bind(OpenHandler).toService(LibraryListWidgetFrontendContribution);
// Sketch list service
bind(SketchesService)
@@ -433,6 +417,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Boards service client to receive and delegate notifications from the backend.
bind(BoardsServiceProvider).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(BoardsServiceProvider);
bind(CommandContribution).toService(BoardsServiceProvider);
// To be able to track, and update the menu based on the core settings (aka. board details) of the currently selected board.
bind(FrontendApplicationContribution)
@@ -463,6 +448,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(FrontendApplicationContribution).toService(
BoardsListWidgetFrontendContribution
);
bind(OpenHandler).toService(BoardsListWidgetFrontendContribution);
// Board select dialog
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
@@ -581,14 +567,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
.to(WorkspaceDeleteHandler)
.inSingletonScope();
rebind(TheiaEditorWidgetFactory).to(EditorWidgetFactory).inSingletonScope();
rebind(TabBarToolbarFactory).toFactory(
({ container: parentContainer }) =>
() => {
const container = parentContainer.createChild();
container.bind(TabBarToolbar).toSelf().inSingletonScope();
return container.get(TabBarToolbar);
}
);
bind(OutputChannelManager).toSelf().inSingletonScope();
rebind(TheiaOutputChannelManager).toService(OutputChannelManager);
bind(OutputChannelRegistryMainImpl).toSelf().inTransientScope();
@@ -600,9 +578,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(MonacoEditorProvider).toSelf().inSingletonScope();
rebind(TheiaMonacoEditorProvider).toService(MonacoEditorProvider);
bind(SearchInWorkspaceWidget).toSelf();
rebind(TheiaSearchInWorkspaceWidget).toService(SearchInWorkspaceWidget);
// Disabled reference counter in the editor manager to avoid opening the same editor (with different opener options) multiple times.
bind(EditorManager).toSelf().inSingletonScope();
rebind(TheiaEditorManager).toService(EditorManager);
@@ -612,17 +587,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
.to(SearchInWorkspaceFactory)
.inSingletonScope();
rebind(TheiaSearchInWorkspaceResultTreeWidget).toDynamicValue(
({ container }) => {
const childContainer = createTreeContainer(container);
childContainer.bind(SearchInWorkspaceResultTreeWidget).toSelf();
childContainer
.rebind(TreeWidget)
.toService(SearchInWorkspaceResultTreeWidget);
return childContainer.get(SearchInWorkspaceResultTreeWidget);
}
);
// Show a disconnected status bar, when the daemon is not available
bind(ApplicationConnectionStatusContribution).toSelf().inSingletonScope();
rebind(TheiaApplicationConnectionStatusContribution).toService(
@@ -757,6 +721,14 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, OpenBoardsConfig);
Contribution.configure(bind, SketchFilesTracker);
Contribution.configure(bind, CheckForUpdates);
Contribution.configure(bind, UserFields);
Contribution.configure(bind, DeleteSketch);
Contribution.configure(bind, UpdateIndexes);
Contribution.configure(bind, InterfaceScale);
Contribution.configure(bind, NewCloudSketch);
bindContributionProvider(bind, StartupTaskProvider);
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
// Disabled the quick-pick customization from Theia when multiple formatters are available.
// Use the default VS Code behavior, and pick the first one. In the IDE2, clang-format has `exclusive` selectors.
@@ -838,9 +810,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(AboutDialog).toSelf().inSingletonScope();
rebind(TheiaAboutDialog).toService(AboutDialog);
// To avoid running `Save All` when there are no dirty editors before starting the debug session.
bind(DebugSessionManager).toSelf().inSingletonScope();
rebind(TheiaDebugSessionManager).toService(DebugSessionManager);
// To remove the `Run` menu item from the application menu.
bind(DebugFrontendApplicationContribution).toSelf().inSingletonScope();
rebind(TheiaDebugFrontendApplicationContribution).toService(
@@ -854,10 +823,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(WidgetManager).toSelf().inSingletonScope();
rebind(TheiaWidgetManager).toService(WidgetManager);
// To avoid running a status bar update on every single `keypress` event from the editor.
bind(StatusBarImpl).toSelf().inSingletonScope();
rebind(TheiaStatusBarImpl).toService(StatusBarImpl);
// Debounced update for the tab-bar toolbar when typing in the editor.
bind(DockPanelRenderer).toSelf();
rebind(TheiaDockPanelRenderer).toService(DockPanelRenderer);
@@ -908,6 +873,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
id: 'arduino-sketchbook-widget',
createWidget: () => container.get(SketchbookWidget),
}));
bind(SketchbookCompositeWidget).toSelf();
bind<WidgetFactory>(WidgetFactory).toDynamicValue((ctx) => ({
id: 'sketchbook-composite-widget',
createWidget: () => ctx.container.get(SketchbookCompositeWidget),
}));
bind(CloudSketchbookWidget).toSelf();
rebind(SketchbookWidget).toService(CloudSketchbookWidget);
@@ -937,12 +907,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(LocalCacheFsProvider).toSelf().inSingletonScope();
bind(FileServiceContribution).toService(LocalCacheFsProvider);
bind(CloudSketchbookCompositeWidget).toSelf();
bind<WidgetFactory>(WidgetFactory).toDynamicValue((ctx) => ({
bind(WidgetFactory).toDynamicValue((ctx) => ({
id: 'cloud-sketchbook-composite-widget',
createWidget: () => ctx.container.get(CloudSketchbookCompositeWidget),
}));
bind(UploadFirmwareDialogWidget).toSelf().inSingletonScope();
bind(UploadFirmwareDialog).toSelf().inSingletonScope();
bind(UploadFirmwareDialogProps).toConstantValue({
title: 'UploadFirmware',
@@ -953,13 +922,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
title: 'UploadCertificate',
});
bind(IDEUpdaterDialogWidget).toSelf().inSingletonScope();
bind(IDEUpdaterDialog).toSelf().inSingletonScope();
bind(IDEUpdaterDialogProps).toConstantValue({
title: 'IDEUpdater',
});
bind(UserFieldsDialogWidget).toSelf().inSingletonScope();
bind(UserFieldsDialog).toSelf().inSingletonScope();
bind(UserFieldsDialogProps).toConstantValue({
title: 'UserFields',
@@ -986,4 +953,55 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TheiaHostedPluginSupport).toService(HostedPluginSupport);
bind(HostedPluginEvents).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(HostedPluginEvents);
// custom window titles
bind(WindowTitleUpdater).toSelf().inSingletonScope();
rebind(TheiaWindowTitleUpdater).toService(WindowTitleUpdater);
// register Arduino themes
bind(ThemeService).toSelf().inSingletonScope();
rebind(TheiaThemeService).toService(ThemeService);
bind(MonacoThemingService).toSelf().inSingletonScope();
rebind(TheiaMonacoThemingService).toService(MonacoThemingService);
// disable type-hierarchy support
// https://github.com/eclipse-theia/theia/commit/16c88a584bac37f5cf3cc5eb92ffdaa541bda5be
bind(TypeHierarchyServiceProvider).toSelf().inSingletonScope();
rebind(TheiaTypeHierarchyServiceProvider).toService(
TypeHierarchyServiceProvider
);
bind(TypeHierarchyContribution).toSelf().inSingletonScope();
rebind(TheiaTypeHierarchyContribution).toService(TypeHierarchyContribution);
// patched the debugger for `cortex-debug@1.5.1`
// https://github.com/eclipse-theia/theia/issues/11871
// https://github.com/eclipse-theia/theia/issues/11879
// https://github.com/eclipse-theia/theia/issues/11880
// https://github.com/eclipse-theia/theia/issues/11885
// https://github.com/eclipse-theia/theia/issues/11886
// https://github.com/eclipse-theia/theia/issues/11916
// based on: https://github.com/eclipse-theia/theia/compare/master...kittaakos:theia:%2311871
bind(DefaultDebugSessionFactory).toSelf().inSingletonScope();
rebind(DebugSessionFactory).toService(DefaultDebugSessionFactory);
bind(DebugSessionManager).toSelf().inSingletonScope();
rebind(TheiaDebugSessionManager).toService(DebugSessionManager);
bind(DebugToolbar).toSelf().inSingletonScope();
rebind(TheiaDebugToolbar).toService(DebugToolbar);
bind(PluginMenuCommandAdapter).toSelf().inSingletonScope();
rebind(TheiaPluginMenuCommandAdapter).toService(PluginMenuCommandAdapter);
bind(WidgetFactory)
.toDynamicValue(({ container }) => ({
id: DebugWidget.ID,
createWidget: () => {
const child = new Container({ defaultScope: 'Singleton' });
child.parent = container;
child.bind(DebugViewModel).toSelf();
child.bind(DebugToolbar).toSelf(); // patched toolbar
child.bind(DebugSessionWidget).toSelf();
child.bind(DebugConfigurationWidget).toSelf();
child.bind(DebugWidget).toSelf();
return child.get(DebugWidget);
},
}))
.inSingletonScope();
});

View File

@@ -114,11 +114,12 @@ export const ArduinoConfigSchema: PreferenceSchema = {
},
'arduino.window.zoomLevel': {
type: 'number',
description: nls.localize(
'arduino/preferences/window.zoomLevel',
'Adjust the zoom level of the window. The original size is 0 and each increment above (e.g. 1) or below (e.g. -1) represents zooming 20% larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity.'
),
description: '',
default: 0,
deprecationMessage: nls.localize(
'arduino/preferences/window.zoomLevel/deprecationMessage',
"Deprecated. Use 'window.zoomLevel' instead."
),
},
'arduino.ide.updateChannel': {
type: 'string',
@@ -249,6 +250,14 @@ export const ArduinoConfigSchema: PreferenceSchema = {
),
default: true,
},
'arduino.sketch.inoBlueprint': {
type: 'string',
markdownDescription: nls.localize(
'arduino/preferences/sketch/inoBlueprint',
'Absolute filesystem path to the default `.ino` blueprint file. If specified, the content of the blueprint file will be used for every new sketch created by the IDE. The sketches will be generated with the default Arduino content if not specified. Unaccessible blueprint files are ignored. **A restart of the IDE is needed** for this setting to take effect.'
),
default: undefined,
},
},
};
@@ -262,7 +271,6 @@ export interface ArduinoConfiguration {
'arduino.upload.verbose': boolean;
'arduino.upload.verify': boolean;
'arduino.window.autoScale': boolean;
'arduino.window.zoomLevel': number;
'arduino.ide.updateChannel': UpdateChannel;
'arduino.ide.updateBaseUrl': string;
'arduino.board.certificates': string;
@@ -278,6 +286,7 @@ export interface ArduinoConfiguration {
'arduino.auth.registerUri': string;
'arduino.survey.notification': boolean;
'arduino.cli.daemon.debug': boolean;
'arduino.sketch.inoBlueprint': string;
'arduino.checkForUpdates': boolean;
}

View File

@@ -34,6 +34,7 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
) {
super({ ...props, maxWidth: 500 });
this.node.id = 'select-board-dialog-container';
this.contentNode.classList.add('select-board-dialog');
this.contentNode.appendChild(this.createDescription());

View File

@@ -6,7 +6,6 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
import {
Board,
Port,
AttachedBoardsChangeEvent,
BoardWithPackage,
} from '../../common/protocol/boards-service';
import { NotificationCenter } from '../notification-center';
@@ -113,11 +112,14 @@ export class BoardsConfig extends React.Component<
);
}
}),
this.props.notificationCenter.onAttachedBoardsDidChange((event) =>
this.updatePorts(
event.newState.ports,
AttachedBoardsChangeEvent.diff(event).detached.ports
)
this.props.boardsServiceProvider.onAvailablePortsChanged(
({ newState, oldState }) => {
const removedPorts = oldState.filter(
(oldPort) =>
!newState.find((newPort) => Port.sameAs(newPort, oldPort))
);
this.updatePorts(newState, removedPorts);
}
),
this.props.boardsServiceProvider.onBoardsConfigChanged(
({ selectedBoard, selectedPort }) => {
@@ -132,7 +134,7 @@ export class BoardsConfig extends React.Component<
this.props.notificationCenter.onPlatformDidUninstall(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onIndexDidUpdate(() =>
this.props.notificationCenter.onIndexUpdateDidComplete(() =>
this.updateBoards(this.state.query)
),
this.props.notificationCenter.onDaemonDidStart(() =>
@@ -259,9 +261,12 @@ export class BoardsConfig extends React.Component<
override render(): React.ReactNode {
return (
<>
{this.renderContainer('boards', this.renderBoards.bind(this))}
{this.renderContainer(
'ports',
nls.localize('arduino/board/boards', 'boards'),
this.renderBoards.bind(this)
)}
{this.renderContainer(
nls.localize('arduino/board/ports', 'ports'),
this.renderPorts.bind(this),
this.renderPortsFooter.bind(this)
)}
@@ -299,6 +304,18 @@ export class BoardsConfig extends React.Component<
}
}
const boardsList = Array.from(distinctBoards.values()).map((board) => (
<Item<BoardWithPackage>
key={toKey(board)}
item={board}
label={board.name}
details={board.details}
selected={board.selected}
onClick={this.selectBoard}
missing={board.missing}
/>
));
return (
<React.Fragment>
<div className="search">
@@ -315,19 +332,17 @@ export class BoardsConfig extends React.Component<
/>
<i className="fa fa-search"></i>
</div>
<div className="boards list">
{Array.from(distinctBoards.values()).map((board) => (
<Item<BoardWithPackage>
key={toKey(board)}
item={board}
label={board.name}
details={board.details}
selected={board.selected}
onClick={this.selectBoard}
missing={board.missing}
/>
))}
</div>
{boardsList.length > 0 ? (
<div className="boards list">{boardsList}</div>
) : (
<div className="no-result">
{nls.localize(
'arduino/board/noBoardsFound',
'No boards found for "{0}"',
query
)}
</div>
)}
</React.Fragment>
);
}
@@ -342,7 +357,7 @@ export class BoardsConfig extends React.Component<
);
}
return !ports.length ? (
<div className="loading noselect">
<div className="no-result">
{nls.localize('arduino/board/noPortsDiscovered', 'No ports discovered')}
</div>
) : (
@@ -374,7 +389,9 @@ export class BoardsConfig extends React.Component<
defaultChecked={this.state.showAllPorts}
onChange={this.toggleFilterPorts}
/>
<span>Show all ports</span>
<span>
{nls.localize('arduino/board/showAllPorts', 'Show all ports')}
</span>
</label>
</div>
);
@@ -413,53 +430,5 @@ export namespace BoardsConfig {
const { name } = selectedBoard;
return `${name}${port ? ` at ${port.address}` : ''}`;
}
export function setConfig(
config: Config | undefined,
urlToAttachTo: URL
): URL {
const copy = new URL(urlToAttachTo.toString());
if (!config) {
copy.searchParams.delete('boards-config');
return copy;
}
const selectedBoard = config.selectedBoard
? {
name: config.selectedBoard.name,
fqbn: config.selectedBoard.fqbn,
}
: undefined;
const selectedPort = config.selectedPort
? {
protocol: config.selectedPort.protocol,
address: config.selectedPort.address,
}
: undefined;
const jsonConfig = JSON.stringify({ selectedBoard, selectedPort });
copy.searchParams.set('boards-config', encodeURIComponent(jsonConfig));
return copy;
}
export function getConfig(url: URL): Config | undefined {
const encoded = url.searchParams.get('boards-config');
if (!encoded) {
return undefined;
}
try {
const raw = decodeURIComponent(encoded);
const candidate = JSON.parse(raw);
if (typeof candidate === 'object') {
return candidate;
}
console.warn(
`Expected candidate to be an object. It was ${typeof candidate}. URL was: ${url}`
);
return undefined;
} catch (e) {
console.log(`Could not get board config from URL: ${url}.`, e);
return undefined;
}
}
}
}

View File

@@ -111,7 +111,7 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
const { label } = commands.get(commandId)!;
this.menuRegistry.registerMenuAction(menuPath, {
commandId,
order: `${i}`,
order: String(i).padStart(4),
label,
});
return Disposable.create(() =>

View File

@@ -1,7 +1,12 @@
import { injectable, inject } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { ILogger } from '@theia/core/lib/common/logger';
import { CommandService } from '@theia/core/lib/common/command';
import {
Command,
CommandContribution,
CommandRegistry,
CommandService,
} from '@theia/core/lib/common/command';
import { MessageService } from '@theia/core/lib/common/message-service';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { RecursiveRequired } from '../../common/types';
@@ -23,9 +28,18 @@ import { nls } from '@theia/core/lib/common';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { Unknown } from '../../common/nls';
import {
StartupTask,
StartupTaskProvider,
} from '../../electron-common/startup-task';
@injectable()
export class BoardsServiceProvider implements FrontendApplicationContribution {
export class BoardsServiceProvider
implements
FrontendApplicationContribution,
StartupTaskProvider,
CommandContribution
{
@inject(ILogger)
protected logger: ILogger;
@@ -49,7 +63,11 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
protected readonly onAvailableBoardsChangedEmitter = new Emitter<
AvailableBoard[]
>();
protected readonly onAvailablePortsChangedEmitter = new Emitter<Port[]>();
protected readonly onAvailablePortsChangedEmitter = new Emitter<{
newState: Port[];
oldState: Port[];
}>();
private readonly inheritedConfig = new Deferred<BoardsConfig.Config>();
/**
* Used for the auto-reconnecting. Sometimes, the attached board gets disconnected after uploading something to it.
@@ -105,8 +123,12 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
const { boards: attachedBoards, ports: availablePorts } =
AvailablePorts.split(state);
this._attachedBoards = attachedBoards;
const oldState = this._availablePorts.slice();
this._availablePorts = availablePorts;
this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
this.onAvailablePortsChangedEmitter.fire({
newState: this._availablePorts.slice(),
oldState,
});
await this.reconcileAvailableBoards();
@@ -115,6 +137,13 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
});
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(USE_INHERITED_CONFIG, {
execute: (inheritedConfig: BoardsConfig.Config) =>
this.inheritedConfig.resolve(inheritedConfig),
});
}
get reconciled(): Promise<void> {
return this._reconciled.promise;
}
@@ -207,8 +236,12 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
}
this._attachedBoards = event.newState.boards;
const oldState = this._availablePorts.slice();
this._availablePorts = event.newState.ports;
this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
this.onAvailablePortsChangedEmitter.fire({
newState: this._availablePorts.slice(),
oldState,
});
this.reconcileAvailableBoards().then(() => {
const { uploadInProgress } = event;
// avoid attempting "auto-selection" while an
@@ -376,14 +409,16 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
}
async selectedBoardUserFields(): Promise<BoardUserField[]> {
if (!this._boardsConfig.selectedBoard || !this._boardsConfig.selectedPort) {
if (!this._boardsConfig.selectedBoard) {
return [];
}
const fqbn = this._boardsConfig.selectedBoard.fqbn;
if (!fqbn) {
return [];
}
const protocol = this._boardsConfig.selectedPort.protocol;
// Protocol must be set to `default` when uploading without a port selected:
// https://arduino.github.io/arduino-cli/dev/platform-specification/#sketch-upload-configuration
const protocol = this._boardsConfig.selectedPort?.protocol || 'default';
return await this.boardsService.getBoardUserFields({ fqbn, protocol });
}
@@ -578,6 +613,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
boardsConfig.selectedBoard &&
availableBoards.every(({ selected }) => !selected)
) {
let port = boardsConfig.selectedPort;
// If the selected board has the same port of an unknown board
// that is already in availableBoards we might get a duplicate port.
// So we remove the one already in the array and add the selected one.
@@ -585,11 +621,15 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
(board) => board.port?.address === boardsConfig.selectedPort?.address
);
if (found >= 0) {
// get the "Unknown board port" that we will substitute,
// then we can include it in the "availableBoard object"
// pushed below; to ensure addressLabel is included
port = availableBoards[found].port;
availableBoards.splice(found, 1);
}
availableBoards.push({
...boardsConfig.selectedBoard,
port: boardsConfig.selectedPort,
port,
selected: true,
state: AvailableBoard.State.incomplete,
});
@@ -655,11 +695,14 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
let storedLatestBoardsConfig = await this.getData<
BoardsConfig.Config | undefined
>('latest-boards-config');
// Try to get from the URL if it was not persisted.
// Try to get from the startup task. Wait for it, then timeout. Maybe it never arrives.
if (!storedLatestBoardsConfig) {
storedLatestBoardsConfig = BoardsConfig.Config.getConfig(
new URL(window.location.href)
);
storedLatestBoardsConfig = await Promise.race([
this.inheritedConfig.promise,
new Promise<undefined>((resolve) =>
setTimeout(() => resolve(undefined), 2_000)
),
]);
}
if (storedLatestBoardsConfig) {
this.latestBoardsConfig = storedLatestBoardsConfig;
@@ -682,8 +725,31 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
key
);
}
tasks(): StartupTask[] {
return [
{
command: USE_INHERITED_CONFIG.id,
args: [this.boardsConfig],
},
];
}
}
/**
* It should be neither visible nor called from outside.
*
* This service creates a startup task with the current board config and
* passes the task to the electron-main process so that the new window
* can inherit the boards config state of this service.
*
* Note that the state is always set, but new windows might ignore it.
* For example, the new window already has a valid boards config persisted to the local storage.
*/
const USE_INHERITED_CONFIG: Command = {
id: 'arduino-use-inherited-boards-config',
};
/**
* Representation of a ready-to-use board, either the user has configured it or was automatically recognized by the CLI.
* An available board was not necessarily recognized by the CLI (e.g.: it is a 3rd party board) or correctly configured but ready for `verify`.

View File

@@ -1,10 +1,11 @@
import { injectable } from '@theia/core/shared/inversify';
import { BoardsListWidget } from './boards-list-widget';
import type {
import {
BoardSearch,
BoardsPackage,
} from '../../common/protocol/boards-service';
import { URI } from '../contributions/contribution';
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
import { BoardsListWidget } from './boards-list-widget';
@injectable()
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<
@@ -24,7 +25,16 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
});
}
override async initializeLayout(): Promise<void> {
this.openView();
protected canParse(uri: URI): boolean {
try {
BoardSearch.UriParser.parse(uri);
return true;
} catch {
return false;
}
}
protected parse(uri: URI): BoardSearch | undefined {
return BoardSearch.UriParser.parse(uri);
}
}

View File

@@ -15,7 +15,7 @@ import { CurrentSketch } from '../../common/protocol/sketches-service-client-imp
@injectable()
export class AddFile extends SketchContribution {
@inject(FileDialogService)
protected readonly fileDialogService: FileDialogService;
private readonly fileDialogService: FileDialogService;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(AddFile.Commands.ADD_FILE, {
@@ -31,7 +31,7 @@ export class AddFile extends SketchContribution {
});
}
protected async addFile(): Promise<void> {
private async addFile(): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch();
if (!CurrentSketch.isValid(sketch)) {
return;
@@ -41,6 +41,7 @@ export class AddFile extends SketchContribution {
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
modal: true,
});
if (!toAddUri) {
return;

View File

@@ -17,13 +17,13 @@ import { nls } from '@theia/core/lib/common';
@injectable()
export class AddZipLibrary extends SketchContribution {
@inject(EnvVariablesServer)
protected readonly envVariableServer: EnvVariablesServer;
private readonly envVariableServer: EnvVariablesServer;
@inject(ResponseServiceClient)
protected readonly responseService: ResponseServiceClient;
private readonly responseService: ResponseServiceClient;
@inject(LibraryService)
protected readonly libraryService: LibraryService;
private readonly libraryService: LibraryService;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(AddZipLibrary.Commands.ADD_ZIP_LIBRARY, {
@@ -43,23 +43,26 @@ export class AddZipLibrary extends SketchContribution {
});
}
async addZipLibrary(): Promise<void> {
private async addZipLibrary(): Promise<void> {
const homeUri = await this.envVariableServer.getHomeDirUri();
const defaultPath = await this.fileService.fsPath(new URI(homeUri));
const { canceled, filePaths } = await remote.dialog.showOpenDialog({
title: nls.localize(
'arduino/selectZip',
"Select a zip file containing the library you'd like to add"
),
defaultPath,
properties: ['openFile'],
filters: [
{
name: nls.localize('arduino/library/zipLibrary', 'Library'),
extensions: ['zip'],
},
],
});
const { canceled, filePaths } = await remote.dialog.showOpenDialog(
remote.getCurrentWindow(),
{
title: nls.localize(
'arduino/selectZip',
"Select a zip file containing the library you'd like to add"
),
defaultPath,
properties: ['openFile'],
filters: [
{
name: nls.localize('arduino/library/zipLibrary', 'Library'),
extensions: ['zip'],
},
],
}
);
if (!canceled && filePaths.length) {
const zipUri = await this.fileSystemExt.getUri(filePaths[0]);
try {

View File

@@ -28,7 +28,7 @@ export class ArchiveSketch extends SketchContribution {
});
}
protected async archiveSketch(): Promise<void> {
private async archiveSketch(): Promise<void> {
const [sketch, config] = await Promise.all([
this.sketchServiceClient.currentSketch(),
this.configService.getConfiguration(),
@@ -43,13 +43,16 @@ export class ArchiveSketch extends SketchContribution {
const defaultPath = await this.fileService.fsPath(
new URI(config.sketchDirUri).resolve(archiveBasename)
);
const { filePath, canceled } = await remote.dialog.showSaveDialog({
title: nls.localize(
'arduino/sketch/saveSketchAs',
'Save sketch folder as...'
),
defaultPath,
});
const { filePath, canceled } = await remote.dialog.showSaveDialog(
remote.getCurrentWindow(),
{
title: nls.localize(
'arduino/sketch/saveSketchAs',
'Save sketch folder as...'
),
defaultPath,
}
);
if (!filePath || canceled) {
return;
}

View File

@@ -5,7 +5,6 @@ import {
DisposableCollection,
Disposable,
} from '@theia/core/lib/common/disposable';
import { firstToUpperCase } from '../../common/utils';
import { BoardsConfig } from '../boards/boards-config';
import { MainMenuManager } from '../../common/main-menu-manager';
import { BoardsListWidget } from '../boards/boards-list-widget';
@@ -200,14 +199,15 @@ PID: ${PID}`;
});
// Installed boards
for (const board of installedBoards) {
installedBoards.forEach((board, index) => {
const { packageId, packageName, fqbn, name, manuallyInstalled } = board;
const packageLabel =
packageName +
`${manuallyInstalled
? nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)')
: ''
`${
manuallyInstalled
? nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)')
: ''
}`;
// Platform submenu
const platformMenuPath = [...boardsPackagesGroup, packageId];
@@ -240,14 +240,18 @@ PID: ${PID}`;
};
// Board menu
const menuAction = { commandId: id, label: name };
const menuAction = {
commandId: id,
label: name,
order: String(index).padStart(4), // pads with leading zeros for alphanumeric sort where order is 1, 2, 11, and NOT 1, 11, 2
};
this.commandRegistry.registerCommand(command, handler);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() => this.commandRegistry.unregisterCommand(command))
);
this.menuModelRegistry.registerMenuAction(platformMenuPath, menuAction);
// Note: we do not dispose the menu actions individually. Calling `unregisterSubmenu` on the parent will wipe the children menu nodes recursively.
}
});
// Installed ports
const registerPorts = (
@@ -267,8 +271,12 @@ PID: ${PID}`;
];
const placeholder = new PlaceholderMenuNode(
menuPath,
`${firstToUpperCase(protocol)} ports`,
{ order: protocolOrder.toString() }
nls.localize(
'arduino/board/typeOfPorts',
'{0} ports',
Port.Protocols.protocolLabel(protocol)
),
{ order: protocolOrder.toString().padStart(4) }
);
this.menuModelRegistry.registerMenuNode(menuPath, placeholder);
this.toDisposeBeforeMenuRebuild.push(
@@ -279,11 +287,13 @@ PID: ${PID}`;
// First we show addresses with recognized boards connected,
// then all the rest.
const sortedIDs = Object.keys(ports).sort((left: string, right: string): number => {
const [, leftBoards] = ports[left];
const [, rightBoards] = ports[right];
return rightBoards.length - leftBoards.length;
});
const sortedIDs = Object.keys(ports).sort(
(left: string, right: string): number => {
const [, leftBoards] = ports[left];
const [, rightBoards] = ports[right];
return rightBoards.length - leftBoards.length;
}
);
for (let i = 0; i < sortedIDs.length; i++) {
const portID = sortedIDs[i];
@@ -319,7 +329,7 @@ PID: ${PID}`;
const menuAction = {
commandId: id,
label,
order: `${protocolOrder + i + 1}`,
order: String(protocolOrder + i + 1).padStart(4),
};
this.commandRegistry.registerCommand(command, handler);
this.toDisposeBeforeMenuRebuild.push(
@@ -351,7 +361,7 @@ PID: ${PID}`;
}
protected async installedBoards(): Promise<InstalledBoardWithPackage[]> {
const allBoards = await this.boardsService.searchBoards({});
const allBoards = await this.boardsService.getInstalledBoards();
return allBoards.filter(InstalledBoardWithPackage.is);
}
}

View File

@@ -37,16 +37,17 @@ export class CheckForIDEUpdates extends Contribution {
}
override onReady(): void {
const checkForUpdates = this.preferences['arduino.checkForUpdates'];
if (!checkForUpdates) {
return;
}
this.updater
.init(
this.preferences.get('arduino.ide.updateChannel'),
this.preferences.get('arduino.ide.updateBaseUrl')
)
.then(() => this.updater.checkForUpdates(true))
.then(() => {
if (!this.preferences['arduino.checkForUpdates']) {
return;
}
return this.updater.checkForUpdates(true);
})
.then(async (updateInfo) => {
if (!updateInfo) return;
const versionToSkip = await this.localStorage.getData<string>(

View File

@@ -65,7 +65,7 @@ export class Close extends SketchContribution {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: Close.Commands.CLOSE.id,
label: nls.localize('vscode/editor.contribution/close', 'Close'),
order: '5',
order: '6',
});
}

View File

@@ -12,7 +12,6 @@ import { MaybePromise } from '@theia/core/lib/common/types';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { MessageService } from '@theia/core/lib/common/message-service';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
import {
@@ -61,6 +60,7 @@ import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { BoardsDataStore } from '../boards/boards-data-store';
import { NotificationManager } from '../theia/messages/notifications-manager';
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
import { WorkspaceService } from '../theia/workspace/workspace-service';
export {
Command,

View File

@@ -0,0 +1,45 @@
import { injectable } from '@theia/core/shared/inversify';
import { SketchesError } from '../../common/protocol';
import {
Command,
CommandRegistry,
SketchContribution,
Sketch,
} from './contribution';
@injectable()
export class DeleteSketch extends SketchContribution {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(DeleteSketch.Commands.DELETE_SKETCH, {
execute: (uri: string) => this.deleteSketch(uri),
});
}
private async deleteSketch(uri: string): Promise<void> {
const sketch = await this.loadSketch(uri);
if (!sketch) {
console.info(`Sketch not found at ${uri}. Skipping deletion.`);
return;
}
return this.sketchService.deleteSketch(sketch);
}
private async loadSketch(uri: string): Promise<Sketch | undefined> {
try {
const sketch = await this.sketchService.loadSketch(uri);
return sketch;
} catch (err) {
if (SketchesError.NotFound.is(err)) {
return undefined;
}
throw err;
}
}
}
export namespace DeleteSketch {
export namespace Commands {
export const DELETE_SKETCH: Command = {
id: 'arduino-delete-sketch',
};
}
}

View File

@@ -49,30 +49,6 @@ export class EditContributions extends Contribution {
registry.registerCommand(EditContributions.Commands.USE_FOR_FIND, {
execute: () => this.run('editor.action.previousSelectionMatchFindAction'),
});
registry.registerCommand(EditContributions.Commands.INCREASE_FONT_SIZE, {
execute: async () => {
const settings = await this.settingsService.settings();
if (settings.autoScaleInterface) {
settings.interfaceScale = settings.interfaceScale + 1;
} else {
settings.editorFontSize = settings.editorFontSize + 1;
}
await this.settingsService.update(settings);
await this.settingsService.save();
},
});
registry.registerCommand(EditContributions.Commands.DECREASE_FONT_SIZE, {
execute: async () => {
const settings = await this.settingsService.settings();
if (settings.autoScaleInterface) {
settings.interfaceScale = settings.interfaceScale - 1;
} else {
settings.editorFontSize = settings.editorFontSize - 1;
}
await this.settingsService.update(settings);
await this.settingsService.save();
},
});
/* Tools */ registry.registerCommand(
EditContributions.Commands.AUTO_FORMAT,
{ execute: () => this.run('editor.action.formatDocument') }
@@ -147,23 +123,6 @@ ${value}
order: '3',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
commandId: EditContributions.Commands.INCREASE_FONT_SIZE.id,
label: nls.localize(
'arduino/editor/increaseFontSize',
'Increase Font Size'
),
order: '0',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
commandId: EditContributions.Commands.DECREASE_FONT_SIZE.id,
label: nls.localize(
'arduino/editor/decreaseFontSize',
'Decrease Font Size'
),
order: '1',
});
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.FIND.id,
label: nls.localize('vscode/findController/startFindAction', 'Find'),
@@ -220,15 +179,6 @@ ${value}
when: 'editorFocus',
});
registry.registerKeybinding({
command: EditContributions.Commands.INCREASE_FONT_SIZE.id,
keybinding: 'CtrlCmd+=',
});
registry.registerKeybinding({
command: EditContributions.Commands.DECREASE_FONT_SIZE.id,
keybinding: 'CtrlCmd+-',
});
registry.registerKeybinding({
command: EditContributions.Commands.FIND.id,
keybinding: 'CtrlCmd+F',
@@ -315,12 +265,6 @@ export namespace EditContributions {
export const USE_FOR_FIND: Command = {
id: 'arduino-for-find',
};
export const INCREASE_FONT_SIZE: Command = {
id: 'arduino-increase-font-size',
};
export const DECREASE_FONT_SIZE: Command = {
id: 'arduino-decrease-font-size',
};
export const AUTO_FORMAT: Command = {
id: 'arduino-auto-format', // `Auto Format` should belong to `Tool`.
};

View File

@@ -21,16 +21,23 @@ import {
MenuModelRegistry,
} from './contribution';
import { NotificationCenter } from '../notification-center';
import { Board, SketchRef, SketchContainer } from '../../common/protocol';
import {
Board,
SketchRef,
SketchContainer,
SketchesError,
Sketch,
CoreService,
} from '../../common/protocol';
import { nls } from '@theia/core/lib/common';
@injectable()
export abstract class Examples extends SketchContribution {
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
private readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
private readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
protected readonly menuManager: MainMenuManager;
@@ -38,6 +45,9 @@ export abstract class Examples extends SketchContribution {
@inject(ExamplesService)
protected readonly examplesService: ExamplesService;
@inject(CoreService)
protected readonly coreService: CoreService;
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
@@ -50,10 +60,16 @@ export abstract class Examples extends SketchContribution {
);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
protected handleBoardChanged(board: Board | undefined): void {
// NOOP
}
protected abstract update(options?: {
board?: Board | undefined;
forceRefresh?: boolean;
}): void;
override registerMenus(registry: MenuModelRegistry): void {
try {
// This is a hack the ensures the desired menu ordering! We cannot use https://github.com/eclipse-theia/theia/pull/8377 due to ATL-222.
@@ -149,23 +165,54 @@ export abstract class Examples extends SketchContribution {
protected createHandler(uri: string): CommandHandler {
return {
execute: async () => {
const sketch = await this.sketchService.cloneExample(uri);
return this.commandService.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id,
sketch
);
const sketch = await this.clone(uri);
if (sketch) {
try {
return this.commandService.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id,
sketch
);
} catch (err) {
if (SketchesError.NotFound.is(err)) {
// Do not toast the error message. It's handled by the `Open Sketch` command.
this.update({
board: this.boardsServiceClient.boardsConfig.selectedBoard,
forceRefresh: true,
});
} else {
throw err;
}
}
}
},
};
}
private async clone(uri: string): Promise<Sketch | undefined> {
try {
const sketch = await this.sketchService.cloneExample(uri);
return sketch;
} catch (err) {
if (SketchesError.NotFound.is(err)) {
this.messageService.error(err.message);
this.update({
board: this.boardsServiceClient.boardsConfig.selectedBoard,
forceRefresh: true,
});
} else {
throw err;
}
}
}
}
@injectable()
export class BuiltInExamples extends Examples {
override async onReady(): Promise<void> {
this.register(); // no `await`
this.update(); // no `await`
}
protected async register(): Promise<void> {
protected override async update(): Promise<void> {
let sketchContainers: SketchContainer[] | undefined;
try {
sketchContainers = await this.examplesService.builtIns();
@@ -197,29 +244,34 @@ export class BuiltInExamples extends Examples {
@injectable()
export class LibraryExamples extends Examples {
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
private readonly notificationCenter: NotificationCenter;
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
private readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
override onStart(): void {
this.notificationCenter.onLibraryDidInstall(() => this.register());
this.notificationCenter.onLibraryDidUninstall(() => this.register());
this.notificationCenter.onLibraryDidInstall(() => this.update());
this.notificationCenter.onLibraryDidUninstall(() => this.update());
}
override async onReady(): Promise<void> {
this.register(); // no `await`
this.update(); // no `await`
}
protected override handleBoardChanged(board: Board | undefined): void {
this.register(board);
this.update({ board });
}
protected async register(
board: Board | undefined = this.boardsServiceClient.boardsConfig
.selectedBoard
protected override async update(
options: { board?: Board; forceRefresh?: boolean } = {
board: this.boardsServiceClient.boardsConfig.selectedBoard,
}
): Promise<void> {
const { board, forceRefresh } = options;
return this.queue.add(async () => {
this.toDispose.dispose();
if (forceRefresh) {
await this.coreService.refresh();
}
const fqbn = board?.fqbn;
const name = board?.name;
// Shows all examples when no board is selected, or the platform of the currently selected board is not installed.

View File

@@ -7,6 +7,8 @@ import {
} from '../../common/protocol';
import { Contribution } from './contribution';
const Arduino_BuiltIn = 'Arduino_BuiltIn';
@injectable()
export class FirstStartupInstaller extends Contribution {
@inject(LocalStorageService)
@@ -25,8 +27,8 @@ export class FirstStartupInstaller extends Contribution {
id: 'arduino:avr',
});
const builtInLibrary = (
await this.libraryService.search({ query: 'Arduino_BuiltIn' })
)[0];
await this.libraryService.search({ query: Arduino_BuiltIn })
).find(({ name }) => name === Arduino_BuiltIn); // Filter by `name` to ensure "exact match". See: https://github.com/arduino/arduino-ide/issues/1526.
let avrPackageError: Error | undefined;
let builtInLibraryError: Error | undefined;
@@ -84,7 +86,7 @@ export class FirstStartupInstaller extends Contribution {
}
if (builtInLibraryError) {
this.messageService.error(
`Could not install ${builtInLibrary.name} library: ${builtInLibraryError}`
`Could not install ${Arduino_BuiltIn} library: ${builtInLibraryError}`
);
}

View File

@@ -16,7 +16,7 @@ export class IndexesUpdateProgress extends Contribution {
| undefined;
override onStart(): void {
this.notificationCenter.onIndexWillUpdate((progressId) =>
this.notificationCenter.onIndexUpdateWillStart(({ progressId }) =>
this.getOrCreateProgress(progressId)
);
this.notificationCenter.onIndexUpdateDidProgress((progress) => {
@@ -24,7 +24,7 @@ export class IndexesUpdateProgress extends Contribution {
delegate.report(progress)
);
});
this.notificationCenter.onIndexDidUpdate((progressId) => {
this.notificationCenter.onIndexUpdateDidComplete(({ progressId }) => {
this.cancelProgress(progressId);
});
this.notificationCenter.onIndexUpdateDidFail(({ progressId, message }) => {

View File

@@ -0,0 +1,228 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import {
Contribution,
Command,
MenuModelRegistry,
KeybindingRegistry,
} from './contribution';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import {
CommandRegistry,
DisposableCollection,
MaybePromise,
nls,
} from '@theia/core/lib/common';
import { Settings } from '../dialogs/settings/settings';
import { MainMenuManager } from '../../common/main-menu-manager';
import debounce = require('lodash.debounce');
@injectable()
export class InterfaceScale extends Contribution {
@inject(MenuModelRegistry)
private readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
private readonly mainMenuManager: MainMenuManager;
private readonly menuActionsDisposables = new DisposableCollection();
private fontScalingEnabled: InterfaceScale.FontScalingEnabled = {
increase: true,
decrease: true,
};
private currentSettings: Settings;
private updateSettingsDebounced = debounce(
async () => {
await this.settingsService.update(this.currentSettings);
await this.settingsService.save();
},
100,
{ maxWait: 200 }
);
override onStart(): MaybePromise<void> {
const updateCurrent = (settings: Settings) => {
this.currentSettings = settings;
this.updateFontScalingEnabled();
};
this.settingsService.onDidChange((settings) => updateCurrent(settings));
this.settingsService.settings().then((settings) => updateCurrent(settings));
}
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(InterfaceScale.Commands.INCREASE_FONT_SIZE, {
execute: () => this.updateFontSize('increase'),
isEnabled: () => this.fontScalingEnabled.increase,
});
registry.registerCommand(InterfaceScale.Commands.DECREASE_FONT_SIZE, {
execute: () => this.updateFontSize('decrease'),
isEnabled: () => this.fontScalingEnabled.decrease,
});
}
override registerMenus(registry: MenuModelRegistry): void {
this.menuActionsDisposables.dispose();
const increaseFontSizeMenuAction = {
commandId: InterfaceScale.Commands.INCREASE_FONT_SIZE.id,
label: nls.localize(
'arduino/editor/increaseFontSize',
'Increase Font Size'
),
order: '0',
};
const decreaseFontSizeMenuAction = {
commandId: InterfaceScale.Commands.DECREASE_FONT_SIZE.id,
label: nls.localize(
'arduino/editor/decreaseFontSize',
'Decrease Font Size'
),
order: '1',
};
if (this.fontScalingEnabled.increase) {
this.menuActionsDisposables.push(
registry.registerMenuAction(
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
increaseFontSizeMenuAction
)
);
} else {
this.menuActionsDisposables.push(
registry.registerMenuNode(
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
new PlaceholderMenuNode(
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
increaseFontSizeMenuAction.label,
{ order: increaseFontSizeMenuAction.order }
)
)
);
}
if (this.fontScalingEnabled.decrease) {
this.menuActionsDisposables.push(
this.menuRegistry.registerMenuAction(
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
decreaseFontSizeMenuAction
)
);
} else {
this.menuActionsDisposables.push(
this.menuRegistry.registerMenuNode(
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
new PlaceholderMenuNode(
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
decreaseFontSizeMenuAction.label,
{ order: decreaseFontSizeMenuAction.order }
)
)
);
}
this.mainMenuManager.update();
}
private updateFontScalingEnabled(): void {
let fontScalingEnabled = {
increase: true,
decrease: true,
};
if (this.currentSettings.autoScaleInterface) {
fontScalingEnabled = {
increase:
this.currentSettings.interfaceScale + InterfaceScale.ZoomLevel.STEP <=
InterfaceScale.ZoomLevel.MAX,
decrease:
this.currentSettings.interfaceScale - InterfaceScale.ZoomLevel.STEP >=
InterfaceScale.ZoomLevel.MIN,
};
} else {
fontScalingEnabled = {
increase:
this.currentSettings.editorFontSize + InterfaceScale.FontSize.STEP <=
InterfaceScale.FontSize.MAX,
decrease:
this.currentSettings.editorFontSize - InterfaceScale.FontSize.STEP >=
InterfaceScale.FontSize.MIN,
};
}
const isChanged = Object.keys(fontScalingEnabled).some(
(key: keyof InterfaceScale.FontScalingEnabled) =>
fontScalingEnabled[key] !== this.fontScalingEnabled[key]
);
if (isChanged) {
this.fontScalingEnabled = fontScalingEnabled;
this.registerMenus(this.menuRegistry);
}
}
private updateFontSize(mode: 'increase' | 'decrease'): void {
if (this.currentSettings.autoScaleInterface) {
mode === 'increase'
? (this.currentSettings.interfaceScale += InterfaceScale.ZoomLevel.STEP)
: (this.currentSettings.interfaceScale -=
InterfaceScale.ZoomLevel.STEP);
} else {
mode === 'increase'
? (this.currentSettings.editorFontSize += InterfaceScale.FontSize.STEP)
: (this.currentSettings.editorFontSize -= InterfaceScale.FontSize.STEP);
}
this.updateFontScalingEnabled();
this.updateSettingsDebounced();
}
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: InterfaceScale.Commands.INCREASE_FONT_SIZE.id,
keybinding: 'CtrlCmd+=',
});
registry.registerKeybinding({
command: InterfaceScale.Commands.DECREASE_FONT_SIZE.id,
keybinding: 'CtrlCmd+-',
});
}
}
export namespace InterfaceScale {
export namespace Commands {
export const INCREASE_FONT_SIZE: Command = {
id: 'arduino-increase-font-size',
};
export const DECREASE_FONT_SIZE: Command = {
id: 'arduino-decrease-font-size',
};
}
export namespace ZoomLevel {
export const MIN = -8;
export const MAX = 9;
export const STEP = 1;
export function toPercentage(scale: number): number {
return scale * 20 + 100;
}
export function fromPercentage(percentage: number): number {
return (percentage - 100) / 20;
}
export namespace Step {
export function toPercentage(step: number): number {
return step * 20;
}
export function fromPercentage(percentage: number): number {
return percentage / 20;
}
}
}
export namespace FontSize {
export const MIN = 8;
export const MAX = 72;
export const STEP = 2;
}
export interface FontScalingEnabled {
increase: boolean;
decrease: boolean;
}
}

View File

@@ -0,0 +1,373 @@
import { DialogError } from '@theia/core/lib/browser/dialogs';
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
import { Widget } from '@theia/core/lib/browser/widgets/widget';
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import {
Progress,
ProgressUpdate,
} from '@theia/core/lib/common/message-service-protocol';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { WorkspaceInputDialogProps } from '@theia/workspace/lib/browser/workspace-input-dialog';
import { v4 } from 'uuid';
import { MainMenuManager } from '../../common/main-menu-manager';
import type { AuthenticationSession } from '../../node/auth/types';
import { AuthenticationClientService } from '../auth/authentication-client-service';
import { CreateApi } from '../create/create-api';
import { CreateUri } from '../create/create-uri';
import { Create } from '../create/typings';
import { ArduinoMenus } from '../menu/arduino-menus';
import { WorkspaceInputDialog } from '../theia/workspace/workspace-input-dialog';
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
import { CloudSketchbookTreeWidget } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-widget';
import { SketchbookCommands } from '../widgets/sketchbook/sketchbook-commands';
import { SketchbookWidget } from '../widgets/sketchbook/sketchbook-widget';
import { SketchbookWidgetContribution } from '../widgets/sketchbook/sketchbook-widget-contribution';
import { Command, CommandRegistry, Contribution, URI } from './contribution';
@injectable()
export class NewCloudSketch extends Contribution {
@inject(CreateApi)
private readonly createApi: CreateApi;
@inject(SketchbookWidgetContribution)
private readonly widgetContribution: SketchbookWidgetContribution;
@inject(AuthenticationClientService)
private readonly authenticationService: AuthenticationClientService;
@inject(MainMenuManager)
private readonly mainMenuManager: MainMenuManager;
private readonly toDispose = new DisposableCollection();
private _session: AuthenticationSession | undefined;
private _enabled: boolean;
override onReady(): void {
this.toDispose.pushAll([
this.authenticationService.onSessionDidChange((session) => {
const oldSession = this._session;
this._session = session;
if (!!oldSession !== !!this._session) {
this.mainMenuManager.update();
}
}),
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
if (preferenceName === 'arduino.cloud.enabled') {
const oldEnabled = this._enabled;
this._enabled = Boolean(newValue);
if (this._enabled !== oldEnabled) {
this.mainMenuManager.update();
}
}
}),
]);
this._enabled = this.preferences['arduino.cloud.enabled'];
this._session = this.authenticationService.session;
if (this._session) {
this.mainMenuManager.update();
}
}
onStop(): void {
this.toDispose.dispose();
}
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(NewCloudSketch.Commands.NEW_CLOUD_SKETCH, {
execute: () => this.createNewSketch(),
isEnabled: () => !!this._session,
isVisible: () => this._enabled,
});
}
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
label: nls.localize('arduino/cloudSketch/new', 'New Remote Sketch'),
order: '1',
});
}
override registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybinding({
command: NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
keybinding: 'CtrlCmd+Alt+N',
});
}
private async createNewSketch(
initialValue?: string | undefined
): Promise<unknown> {
const widget = await this.widgetContribution.widget;
const treeModel = this.treeModelFrom(widget);
if (!treeModel) {
return undefined;
}
const rootNode = CompositeTreeNode.is(treeModel.root)
? treeModel.root
: undefined;
if (!rootNode) {
return undefined;
}
return this.openWizard(rootNode, treeModel, initialValue);
}
private withProgress(
value: string,
treeModel: CloudSketchbookTreeModel
): (progress: Progress) => Promise<unknown> {
return async (progress: Progress) => {
let result: Create.Sketch | undefined | 'conflict';
try {
progress.report({
message: nls.localize(
'arduino/cloudSketch/creating',
"Creating remote sketch '{0}'...",
value
),
});
result = await this.createApi.createSketch(value);
} catch (err) {
if (isConflict(err)) {
result = 'conflict';
} else {
throw err;
}
} finally {
if (result) {
progress.report({
message: nls.localize(
'arduino/cloudSketch/synchronizing',
"Synchronizing sketchbook, pulling '{0}'...",
value
),
});
await treeModel.refresh();
}
}
if (result === 'conflict') {
return this.createNewSketch(value);
}
if (result) {
return this.open(treeModel, result);
}
return undefined;
};
}
private async open(
treeModel: CloudSketchbookTreeModel,
newSketch: Create.Sketch
): Promise<URI | undefined> {
const id = CreateUri.toUri(newSketch).path.toString();
const node = treeModel.getNode(id);
if (!node) {
throw new Error(
`Could not find remote sketchbook tree node with Tree node ID: ${id}.`
);
}
if (!CloudSketchbookTree.CloudSketchDirNode.is(node)) {
throw new Error(
`Remote sketchbook tree node expected to represent a directory but it did not. Tree node ID: ${id}.`
);
}
try {
await treeModel.sketchbookTree().pull({ node });
} catch (err) {
if (isNotFound(err)) {
await treeModel.refresh();
this.messageService.error(
nls.localize(
'arduino/newCloudSketch/notFound',
"Could not pull the remote sketch '{0}'. It does not exist.",
newSketch.name
)
);
return undefined;
}
throw err;
}
return this.commandService.executeCommand(
SketchbookCommands.OPEN_NEW_WINDOW.id,
{ node }
);
}
private treeModelFrom(
widget: SketchbookWidget
): CloudSketchbookTreeModel | undefined {
for (const treeWidget of widget.getTreeWidgets()) {
if (treeWidget instanceof CloudSketchbookTreeWidget) {
const model = treeWidget.model;
if (model instanceof CloudSketchbookTreeModel) {
return model;
}
}
}
return undefined;
}
private async openWizard(
rootNode: CompositeTreeNode,
treeModel: CloudSketchbookTreeModel,
initialValue?: string | undefined
): Promise<unknown> {
const existingNames = rootNode.children
.filter(CloudSketchbookTree.CloudSketchDirNode.is)
.map(({ fileStat }) => fileStat.name);
return new NewCloudSketchDialog(
{
title: nls.localize(
'arduino/newCloudSketch/newSketchTitle',
'Name of a new Remote Sketch'
),
parentUri: CreateUri.root,
initialValue,
validate: (input) => {
if (existingNames.includes(input)) {
return nls.localize(
'arduino/newCloudSketch/sketchAlreadyExists',
"Remote sketch '{0}' already exists.",
input
);
}
// This is how https://create.arduino.cc/editor/ works when renaming a sketch.
if (/^[0-9a-zA-Z_]{1,36}$/.test(input)) {
return '';
}
return nls.localize(
'arduino/newCloudSketch/invalidSketchName',
'The name must consist of basic letters, numbers, or underscores. The maximum length is 36 characters.'
);
},
},
this.labelProvider,
(value) => this.withProgress(value, treeModel)
).open();
}
}
export namespace NewCloudSketch {
export namespace Commands {
export const NEW_CLOUD_SKETCH: Command = {
id: 'arduino-new-cloud-sketch',
};
}
}
function isConflict(err: unknown): boolean {
return isErrorWithStatusOf(err, 409);
}
function isNotFound(err: unknown): boolean {
return isErrorWithStatusOf(err, 404);
}
function isErrorWithStatusOf(
err: unknown,
status: number
): err is Error & { status: number } {
if (err instanceof Error) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const object = err as any;
return 'status' in object && object.status === status;
}
return false;
}
@injectable()
class NewCloudSketchDialog extends WorkspaceInputDialog {
constructor(
@inject(WorkspaceInputDialogProps)
protected override readonly props: WorkspaceInputDialogProps,
@inject(LabelProvider)
protected override readonly labelProvider: LabelProvider,
private readonly withProgress: (
value: string
) => (progress: Progress) => Promise<unknown>
) {
super(props, labelProvider);
}
protected override async accept(): Promise<void> {
if (!this.resolve) {
return;
}
this.acceptCancellationSource.cancel();
this.acceptCancellationSource = new CancellationTokenSource();
const token = this.acceptCancellationSource.token;
const value = this.value;
const error = await this.isValid(value, 'open');
if (token.isCancellationRequested) {
return;
}
if (!DialogError.getResult(error)) {
this.setErrorMessage(error);
} else {
const spinner = document.createElement('div');
spinner.classList.add('spinner');
const disposables = new DisposableCollection();
try {
this.toggleButtons(true);
disposables.push(Disposable.create(() => this.toggleButtons(false)));
const closeParent = this.closeCrossNode.parentNode;
closeParent?.removeChild(this.closeCrossNode);
disposables.push(
Disposable.create(() => {
closeParent?.appendChild(this.closeCrossNode);
})
);
this.errorMessageNode.classList.add('progress');
disposables.push(
Disposable.create(() =>
this.errorMessageNode.classList.remove('progress')
)
);
const errorParent = this.errorMessageNode.parentNode;
errorParent?.insertBefore(spinner, this.errorMessageNode);
disposables.push(
Disposable.create(() => errorParent?.removeChild(spinner))
);
const cancellationSource = new CancellationTokenSource();
const progress: Progress = {
id: v4(),
cancel: () => cancellationSource.cancel(),
report: (update: ProgressUpdate) => {
this.setProgressMessage(update);
},
result: Promise.resolve(value),
};
await this.withProgress(value)(progress);
} finally {
disposables.dispose();
}
this.resolve(value);
Widget.detach(this);
}
}
private toggleButtons(disabled: boolean): void {
if (this.acceptButton) {
this.acceptButton.disabled = disabled;
}
if (this.closeButton) {
this.closeButton.disabled = disabled;
}
}
private setProgressMessage(update: ProgressUpdate): void {
if (update.work && update.work.done === update.work.total) {
this.errorMessageNode.innerText = '';
} else {
if (update.message) {
this.errorMessageNode.innerText = update.message;
}
}
}
}

View File

@@ -1,7 +1,6 @@
import { nls } from '@theia/core/lib/common';
import { injectable } from '@theia/core/shared/inversify';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import {
SketchContribution,
URI,
@@ -17,17 +16,12 @@ export class NewSketch extends SketchContribution {
registry.registerCommand(NewSketch.Commands.NEW_SKETCH, {
execute: () => this.newSketch(),
});
registry.registerCommand(NewSketch.Commands.NEW_SKETCH__TOOLBAR, {
isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left',
execute: () => registry.executeCommand(NewSketch.Commands.NEW_SKETCH.id),
});
}
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: NewSketch.Commands.NEW_SKETCH.id,
label: nls.localize('arduino/sketch/new', 'New'),
label: nls.localize('arduino/sketch/new', 'New Sketch'),
order: '0',
});
}
@@ -54,8 +48,5 @@ export namespace NewSketch {
export const NEW_SKETCH: Command = {
id: 'arduino-new-sketch',
};
export const NEW_SKETCH__TOOLBAR: Command = {
id: 'arduino-new-sketch--toolbar',
};
}
}

View File

@@ -15,6 +15,7 @@ import { MainMenuManager } from '../../common/main-menu-manager';
import { OpenSketch } from './open-sketch';
import { NotificationCenter } from '../notification-center';
import { nls } from '@theia/core/lib/common';
import { SketchesError } from '../../common/protocol';
@injectable()
export class OpenRecentSketch extends SketchContribution {
@@ -33,7 +34,7 @@ export class OpenRecentSketch extends SketchContribution {
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
protected toDisposeBeforeRegister = new Map<string, DisposableCollection>();
protected toDispose = new DisposableCollection();
override onStart(): void {
this.notificationCenter.onRecentSketchesDidChange(({ sketches }) =>
@@ -42,8 +43,12 @@ export class OpenRecentSketch extends SketchContribution {
}
override async onReady(): Promise<void> {
this.update();
}
private update(forceUpdate?: boolean): void {
this.sketchService
.recentlyOpenedSketches()
.recentlyOpenedSketches(forceUpdate)
.then((sketches) => this.refreshMenu(sketches));
}
@@ -62,19 +67,25 @@ export class OpenRecentSketch extends SketchContribution {
protected register(sketches: Sketch[]): void {
const order = 0;
this.toDispose.dispose();
for (const sketch of sketches) {
const { uri } = sketch;
const toDispose = this.toDisposeBeforeRegister.get(uri);
if (toDispose) {
toDispose.dispose();
}
const command = { id: `arduino-open-recent--${uri}` };
const handler = {
execute: () =>
this.commandRegistry.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id,
sketch
),
execute: async () => {
try {
await this.commandRegistry.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id,
sketch
);
} catch (err) {
if (SketchesError.NotFound.is(err)) {
this.update(true);
} else {
throw err;
}
}
},
};
this.commandRegistry.registerCommand(command, handler);
this.menuRegistry.registerMenuAction(
@@ -85,8 +96,7 @@ export class OpenRecentSketch extends SketchContribution {
order: String(order),
}
);
this.toDisposeBeforeRegister.set(
sketch.uri,
this.toDispose.pushAll([
new DisposableCollection(
Disposable.create(() =>
this.commandRegistry.unregisterCommand(command)
@@ -94,8 +104,8 @@ export class OpenRecentSketch extends SketchContribution {
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(command)
)
)
);
),
]);
}
}
}

View File

@@ -2,7 +2,7 @@ import { nls } from '@theia/core/lib/common/nls';
import { injectable } from '@theia/core/shared/inversify';
import type { EditorOpenerOptions } from '@theia/editor/lib/browser/editor-manager';
import { Later } from '../../common/nls';
import { SketchesError } from '../../common/protocol';
import { Sketch, SketchesError } from '../../common/protocol';
import {
Command,
CommandRegistry,
@@ -10,12 +10,18 @@ import {
URI,
} from './contribution';
import { SaveAsSketch } from './save-as-sketch';
import { promptMoveSketch } from './open-sketch';
import { ApplicationError } from '@theia/core/lib/common/application-error';
import { Deferred, wait } from '@theia/core/lib/common/promise-util';
import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
@injectable()
export class OpenSketchFiles extends SketchContribution {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenSketchFiles.Commands.OPEN_SKETCH_FILES, {
execute: (uri: URI) => this.openSketchFiles(uri),
execute: (uri: URI, focusMainSketchFile) =>
this.openSketchFiles(uri, focusMainSketchFile),
});
registry.registerCommand(OpenSketchFiles.Commands.ENSURE_OPENED, {
execute: (
@@ -28,13 +34,19 @@ export class OpenSketchFiles extends SketchContribution {
});
}
private async openSketchFiles(uri: URI): Promise<void> {
private async openSketchFiles(
uri: URI,
focusMainSketchFile = false
): Promise<void> {
try {
const sketch = await this.sketchService.loadSketch(uri.toString());
const { mainFileUri, rootFolderFileUris } = sketch;
for (const uri of [mainFileUri, ...rootFolderFileUris]) {
await this.ensureOpened(uri);
}
if (focusMainSketchFile) {
await this.ensureOpened(mainFileUri, true, { mode: 'activate' });
}
if (mainFileUri.endsWith('.pde')) {
const message = nls.localize(
'arduino/common/oldFormat',
@@ -55,9 +67,25 @@ export class OpenSketchFiles extends SketchContribution {
}
});
}
const { workspaceError } = this.workspaceService;
// This happens when the IDE2 has been started (from either a terminal or clicking on an `ino` file) with a /path/to/invalid/sketch. (#964)
if (SketchesError.InvalidName.is(workspaceError)) {
await this.promptMove(workspaceError);
}
} catch (err) {
// This happens when the user gracefully closed IDE2, all went well
// but the main sketch file was renamed outside of IDE2 and when the user restarts the IDE2
// the workspace path still exists, but the sketch path is not valid anymore. (#964)
if (SketchesError.InvalidName.is(err)) {
const movedSketch = await this.promptMove(err);
if (!movedSketch) {
// If user did not accept the move, or move was not possible, force reload with a fallback.
return this.openFallbackSketch();
}
}
if (SketchesError.NotFound.is(err)) {
this.openFallbackSketch();
return this.openFallbackSketch();
} else {
console.error(err);
const message =
@@ -71,6 +99,31 @@ export class OpenSketchFiles extends SketchContribution {
}
}
private async promptMove(
err: ApplicationError<
number,
{
invalidMainSketchUri: string;
}
>
): Promise<Sketch | undefined> {
const { invalidMainSketchUri } = err.data;
requestAnimationFrame(() => this.messageService.error(err.message));
await wait(250); // let IDE2 open the editor and toast the error message, then open the modal dialog
const movedSketch = await promptMoveSketch(invalidMainSketchUri, {
fileService: this.fileService,
sketchService: this.sketchService,
labelProvider: this.labelProvider,
});
if (movedSketch) {
this.workspaceService.open(new URI(movedSketch.uri), {
preserveWindow: true,
});
return movedSketch;
}
return undefined;
}
private async openFallbackSketch(): Promise<void> {
const sketch = await this.sketchService.createNewSketch();
this.workspaceService.open(new URI(sketch.uri), { preserveWindow: true });
@@ -80,20 +133,84 @@ export class OpenSketchFiles extends SketchContribution {
uri: string,
forceOpen = false,
options?: EditorOpenerOptions
): Promise<unknown> {
): Promise<EditorWidget | undefined> {
const widget = this.editorManager.all.find(
(widget) => widget.editor.uri.toString() === uri
);
if (!widget || forceOpen) {
return this.editorManager.open(
if (widget && !forceOpen) {
return widget;
}
const disposables = new DisposableCollection();
const deferred = new Deferred<EditorWidget>();
// An editor can be in two primary states:
// - The editor is not yet opened. The `widget` is `undefined`. With `editorManager#open`, Theia will create an editor and fire an `editorManager#onCreated` event.
// - The editor is opened. Can be active, current, or open.
// - If the editor has the focus (the cursor blinks in the editor): it's the active editor.
// - If the editor does not have the focus (the focus is on a different widget or the context menu is opened in the editor): it's the current editor.
// - If the editor is not the top editor in the main area, it's opened.
if (!widget) {
// If the widget is `undefined`, IDE2 expects one `onCreate` event. Subscribe to the `onCreated` event
// and resolve the promise with the editor only when the new editor's visibility changes.
disposables.push(
this.editorManager.onCreated((editor) => {
if (editor.editor.uri.toString() === uri) {
if (editor.isAttached && editor.isVisible) {
deferred.resolve(editor);
} else {
disposables.push(
editor.onDidChangeVisibility((visible) => {
if (visible) {
// wait an animation frame. although the visible and attached props are true the editor is not there.
// let the browser render the widget
setTimeout(
() =>
requestAnimationFrame(() => deferred.resolve(editor)),
0
);
}
})
);
}
}
})
);
}
this.editorManager
.open(
new URI(uri),
options ?? {
mode: 'reveal',
preview: false,
counter: 0,
}
)
.then((editorWidget) => {
// If the widget was defined, it was already opened.
// The editor is expected to be attached to the shell and visible in the UI.
// The deferred promise does not have to wait for the `editorManager#onCreated` event.
// It can resolve earlier.
if (widget) {
deferred.resolve(editorWidget);
}
});
const timeout = 5_000; // number of ms IDE2 waits for the editor to show up in the UI
const result: EditorWidget | undefined | 'timeout' = await Promise.race([
deferred.promise,
wait(timeout).then(() => {
disposables.dispose();
return 'timeout' as const;
}),
]);
if (result === 'timeout') {
console.warn(
`Timeout after ${timeout} millis. The editor has not shown up in time. URI: ${uri}`
);
return undefined;
}
return result;
}
}
export namespace OpenSketchFiles {

View File

@@ -1,115 +1,50 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { MaybePromise } from '@theia/core/lib/common/types';
import { Widget, ContextMenuRenderer } from '@theia/core/lib/browser';
import { nls } from '@theia/core/lib/common/nls';
import { injectable } from '@theia/core/shared/inversify';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
SketchesError,
SketchesService,
SketchRef,
} from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import {
SketchContribution,
Sketch,
URI,
Command,
CommandRegistry,
MenuModelRegistry,
KeybindingRegistry,
MenuModelRegistry,
Sketch,
SketchContribution,
URI,
} from './contribution';
import { ExamplesService } from '../../common/protocol/examples-service';
import { BuiltInExamples } from './examples';
import { Sketchbook } from './sketchbook';
import { SketchContainer } from '../../common/protocol';
import { nls } from '@theia/core/lib/common';
export type SketchLocation = string | URI | SketchRef;
export namespace SketchLocation {
export function toUri(location: SketchLocation): URI {
if (typeof location === 'string') {
return new URI(location);
} else if (SketchRef.is(location)) {
return toUri(location.uri);
} else {
return location;
}
}
export function is(arg: unknown): arg is SketchLocation {
return typeof arg === 'string' || arg instanceof URI || SketchRef.is(arg);
}
}
@injectable()
export class OpenSketch extends SketchContribution {
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(BuiltInExamples)
protected readonly builtInExamples: BuiltInExamples;
@inject(ExamplesService)
protected readonly examplesService: ExamplesService;
@inject(Sketchbook)
protected readonly sketchbook: Sketchbook;
protected readonly toDispose = new DisposableCollection();
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, {
execute: (arg) =>
Sketch.is(arg) ? this.openSketch(arg) : this.openSketch(),
});
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH__TOOLBAR, {
isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left',
execute: async (_: Widget, target: EventTarget) => {
const container = await this.sketchService.getSketches({
exclude: ['**/hardware/**'],
});
if (SketchContainer.isEmpty(container)) {
this.openSketch();
} else {
this.toDispose.dispose();
if (!(target instanceof HTMLElement)) {
return;
}
const { parentElement } = target;
if (!parentElement) {
return;
}
this.menuRegistry.registerMenuAction(
ArduinoMenus.OPEN_SKETCH__CONTEXT__OPEN_GROUP,
{
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
label: nls.localize(
'vscode/workspaceActions/openFileFolder',
'Open...'
),
}
);
this.toDispose.push(
Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(
OpenSketch.Commands.OPEN_SKETCH
)
)
);
this.sketchbook.registerRecursively(
[...container.children, ...container.sketches],
ArduinoMenus.OPEN_SKETCH__CONTEXT__RECENT_GROUP,
this.toDispose
);
try {
const containers = await this.examplesService.builtIns();
for (const container of containers) {
this.builtInExamples.registerRecursively(
container,
ArduinoMenus.OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP,
this.toDispose
);
}
} catch (e) {
console.error('Error when collecting built-in examples.', e);
}
const options = {
menuPath: ArduinoMenus.OPEN_SKETCH__CONTEXT,
anchor: {
x: parentElement.getBoundingClientRect().left,
y:
parentElement.getBoundingClientRect().top +
parentElement.offsetHeight,
},
};
this.contextMenuRenderer.render(options);
execute: async (arg) => {
const toOpen = !SketchLocation.is(arg)
? await this.selectSketch()
: arg;
if (toOpen) {
return this.openSketch(toOpen);
}
},
});
@@ -119,7 +54,7 @@ export class OpenSketch extends SketchContribution {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
label: nls.localize('vscode/workspaceActions/openFileFolder', 'Open...'),
order: '1',
order: '2',
});
}
@@ -130,30 +65,40 @@ export class OpenSketch extends SketchContribution {
});
}
async openSketch(
toOpen: MaybePromise<Sketch | undefined> = this.selectSketch()
): Promise<void> {
const sketch = await toOpen;
if (sketch) {
this.workspaceService.open(new URI(sketch.uri));
private async openSketch(toOpen: SketchLocation | undefined): Promise<void> {
if (!toOpen) {
return;
}
const uri = SketchLocation.toUri(toOpen);
try {
await this.sketchService.loadSketch(uri.toString());
} catch (err) {
if (SketchesError.NotFound.is(err)) {
this.messageService.error(err.message);
}
throw err;
}
this.workspaceService.open(uri);
}
protected async selectSketch(): Promise<Sketch | undefined> {
private async selectSketch(): Promise<Sketch | undefined> {
const config = await this.configService.getConfiguration();
const defaultPath = await this.fileService.fsPath(
new URI(config.sketchDirUri)
);
const { filePaths } = await remote.dialog.showOpenDialog({
defaultPath,
properties: ['createDirectory', 'openFile'],
filters: [
{
name: nls.localize('arduino/sketch/sketch', 'Sketch'),
extensions: ['ino', 'pde'],
},
],
});
const { filePaths } = await remote.dialog.showOpenDialog(
remote.getCurrentWindow(),
{
defaultPath,
properties: ['createDirectory', 'openFile'],
filters: [
{
name: nls.localize('arduino/sketch/sketch', 'Sketch'),
extensions: ['ino', 'pde'],
},
],
}
);
if (!filePaths.length) {
return undefined;
}
@@ -169,45 +114,11 @@ export class OpenSketch extends SketchContribution {
return sketch;
}
if (Sketch.isSketchFile(sketchFileUri)) {
const name = new URI(sketchFileUri).path.name;
const nameWithExt = this.labelProvider.getName(new URI(sketchFileUri));
const { response } = await remote.dialog.showMessageBox({
title: nls.localize('arduino/sketch/moving', 'Moving'),
type: 'question',
buttons: [
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
nls.localize('vscode/issueMainService/ok', 'OK'),
],
message: nls.localize(
'arduino/sketch/movingMsg',
'The file "{0}" needs to be inside a sketch folder named "{1}".\nCreate this folder, move the file, and continue?',
nameWithExt,
name
),
return promptMoveSketch(sketchFileUri, {
fileService: this.fileService,
sketchService: this.sketchService,
labelProvider: this.labelProvider,
});
if (response === 1) {
// OK
const newSketchUri = new URI(sketchFileUri).parent.resolve(name);
const exists = await this.fileService.exists(newSketchUri);
if (exists) {
await remote.dialog.showMessageBox({
type: 'error',
title: nls.localize('vscode/dialog/dialogErrorMessage', 'Error'),
message: nls.localize(
'arduino/sketch/cantOpen',
'A folder named "{0}" already exists. Can\'t open sketch.',
name
),
});
return undefined;
}
await this.fileService.createFolder(newSketchUri);
await this.fileService.move(
new URI(sketchFileUri),
new URI(newSketchUri.resolve(nameWithExt).toString())
);
return this.sketchService.getSketchFolder(newSketchUri.toString());
}
}
}
}
@@ -217,8 +128,57 @@ export namespace OpenSketch {
export const OPEN_SKETCH: Command = {
id: 'arduino-open-sketch',
};
export const OPEN_SKETCH__TOOLBAR: Command = {
id: 'arduino-open-sketch--toolbar',
};
}
}
export async function promptMoveSketch(
sketchFileUri: string | URI,
options: {
fileService: FileService;
sketchService: SketchesService;
labelProvider: LabelProvider;
}
): Promise<Sketch | undefined> {
const { fileService, sketchService, labelProvider } = options;
const uri =
sketchFileUri instanceof URI ? sketchFileUri : new URI(sketchFileUri);
const name = uri.path.name;
const nameWithExt = labelProvider.getName(uri);
const { response } = await remote.dialog.showMessageBox({
title: nls.localize('arduino/sketch/moving', 'Moving'),
type: 'question',
buttons: [
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
nls.localize('vscode/issueMainService/ok', 'OK'),
],
message: nls.localize(
'arduino/sketch/movingMsg',
'The file "{0}" needs to be inside a sketch folder named "{1}".\nCreate this folder, move the file, and continue?',
nameWithExt,
name
),
});
if (response === 1) {
// OK
const newSketchUri = uri.parent.resolve(name);
const exists = await fileService.exists(newSketchUri);
if (exists) {
await remote.dialog.showMessageBox({
type: 'error',
title: nls.localize('vscode/dialog/dialogErrorMessage', 'Error'),
message: nls.localize(
'arduino/sketch/cantOpen',
'A folder named "{0}" already exists. Can\'t open sketch.',
name
),
});
return undefined;
}
await fileService.createFolder(newSketchUri);
await fileService.move(
uri,
new URI(newSketchUri.resolve(nameWithExt).toString())
);
return sketchService.getSketchFolder(newSketchUri.toString());
}
}

View File

@@ -12,21 +12,19 @@ import {
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { ApplicationShell, NavigatableWidget, Saveable } from '@theia/core/lib/browser';
import { EditorManager } from '@theia/editor/lib/browser';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { WorkspaceInput } from '@theia/workspace/lib/browser';
import { StartupTask } from '../../electron-common/startup-task';
import { DeleteSketch } from './delete-sketch';
@injectable()
export class SaveAsSketch extends SketchContribution {
@inject(ApplicationShell)
protected readonly applicationShell: ApplicationShell;
@inject(EditorManager)
protected override readonly editorManager: EditorManager;
private readonly applicationShell: ApplicationShell;
@inject(WindowService)
protected readonly windowService: WindowService;
private readonly windowService: WindowService;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH, {
@@ -52,7 +50,7 @@ export class SaveAsSketch extends SketchContribution {
/**
* Resolves `true` if the sketch was successfully saved as something.
*/
async saveAs(
private async saveAs(
{
execOnlyIfTemp,
openAfterMove,
@@ -60,7 +58,10 @@ export class SaveAsSketch extends SketchContribution {
markAsRecentlyOpened,
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
): Promise<boolean> {
const sketch = await this.sketchServiceClient.currentSketch();
const [sketch, configuration] = await Promise.all([
this.sketchServiceClient.currentSketch(),
this.configService.getConfiguration(),
]);
if (!CurrentSketch.isValid(sketch)) {
return false;
}
@@ -70,27 +71,38 @@ export class SaveAsSketch extends SketchContribution {
return false;
}
const sketchUri = new URI(sketch.uri);
const sketchbookDirUri = new URI(configuration.sketchDirUri);
// If the sketch is temp, IDE2 proposes the default sketchbook folder URI.
// If the sketch is not temp, but not contained in the default sketchbook folder, IDE2 proposes the default location.
// Otherwise, it proposes the parent folder of the current sketch.
const containerDirUri = isTemp
? sketchbookDirUri
: !sketchbookDirUri.isEqualOrParent(sketchUri)
? sketchbookDirUri
: sketchUri.parent;
const exists = await this.fileService.exists(
containerDirUri.resolve(sketch.name)
);
// If target does not exist, propose a `directories.user`/${sketch.name} path
// If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss}
const sketchDirUri = new URI(
(await this.configService.getConfiguration()).sketchDirUri
);
const exists = await this.fileService.exists(
sketchDirUri.resolve(sketch.name)
);
const defaultUri = sketchDirUri.resolve(
const defaultUri = containerDirUri.resolve(
exists
? `${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`
: sketch.name
);
const defaultPath = await this.fileService.fsPath(defaultUri);
const { filePath, canceled } = await remote.dialog.showSaveDialog({
title: nls.localize(
'arduino/sketch/saveFolderAs',
'Save sketch folder as...'
),
defaultPath,
});
const { filePath, canceled } = await remote.dialog.showSaveDialog(
remote.getCurrentWindow(),
{
title: nls.localize(
'arduino/sketch/saveFolderAs',
'Save sketch folder as...'
),
defaultPath,
}
);
if (!filePath || canceled) {
return false;
}
@@ -107,21 +119,19 @@ export class SaveAsSketch extends SketchContribution {
this.sketchService.markAsRecentlyOpened(workspaceUri);
}
}
const options: WorkspaceInput & StartupTask.Owner = {
preserveWindow: true,
tasks: [],
};
if (workspaceUri && openAfterMove) {
this.windowService.setSafeToShutDown();
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
// This window will navigate away.
// Explicitly stop the contribution to dispose the file watcher before deleting the temp sketch.
// Otherwise, users might see irrelevant _Unable to watch for file changes in this large workspace._ notification.
// https://github.com/arduino/arduino-ide/issues/39.
this.sketchServiceClient.onStop();
// TODO: consider implementing the temp sketch deletion the following way:
// Open the other sketch with a `delete the temp sketch` startup-task.
this.sketchService.notifyDeleteSketch(sketch); // This is a notification and will execute on the backend.
options.tasks.push({
command: DeleteSketch.Commands.DELETE_SKETCH.id,
args: [sketch.uri],
});
}
this.workspaceService.open(new URI(workspaceUri), {
preserveWindow: true,
});
this.workspaceService.open(new URI(workspaceUri), options);
}
return !!workspaceUri;
}

View File

@@ -1,7 +1,6 @@
import { injectable } from '@theia/core/shared/inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { SaveAsSketch } from './save-as-sketch';
import {
SketchContribution,
@@ -19,19 +18,13 @@ export class SaveSketch extends SketchContribution {
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH, {
execute: () => this.saveSketch(),
});
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH__TOOLBAR, {
isVisible: (widget) =>
ArduinoToolbar.is(widget) && widget.side === 'left',
execute: () =>
registry.executeCommand(SaveSketch.Commands.SAVE_SKETCH.id),
});
}
override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: SaveSketch.Commands.SAVE_SKETCH.id,
label: nls.localize('vscode/fileCommands/save', 'Save'),
order: '6',
order: '7',
});
}
@@ -68,8 +61,5 @@ export namespace SaveSketch {
export const SAVE_SKETCH: Command = {
id: 'arduino-save-sketch',
};
export const SAVE_SKETCH__TOOLBAR: Command = {
id: 'arduino-save-sketch--toolbar',
};
}
}

View File

@@ -176,7 +176,7 @@ export class SketchControl extends SketchContribution {
{
commandId: command.id,
label: this.labelProvider.getName(uri),
order: `${i}`,
order: String(i).padStart(4),
}
);
this.toDisposeBeforeCreateNewContextMenu.push(
@@ -235,7 +235,7 @@ export class SketchControl extends SketchContribution {
});
registry.registerKeybinding({
command: CommonCommands.PREVIOUS_TAB.id,
keybinding: 'CtrlCmd+Alt+Left', // TODO: check why electron does not show the keybindings in the UI.
keybinding: 'CtrlCmd+Alt+Left',
});
registry.registerKeybinding({
command: CommonCommands.NEXT_TAB.id,

View File

@@ -4,7 +4,7 @@ import { inject, injectable } from '@theia/core/shared/inversify';
import { FileSystemFrontendContribution } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution';
import { FileChangeType } from '@theia/filesystem/lib/common/files';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { Sketch, SketchContribution, URI } from './contribution';
import { Sketch, SketchContribution } from './contribution';
import { OpenSketchFiles } from './open-sketch-files';
@injectable()
@@ -31,7 +31,6 @@ export class SketchFilesTracker extends SketchContribution {
override onReady(): void {
this.sketchServiceClient.currentSketch().then(async (sketch) => {
if (CurrentSketch.isValid(sketch)) {
this.toDisposeOnStop.push(this.fileService.watch(new URI(sketch.uri)));
this.toDisposeOnStop.push(
this.fileService.onDidFilesChange(async (event) => {
for (const { type, resource } of event.changes) {

View File

@@ -1,32 +1,14 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { injectable } from '@theia/core/shared/inversify';
import { CommandHandler } from '@theia/core/lib/common/command';
import { CommandRegistry, MenuModelRegistry } from './contribution';
import { MenuModelRegistry } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { MainMenuManager } from '../../common/main-menu-manager';
import { NotificationCenter } from '../notification-center';
import { Examples } from './examples';
import {
SketchContainer,
SketchesError,
SketchRef,
} from '../../common/protocol';
import { SketchContainer, SketchesError } from '../../common/protocol';
import { OpenSketch } from './open-sketch';
import { nls } from '@theia/core/lib/common';
import { nls } from '@theia/core/lib/common/nls';
@injectable()
export class Sketchbook extends Examples {
@inject(CommandRegistry)
protected override readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry)
protected override readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;
override onStart(): void {
this.sketchServiceClient.onSketchbookDidChange(() => this.update());
}
@@ -35,10 +17,10 @@ export class Sketchbook extends Examples {
this.update();
}
private update() {
protected override update(): void {
this.sketchService.getSketches({}).then((container) => {
this.register(container);
this.mainMenuManager.update();
this.menuManager.update();
});
}
@@ -50,7 +32,7 @@ export class Sketchbook extends Examples {
);
}
protected register(container: SketchContainer): void {
private register(container: SketchContainer): void {
this.toDispose.dispose();
this.registerRecursively(
[...container.children, ...container.sketches],
@@ -62,23 +44,18 @@ export class Sketchbook extends Examples {
protected override createHandler(uri: string): CommandHandler {
return {
execute: async () => {
let sketch: SketchRef | undefined = undefined;
try {
sketch = await this.sketchService.loadSketch(uri);
} catch (err) {
if (SketchesError.NotFound.is(err)) {
// To handle the following:
// Open IDE2, delete a sketch from sketchbook, click on File > Sketchbook > the deleted sketch.
// Filesystem watcher misses out delete events on macOS; hence IDE2 has no chance to update the menu items.
this.messageService.error(err.message);
this.update();
}
}
if (sketch) {
await this.commandService.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id,
sketch
uri
);
} catch (err) {
if (SketchesError.NotFound.is(err)) {
// Force update the menu items to remove the absent sketch.
this.update();
} else {
throw err;
}
}
},
};

View File

@@ -0,0 +1,52 @@
import * as remote from '@theia/core/electron-shared/@electron/remote';
import type { IpcRendererEvent } from '@theia/core/electron-shared/electron';
import { ipcRenderer } from '@theia/core/electron-shared/electron';
import { injectable } from '@theia/core/shared/inversify';
import { StartupTask } from '../../electron-common/startup-task';
import { Contribution } from './contribution';
@injectable()
export class StartupTasks extends Contribution {
override onReady(): void {
ipcRenderer.once(
StartupTask.Messaging.STARTUP_TASKS_SIGNAL,
(_: IpcRendererEvent, args: unknown) => {
console.debug(
`Received the startup tasks from the electron main process. Args: ${JSON.stringify(
args
)}`
);
if (!StartupTask.has(args)) {
console.warn(`Could not detect 'tasks' from the signal. Skipping.`);
return;
}
const tasks = args.tasks;
if (tasks.length) {
console.log(`Executing startup tasks:`);
tasks.forEach(({ command, args = [] }) => {
console.log(
` - '${command}' ${
args.length ? `, args: ${JSON.stringify(args)}` : ''
}`
);
this.commandService
.executeCommand(command, ...args)
.catch((err) =>
console.error(
`Error occurred when executing the startup task '${command}'${
args?.length ? ` with args: '${JSON.stringify(args)}` : ''
}.`,
err
)
);
});
}
}
);
const { id } = remote.getCurrentWindow();
console.debug(
`Signalling app ready event to the electron main process. Sender ID: ${id}.`
);
ipcRenderer.send(StartupTask.Messaging.APP_READY_SIGNAL(id));
}
}

View File

@@ -0,0 +1,193 @@
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CoreService, IndexType } from '../../common/protocol';
import { NotificationCenter } from '../notification-center';
import { WindowServiceExt } from '../theia/core/window-service-ext';
import { Command, CommandRegistry, Contribution } from './contribution';
@injectable()
export class UpdateIndexes extends Contribution {
@inject(WindowServiceExt)
private readonly windowService: WindowServiceExt;
@inject(LocalStorageService)
private readonly localStorage: LocalStorageService;
@inject(CoreService)
private readonly coreService: CoreService;
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
protected override init(): void {
super.init();
this.notificationCenter.onIndexUpdateDidComplete(({ summary }) =>
Promise.all(
Object.entries(summary).map(([type, updatedAt]) =>
this.setLastUpdateDateTime(type as IndexType, updatedAt)
)
)
);
}
override onReady(): void {
this.checkForUpdates();
}
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UpdateIndexes.Commands.UPDATE_INDEXES, {
execute: () => this.updateIndexes(IndexType.All, true),
});
registry.registerCommand(UpdateIndexes.Commands.UPDATE_PLATFORM_INDEX, {
execute: () => this.updateIndexes(['platform'], true),
});
registry.registerCommand(UpdateIndexes.Commands.UPDATE_LIBRARY_INDEX, {
execute: () => this.updateIndexes(['library'], true),
});
}
private async checkForUpdates(): Promise<void> {
const checkForUpdates = this.preferences['arduino.checkForUpdates'];
if (!checkForUpdates) {
console.debug(
'[update-indexes]: `arduino.checkForUpdates` is `false`. Skipping updating the indexes.'
);
return;
}
if (await this.windowService.isFirstWindow()) {
const summary = await this.coreService.indexUpdateSummaryBeforeInit();
if (summary.message) {
this.messageService.error(summary.message);
}
const typesToCheck = IndexType.All.filter((type) => !(type in summary));
if (Object.keys(summary).length) {
console.debug(
`[update-indexes]: Detected an index update summary before the core gRPC client initialization. Updating local storage with ${JSON.stringify(
summary
)}`
);
} else {
console.debug(
'[update-indexes]: No index update summary was available before the core gRPC client initialization. Checking the status of the all the index types.'
);
}
await Promise.allSettled([
...Object.entries(summary).map(([type, updatedAt]) =>
this.setLastUpdateDateTime(type as IndexType, updatedAt)
),
this.updateIndexes(typesToCheck),
]);
}
}
private async updateIndexes(
types: IndexType[],
force = false
): Promise<void> {
const updatedAt = new Date().toISOString();
return Promise.all(
types.map((type) => this.needsIndexUpdate(type, updatedAt, force))
).then((needsIndexUpdateResults) => {
const typesToUpdate = needsIndexUpdateResults.filter(IndexType.is);
if (typesToUpdate.length) {
console.debug(
`[update-indexes]: Requesting the index update of type: ${JSON.stringify(
typesToUpdate
)} with date time: ${updatedAt}.`
);
return this.coreService.updateIndex({ types: typesToUpdate });
}
});
}
private async needsIndexUpdate(
type: IndexType,
now: string,
force = false
): Promise<IndexType | false> {
if (force) {
console.debug(
`[update-indexes]: Update for index type: '${type}' was forcefully requested.`
);
return type;
}
const lastUpdateIsoDateTime = await this.getLastUpdateDateTime(type);
if (!lastUpdateIsoDateTime) {
console.debug(
`[update-indexes]: No last update date time was persisted for index type: '${type}'. Index update is required.`
);
return type;
}
const lastUpdateDateTime = Date.parse(lastUpdateIsoDateTime);
if (Number.isNaN(lastUpdateDateTime)) {
console.debug(
`[update-indexes]: Invalid last update date time was persisted for index type: '${type}'. Last update date time was: ${lastUpdateDateTime}. Index update is required.`
);
return type;
}
const diff = new Date(now).getTime() - lastUpdateDateTime;
const needsIndexUpdate = diff >= this.threshold;
console.debug(
`[update-indexes]: Update for index type '${type}' is ${
needsIndexUpdate ? '' : 'not '
}required. Now: ${now}, Last index update date time: ${new Date(
lastUpdateDateTime
).toISOString()}, diff: ${diff} ms, threshold: ${this.threshold} ms.`
);
return needsIndexUpdate ? type : false;
}
private async getLastUpdateDateTime(
type: IndexType
): Promise<string | undefined> {
const key = this.storageKeyOf(type);
return this.localStorage.getData<string>(key);
}
private async setLastUpdateDateTime(
type: IndexType,
updatedAt: string
): Promise<void> {
const key = this.storageKeyOf(type);
return this.localStorage.setData<string>(key, updatedAt).finally(() => {
console.debug(
`[update-indexes]: Updated the last index update date time of '${type}' to ${updatedAt}.`
);
});
}
private storageKeyOf(type: IndexType): string {
return `index-last-update-time--${type}`;
}
private get threshold(): number {
return 4 * 60 * 60 * 1_000; // four hours in millis
}
}
export namespace UpdateIndexes {
export namespace Commands {
export const UPDATE_INDEXES: Command & { label: string } = {
id: 'arduino-update-indexes',
label: nls.localize(
'arduino/updateIndexes/updateIndexes',
'Update Indexes'
),
category: 'Arduino',
};
export const UPDATE_PLATFORM_INDEX: Command & { label: string } = {
id: 'arduino-update-package-index',
label: nls.localize(
'arduino/updateIndexes/updatePackageIndex',
'Update Package Index'
),
category: 'Arduino',
};
export const UPDATE_LIBRARY_INDEX: Command & { label: string } = {
id: 'arduino-update-library-index',
label: nls.localize(
'arduino/updateIndexes/updateLibraryIndex',
'Update Library Index'
),
category: 'Arduino',
};
}
}

View File

@@ -1,7 +1,7 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { BoardUserField, CoreService, Port } from '../../common/protocol';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { CoreService, Port } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import {
Command,
@@ -11,96 +11,36 @@ import {
TabBarToolbarRegistry,
CoreServiceContribution,
} from './contribution';
import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog';
import { deepClone, DisposableCollection, nls } from '@theia/core/lib/common';
import { deepClone, nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import type { VerifySketchParams } from './verify-sketch';
import { UserFields } from './user-fields';
@injectable()
export class UploadSketch extends CoreServiceContribution {
@inject(MenuModelRegistry)
private readonly menuRegistry: MenuModelRegistry;
@inject(UserFieldsDialog)
private readonly userFieldsDialog: UserFieldsDialog;
private boardRequiresUserFields = false;
private readonly cachedUserFields: Map<string, BoardUserField[]> = new Map();
private readonly menuActionsDisposables = new DisposableCollection();
private readonly onDidChangeEmitter = new Emitter<void>();
private readonly onDidChange = this.onDidChangeEmitter.event;
private uploadInProgress = false;
protected override init(): void {
super.init();
this.boardsServiceProvider.onBoardsConfigChanged(async () => {
const userFields =
await this.boardsServiceProvider.selectedBoardUserFields();
this.boardRequiresUserFields = userFields.length > 0;
this.registerMenus(this.menuRegistry);
});
}
private selectedFqbnAddress(): string {
const { boardsConfig } = this.boardsServiceProvider;
const fqbn = boardsConfig.selectedBoard?.fqbn;
if (!fqbn) {
return '';
}
const address =
boardsConfig.selectedBoard?.port?.address ||
boardsConfig.selectedPort?.address;
if (!address) {
return '';
}
return fqbn + '|' + address;
}
@inject(UserFields)
private readonly userFields: UserFields;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, {
execute: async () => {
const key = this.selectedFqbnAddress();
if (
this.boardRequiresUserFields &&
key &&
!this.cachedUserFields.has(key)
) {
// Deep clone the array of board fields to avoid editing the cached ones
this.userFieldsDialog.value = (
await this.boardsServiceProvider.selectedBoardUserFields()
).map((f) => ({ ...f }));
const result = await this.userFieldsDialog.open();
if (!result) {
return;
}
this.cachedUserFields.set(key, result);
if (await this.userFields.checkUserFieldsDialog()) {
this.uploadSketch();
}
this.uploadSketch();
},
isEnabled: () => !this.uploadInProgress,
});
registry.registerCommand(UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION, {
execute: async () => {
const key = this.selectedFqbnAddress();
if (!key) {
return;
if (await this.userFields.checkUserFieldsDialog(true)) {
this.uploadSketch();
}
const cached = this.cachedUserFields.get(key);
// Deep clone the array of board fields to avoid editing the cached ones
this.userFieldsDialog.value = (
cached ?? (await this.boardsServiceProvider.selectedBoardUserFields())
).map((f) => ({ ...f }));
const result = await this.userFieldsDialog.open();
if (!result) {
return;
}
this.cachedUserFields.set(key, result);
this.uploadSketch();
},
isEnabled: () => !this.uploadInProgress && this.boardRequiresUserFields,
isEnabled: () => !this.uploadInProgress && this.userFields.isRequired(),
});
registry.registerCommand(
UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER,
@@ -120,45 +60,20 @@ export class UploadSketch extends CoreServiceContribution {
}
override registerMenus(registry: MenuModelRegistry): void {
this.menuActionsDisposables.dispose();
this.menuActionsDisposables.push(
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
label: nls.localize('arduino/sketch/upload', 'Upload'),
order: '1',
})
);
if (this.boardRequiresUserFields) {
this.menuActionsDisposables.push(
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
label: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
order: '2',
})
);
} else {
this.menuActionsDisposables.push(
registry.registerMenuNode(
ArduinoMenus.SKETCH__MAIN_GROUP,
new PlaceholderMenuNode(
ArduinoMenus.SKETCH__MAIN_GROUP,
// commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
{ order: '2' }
)
)
);
}
this.menuActionsDisposables.push(
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
label: nls.localize(
'arduino/sketch/uploadUsingProgrammer',
'Upload Using Programmer'
),
order: '3',
})
);
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
label: nls.localize('arduino/sketch/upload', 'Upload'),
order: '1',
});
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
label: nls.localize(
'arduino/sketch/uploadUsingProgrammer',
'Upload Using Programmer'
),
order: '3',
});
}
override registerKeybindings(registry: KeybindingRegistry): void {
@@ -215,18 +130,7 @@ export class UploadSketch extends CoreServiceContribution {
return;
}
// TODO: This does not belong here.
// IDE2 should not do any preliminary checks but let the CLI fail and then toast a user consumable error message.
if (
uploadOptions.userFields.length === 0 &&
this.boardRequiresUserFields
) {
this.messageService.error(
nls.localize(
'arduino/sketch/userFieldsNotFoundError',
"Can't find user fields for connected board"
)
);
if (!this.userFields.checkUserFieldsForUpload()) {
return;
}
@@ -242,6 +146,7 @@ export class UploadSketch extends CoreServiceContribution {
{ timeout: 3000 }
);
} catch (e) {
this.userFields.notifyFailedWithError(e);
this.handleError(e);
} finally {
this.uploadInProgress = false;
@@ -258,7 +163,7 @@ export class UploadSketch extends CoreServiceContribution {
if (!CurrentSketch.isValid(sketch)) {
return undefined;
}
const userFields = this.userFields();
const userFields = this.userFields.getUserFields();
const { boardsConfig } = this.boardsServiceProvider;
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
await Promise.all([
@@ -301,10 +206,6 @@ export class UploadSketch extends CoreServiceContribution {
return port;
}
private userFields(): BoardUserField[] {
return this.cachedUserFields.get(this.selectedFqbnAddress()) ?? [];
}
/**
* Converts the `VENDOR:ARCHITECTURE:BOARD_ID[:MENU_ID=OPTION_ID[,MENU2_ID=OPTION_ID ...]]` FQBN to
* `VENDOR:ARCHITECTURE:BOARD_ID` format.
@@ -328,7 +229,7 @@ export namespace UploadSketch {
id: 'arduino-upload-with-configuration-sketch',
label: nls.localize(
'arduino/sketch/configureAndUpload',
'Configure And Upload'
'Configure and Upload'
),
category: 'Arduino',
};

View File

@@ -0,0 +1,147 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { DisposableCollection, nls } from '@theia/core/lib/common';
import { BoardUserField, CoreError } from '../../common/protocol';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog';
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { MenuModelRegistry, Contribution } from './contribution';
import { UploadSketch } from './upload-sketch';
@injectable()
export class UserFields extends Contribution {
private boardRequiresUserFields = false;
private userFieldsSet = false;
private readonly cachedUserFields: Map<string, BoardUserField[]> = new Map();
private readonly menuActionsDisposables = new DisposableCollection();
@inject(UserFieldsDialog)
private readonly userFieldsDialog: UserFieldsDialog;
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
@inject(MenuModelRegistry)
private readonly menuRegistry: MenuModelRegistry;
protected override init(): void {
super.init();
this.boardsServiceProvider.onBoardsConfigChanged(async () => {
const userFields =
await this.boardsServiceProvider.selectedBoardUserFields();
this.boardRequiresUserFields = userFields.length > 0;
this.registerMenus(this.menuRegistry);
});
}
override registerMenus(registry: MenuModelRegistry): void {
this.menuActionsDisposables.dispose();
if (this.boardRequiresUserFields) {
this.menuActionsDisposables.push(
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
label: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
order: '2',
})
);
} else {
this.menuActionsDisposables.push(
registry.registerMenuNode(
ArduinoMenus.SKETCH__MAIN_GROUP,
new PlaceholderMenuNode(
ArduinoMenus.SKETCH__MAIN_GROUP,
// commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
{ order: '2' }
)
)
);
}
}
private selectedFqbnAddress(): string | undefined {
const { boardsConfig } = this.boardsServiceProvider;
const fqbn = boardsConfig.selectedBoard?.fqbn;
if (!fqbn) {
return undefined;
}
const address =
boardsConfig.selectedBoard?.port?.address ||
boardsConfig.selectedPort?.address ||
'';
return fqbn + '|' + address;
}
private async showUserFieldsDialog(
key: string
): Promise<BoardUserField[] | undefined> {
const cached = this.cachedUserFields.get(key);
// Deep clone the array of board fields to avoid editing the cached ones
this.userFieldsDialog.value = cached
? cached.slice()
: await this.boardsServiceProvider.selectedBoardUserFields();
const result = await this.userFieldsDialog.open();
if (!result) {
return;
}
this.userFieldsSet = true;
this.cachedUserFields.set(key, result);
return result;
}
async checkUserFieldsDialog(forceOpen = false): Promise<boolean> {
const key = this.selectedFqbnAddress();
if (!key) {
return false;
}
/*
If the board requires to be configured with user fields, we want
to show the user fields dialog, but only if they weren't already
filled in or if they were filled in, but the previous upload failed.
*/
if (
!forceOpen &&
(!this.boardRequiresUserFields ||
(this.cachedUserFields.has(key) && this.userFieldsSet))
) {
return true;
}
const userFieldsFilledIn = Boolean(await this.showUserFieldsDialog(key));
return userFieldsFilledIn;
}
checkUserFieldsForUpload(): boolean {
// TODO: This does not belong here.
// IDE2 should not do any preliminary checks but let the CLI fail and then toast a user consumable error message.
if (!this.boardRequiresUserFields || this.getUserFields().length > 0) {
this.userFieldsSet = true;
return true;
}
this.messageService.error(
nls.localize(
'arduino/sketch/userFieldsNotFoundError',
"Can't find user fields for connected board"
)
);
this.userFieldsSet = false;
return false;
}
getUserFields(): BoardUserField[] {
const fqbnAddress = this.selectedFqbnAddress();
if (!fqbnAddress) {
return [];
}
return this.cachedUserFields.get(fqbnAddress) ?? [];
}
isRequired(): boolean {
return this.boardRequiresUserFields;
}
notifyFailedWithError(e: Error): void {
if (this.boardRequiresUserFields && CoreError.UploadFailed.is(e)) {
this.userFieldsSet = false;
}
}
}

View File

@@ -1,4 +1,4 @@
import { URI as Uri } from 'vscode-uri';
import { URI as Uri } from '@theia/core/shared/vscode-uri';
import URI from '@theia/core/lib/common/uri';
import { toPosixPath, parentPosix, posix } from './create-paths';
import { Create } from './typings';
@@ -7,7 +7,9 @@ export namespace CreateUri {
export const scheme = 'arduino-create';
export const root = toUri(posix.sep);
export function toUri(posixPathOrResource: string | Create.Resource): URI {
export function toUri(
posixPathOrResource: string | Create.Resource | Create.Sketch
): URI {
const posixPath =
typeof posixPathOrResource === 'string'
? posixPathOrResource

View File

@@ -171,6 +171,9 @@ export class UploadCertificateDialog extends AbstractDialog<void> {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
const firstButton = this.widget.node.querySelector('button');
firstButton?.focus();
this.widget.busyCallback = this.busyCallback.bind(this);
super.onAfterAttach(msg);
this.update();

View File

@@ -5,10 +5,8 @@ import {
postConstruct,
} from '@theia/core/shared/inversify';
import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { AbstractDialog } from '../../theia/dialogs/dialogs';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { ReactDialog } from '../../theia/dialogs/dialogs';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import {
AvailableBoard,
BoardsServiceProvider,
@@ -23,26 +21,30 @@ import { Port } from '../../../common/protocol';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
@injectable()
export class UploadFirmwareDialogWidget extends ReactWidget {
export class UploadFirmwareDialogProps extends DialogProps {}
@injectable()
export class UploadFirmwareDialog extends ReactDialog<void> {
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
private readonly boardsServiceClient: BoardsServiceProvider;
@inject(ArduinoFirmwareUploader)
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
private readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
@inject(FrontendApplicationStateService)
private readonly appStatusService: FrontendApplicationStateService;
protected updatableFqbns: string[] = [];
protected availableBoards: AvailableBoard[] = [];
protected isOpen = new Object();
private updatableFqbns: string[] = [];
private availableBoards: AvailableBoard[] = [];
private isOpen = new Object();
private busy = false;
public busyCallback = (busy: boolean) => {
return;
};
constructor() {
super();
constructor(
@inject(UploadFirmwareDialogProps)
protected override readonly props: UploadFirmwareDialogProps
) {
super({ title: UploadFirmware.Commands.OPEN.label || '' });
this.node.id = 'firmware-uploader-dialog-container';
this.contentNode.classList.add('firmware-uploader-dialog');
this.acceptButton = undefined;
}
@postConstruct()
@@ -59,77 +61,34 @@ export class UploadFirmwareDialogWidget extends ReactWidget {
});
}
protected flashFirmware(firmware: FirmwareInfo, port: Port): Promise<any> {
this.busyCallback(true);
return this.arduinoFirmwareUploader
.flash(firmware, port)
.finally(() => this.busyCallback(false));
}
protected override onCloseRequest(msg: Message): void {
super.onCloseRequest(msg);
this.isOpen = new Object();
}
protected render(): React.ReactNode {
return (
<form>
<FirmwareUploaderComponent
availableBoards={this.availableBoards}
firmwareUploader={this.arduinoFirmwareUploader}
flashFirmware={this.flashFirmware.bind(this)}
updatableFqbns={this.updatableFqbns}
isOpen={this.isOpen}
/>
</form>
);
}
}
@injectable()
export class UploadFirmwareDialogProps extends DialogProps {}
@injectable()
export class UploadFirmwareDialog extends AbstractDialog<void> {
@inject(UploadFirmwareDialogWidget)
protected readonly widget: UploadFirmwareDialogWidget;
private busy = false;
constructor(
@inject(UploadFirmwareDialogProps)
protected override readonly props: UploadFirmwareDialogProps
) {
super({ title: UploadFirmware.Commands.OPEN.label || '' });
this.node.id = 'firmware-uploader-dialog-container';
this.contentNode.classList.add('firmware-uploader-dialog');
this.acceptButton = undefined;
}
get value(): void {
return;
}
protected override render(): React.ReactNode {
return (
<div>
<form>
<FirmwareUploaderComponent
availableBoards={this.availableBoards}
firmwareUploader={this.arduinoFirmwareUploader}
flashFirmware={this.flashFirmware.bind(this)}
updatableFqbns={this.updatableFqbns}
isOpen={this.isOpen}
/>
</form>
</div>
);
}
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
this.widget.busyCallback = this.busyCallback.bind(this);
const firstButton = this.node.querySelector('button');
firstButton?.focus();
super.onAfterAttach(msg);
this.update();
}
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
protected override handleEnter(event: KeyboardEvent): boolean | void {
return false;
}
@@ -138,11 +97,11 @@ export class UploadFirmwareDialog extends AbstractDialog<void> {
if (this.busy) {
return;
}
this.widget.close();
super.close();
this.isOpen = new Object();
}
busyCallback(busy: boolean): void {
private busyCallback(busy: boolean): void {
this.busy = busy;
if (busy) {
this.closeCrossNode.classList.add('disabled');
@@ -150,4 +109,11 @@ export class UploadFirmwareDialog extends AbstractDialog<void> {
this.closeCrossNode.classList.remove('disabled');
}
}
private flashFirmware(firmware: FirmwareInfo, port: Port): Promise<any> {
this.busyCallback(true);
return this.arduinoFirmwareUploader
.flash(firmware, port)
.finally(() => this.busyCallback(false));
}
}

View File

@@ -1,7 +1,6 @@
import { nls } from '@theia/core/lib/common';
import { shell } from 'electron';
import { shell } from '@theia/core/electron-shared/@electron/remote';
import * as React from '@theia/core/shared/react';
import * as ReactDOM from '@theia/core/shared/react-dom';
import ReactMarkdown from 'react-markdown';
import { ProgressInfo, UpdateInfo } from '../../../common/protocol/ide-updater';
import ProgressBar from '../../components/ProgressBar';
@@ -28,32 +27,19 @@ export const IDEUpdaterComponent = ({
},
}: IDEUpdaterComponentProps): React.ReactElement => {
const { version, releaseNotes } = updateInfo;
const changelogDivRef =
React.useRef() as React.MutableRefObject<HTMLDivElement>;
const [changelog, setChangelog] = React.useState<string>('');
React.useEffect(() => {
if (!!releaseNotes && changelogDivRef.current) {
let changelog: string;
if (typeof releaseNotes === 'string') changelog = releaseNotes;
else
changelog = releaseNotes.reduce((acc, item) => {
return item.note ? (acc += `${item.note}\n\n`) : acc;
}, '');
ReactDOM.render(
<ReactMarkdown
components={{
a: ({ href, children, ...props }) => (
<a onClick={() => href && shell.openExternal(href)} {...props}>
{children}
</a>
),
}}
>
{changelog}
</ReactMarkdown>,
changelogDivRef.current
if (releaseNotes) {
setChangelog(
typeof releaseNotes === 'string'
? releaseNotes
: releaseNotes.reduce(
(acc, item) => (item.note ? (acc += `${item.note}\n\n`) : acc),
''
)
);
}
}, [updateInfo]);
}, [releaseNotes, changelog]);
const DownloadCompleted: () => React.ReactElement = () => (
<div className="ide-updater-dialog--downloaded">
@@ -106,9 +92,24 @@ export const IDEUpdaterComponent = ({
version
)}
</div>
{releaseNotes && (
{changelog && (
<div className="dialogRow changelog-container">
<div className="changelog" ref={changelogDivRef} />
<div className="changelog">
<ReactMarkdown
components={{
a: ({ href, children, ...props }) => (
<a
onClick={() => href && shell.openExternal(href)}
{...props}
>
{children}
</a>
),
}}
>
{changelog}
</ReactMarkdown>
</div>
</div>
)}
</div>

View File

@@ -5,10 +5,8 @@ import {
postConstruct,
} from '@theia/core/shared/inversify';
import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { AbstractDialog } from '../../theia/dialogs/dialogs';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { ReactDialog } from '../../theia/dialogs/dialogs';
import { nls } from '@theia/core';
import { IDEUpdaterComponent, UpdateProgress } from './ide-updater-component';
import {
@@ -20,50 +18,13 @@ import {
import { LocalStorageService } from '@theia/core/lib/browser';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
const DOWNLOAD_PAGE_URL =
'https://www.arduino.cc/en/software#experimental-software';
@injectable()
export class IDEUpdaterDialogWidget extends ReactWidget {
private _updateInfo: UpdateInfo;
private _updateProgress: UpdateProgress = {};
setUpdateInfo(updateInfo: UpdateInfo): void {
this._updateInfo = updateInfo;
this.update();
}
mergeUpdateProgress(updateProgress: UpdateProgress): void {
this._updateProgress = { ...this._updateProgress, ...updateProgress };
this.update();
}
get updateInfo(): UpdateInfo {
return this._updateInfo;
}
get updateProgress(): UpdateProgress {
return this._updateProgress;
}
protected render(): React.ReactNode {
return !!this._updateInfo ? (
<IDEUpdaterComponent
updateInfo={this._updateInfo}
updateProgress={this._updateProgress}
/>
) : null;
}
}
const DOWNLOAD_PAGE_URL = 'https://www.arduino.cc/en/software';
@injectable()
export class IDEUpdaterDialogProps extends DialogProps {}
@injectable()
export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
@inject(IDEUpdaterDialogWidget)
private readonly widget: IDEUpdaterDialogWidget;
export class IDEUpdaterDialog extends ReactDialog<UpdateInfo | undefined> {
@inject(IDEUpdater)
private readonly updater: IDEUpdater;
@@ -76,6 +37,9 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
@inject(WindowService)
private readonly windowService: WindowService;
private _updateInfo: UpdateInfo | undefined;
private _updateProgress: UpdateProgress = {};
constructor(
@inject(IDEUpdaterDialogProps)
protected override readonly props: IDEUpdaterDialogProps
@@ -95,26 +59,34 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
protected init(): void {
this.updaterClient.onUpdaterDidFail((error) => {
this.appendErrorButtons();
this.widget.mergeUpdateProgress({ error });
this.mergeUpdateProgress({ error });
});
this.updaterClient.onDownloadProgressDidChange((progressInfo) => {
this.widget.mergeUpdateProgress({ progressInfo });
this.mergeUpdateProgress({ progressInfo });
});
this.updaterClient.onDownloadDidFinish(() => {
this.appendInstallButtons();
this.widget.mergeUpdateProgress({ downloadFinished: true });
this.mergeUpdateProgress({ downloadFinished: true });
});
}
get value(): UpdateInfo {
return this.widget.updateInfo;
protected render(): React.ReactNode {
return (
this.updateInfo && (
<IDEUpdaterComponent
updateInfo={this.updateInfo}
updateProgress={this.updateProgress}
/>
)
);
}
get value(): UpdateInfo | undefined {
return this.updateInfo;
}
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
this.update();
this.appendInitialButtons();
super.onAfterAttach(msg);
}
@@ -197,15 +169,19 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
}
private skipVersion(): void {
if (!this.updateInfo) {
console.warn(`Nothing to skip. No update info is available`);
return;
}
this.localStorageService.setData<string>(
SKIP_IDE_VERSION,
this.widget.updateInfo.version
this.updateInfo.version
);
this.close();
}
private startDownload(): void {
this.widget.mergeUpdateProgress({
this.mergeUpdateProgress({
downloadStarted: true,
});
this.clearButtons();
@@ -217,31 +193,48 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
this.close();
}
private set updateInfo(updateInfo: UpdateInfo | undefined) {
this._updateInfo = updateInfo;
this.update();
}
private get updateInfo(): UpdateInfo | undefined {
return this._updateInfo;
}
private get updateProgress(): UpdateProgress {
return this._updateProgress;
}
private mergeUpdateProgress(updateProgress: UpdateProgress): void {
this._updateProgress = { ...this._updateProgress, ...updateProgress };
this.update();
}
override async open(
data: UpdateInfo | undefined = undefined
): Promise<UpdateInfo | undefined> {
if (data && data.version) {
this.widget.mergeUpdateProgress({
this.mergeUpdateProgress({
progressInfo: undefined,
downloadStarted: false,
downloadFinished: false,
error: undefined,
});
this.widget.setUpdateInfo(data);
this.updateInfo = data;
return super.open();
}
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
this.update();
}
override close(): void {
this.widget.dispose();
if (
this.widget.updateProgress?.downloadStarted &&
!this.widget.updateProgress?.downloadFinished
this.updateProgress?.downloadStarted &&
!this.updateProgress?.downloadFinished
) {
this.updater.stopDownload();
}

View File

@@ -10,6 +10,7 @@ import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog/fil
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import {
AdditionalUrls,
CompilerWarnings,
CompilerWarningLiterals,
Network,
ProxySettings,
@@ -22,14 +23,22 @@ import {
LanguageInfo,
} from '@theia/core/lib/common/i18n/localization';
import SettingsStepInput from './settings-step-input';
import { InterfaceScale } from '../../contributions/interface-scale';
const maxScale = 280;
const minScale = -60;
const scaleStep = 20;
const maxScale = InterfaceScale.ZoomLevel.toPercentage(
InterfaceScale.ZoomLevel.MAX
);
const minScale = InterfaceScale.ZoomLevel.toPercentage(
InterfaceScale.ZoomLevel.MIN
);
const scaleStep = InterfaceScale.ZoomLevel.Step.toPercentage(
InterfaceScale.ZoomLevel.STEP
);
const maxFontSize = InterfaceScale.FontSize.MAX;
const minFontSize = InterfaceScale.FontSize.MIN;
const fontSizeStep = InterfaceScale.FontSize.STEP;
const maxFontSize = 72;
const minFontSize = 0;
const fontSizeStep = 2;
export class SettingsComponent extends React.Component<
SettingsComponent.Props,
SettingsComponent.State
@@ -171,7 +180,8 @@ export class SettingsComponent extends React.Component<
<div className="column">
<div className="flex-line">
<SettingsStepInput
value={this.state.editorFontSize}
key={`font-size-stepper-${String(this.state.editorFontSize)}`}
initialValue={this.state.editorFontSize}
setSettingsStateValue={this.setFontSize}
step={fontSizeStep}
maxValue={maxFontSize}
@@ -190,29 +200,32 @@ export class SettingsComponent extends React.Component<
</label>
<div>
<SettingsStepInput
value={scalePercentage}
key={`scale-stepper-${String(scalePercentage)}`}
initialValue={scalePercentage}
setSettingsStateValue={this.setInterfaceScale}
step={scaleStep}
maxValue={maxScale}
minValue={minScale}
unitOfMeasure="%"
classNames={{ input: 'theia-input small with-margin' }}
classNames={{
input: 'theia-input small with-margin',
buttonsContainer:
'settings-step-input-buttons-container-perc',
}}
/>
</div>
</div>
<div className="flex-line">
<select
className="theia-select"
value={ThemeService.get().getCurrentTheme().label}
value={this.props.themeService.getCurrentTheme().label}
onChange={this.themeDidChange}
>
{ThemeService.get()
.getThemes()
.map(({ id, label }) => (
<option key={id} value={label}>
{label}
</option>
))}
{this.props.themeService.getThemes().map(({ id, label }) => (
<option key={id} value={label}>
{label}
</option>
))}
</select>
</div>
<div className="flex-line">
@@ -260,7 +273,7 @@ export class SettingsComponent extends React.Component<
>
{CompilerWarningLiterals.map((value) => (
<option key={value} value={value}>
{value}
{CompilerWarnings.labelOf(value)}
</option>
))}
</select>
@@ -398,10 +411,22 @@ export class SettingsComponent extends React.Component<
</form>
<div className="flex-line proxy-settings">
<div className="column">
<div className="flex-line">Host name:</div>
<div className="flex-line">Port number:</div>
<div className="flex-line">Username:</div>
<div className="flex-line">Password:</div>
<div className="flex-line">{`${nls.localize(
'arduino/preferences/proxySettings/hostname',
'Host name'
)}:`}</div>
<div className="flex-line">{`${nls.localize(
'arduino/preferences/proxySettings/port',
'Port number'
)}:`}</div>
<div className="flex-line">{`${nls.localize(
'arduino/preferences/proxySettings/username',
'Username'
)}:`}</div>
<div className="flex-line">{`${nls.localize(
'arduino/preferences/proxySettings/password',
'Password'
)}:`}</div>
</div>
<div className="column stretch">
<div className="flex-line">
@@ -502,6 +527,7 @@ export class SettingsComponent extends React.Component<
canSelectFiles: false,
canSelectMany: false,
canSelectFolders: true,
modal: true,
});
if (uri) {
const sketchbookPath = await this.props.fileService.fsPath(uri);
@@ -540,8 +566,7 @@ export class SettingsComponent extends React.Component<
};
private setInterfaceScale = (percentage: number) => {
const interfaceScale = (percentage - 100) / 20;
const interfaceScale = InterfaceScale.ZoomLevel.fromPercentage(percentage);
this.setState({ interfaceScale });
};
@@ -585,11 +610,11 @@ export class SettingsComponent extends React.Component<
event: React.ChangeEvent<HTMLSelectElement>
): void => {
const { selectedIndex } = event.target.options;
const theme = ThemeService.get().getThemes()[selectedIndex];
const theme = this.props.themeService.getThemes()[selectedIndex];
if (theme) {
this.setState({ themeId: theme.id });
if (ThemeService.get().getCurrentTheme().id !== theme.id) {
ThemeService.get().setCurrentTheme(theme.id);
if (this.props.themeService.getCurrentTheme().id !== theme.id) {
this.props.themeService.setCurrentTheme(theme.id);
}
}
};
@@ -728,6 +753,7 @@ export namespace SettingsComponent {
readonly fileDialogService: FileDialogService;
readonly windowService: WindowService;
readonly localizationProvider: AsyncLocalizationProvider;
readonly themeService: ThemeService;
}
export type State = Settings & {
rawAdditionalUrlsValue: string;

View File

@@ -35,6 +35,9 @@ export class SettingsWidget extends ReactWidget {
@inject(AsyncLocalizationProvider)
protected readonly localizationProvider: AsyncLocalizationProvider;
@inject(ThemeService)
private readonly themeService: ThemeService;
protected render(): React.ReactNode {
return (
<SettingsComponent
@@ -43,6 +46,7 @@ export class SettingsWidget extends ReactWidget {
fileDialogService={this.fileDialogService}
windowService={this.windowService}
localizationProvider={this.localizationProvider}
themeService={this.themeService}
/>
);
}
@@ -59,6 +63,9 @@ export class SettingsDialog extends AbstractDialog<Promise<Settings>> {
@inject(SettingsWidget)
protected readonly widget: SettingsWidget;
@inject(ThemeService)
private readonly themeService: ThemeService;
constructor(
@inject(SettingsDialogProps)
protected override readonly props: SettingsDialogProps
@@ -121,11 +128,11 @@ export class SettingsDialog extends AbstractDialog<Promise<Settings>> {
}
override async open(): Promise<Promise<Settings> | undefined> {
const themeIdBeforeOpen = ThemeService.get().getCurrentTheme().id;
const themeIdBeforeOpen = this.themeService.getCurrentTheme().id;
const result = await super.open();
if (!result) {
if (ThemeService.get().getCurrentTheme().id !== themeIdBeforeOpen) {
ThemeService.get().setCurrentTheme(themeIdBeforeOpen);
if (this.themeService.getCurrentTheme().id !== themeIdBeforeOpen) {
this.themeService.setCurrentTheme(themeIdBeforeOpen);
}
}
return result;
@@ -155,7 +162,6 @@ export class AdditionalUrlsDialog extends AbstractDialog<string[]> {
this.textArea = document.createElement('textarea');
this.textArea.className = 'theia-input';
this.textArea.setAttribute('style', 'flex: 0;');
this.textArea.value = urls
.filter((url) => url.trim())
.filter((url) => !!url)
@@ -181,10 +187,10 @@ export class AdditionalUrlsDialog extends AbstractDialog<string[]> {
);
this.contentNode.appendChild(anchor);
this.appendAcceptButton(nls.localize('vscode/issueMainService/ok', 'OK'));
this.appendCloseButton(
nls.localize('vscode/issueMainService/cancel', 'Cancel')
);
this.appendAcceptButton(nls.localize('vscode/issueMainService/ok', 'OK'));
}
get value(): string[] {

View File

@@ -2,7 +2,7 @@ import * as React from '@theia/core/shared/react';
import classnames from 'classnames';
interface SettingsStepInputProps {
value: number;
initialValue: number;
setSettingsStateValue: (value: number) => void;
step: number;
maxValue: number;
@@ -15,7 +15,7 @@ const SettingsStepInput: React.FC<SettingsStepInputProps> = (
props: SettingsStepInputProps
) => {
const {
value,
initialValue,
setSettingsStateValue,
step,
maxValue,
@@ -24,18 +24,35 @@ const SettingsStepInput: React.FC<SettingsStepInputProps> = (
classNames,
} = props;
const [valueState, setValueState] = React.useState<{
currentValue: number;
isEmptyString: boolean;
}>({
currentValue: initialValue,
isEmptyString: false,
});
const { currentValue, isEmptyString } = valueState;
const clamp = (value: number, min: number, max: number): number => {
return Math.min(Math.max(value, min), max);
};
const resetToInitialState = (): void => {
setValueState({
currentValue: initialValue,
isEmptyString: false,
});
};
const onStep = (
roundingOperation: 'ceil' | 'floor',
stepOperation: (a: number, b: number) => number
): void => {
const valueRoundedToScale = Math[roundingOperation](value / step) * step;
const valueRoundedToScale =
Math[roundingOperation](currentValue / step) * step;
const calculatedValue =
valueRoundedToScale === value
? stepOperation(value, step)
valueRoundedToScale === currentValue
? stepOperation(currentValue, step)
: valueRoundedToScale;
const newValue = clamp(calculatedValue, minValue, maxValue);
@@ -52,33 +69,53 @@ const SettingsStepInput: React.FC<SettingsStepInputProps> = (
const onUserInput = (event: React.ChangeEvent<HTMLInputElement>): void => {
const { value: eventValue } = event.target;
if (eventValue === '') {
setSettingsStateValue(0);
}
const number = Number(eventValue);
if (!isNaN(number) && number !== value) {
const newValue = clamp(number, minValue, maxValue);
setSettingsStateValue(newValue);
}
setValueState({
currentValue: Number(eventValue),
isEmptyString: eventValue === '',
});
};
const upDisabled = value >= maxValue;
const downDisabled = value <= minValue;
/* Prevent the user from entering invalid values */
const onBlur = (event: React.FocusEvent): void => {
if (
(currentValue === initialValue && !isEmptyString) ||
event.currentTarget.contains(event.relatedTarget as Node)
) {
return;
}
const clampedValue = clamp(currentValue, minValue, maxValue);
if (clampedValue === initialValue || isNaN(currentValue) || isEmptyString) {
resetToInitialState();
return;
}
setSettingsStateValue(clampedValue);
};
const valueIsNotWithinRange =
currentValue < minValue || currentValue > maxValue;
const isDisabledException =
valueIsNotWithinRange || isEmptyString || isNaN(currentValue);
const upDisabled = isDisabledException || currentValue >= maxValue;
const downDisabled = isDisabledException || currentValue <= minValue;
return (
<div className="settings-step-input-container">
<div className="settings-step-input-container" onBlur={onBlur}>
<input
className={classnames('settings-step-input-element', classNames?.input)}
value={value.toString()}
value={isEmptyString ? '' : String(currentValue)}
onChange={onUserInput}
type="number"
pattern="[0-9]+"
/>
<div className="settings-step-input-buttons-container">
<div
className={classnames(
'settings-step-input-buttons-container',
classNames?.buttonsContainer
)}
>
<button
className="settings-step-input-button settings-step-input-up-button"
disabled={upDisabled}

View File

@@ -5,7 +5,7 @@ import {
} from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { Emitter } from '@theia/core/lib/common/event';
import { Deferred, timeout } from '@theia/core/lib/common/promise-util';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { deepClone } from '@theia/core/lib/common/objects';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { ThemeService } from '@theia/core/lib/browser/theming';
@@ -25,17 +25,20 @@ import {
LanguageInfo,
} from '@theia/core/lib/common/i18n/localization';
import { ElectronCommands } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution';
import { DefaultTheme } from '@theia/application-package/lib/application-props';
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
export const WINDOW_SETTING = 'window';
export const EDITOR_SETTING = 'editor';
export const FONT_SIZE_SETTING = `${EDITOR_SETTING}.fontSize`;
export const AUTO_SAVE_SETTING = `files.autoSave`;
export const QUICK_SUGGESTIONS_SETTING = `${EDITOR_SETTING}.quickSuggestions`;
export const ARDUINO_SETTING = 'arduino';
export const WINDOW_SETTING = `${ARDUINO_SETTING}.window`;
export const ARDUINO_WINDOW_SETTING = `${ARDUINO_SETTING}.window`;
export const COMPILE_SETTING = `${ARDUINO_SETTING}.compile`;
export const UPLOAD_SETTING = `${ARDUINO_SETTING}.upload`;
export const SKETCHBOOK_SETTING = `${ARDUINO_SETTING}.sketchbook`;
export const AUTO_SCALE_SETTING = `${WINDOW_SETTING}.autoScale`;
export const AUTO_SCALE_SETTING = `${ARDUINO_WINDOW_SETTING}.autoScale`;
export const ZOOM_LEVEL_SETTING = `${WINDOW_SETTING}.zoomLevel`;
export const COMPILE_VERBOSE_SETTING = `${COMPILE_SETTING}.verbose`;
export const COMPILE_WARNINGS_SETTING = `${COMPILE_SETTING}.warnings`;
@@ -53,7 +56,7 @@ export interface Settings {
currentLanguage: string;
autoScaleInterface: boolean; // `arduino.window.autoScale`
interfaceScale: number; // `arduino.window.zoomLevel` https://github.com/eclipse-theia/theia/issues/8751
interfaceScale: number; // `window.zoomLevel`
verboseOnCompile: boolean; // `arduino.compile.verbose`
compilerWarnings: CompilerWarnings; // `arduino.compile.warnings`
verboseOnUpload: boolean; // `arduino.upload.verbose`
@@ -101,6 +104,9 @@ export class SettingsService {
@inject(CommandService)
protected commandService: CommandService;
@inject(ThemeService)
private readonly themeService: ThemeService;
protected readonly onDidChangeEmitter = new Emitter<Readonly<Settings>>();
readonly onDidChange = this.onDidChangeEmitter.event;
protected readonly onDidResetEmitter = new Emitter<Readonly<Settings>>();
@@ -141,10 +147,9 @@ export class SettingsService {
this.preferenceService.get<number>(FONT_SIZE_SETTING, 12),
this.preferenceService.get<string>(
'workbench.colorTheme',
window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches
? 'arduino-theme-dark'
: 'arduino-theme'
DefaultTheme.defaultForOSTheme(
FrontendApplicationConfigProvider.get().defaultTheme
)
),
this.preferenceService.get<Settings.AutoSave>(
AUTO_SAVE_SETTING,
@@ -231,11 +236,7 @@ export class SettingsService {
'Invalid editor font size. It must be a positive integer.'
);
}
if (
!ThemeService.get()
.getThemes()
.find(({ id }) => id === themeId)
) {
if (!this.themeService.getThemes().find(({ id }) => id === themeId)) {
return nls.localize(
'arduino/preferences/invalid.theme',
'Invalid theme.'
@@ -252,7 +253,6 @@ export class SettingsService {
private async savePreference(name: string, value: unknown): Promise<void> {
await this.preferenceService.set(name, value, PreferenceScope.User);
await timeout(5);
}
async save(): Promise<string | true> {
@@ -283,19 +283,20 @@ export class SettingsService {
(config as any).network = network;
(config as any).locale = currentLanguage;
await this.savePreference('editor.fontSize', editorFontSize);
await this.savePreference('workbench.colorTheme', themeId);
await this.savePreference(AUTO_SAVE_SETTING, autoSave);
await this.savePreference('editor.quickSuggestions', quickSuggestions);
await this.savePreference(AUTO_SCALE_SETTING, autoScaleInterface);
await this.savePreference(ZOOM_LEVEL_SETTING, interfaceScale);
await this.savePreference(ZOOM_LEVEL_SETTING, interfaceScale);
await this.savePreference(COMPILE_VERBOSE_SETTING, verboseOnCompile);
await this.savePreference(COMPILE_WARNINGS_SETTING, compilerWarnings);
await this.savePreference(UPLOAD_VERBOSE_SETTING, verboseOnUpload);
await this.savePreference(UPLOAD_VERIFY_SETTING, verifyAfterUpload);
await this.savePreference(SHOW_ALL_FILES_SETTING, sketchbookShowAllFiles);
await this.configService.setConfiguration(config);
await Promise.all([
this.savePreference('editor.fontSize', editorFontSize),
this.savePreference('workbench.colorTheme', themeId),
this.savePreference(AUTO_SAVE_SETTING, autoSave),
this.savePreference('editor.quickSuggestions', quickSuggestions),
this.savePreference(AUTO_SCALE_SETTING, autoScaleInterface),
this.savePreference(ZOOM_LEVEL_SETTING, interfaceScale),
this.savePreference(COMPILE_VERBOSE_SETTING, verboseOnCompile),
this.savePreference(COMPILE_WARNINGS_SETTING, compilerWarnings),
this.savePreference(UPLOAD_VERBOSE_SETTING, verboseOnUpload),
this.savePreference(UPLOAD_VERIFY_SETTING, verifyAfterUpload),
this.savePreference(SHOW_ALL_FILES_SETTING, sketchbookShowAllFiles),
this.configService.setConfiguration(config),
]);
this.onDidChangeEmitter.fire(this._settings);
// after saving all the settings, if we need to change the language we need to perform a reload

View File

@@ -16,9 +16,9 @@ export const UserFieldsComponent = ({
const [boardUserFields, setBoardUserFields] = React.useState<
BoardUserField[]
>(initialBoardUserFields);
const [uploadButtonDisabled, setUploadButtonDisabled] =
React.useState<boolean>(true);
const firstInputElement = React.useRef<HTMLInputElement>(null);
React.useEffect(() => {
setBoardUserFields(initialBoardUserFields);
@@ -48,7 +48,10 @@ export const UserFieldsComponent = ({
React.useEffect(() => {
updateUserFields(boardUserFields);
setUploadButtonDisabled(!allFieldsHaveValues(boardUserFields));
}, [boardUserFields]);
if (firstInputElement.current) {
firstInputElement.current.focus();
}
}, [boardUserFields, updateUserFields]);
return (
<div>
@@ -71,6 +74,7 @@ export const UserFieldsComponent = ({
field.label
)}
onChange={updateUserField(index)}
ref={index === 0 ? firstInputElement : undefined}
/>
</div>
</div>

View File

@@ -1,63 +1,18 @@
import * as React from '@theia/core/shared/react';
import { inject, injectable } from '@theia/core/shared/inversify';
import {
AbstractDialog,
DialogProps,
ReactWidget,
} from '@theia/core/lib/browser';
import { Widget } from '@theia/core/shared/@phosphor/widgets';
import { DialogProps } from '@theia/core/lib/browser/dialogs';
import { Message } from '@theia/core/shared/@phosphor/messaging';
import { UploadSketch } from '../../contributions/upload-sketch';
import { UserFieldsComponent } from './user-fields-component';
import { BoardUserField } from '../../../common/protocol';
@injectable()
export class UserFieldsDialogWidget extends ReactWidget {
protected _currentUserFields: BoardUserField[] = [];
constructor(private cancel: () => void, private accept: () => Promise<void>) {
super();
}
set currentUserFields(userFields: BoardUserField[]) {
this.setUserFields(userFields);
}
get currentUserFields(): BoardUserField[] {
return this._currentUserFields;
}
resetUserFieldsValue(): void {
this._currentUserFields = this._currentUserFields.map((field) => {
field.value = '';
return field;
});
}
protected setUserFields(userFields: BoardUserField[]): void {
this._currentUserFields = userFields;
}
protected render(): React.ReactNode {
return (
<form>
<UserFieldsComponent
initialBoardUserFields={this._currentUserFields}
updateUserFields={this.setUserFields.bind(this)}
cancel={this.cancel}
accept={this.accept}
/>
</form>
);
}
}
import { ReactDialog } from '../../theia/dialogs/dialogs';
@injectable()
export class UserFieldsDialogProps extends DialogProps {}
@injectable()
export class UserFieldsDialog extends AbstractDialog<BoardUserField[]> {
protected readonly widget: UserFieldsDialogWidget;
export class UserFieldsDialog extends ReactDialog<BoardUserField[]> {
private _currentUserFields: BoardUserField[] = [];
constructor(
@inject(UserFieldsDialogProps)
@@ -69,39 +24,36 @@ export class UserFieldsDialog extends AbstractDialog<BoardUserField[]> {
this.titleNode.classList.add('user-fields-dialog-title');
this.contentNode.classList.add('user-fields-dialog-content');
this.acceptButton = undefined;
this.widget = new UserFieldsDialogWidget(
this.close.bind(this),
this.accept.bind(this)
);
}
set value(userFields: BoardUserField[]) {
this.widget.currentUserFields = userFields;
}
get value(): BoardUserField[] {
return this.widget.currentUserFields;
return this._currentUserFields;
}
set value(userFields: BoardUserField[]) {
this._currentUserFields = userFields;
}
protected override render(): React.ReactNode {
return (
<div>
<form>
<UserFieldsComponent
initialBoardUserFields={this.value}
updateUserFields={this.doUpdateUserFields}
cancel={this.doCancel}
accept={this.doAccept}
/>
</form>
</div>
);
}
protected override onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
super.onAfterAttach(msg);
this.update();
}
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.widget.update();
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected override async accept(): Promise<void> {
// If the user presses enter and at least
// a field is empty don't accept the input
@@ -114,8 +66,21 @@ export class UserFieldsDialog extends AbstractDialog<BoardUserField[]> {
}
override close(): void {
this.widget.resetUserFieldsValue();
this.widget.close();
this.resetUserFieldsValue();
super.close();
}
private resetUserFieldsValue(): void {
this.value = this.value.map((field) => {
field.value = '';
return field;
});
}
private readonly doCancel: () => void = () => this.close();
private readonly doAccept: () => Promise<void> = () => this.accept();
private readonly doUpdateUserFields: (userFields: BoardUserField[]) => void =
(userFields: BoardUserField[]) => {
this.value = userFields;
};
}

View File

@@ -0,0 +1,6 @@
<!--Copyright (c) Microsoft Corporation. All rights reserved.-->
<!--Copyright (C) 2020 TypeFox and others.-->
<!--Copied from https://raw.githubusercontent.com/microsoft/vscode-icons/9c90ce81b1f3c309000b80da0763aa09975a85f0/icons/dark/loading.svg -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 0.75C8.17188 0.75 8.33333 0.783854 8.48438 0.851562C8.63542 0.914062 8.76823 1.0026 8.88281 1.11719C8.9974 1.23177 9.08594 1.36458 9.14844 1.51562C9.21615 1.66667 9.25 1.82812 9.25 2C9.25 2.17188 9.21615 2.33333 9.14844 2.48438C9.08594 2.63542 8.9974 2.76823 8.88281 2.88281C8.76823 2.9974 8.63542 3.08854 8.48438 3.15625C8.33333 3.21875 8.17188 3.25 8 3.25C7.82812 3.25 7.66667 3.21875 7.51562 3.15625C7.36458 3.08854 7.23177 2.9974 7.11719 2.88281C7.0026 2.76823 6.91146 2.63542 6.84375 2.48438C6.78125 2.33333 6.75 2.17188 6.75 2C6.75 1.82812 6.78125 1.66667 6.84375 1.51562C6.91146 1.36458 7.0026 1.23177 7.11719 1.11719C7.23177 1.0026 7.36458 0.914062 7.51562 0.851562C7.66667 0.783854 7.82812 0.75 8 0.75ZM2.63281 3.75781C2.63281 3.60156 2.66146 3.45573 2.71875 3.32031C2.77604 3.1849 2.85417 3.06771 2.95312 2.96875C3.05729 2.86458 3.17708 2.78385 3.3125 2.72656C3.45312 2.66406 3.60156 2.63281 3.75781 2.63281C3.91406 2.63281 4.0599 2.66406 4.19531 2.72656C4.33073 2.78385 4.44792 2.86458 4.54688 2.96875C4.65104 3.06771 4.73177 3.1849 4.78906 3.32031C4.85156 3.45573 4.88281 3.60156 4.88281 3.75781C4.88281 3.91406 4.85156 4.0625 4.78906 4.20312C4.73177 4.33854 4.65104 4.45833 4.54688 4.5625C4.44792 4.66146 4.33073 4.73958 4.19531 4.79688C4.0599 4.85417 3.91406 4.88281 3.75781 4.88281C3.60156 4.88281 3.45312 4.85417 3.3125 4.79688C3.17708 4.73958 3.05729 4.66146 2.95312 4.5625C2.85417 4.45833 2.77604 4.33854 2.71875 4.20312C2.66146 4.0625 2.63281 3.91406 2.63281 3.75781ZM2 7C2.14062 7 2.27083 7.02604 2.39062 7.07812C2.51042 7.13021 2.61458 7.20312 2.70312 7.29688C2.79688 7.38542 2.86979 7.48958 2.92188 7.60938C2.97396 7.72917 3 7.85938 3 8C3 8.14062 2.97396 8.27083 2.92188 8.39062C2.86979 8.51042 2.79688 8.61719 2.70312 8.71094C2.61458 8.79948 2.51042 8.86979 2.39062 8.92188C2.27083 8.97396 2.14062 9 2 9C1.85938 9 1.72917 8.97396 1.60938 8.92188C1.48958 8.86979 1.38281 8.79948 1.28906 8.71094C1.20052 8.61719 1.13021 8.51042 1.07812 8.39062C1.02604 8.27083 1 8.14062 1 8C1 7.85938 1.02604 7.72917 1.07812 7.60938C1.13021 7.48958 1.20052 7.38542 1.28906 7.29688C1.38281 7.20312 1.48958 7.13021 1.60938 7.07812C1.72917 7.02604 1.85938 7 2 7ZM2.88281 12.2422C2.88281 12.1224 2.90625 12.0104 2.95312 11.9062C3 11.7969 3.0625 11.7031 3.14062 11.625C3.21875 11.5469 3.3099 11.4844 3.41406 11.4375C3.52344 11.3906 3.63802 11.3672 3.75781 11.3672C3.8776 11.3672 3.98958 11.3906 4.09375 11.4375C4.20312 11.4844 4.29688 11.5469 4.375 11.625C4.45312 11.7031 4.51562 11.7969 4.5625 11.9062C4.60938 12.0104 4.63281 12.1224 4.63281 12.2422C4.63281 12.362 4.60938 12.4766 4.5625 12.5859C4.51562 12.6901 4.45312 12.7812 4.375 12.8594C4.29688 12.9375 4.20312 13 4.09375 13.0469C3.98958 13.0938 3.8776 13.1172 3.75781 13.1172C3.63802 13.1172 3.52344 13.0938 3.41406 13.0469C3.3099 13 3.21875 12.9375 3.14062 12.8594C3.0625 12.7812 3 12.6901 2.95312 12.5859C2.90625 12.4766 2.88281 12.362 2.88281 12.2422ZM8 13.25C8.20833 13.25 8.38542 13.3229 8.53125 13.4688C8.67708 13.6146 8.75 13.7917 8.75 14C8.75 14.2083 8.67708 14.3854 8.53125 14.5312C8.38542 14.6771 8.20833 14.75 8 14.75C7.79167 14.75 7.61458 14.6771 7.46875 14.5312C7.32292 14.3854 7.25 14.2083 7.25 14C7.25 13.7917 7.32292 13.6146 7.46875 13.4688C7.61458 13.3229 7.79167 13.25 8 13.25ZM11.6172 12.2422C11.6172 12.0651 11.6771 11.9167 11.7969 11.7969C11.9167 11.6771 12.0651 11.6172 12.2422 11.6172C12.4193 11.6172 12.5677 11.6771 12.6875 11.7969C12.8073 11.9167 12.8672 12.0651 12.8672 12.2422C12.8672 12.4193 12.8073 12.5677 12.6875 12.6875C12.5677 12.8073 12.4193 12.8672 12.2422 12.8672C12.0651 12.8672 11.9167 12.8073 11.7969 12.6875C11.6771 12.5677 11.6172 12.4193 11.6172 12.2422ZM14 7.5C14.1354 7.5 14.2526 7.54948 14.3516 7.64844C14.4505 7.7474 14.5 7.86458 14.5 8C14.5 8.13542 14.4505 8.2526 14.3516 8.35156C14.2526 8.45052 14.1354 8.5 14 8.5C13.8646 8.5 13.7474 8.45052 13.6484 8.35156C13.5495 8.2526 13.5 8.13542 13.5 8C13.5 7.86458 13.5495 7.7474 13.6484 7.64844C13.7474 7.54948 13.8646 7.5 14 7.5ZM12.2422 2.38281C12.4297 2.38281 12.6068 2.41927 12.7734 2.49219C12.9401 2.5651 13.0859 2.66406 13.2109 2.78906C13.3359 2.91406 13.4349 3.0599 13.5078 3.22656C13.5807 3.39323 13.6172 3.57031 13.6172 3.75781C13.6172 3.94531 13.5807 4.1224 13.5078 4.28906C13.4349 4.45573 13.3359 4.60156 13.2109 4.72656C13.0859 4.85156 12.9401 4.95052 12.7734 5.02344C12.6068 5.09635 12.4297 5.13281 12.2422 5.13281C12.0547 5.13281 11.8776 5.09635 11.7109 5.02344C11.5443 4.95052 11.3984 4.85156 11.2734 4.72656C11.1484 4.60156 11.0495 4.45573 10.9766 4.28906C10.9036 4.1224 10.8672 3.94531 10.8672 3.75781C10.8672 3.57031 10.9036 3.39323 10.9766 3.22656C11.0495 3.0599 11.1484 2.91406 11.2734 2.78906C11.3984 2.66406 11.5443 2.5651 11.7109 2.49219C11.8776 2.41927 12.0547 2.38281 12.2422 2.38281Z" fill="#C5C5C5" />
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -0,0 +1,6 @@
<!--Copyright (c) Microsoft Corporation. All rights reserved.-->
<!--Copyright (C) 2020 TypeFox and others.-->
<!--Copied from https://raw.githubusercontent.com/microsoft/vscode-icons/9c90ce81b1f3c309000b80da0763aa09975a85f0/icons/light/loading.svg -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 0.75C8.17188 0.75 8.33333 0.783854 8.48438 0.851562C8.63542 0.914062 8.76823 1.0026 8.88281 1.11719C8.9974 1.23177 9.08594 1.36458 9.14844 1.51562C9.21615 1.66667 9.25 1.82812 9.25 2C9.25 2.17188 9.21615 2.33333 9.14844 2.48438C9.08594 2.63542 8.9974 2.76823 8.88281 2.88281C8.76823 2.9974 8.63542 3.08854 8.48438 3.15625C8.33333 3.21875 8.17188 3.25 8 3.25C7.82812 3.25 7.66667 3.21875 7.51562 3.15625C7.36458 3.08854 7.23177 2.9974 7.11719 2.88281C7.0026 2.76823 6.91146 2.63542 6.84375 2.48438C6.78125 2.33333 6.75 2.17188 6.75 2C6.75 1.82812 6.78125 1.66667 6.84375 1.51562C6.91146 1.36458 7.0026 1.23177 7.11719 1.11719C7.23177 1.0026 7.36458 0.914062 7.51562 0.851562C7.66667 0.783854 7.82812 0.75 8 0.75ZM2.63281 3.75781C2.63281 3.60156 2.66146 3.45573 2.71875 3.32031C2.77604 3.1849 2.85417 3.06771 2.95312 2.96875C3.05729 2.86458 3.17708 2.78385 3.3125 2.72656C3.45312 2.66406 3.60156 2.63281 3.75781 2.63281C3.91406 2.63281 4.0599 2.66406 4.19531 2.72656C4.33073 2.78385 4.44792 2.86458 4.54688 2.96875C4.65104 3.06771 4.73177 3.1849 4.78906 3.32031C4.85156 3.45573 4.88281 3.60156 4.88281 3.75781C4.88281 3.91406 4.85156 4.0625 4.78906 4.20312C4.73177 4.33854 4.65104 4.45833 4.54688 4.5625C4.44792 4.66146 4.33073 4.73958 4.19531 4.79688C4.0599 4.85417 3.91406 4.88281 3.75781 4.88281C3.60156 4.88281 3.45312 4.85417 3.3125 4.79688C3.17708 4.73958 3.05729 4.66146 2.95312 4.5625C2.85417 4.45833 2.77604 4.33854 2.71875 4.20312C2.66146 4.0625 2.63281 3.91406 2.63281 3.75781ZM2 7C2.14062 7 2.27083 7.02604 2.39062 7.07812C2.51042 7.13021 2.61458 7.20312 2.70312 7.29688C2.79688 7.38542 2.86979 7.48958 2.92188 7.60938C2.97396 7.72917 3 7.85938 3 8C3 8.14062 2.97396 8.27083 2.92188 8.39062C2.86979 8.51042 2.79688 8.61719 2.70312 8.71094C2.61458 8.79948 2.51042 8.86979 2.39062 8.92188C2.27083 8.97396 2.14062 9 2 9C1.85938 9 1.72917 8.97396 1.60938 8.92188C1.48958 8.86979 1.38281 8.79948 1.28906 8.71094C1.20052 8.61719 1.13021 8.51042 1.07812 8.39062C1.02604 8.27083 1 8.14062 1 8C1 7.85938 1.02604 7.72917 1.07812 7.60938C1.13021 7.48958 1.20052 7.38542 1.28906 7.29688C1.38281 7.20312 1.48958 7.13021 1.60938 7.07812C1.72917 7.02604 1.85938 7 2 7ZM2.88281 12.2422C2.88281 12.1224 2.90625 12.0104 2.95312 11.9062C3 11.7969 3.0625 11.7031 3.14062 11.625C3.21875 11.5469 3.3099 11.4844 3.41406 11.4375C3.52344 11.3906 3.63802 11.3672 3.75781 11.3672C3.8776 11.3672 3.98958 11.3906 4.09375 11.4375C4.20312 11.4844 4.29688 11.5469 4.375 11.625C4.45312 11.7031 4.51562 11.7969 4.5625 11.9062C4.60938 12.0104 4.63281 12.1224 4.63281 12.2422C4.63281 12.362 4.60938 12.4766 4.5625 12.5859C4.51562 12.6901 4.45312 12.7812 4.375 12.8594C4.29688 12.9375 4.20312 13 4.09375 13.0469C3.98958 13.0938 3.8776 13.1172 3.75781 13.1172C3.63802 13.1172 3.52344 13.0938 3.41406 13.0469C3.3099 13 3.21875 12.9375 3.14062 12.8594C3.0625 12.7812 3 12.6901 2.95312 12.5859C2.90625 12.4766 2.88281 12.362 2.88281 12.2422ZM8 13.25C8.20833 13.25 8.38542 13.3229 8.53125 13.4688C8.67708 13.6146 8.75 13.7917 8.75 14C8.75 14.2083 8.67708 14.3854 8.53125 14.5312C8.38542 14.6771 8.20833 14.75 8 14.75C7.79167 14.75 7.61458 14.6771 7.46875 14.5312C7.32292 14.3854 7.25 14.2083 7.25 14C7.25 13.7917 7.32292 13.6146 7.46875 13.4688C7.61458 13.3229 7.79167 13.25 8 13.25ZM11.6172 12.2422C11.6172 12.0651 11.6771 11.9167 11.7969 11.7969C11.9167 11.6771 12.0651 11.6172 12.2422 11.6172C12.4193 11.6172 12.5677 11.6771 12.6875 11.7969C12.8073 11.9167 12.8672 12.0651 12.8672 12.2422C12.8672 12.4193 12.8073 12.5677 12.6875 12.6875C12.5677 12.8073 12.4193 12.8672 12.2422 12.8672C12.0651 12.8672 11.9167 12.8073 11.7969 12.6875C11.6771 12.5677 11.6172 12.4193 11.6172 12.2422ZM14 7.5C14.1354 7.5 14.2526 7.54948 14.3516 7.64844C14.4505 7.7474 14.5 7.86458 14.5 8C14.5 8.13542 14.4505 8.2526 14.3516 8.35156C14.2526 8.45052 14.1354 8.5 14 8.5C13.8646 8.5 13.7474 8.45052 13.6484 8.35156C13.5495 8.2526 13.5 8.13542 13.5 8C13.5 7.86458 13.5495 7.7474 13.6484 7.64844C13.7474 7.54948 13.8646 7.5 14 7.5ZM12.2422 2.38281C12.4297 2.38281 12.6068 2.41927 12.7734 2.49219C12.9401 2.5651 13.0859 2.66406 13.2109 2.78906C13.3359 2.91406 13.4349 3.0599 13.5078 3.22656C13.5807 3.39323 13.6172 3.57031 13.6172 3.75781C13.6172 3.94531 13.5807 4.1224 13.5078 4.28906C13.4349 4.45573 13.3359 4.60156 13.2109 4.72656C13.0859 4.85156 12.9401 4.95052 12.7734 5.02344C12.6068 5.09635 12.4297 5.13281 12.2422 5.13281C12.0547 5.13281 11.8776 5.09635 11.7109 5.02344C11.5443 4.95052 11.3984 4.85156 11.2734 4.72656C11.1484 4.60156 11.0495 4.45573 10.9766 4.28906C10.9036 4.1224 10.8672 3.94531 10.8672 3.75781C10.8672 3.57031 10.9036 3.39323 10.9766 3.22656C11.0495 3.0599 11.1484 2.91406 11.2734 2.78906C11.3984 2.66406 11.5443 2.5651 11.7109 2.49219C11.8776 2.41927 12.0547 2.38281 12.2422 2.38281Z" fill="#424242" />
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -119,20 +119,16 @@ export class LibraryListWidget extends ListWidget<
message.appendChild(question);
const result = await new MessageBoxDialog({
title: nls.localize(
'arduino/library/dependenciesForLibrary',
'Dependencies for library {0}:{1}',
item.name,
version
'arduino/library/installLibraryDependencies',
'Install library dependencies'
),
message,
buttons: [
nls.localize('arduino/library/installAll', 'Install all'),
nls.localize(
'arduino/library/installOnly',
'Install {0} only',
item.name
'arduino/library/installWithoutDependencies',
'Install without dependencies'
),
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
nls.localize('arduino/library/installAll', 'Install All'),
],
maxWidth: 740, // Aligned with `settings-dialog.css`.
}).open();
@@ -140,11 +136,11 @@ export class LibraryListWidget extends ListWidget<
if (result) {
const { response } = result;
if (response === 0) {
// All
installDependencies = true;
} else if (response === 1) {
// Current only
installDependencies = false;
} else if (response === 1) {
// All
installDependencies = true;
}
}
} else {
@@ -201,7 +197,13 @@ class MessageBoxDialog extends AbstractDialog<MessageBoxDialog.Result> {
options.buttons || [nls.localize('vscode/issueMainService/ok', 'OK')]
).forEach((text, index) => {
const button = this.createButton(text);
button.classList.add(index === 0 ? 'main' : 'secondary');
const isPrimaryButton =
index === (options.buttons ? options.buttons.length - 1 : 0);
button.title = text;
button.classList.add(
isPrimaryButton ? 'main' : 'secondary',
'message-box-dialog-button'
);
this.controlPanel.appendChild(button);
this.toDisposeOnDetach.push(
addEventListener(button, 'click', () => {

View File

@@ -1,16 +1,17 @@
import { injectable } from '@theia/core/shared/inversify';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { MenuModelRegistry } from '@theia/core';
import { LibraryListWidget } from './library-list-widget';
import { ArduinoMenus } from '../menu/arduino-menus';
import { nls } from '@theia/core/lib/common';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { injectable } from '@theia/core/shared/inversify';
import { LibraryPackage, LibrarySearch } from '../../common/protocol';
import { URI } from '../contributions/contribution';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
import { LibraryListWidget } from './library-list-widget';
@injectable()
export class LibraryListWidgetFrontendContribution
extends AbstractViewContribution<LibraryListWidget>
implements FrontendApplicationContribution
{
export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendContribution<
LibraryPackage,
LibrarySearch
> {
constructor() {
super({
widgetId: LibraryListWidget.WIDGET_ID,
@@ -24,10 +25,6 @@ export class LibraryListWidgetFrontendContribution
});
}
async initializeLayout(): Promise<void> {
this.openView();
}
override registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
@@ -40,4 +37,17 @@ export class LibraryListWidgetFrontendContribution
});
}
}
protected canParse(uri: URI): boolean {
try {
LibrarySearch.UriParser.parse(uri);
return true;
} catch {
return false;
}
}
protected parse(uri: URI): LibrarySearch | undefined {
return LibrarySearch.UriParser.parse(uri);
}
}

View File

@@ -1,5 +1,5 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { URI as Uri } from 'vscode-uri';
import { URI as Uri } from '@theia/core/shared/vscode-uri';
import URI from '@theia/core/lib/common/uri';
import { Deferred } from '@theia/core/lib/common/promise-util';
import {
@@ -34,7 +34,6 @@ export class LocalCacheFsProvider
@inject(AuthenticationClientService)
protected readonly authenticationService: AuthenticationClientService;
// TODO: do we need this? Cannot we `await` on the `init` call from `registerFileSystemProviders`?
readonly ready = new Deferred<void>();
private _localCacheRoot: URI;
@@ -153,7 +152,7 @@ export class LocalCacheFsProvider
return uri;
}
private toUri(session: AuthenticationSession): URI {
toUri(session: AuthenticationSession): URI {
// Hack: instead of getting the UUID only, we get `auth0|UUID` after the authentication. `|` cannot be part of filesystem path or filename.
return this._localCacheRoot.resolve(session.id.split('|')[1]);
}

View File

@@ -8,6 +8,9 @@ import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import {
IndexUpdateDidCompleteParams,
IndexUpdateDidFailParams,
IndexUpdateWillStartParams,
NotificationServiceClient,
NotificationServiceServer,
} from '../common/protocol/notification-service';
@@ -29,48 +32,48 @@ export class NotificationCenter
implements NotificationServiceClient, FrontendApplicationContribution
{
@inject(NotificationServiceServer)
protected readonly server: JsonRpcProxy<NotificationServiceServer>;
private readonly server: JsonRpcProxy<NotificationServiceServer>;
@inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService;
protected readonly indexDidUpdateEmitter = new Emitter<string>();
protected readonly indexWillUpdateEmitter = new Emitter<string>();
protected readonly indexUpdateDidProgressEmitter =
private readonly indexUpdateDidCompleteEmitter =
new Emitter<IndexUpdateDidCompleteParams>();
private readonly indexUpdateWillStartEmitter =
new Emitter<IndexUpdateWillStartParams>();
private readonly indexUpdateDidProgressEmitter =
new Emitter<ProgressMessage>();
protected readonly indexUpdateDidFailEmitter = new Emitter<{
progressId: string;
message: string;
}>();
protected readonly daemonDidStartEmitter = new Emitter<string>();
protected readonly daemonDidStopEmitter = new Emitter<void>();
protected readonly configDidChangeEmitter = new Emitter<{
private readonly indexUpdateDidFailEmitter =
new Emitter<IndexUpdateDidFailParams>();
private readonly daemonDidStartEmitter = new Emitter<string>();
private readonly daemonDidStopEmitter = new Emitter<void>();
private readonly configDidChangeEmitter = new Emitter<{
config: Config | undefined;
}>();
protected readonly platformDidInstallEmitter = new Emitter<{
private readonly platformDidInstallEmitter = new Emitter<{
item: BoardsPackage;
}>();
protected readonly platformDidUninstallEmitter = new Emitter<{
private readonly platformDidUninstallEmitter = new Emitter<{
item: BoardsPackage;
}>();
protected readonly libraryDidInstallEmitter = new Emitter<{
private readonly libraryDidInstallEmitter = new Emitter<{
item: LibraryPackage;
}>();
protected readonly libraryDidUninstallEmitter = new Emitter<{
private readonly libraryDidUninstallEmitter = new Emitter<{
item: LibraryPackage;
}>();
protected readonly attachedBoardsDidChangeEmitter =
private readonly attachedBoardsDidChangeEmitter =
new Emitter<AttachedBoardsChangeEvent>();
protected readonly recentSketchesChangedEmitter = new Emitter<{
private readonly recentSketchesChangedEmitter = new Emitter<{
sketches: Sketch[];
}>();
private readonly onAppStateDidChangeEmitter =
new Emitter<FrontendApplicationState>();
protected readonly toDispose = new DisposableCollection(
this.indexWillUpdateEmitter,
private readonly toDispose = new DisposableCollection(
this.indexUpdateWillStartEmitter,
this.indexUpdateDidProgressEmitter,
this.indexDidUpdateEmitter,
this.indexUpdateDidCompleteEmitter,
this.indexUpdateDidFailEmitter,
this.daemonDidStartEmitter,
this.daemonDidStopEmitter,
@@ -82,8 +85,8 @@ export class NotificationCenter
this.attachedBoardsDidChangeEmitter
);
readonly onIndexDidUpdate = this.indexDidUpdateEmitter.event;
readonly onIndexWillUpdate = this.indexDidUpdateEmitter.event;
readonly onIndexUpdateDidComplete = this.indexUpdateDidCompleteEmitter.event;
readonly onIndexUpdateWillStart = this.indexUpdateWillStartEmitter.event;
readonly onIndexUpdateDidProgress = this.indexUpdateDidProgressEmitter.event;
readonly onIndexUpdateDidFail = this.indexUpdateDidFailEmitter.event;
readonly onDaemonDidStart = this.daemonDidStartEmitter.event;
@@ -112,26 +115,20 @@ export class NotificationCenter
this.toDispose.dispose();
}
notifyIndexWillUpdate(progressId: string): void {
this.indexWillUpdateEmitter.fire(progressId);
notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void {
this.indexUpdateWillStartEmitter.fire(params);
}
notifyIndexUpdateDidProgress(progressMessage: ProgressMessage): void {
this.indexUpdateDidProgressEmitter.fire(progressMessage);
}
notifyIndexDidUpdate(progressId: string): void {
this.indexDidUpdateEmitter.fire(progressId);
notifyIndexUpdateDidComplete(params: IndexUpdateDidCompleteParams): void {
this.indexUpdateDidCompleteEmitter.fire(params);
}
notifyIndexUpdateDidFail({
progressId,
message,
}: {
progressId: string;
message: string;
}): void {
this.indexUpdateDidFailEmitter.fire({ progressId, message });
notifyIndexUpdateDidFail(params: IndexUpdateDidFailParams): void {
this.indexUpdateDidFailEmitter.fire(params);
}
notifyDaemonDidStart(port: string): void {

View File

@@ -1,6 +1,5 @@
import * as React from '@theia/core/shared/react';
import { injectable, inject } from '@theia/core/shared/inversify';
import { OptionsType } from 'react-select/src/types';
import { Emitter } from '@theia/core/lib/common/event';
import { Disposable } from '@theia/core/lib/common/disposable';
import {
@@ -128,9 +127,7 @@ export class MonitorWidget extends ReactWidget {
);
};
protected get lineEndings(): OptionsType<
SerialMonitorOutput.SelectOption<MonitorModel.EOL>
> {
protected get lineEndings(): SerialMonitorOutput.SelectOption<MonitorModel.EOL>[] {
return [
{
label: nls.localize('arduino/serial/noLineEndings', 'No Line Ending'),

View File

@@ -1,12 +1,58 @@
import * as React from '@theia/core/shared/react';
import { Key, KeyCode } from '@theia/core/lib/browser/keys';
import { Board } from '../../../common/protocol/boards-service';
import { isOSX } from '@theia/core/lib/common/os';
import { DisposableCollection, nls } from '@theia/core/lib/common';
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
import { MonitorModel } from '../../monitor-model';
import { Unknown } from '../../../common/nls';
class HistoryList {
private readonly items: string[] = [];
private index = -1;
constructor(private readonly size = 100) {}
push(val: string): void {
if (val !== this.items[this.items.length - 1]) {
this.items.push(val);
}
while (this.items.length > this.size) {
this.items.shift();
}
this.index = -1;
}
previous(): string {
if (this.index === -1) {
this.index = this.items.length - 1;
return this.items[this.index];
}
if (this.hasPrevious) {
return this.items[--this.index];
}
return this.items[this.index];
}
private get hasPrevious(): boolean {
return this.index >= 1;
}
next(): string {
if (this.index === this.items.length - 1) {
this.index = -1;
return '';
}
if (this.hasNext) {
return this.items[++this.index];
}
return '';
}
private get hasNext(): boolean {
return this.index >= 0 && this.index !== this.items.length - 1;
}
}
export namespace SerialMonitorSendInput {
export interface Props {
readonly boardsServiceProvider: BoardsServiceProvider;
@@ -17,6 +63,7 @@ export namespace SerialMonitorSendInput {
export interface State {
text: string;
connected: boolean;
history: HistoryList;
}
}
@@ -28,7 +75,7 @@ export class SerialMonitorSendInput extends React.Component<
constructor(props: Readonly<SerialMonitorSendInput.Props>) {
super(props);
this.state = { text: '', connected: true };
this.state = { text: '', connected: true, history: new HistoryList() };
this.onChange = this.onChange.bind(this);
this.onSend = this.onSend.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
@@ -81,8 +128,7 @@ export class SerialMonitorSendInput extends React.Component<
const port = this.props.boardsServiceProvider.boardsConfig.selectedPort;
return nls.localize(
'arduino/serial/message',
"Message ({0} + Enter to send message to '{1}' on '{2}')",
isOSX ? '⌘' : nls.localize('vscode/keybindingLabels/ctrlKey', 'Ctrl'),
"Message (Enter to send message to '{0}' on '{1}')",
board
? Board.toString(board, {
useFqbn: false,
@@ -92,7 +138,7 @@ export class SerialMonitorSendInput extends React.Component<
);
}
protected setRef = (element: HTMLElement | null) => {
protected setRef = (element: HTMLElement | null): void => {
if (this.props.resolveFocus) {
this.props.resolveFocus(element || undefined);
}
@@ -110,9 +156,19 @@ export class SerialMonitorSendInput extends React.Component<
protected onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
const keyCode = KeyCode.createKeyCode(event.nativeEvent);
if (keyCode) {
const { key, meta, ctrl } = keyCode;
if (key === Key.ENTER && ((isOSX && meta) || (!isOSX && ctrl))) {
const { key } = keyCode;
if (key === Key.ENTER) {
const { text } = this.state;
this.onSend();
if (text) {
this.state.history.push(text);
}
} else if (key === Key.ARROW_UP) {
this.setState({ text: this.state.history.previous() });
} else if (key === Key.ARROW_DOWN) {
this.setState({ text: this.state.history.next() });
} else if (key === Key.ESCAPE) {
this.setState({ text: '' });
}
}
}

View File

@@ -109,7 +109,7 @@ const _Row = ({
}) => {
const timestamp =
(data.timestamp &&
`${dateFormat(data.lines[index].timestamp, 'H:M:ss.l')} -> `) ||
`${dateFormat(data.lines[index].timestamp, 'HH:MM:ss.l')} -> `) ||
'';
return (
(data.lines[index].lineLen && (

View File

@@ -14,6 +14,11 @@ import { MonitorManagerProxyClient } from '../../../common/protocol';
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
import { MonitorModel } from '../../monitor-model';
import { ArduinoToolbar } from '../../toolbar/arduino-toolbar';
import {
CLOSE_PLOTTER_WINDOW,
SHOW_PLOTTER_WINDOW,
} from '../../../common/ipc-communication';
import { nls } from '@theia/core/lib/common/nls';
const queryString = require('query-string');
@@ -58,7 +63,7 @@ export class PlotterFrontendContribution extends Contribution {
override onStart(app: FrontendApplication): MaybePromise<void> {
this.url = new Endpoint({ path: '/plotter' }).getRestUrl().toString();
ipcRenderer.on('CLOSE_CHILD_WINDOW', async () => {
ipcRenderer.on(CLOSE_PLOTTER_WINDOW, async () => {
if (!!this.window) {
this.window = null;
}
@@ -96,14 +101,19 @@ export class PlotterFrontendContribution extends Contribution {
async startPlotter(): Promise<void> {
await this.monitorManagerProxy.startMonitor();
if (!!this.window) {
this.window.focus();
ipcRenderer.send(SHOW_PLOTTER_WINDOW);
return;
}
const wsPort = this.monitorManagerProxy.getWebSocketPort();
if (wsPort) {
this.open(wsPort);
} else {
this.messageService.error(`Couldn't open serial plotter`);
this.messageService.error(
nls.localize(
'arduino/contributions/plotter/couldNotOpen',
"Couldn't open serial plotter"
)
);
}
}

View File

@@ -1,5 +1,11 @@
#select-board-dialog-container > .dialogBlock {
width: 640px;
height: 500px;
}
div#select-board-dialog {
margin: 5px;
height: 100%;
}
div#select-board-dialog .selectBoardContainer {
@@ -7,12 +13,17 @@ div#select-board-dialog .selectBoardContainer {
gap: 10px;
overflow: hidden;
max-height: 100%;
height: 100%;
}
.select-board-dialog .head {
margin: 5px;
}
.dialogContent.select-board-dialog {
height: 100%;
}
div.dialogContent.select-board-dialog > div.head .title {
font-weight: 400;
letter-spacing: 0.02em;
@@ -63,6 +74,7 @@ div#select-board-dialog .selectBoardContainer .list .item.selected i {
display: flex;
flex-direction: column;
max-height: 100%;
height: 100%;
}
#select-board-dialog .selectBoardContainer .left.container .content {
@@ -131,6 +143,7 @@ div#select-board-dialog .selectBoardContainer .list .item.selected i {
#select-board-dialog .selectBoardContainer .list {
max-height: 200px;
overflow-y: auto;
flex: 1;
}
#select-board-dialog .selectBoardContainer .ports.list {
@@ -282,3 +295,11 @@ div#select-board-dialog .selectBoardContainer .list .item.selected i {
display: none;
}
}
#select-board-dialog .no-result {
text-transform: uppercase;
height: 100%;
user-select: none;
padding: 10px 5px;
overflow-wrap: break-word;
}

View File

@@ -9,7 +9,8 @@
total = padding + margin = 96px
*/
max-width: calc(100% - 96px) !important;
min-width: unset;
min-width: 424px;
max-height: 560px;
padding: 0 28px;
}
@@ -54,6 +55,7 @@
align-items: center;
}
.p-Widget.dialogOverlay .dialogControl .spinner,
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection .dialogRow .spinner {
background: var(--theia-icon-loading) center center no-repeat;
animation: theia-spin 1.25s linear infinite;
@@ -62,11 +64,11 @@
}
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection .dialogRow:first-child {
margin-top: 0px;
margin-top: 0px;
height: 32px;
}
.fl1{
.fl1 {
flex: 1;
}
@@ -79,9 +81,13 @@
opacity: .4;
}
@media only screen and (max-height: 560px) {
.p-Widget.dialogOverlay .dialogBlock {
max-height: 400px;
}
}
.p-Widget.dialogOverlay .error.progress {
color: var(--theia-button-background);
align-self: center;
}

View File

@@ -7,7 +7,7 @@
}
.firmware-uploader-dialog .arduino-select__control {
height: 31px;
background: var(--theia-menubar-selectionBackground) !important;
background: var(--theia-input-background) !important;
}
.firmware-uploader-dialog .dialogRow > button{
@@ -28,4 +28,4 @@
.firmware-uploader-dialog .status-icon {
margin-right: 10px;
}
}

View File

@@ -20,6 +20,16 @@
@import './progress-bar.css';
@import './settings-step-input.css';
/* Revive of the `--theia-icon-loading`. The variable has been removed from Theia while IDE2 still uses is. */
/* The SVG icons are still part of Theia (1.31.1) */
/* https://github.com/arduino/arduino-ide/pull/1662#issuecomment-1324997134 */
body {
--theia-icon-loading: url(../icons/loading-light.svg);
}
body.theia-dark {
--theia-icon-loading: url(../icons/loading-dark.svg);
}
.theia-input.warning:focus {
outline-width: 1px;
outline-style: solid;
@@ -55,7 +65,8 @@
/* Makes the sidepanel a bit wider when opening the widget */
.p-DockPanel-widget {
min-width: 200px;
min-height: 200px;
min-height: 20px;
height: 200px;
}
/* Overrule the default Theia CSS button styles. */
@@ -108,6 +119,13 @@ button.secondary[disabled], .theia-button.secondary[disabled] {
background-color: var(--theia-secondaryButton-background);
}
button.theia-button.message-box-dialog-button {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
}
/* To make the progress-bar slightly thicker, and use the color from the status bar */
.theia-progress-bar-container {
width: 100%;
@@ -136,6 +154,9 @@ button.secondary[disabled], .theia-button.secondary[disabled] {
font-size: 14px;
}
.uppercase {
text-transform: uppercase;
}
/* High Contrast Theme rules */
/* TODO: Remove it when the Theia version is upgraded to 1.27.0 and use Theia APIs to implement it*/
@@ -155,3 +176,13 @@ button.secondary[disabled], .theia-button.secondary[disabled] {
outline: 1px dashed var(--theia-focusBorder);
outline-offset: -2px;
}
.debug-toolbar .debug-action>div {
font-family: var(--theia-ui-font-family);
font-size: var(--theia-ui-font-size0);
display: flex;
align-items: center;
align-self: center;
justify-content: center;
min-height: inherit;
}

View File

@@ -111,13 +111,13 @@
font-weight: bold;
max-height: calc(1em + 4px);
color: var(--theia-button-foreground);
content: 'INSTALLED';
content: attr(install);
}
.component-list-item .header .installed:hover:before {
background-color: var(--theia-button-foreground);
color: var(--theia-button-background);
content: 'UNINSTALL';
content: attr(uninstall);
}
.component-list-item[min-width~="170px"] .footer {
@@ -131,7 +131,7 @@
flex-direction: column-reverse;
}
.component-list-item:hover .footer > * {
.component-list-item .footer > * {
display: inline-block;
margin: 5px 0px 0px 10px;
}
@@ -160,4 +160,4 @@
.hc-black.hc-theia.theia-hc .component-list-item .header .installed:before {
border: 1px solid var(--theia-button-border);
}
}

View File

@@ -2,6 +2,16 @@
background: var(--theia-editorGroupHeader-tabsBackground);
}
/* Avoid the Intellisense widget may be cover by the bottom panel partially.
TODO: This issue may be resolved after monaco-editor upgrade */
#theia-main-content-panel {
z-index: auto
}
#theia-main-content-panel div[id^="code-editor-opener"] {
z-index: auto;
}
.p-TabBar-toolbar .item.arduino-tool-item {
margin-left: 0;
}
@@ -87,8 +97,7 @@
display: flex;
justify-content: center;
align-items: center;
background-color: var(--theia-titleBar-activeBackground);
background-color: var(--theia-titleBar-activeBackground);
}
#arduino-toolbar-container {
@@ -243,3 +252,10 @@
outline: 1px solid var(--theia-contrastBorder);
outline-offset: -1px;
}
.monaco-hover .hover-row.markdown-hover:first-child p {
margin-top: 8px;
}
.monaco-hover .hover-row.markdown-hover:first-child .monaco-tokenized-source {
margin-top: 8px;
}

View File

@@ -14,6 +14,10 @@
font-family: monospace
}
.serial-monitor-messages pre {
margin: 0px;
}
.serial-monitor .head {
display: flex;
padding: 5px;

View File

@@ -88,9 +88,13 @@
}
.additional-urls-dialog textarea {
width: 100%;
resize: none;
white-space: nowrap;
}
.p-Widget.dialogOverlay .dialogBlock .dialogContent.additional-urls-dialog {
display: block;
display: flex;
overflow: hidden;
padding: 0 1px;
margin: 0 -1px;
}

View File

@@ -2,17 +2,17 @@
position: relative
}
.settings-step-input-element::-webkit-inner-spin-button,
.settings-step-input-element::-webkit-inner-spin-button,
.settings-step-input-element::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
-webkit-appearance: none;
margin: 0;
}
.settings-step-input-buttons-container {
display: none;
flex-direction: column;
position: absolute;
right: 14px;
right: 0px;
top: 50%;
transform: translate(0px, -50%);
height: calc(100% - 4px);
@@ -21,7 +21,11 @@
background: var(--theia-input-background);
}
.settings-step-input-container:hover > .settings-step-input-buttons-container {
.settings-step-input-buttons-container-perc {
right: 14px;
}
.settings-step-input-container:hover>.settings-step-input-buttons-container {
display: flex;
}
@@ -43,4 +47,4 @@
.settings-step-input-button:hover {
background: rgba(128, 128, 128, 0.8);
}
}

View File

@@ -33,6 +33,22 @@
height: 100%;
}
.sketchbook-trees-container .create-new {
min-height: 58px;
height: 58px;
display: flex;
align-items: center;
justify-content: center;
}
/*
By default, theia-button has a left-margin. IDE2 does not need the left margin
for the _New Remote? Sketch_. Otherwise, the button does not fit the default
widget width.
*/
.sketchbook-trees-container .create-new .theia-button {
margin-left: unset;
}
.sketchbook-tree__opts {
background-color: var(--theia-foreground);
-webkit-mask: url(./sketchbook-opts-icon.svg);

View File

@@ -1,8 +1,6 @@
import { AboutDialog as TheiaAboutDialog } from '@theia/core/lib/browser/about-dialog';
import { duration } from '../../../common/decorators';
export class AboutDialog extends TheiaAboutDialog {
@duration({ name: 'theia-about#init' })
protected override async init(): Promise<void> {
// NOOP
// IDE2 has a custom about dialog, so it does not make sense to collect Theia extensions at startup time.

View File

@@ -1,26 +0,0 @@
import { injectable } from '@theia/core/shared/inversify';
import {
BrowserMainMenuFactory as TheiaBrowserMainMenuFactory,
MenuBarWidget,
} from '@theia/core/lib/browser/menu/browser-menu-plugin';
import { MainMenuManager } from '../../../common/main-menu-manager';
@injectable()
export class BrowserMainMenuFactory
extends TheiaBrowserMainMenuFactory
implements MainMenuManager
{
protected menuBar: MenuBarWidget | undefined;
override createMenuBar(): MenuBarWidget {
this.menuBar = super.createMenuBar();
return this.menuBar;
}
update(): void {
if (this.menuBar) {
this.menuBar.clearMenus();
this.fillMenuBar(this.menuBar);
}
}
}

View File

@@ -1,18 +0,0 @@
import '../../../../src/browser/style/browser-menu.css';
import { ContainerModule } from '@theia/core/shared/inversify';
import {
BrowserMenuBarContribution,
BrowserMainMenuFactory as TheiaBrowserMainMenuFactory,
} from '@theia/core/lib/browser/menu/browser-menu-plugin';
import { MainMenuManager } from '../../../common/main-menu-manager';
import { ArduinoMenuContribution } from './browser-menu-plugin';
import { BrowserMainMenuFactory } from './browser-main-menu-factory';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BrowserMainMenuFactory).toSelf().inSingletonScope();
bind(MainMenuManager).toService(BrowserMainMenuFactory);
rebind(TheiaBrowserMainMenuFactory).toService(BrowserMainMenuFactory);
rebind(BrowserMenuBarContribution)
.to(ArduinoMenuContribution)
.inSingletonScope();
});

View File

@@ -1,10 +0,0 @@
import { DefaultWindowService as TheiaDefaultWindowService } from '@theia/core/lib/browser/window/default-window-service';
import { ContainerModule } from '@theia/core/shared/inversify';
import { DefaultWindowService } from './default-window-service';
import { WindowServiceExt } from './window-service-ext';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(DefaultWindowService).toSelf().inSingletonScope();
rebind(TheiaDefaultWindowService).toService(DefaultWindowService);
bind(WindowServiceExt).toService(DefaultWindowService);
});

View File

@@ -6,6 +6,8 @@ import {
} from '@theia/core/lib/browser/common-frontend-contribution';
import { CommandRegistry } from '@theia/core/lib/common/command';
import type { OnWillStopAction } from '@theia/core/lib/browser/frontend-application';
import { KeybindingRegistry } from '@theia/core/lib/browser';
import { isOSX } from '@theia/core';
@injectable()
export class CommonFrontendContribution extends TheiaCommonFrontendContribution {
@@ -22,7 +24,7 @@ export class CommonFrontendContribution extends TheiaCommonFrontendContribution
CommonCommands.TOGGLE_MAXIMIZED,
CommonCommands.PIN_TAB,
CommonCommands.UNPIN_TAB,
CommonCommands.NEW_FILE,
CommonCommands.NEW_UNTITLED_FILE,
]) {
commandRegistry.unregisterCommand(command);
}
@@ -50,6 +52,36 @@ export class CommonFrontendContribution extends TheiaCommonFrontendContribution
}
}
override registerKeybindings(registry: KeybindingRegistry): void {
super.registerKeybindings(registry);
// Workaround for https://github.com/eclipse-theia/theia/issues/11875
if (isOSX) {
registry.unregisterKeybinding('ctrlcmd+tab');
registry.unregisterKeybinding('ctrlcmd+alt+d');
registry.unregisterKeybinding('ctrlcmd+shift+tab');
registry.unregisterKeybinding('ctrlcmd+alt+a');
registry.registerKeybindings(
{
command: CommonCommands.NEXT_TAB.id,
keybinding: 'ctrl+tab',
},
{
command: CommonCommands.NEXT_TAB.id,
keybinding: 'ctrl+alt+d',
},
{
command: CommonCommands.PREVIOUS_TAB.id,
keybinding: 'ctrl+shift+tab',
},
{
command: CommonCommands.PREVIOUS_TAB.id,
keybinding: 'ctrl+alt+a',
}
);
}
}
override onWillStop(): OnWillStopAction | undefined {
// This is NOOP here. All window close and app quit requests are handled in the `Close` contribution.
return undefined;

View File

@@ -1,17 +0,0 @@
import { DefaultWindowService as TheiaDefaultWindowService } from '@theia/core/lib/browser/window/default-window-service';
import { injectable } from '@theia/core/shared/inversify';
import { WindowServiceExt } from './window-service-ext';
@injectable()
export class DefaultWindowService
extends TheiaDefaultWindowService
implements WindowServiceExt
{
/**
* The default implementation always resolves to `true`.
* IDE2 does not use it. It's currently an electron-only app.
*/
async isFirstWindow(): Promise<boolean> {
return true;
}
}

View File

@@ -1,5 +1,4 @@
import { injectable, inject } from '@theia/core/shared/inversify';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { CommandService } from '@theia/core/lib/common/command';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { FrontendApplication as TheiaFrontendApplication } from '@theia/core/lib/browser/frontend-application';
@@ -8,17 +7,16 @@ import { OpenSketchFiles } from '../../contributions/open-sketch-files';
@injectable()
export class FrontendApplication extends TheiaFrontendApplication {
@inject(FileService)
protected readonly fileService: FileService;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
private readonly workspaceService: WorkspaceService;
@inject(CommandService)
protected readonly commandService: CommandService;
private readonly commandService: CommandService;
@inject(SketchesService)
protected readonly sketchesService: SketchesService;
private readonly sketchesService: SketchesService;
private layoutWasRestored = false;
protected override async initializeLayout(): Promise<void> {
await super.initializeLayout();
@@ -26,10 +24,16 @@ export class FrontendApplication extends TheiaFrontendApplication {
for (const root of roots) {
await this.commandService.executeCommand(
OpenSketchFiles.Commands.OPEN_SKETCH_FILES.id,
root.resource
root.resource,
!this.layoutWasRestored
);
this.sketchesService.markAsRecentlyOpened(root.resource.toString()); // no await, will get the notification later and rebuild the menu
}
});
}
protected override async restoreLayout(): Promise<boolean> {
this.layoutWasRestored = await super.restoreLayout();
return this.layoutWasRestored;
}
}

View File

@@ -1,13 +0,0 @@
import { injectable } from '@theia/core/shared/inversify';
import { StatusBarImpl as TheiaStatusBarImpl } from '@theia/core/lib/browser';
@injectable()
export class StatusBarImpl extends TheiaStatusBarImpl {
override async removeElement(id: string): Promise<void> {
await this.ready;
if (this.entries.delete(id)) {
// Unlike Theia, IDE2 updates the status bar only if the element to remove was among the entries. Otherwise, it's a NOOP.
this.update();
}
}
}

View File

@@ -1,61 +0,0 @@
import * as React from '@theia/core/shared/react';
import { injectable } from '@theia/core/shared/inversify';
import { LabelIcon } from '@theia/core/lib/browser/label-parser';
import {
TabBarToolbar as TheiaTabBarToolbar,
TabBarToolbarItem,
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
@injectable()
export class TabBarToolbar extends TheiaTabBarToolbar {
/**
* Copied over from Theia. Added an ID to the parent of the toolbar item (`--container`).
* CSS3 does not support parent selectors but we want to style the parent of the toolbar item.
*/
protected override renderItem(item: TabBarToolbarItem): React.ReactNode {
let innerText = '';
const classNames = [];
if (item.text) {
for (const labelPart of this.labelParser.parse(item.text)) {
if (typeof labelPart !== 'string' && LabelIcon.is(labelPart)) {
const className = `fa fa-${labelPart.name}${
labelPart.animation ? ' fa-' + labelPart.animation : ''
}`;
classNames.push(...className.split(' '));
} else {
innerText = labelPart;
}
}
}
const command = this.commands.getCommand(item.command);
const iconClass =
(typeof item.icon === 'function' && item.icon()) ||
item.icon ||
(command && command.iconClass);
if (iconClass) {
classNames.push(iconClass);
}
const tooltip = item.tooltip || (command && command.label);
return (
<div
id={`${item.id}--container`}
key={item.id}
className={`${TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM}${
command && this.commandIsEnabled(command.id) ? ' enabled' : ''
}`}
onMouseDown={this.onMouseDownEvent}
onMouseUp={this.onMouseUpEvent}
onMouseOut={this.onMouseUpEvent}
>
<div
id={item.id}
className={classNames.join(' ')}
onClick={this.executeCommand}
title={tooltip}
>
{innerText}
</div>
</div>
);
}
}

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