diff --git a/.classpath b/.classpath
index eabc35c4180..574e1c7f83d 100644
--- a/.classpath
+++ b/.classpath
@@ -4,7 +4,6 @@
-
@@ -16,17 +15,17 @@
-
+
-
+
-
+
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 00000000000..e1a034449f9
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1 @@
+Please read the Issues section of the Contributing Rules at the "Contributing" link to the right before submitting an issue report.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000000..0257f89eea7
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,17 @@
+### All Submissions:
+
+* [ ] Have you followed the guidelines in our Contributing document?
+* [ ] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/arduino/Arduino/pulls?q=) for the same update/change?
+
+
+
+### New Feature Submissions:
+
+1. [ ] Does your submission pass tests?
+2. [ ] Have you lint your code locally prior to submission?
+
+### Changes to Core Features:
+
+* [ ] Have you added an explanation of what your changes do and why you'd like us to include them?
+* [ ] Have you written new tests for your core changes, as applicable?
+* [ ] Have you successfully ran tests with your changes
diff --git a/.github/workflows/ant.yml b/.github/workflows/ant.yml
new file mode 100644
index 00000000000..0a2e0a343e6
--- /dev/null
+++ b/.github/workflows/ant.yml
@@ -0,0 +1,34 @@
+name: Java CI
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up JDK 1.8
+ uses: actions/setup-java@v1
+ with:
+ java-version: 1.8
+ - name: Build with Ant
+ working-directory: ./build
+ run: |
+ sed -i 's###' build.xml
+ ant clean dist
+ - name: Install X virtual framebuffer
+ run: sudo apt-get install -y xvfb
+ - name: Run tests
+ working-directory: ./app
+ run: xvfb-run --auto-servernum --server-args "-screen 0 1024x768x24" ant test -Drunning-from-github-action=1
+ - name: Publish results
+ uses: actions/upload-artifact@v1
+ with:
+ name: html-results
+ path: app/test-bin/results/html/
+ - name: Cleanup xvfb
+ uses: bcomnes/cleanup-xvfb@v1
diff --git a/.gitignore b/.gitignore
index 0ff213c4047..52ef58c5d36 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,6 +48,7 @@ build/linux/*.tar.bz2
build/linux/*.zip
build/linux/libastylej*
build/linux/liblistSerials*
+build/shared/arduino-examples*
build/shared/reference*.zip
build/shared/Edison*.zip
build/shared/Galileo*.zip
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d4b6b1e8dd7..e84deb9c3a0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,102 +1,80 @@
-## Contributing guide
-This document serves as a checklist before contributing to this repository. It includes includes links to read up on if topics are unclear to you.
-
-This guide mainly focuses on the proper use of Git. It has some overlap with the more general information found in the [Development Policy File](https://github.com/arduino/Arduino/wiki/Development-Policy).
-
-### 1. Before using the issue tracker
-To report a bug or a small enhancement please use the [issue tracker](http://github.com/arduino/Arduino/issues). But check the following boxes before posting an issue:
-
-- [ ] `Your issue is NOT a question about an Arduino sketch.` Sketch questions are handled on the [Arduino Forum](http://forum.arduino.cc/).
-- [ ] `Check if your issue has already been resolved in the` [hourly build](http://www.arduino.cc/en/Main/Software#hourly)
-- [ ] `Your issue is not a duplicate.` So search for similar open and closed issues and pull-requests.
-- [ ] `Make sure you are working on the right repository. See the table below.`
-
-| Repositories | Projects |
-|---|---|
-|[Arduino](https://github.com/arduino/Arduino) | Arduino IDE, arduino.cc (but not the Arduino Playground), Library Manager |
-|[Arduino Playground](http://forum.arduino.cc/index.php?board=24.0) | This is a publicly editable wiki. Please either make the edit yourself or create a post |
-|[Arduino Forum](https://github.com/arduino/forum-issues) | Issues about the Arduino Forum |
-| [Libraries for Arduino IDE](https://github.com/arduino-libraries) | Changing libraries for the IDE |
-| [Arduino-builder](https://github.com/arduino/arduino-builder)| |
-|[Arduino Web Editor](https://github.com/arduino/arduino-create-agent) | |
-|[Arduino SAMD Boards](https://github.com/arduino/ArduinoCore-samd)|Zero, MKR1000, MKRZero, etc. |
-|[Arduino SAM Boards](https://github.com/arduino/ArduinoCore-sam)| Due |
-|[Arduino AVR Boards toolchain (avr-gcc)](https://github.com/arduino/toolchain-avr)| |
-|[Arduino's build of AVRDUDE](https://github.com/arduino/avrdude-build-script)||
-|Third party repository | for third party libraries, hardware packages or sketches |
-
-### 2. Posting the issue
-When you have checked the previous boxes. Please consider the following points before posting the issue.
-
-- [ ] `Describe the issue based on the behaviour you were expecting`
-- [ ] `Post complete error messages using markdown code fencing:` [Markdown Code Fencing Example](https://guides.github.com/features/mastering-markdown/#examples)
-- [ ] `Provide a full set of steps necessary to reproduce the issue`
-- [ ] `Demonstration code should be complete, correct and the minimum amount necessary to reproduce the issue`
-- [ ] `Library Manager submissions: make sure your library meets all the requirements listed in the` [Library Manager FAQ](https://github.com/arduino/Arduino/wiki/Library-Manager-FAQ)
-
-### 3. Pull Requests
-Before starting to work on bigger topics like modifying the API or changes with backward compatibility trade-offs please discuss them in the [mailing list](https://groups.google.com/a/arduino.cc/forum/#!forum/developers) first.
-
-### 4. Commit messages
-An easy to read pull request will speed up the merging process. Your commit messages need to be logically separate. And containing enough information on their own. When this is done consistently your pull request will have an easy to read log of changes.
-
-Your commits need to be [atomic](https://www.freshconsulting.com/atomic-commits/) which allows the repository to remain flexible after merging.
-
-If you did not read the following 7 points before or just want to fresh up. Please read up on them on this [website](https://chris.beams.io/posts/git-commit) by Chris Beams.
-
-1. Separate subject from body with a blank line
-2. Limit the subject line (first line) to 50 characters
-3. Capitalize the subject line
-4. Do not end the subject line with a period `(.)`
-5. Use the imperative mood in the subject line.
-This should be in the written as giving an instruction for example "Fixed save-as bug" (it shows what the PR achieves when merging it)
-6. Wrap body at 72 characters
-7. Use the body to explain what, why and how
-
-If your pull request fixes, closes or resolves an issue please reference it in the body with the following [syntax](https://help.github.com/articles/closing-issues-via-commit-messages/). Also see the last lines of the following example.
-
-A general example with these 7 guidelines in mind is shown below (from the same website of [Chris Beams](https://chris.beams.io/posts/git-commit)):
-```
-Summarize changes in around 50 characters or less
-
-More detailed explanatory text, if necessary. Wrap it to about 72
-characters or so. In some contexts, the first line is treated as the
-subject of the commit and the rest of the text as the body. The
-blank line separating the summary from the body is critical (unless
-you omit the body entirely); various tools like `log`, `shortlog`
-and `rebase` can get confused if you run the two together.
-
-Explain the problem that this commit is solving. Focus on why you
-are making this change as opposed to how (the code explains that).
-Are there side effects or other unintuitive consequences of this
-change? Here's the place to explain them.
-
-Further paragraphs come after blank lines.
-
- - Bullet points are okay, too
-
- - Typically a hyphen or asterisk is used for the bullet, preceded
- by a single space, with blank lines in between, but conventions
- vary here
-
-If you use an issue tracker, put references to them at the bottom,
-like this:
-
-Resolves: #123
-See also: #456, #789
-```
-
-### 5. Rebasing pull requests
-When different people are working on the Arduino project simultaneously, pull requests can go stale quickly. A "stale" pull request is one that is no longer up to date with the latest merges in the project. It needs to be updated before it can be merged.
-
-Most often pull requests become stale when merge conflicts occur. This happens when two pull requests both modify similar lines in the same file and one gets merged, the unmerged request will now have a merge conflict and needs updating.
-
-When your pull request is stale, you will need to rebase your branch on the current master branch before you can merge it without conflicts.
-
-More information about rebasing can be found at the repository of [edX](https://github.com/edx/edx-platform/wiki/How-to-Rebase-a-Pull-Request).
-
-### 6. Merged!
-When your pull request is merged please update the documentation if the changes require it:
-
-- [ ] Edit appropiate [Wiki pages](https://github.com/arduino/Arduino/wiki/_pages)
-- [ ] Submit an [issue report](https://github.com/arduino/Arduino/issues/new) requesting changes to the [arduino.cc reference pages](https://www.arduino.cc/en/Reference/HomePage)
+# Contributing Rules
+Thanks for your interest in contributing to this free open source project! Arduino welcomes help from the community. There are several ways you can get involved:
+
+| Type of contribution | Contribution method |
+|-|-|
+| - Support request - Question - Problem with your Arduino - Discussion | Post on the [Arduino Forum](http://forum.arduino.cc) |
+| - Bug report - [Arduino website](https://www.arduino.cc/) issue or improvement - Feature request | Issue report (read the [issue guidelines](#issues)) |
+| - Bug fix - Enhancement | Pull Request (read the [pull request guidelines](#pull-requests)) |
+| Translations for the Arduino IDE | [transifex](https://www.transifex.com/mbanzi/arduino-ide-15/) |
+| Translations for the [Language Reference](https://www.arduino.cc/reference) | [Reference repositories](https://github.com/arduino?q=reference-) |
+| Monetary | - [Donate](https://www.arduino.cc/en/Main/Contribute) - [Buy official products](https://store.arduino.cc) |
+
+
+## Issues
+- Do you need help or have a question about using Arduino? Support requests should be made to the appropriate section of the [Arduino forum](http://forum.arduino.cc) rather than an issue report. **Issue reports are to be used to report bugs or make feature requests only.**
+- Check if your issue has already been resolved in the [hourly build](http://www.arduino.cc/en/Main/Software#hourly).
+- Submit issue reports to the correct repository:
+
+| Issue topic | Report at |
+|-|-|
+| Arduino IDE, arduino.cc (but not the Arduino Forum), Library Manager additions | [arduino/Arduino](https://github.com/arduino/Arduino/issues) |
+| [Language Reference](https://www.arduino.cc/reference) | [Reference repositories](https://github.com/arduino?q=reference-) |
+| Arduino Forum | [arduino/forum-issues](https://github.com/arduino/forum-issues/issues) |
+| Arduino libraries | [arduino-libraries](https://github.com/arduino-libraries) |
+| Built-in examples | [arduino/arduino-examples](https://github.com/arduino/arduino-examples/issues) |
+| arduino-builder | [arduino/arduino-builder](https://github.com/arduino/arduino-builder/issues) |
+| [Arduino Web Editor](https://create.arduino.cc/editor) | [**Create > Editor** section of the Arduino Forum](http://forum.arduino.cc/index.php?board=101.0) |
+| Arduino AVR Boards (Uno, Mega, Leonardo, etc.) | [arduino/ArduinoCore-avr](https://github.com/arduino/ArduinoCore-avr/issues) |
+| Arduino SAMD Boards (Zero, MKR1000, MKRZero, etc.) | [arduino/ArduinoCore-samd](https://github.com/arduino/ArduinoCore-samd/issues) |
+| Arduino SAM Boards (Due) | [arduino/ArduinoCore-sam](https://github.com/arduino/ArduinoCore-sam/issues) |
+| AVR Toolchain for Arduino | [arduino/toolchain-avr](https://github.com/arduino/toolchain-avr/issues) |
+| Arduino's build of AVRDUDE | [arduino/avrdude-build-script](https://github.com/arduino/avrdude-build-script/issues) |
+| Security vulnerability | See: [Coordinated Vulnerability Disclosure Policy](https://github.com/arduino/arduino-cvd-policy) |
+| 3rd party libraries, hardware, or sketches | Report issues to the author of the software, *not* Arduino. |
+
+When you're not sure where your issue belongs, report it at [arduino/Arduino](https://github.com/arduino/Arduino) and we'll move it to where it belongs (but remember: Only bug reports and feature requests, do not ask for help with your own code there).
+
+- Search [existing pull requests and issues](https://github.com/arduino/Arduino/issues?q=) to be sure it hasn't already been reported. If you have additional information to provide about an existing issue then please comment on that issue. If you simply want to express your support then use the [Reactions feature](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments).
+- State the newest version of the Arduino IDE you have verified the issue with and which operating system you are using.
+- The issue title should be concise yet descriptive. Vague titles make it difficult to know the purpose of the issue when looking through the list of reports and may cause your issue to not be given proper attention.
+- Describe the issue and what behavior you were expecting. Post complete error messages using [Markdown code fencing](https://guides.github.com/features/mastering-markdown/#examples), three backticks before and after the error message:
+ ````
+ ```
+ my error message here
+ ```
+ ````
+- Provide a full set of steps necessary to reproduce the issue. Demonstration code should be complete, correct, and simplified to the minimum amount of code necessary to reproduce the issue. Please use [Markdown code fencing](https://guides.github.com/features/mastering-markdown/#examples), three backticks before and after the code:
+ ````
+ ```
+ my code here
+ ```
+ ````
+- Be responsive. We may need you to provide more information, please respond as soon as possible.
+- If you find a solution to your problem update your issue report with an explanation of how you were able to fix it and close the issue.
+- Library Manager submissions: make sure your library meets all the requirements listed in the [Library Manager FAQ](https://github.com/arduino/Arduino/wiki/Library-Manager-FAQ).
+
+
+## Pull Requests
+Pull requests are an easy and effective way to submit a proposal for a change to the content of one of Arduino's repositories. The Arduino team can merge your change with a single click! You can find more information about pull requests [here](https://help.github.com/articles/creating-a-pull-request/).
+- Big changes, changes to the API, or changes with backward compatibility trade-offs should be first discussed in the [Arduino Developers Mailing List](https://groups.google.com/a/arduino.cc/forum/#!forum/developers).
+- Search [existing pull requests](https://github.com/arduino/Arduino/pulls?q=) to see if one has already been submitted for this change. Search the [issues](https://github.com/arduino/Arduino/issues?q=is%3Aissue) to see if there has been a discussion on this topic and whether your pull request can close any issues.
+- Code formatting should be consistent with the style used in the existing code.
+- Don't leave commented out code. A record of this code is already preserved in the commit history.
+- All commits must be atomic. This means that the commit completely accomplishes a single task. Each commit should result in fully functional code. Multiple tasks should not be combined in a single commit, but a single task should not be split over multiple commits (e.g. one commit per file modified is not a good practice). For more information see http://www.freshconsulting.com/atomic-commits.
+- Each pull request should address a single bug fix or enhancement. This may consist of multiple commits. If you have multiple, unrelated fixes or enhancements to contribute, submit them as separate pull requests.
+- Commit messages:
+ - Use the [imperative mood](http://chris.beams.io/posts/git-commit/#imperative) in the title. For example: "Apply editor.indent preference"
+ - Capitalize the title.
+ - Do not end the title with a period.
+ - Separate title from the body with a blank line. If you're committing via GitHub or GitHub Desktop this will be done automatically.
+ - Wrap body at 72 characters.
+ - Completely explain the purpose of the commit. Include a rationale for the change, any caveats, side-effects, etc.
+ - If your pull request fixes an issue in the issue tracker, use the [closes/fixes/resolves syntax](https://help.github.com/articles/closing-issues-via-commit-messages) in the body to indicate this.
+ - See http://chris.beams.io/posts/git-commit for more tips on writing good commit messages.
+- Pull request title and description should follow the same guidelines as commit messages.
+- Rebasing pull requests is OK and encouraged. After submitting your pull request some changes may be requested. Rather than adding unnecessary extra commits to the pull request, you can squash these changes into the existing commit and then do a force push to your fork. When you do a force push to your fork, the PR will be updated with your new changes, so there is no need to open a new PR to make changes. Leave a comment on the pull request thread to explain that the history has been changed. This will help to keep the commit history of the repository clean.
+- After your pull request is merged please update the documentation if the changes require it:
+ - Edit appropriate [Wiki pages](https://github.com/arduino/Arduino/wiki/_pages).
+ - Submit an [issue report](https://github.com/arduino/Arduino/issues/new) requesting changes to the [arduino.cc reference pages](https://www.arduino.cc/en/Reference/HomePage).
+- For more contributing guidelines, see the [Arduino Development Policy](https://github.com/arduino/Arduino/wiki/Development-Policy).
diff --git a/README.md b/README.md
index 0bc3e34fb3a..ab7504dd2fc 100644
--- a/README.md
+++ b/README.md
@@ -1,54 +1,59 @@
-Arduino
-========
-
-* Arduino is an open-source physical computing platform based on a simple I/O
-board and a development environment that implements the Processing/Wiring
-language. Arduino can be used to develop stand-alone interactive objects or
-can be connected to software on your computer (e.g. Flash, Processing and MaxMSP).
-The boards can be assembled by hand or purchased preassembled; the open-source
-IDE can be downloaded for free at https://www.arduino.cc/en/Main/Software
-
-* For more information, see the website at: https://www.arduino.cc/
-or the forums at: https://forum.arduino.cc/
-You can also follow Arduino on Twitter at: https://twitter.com/arduino or
-like Arduino on Facebook at: https://www.facebook.com/official.arduino
-
-* To report a *bug* in the software or to request *a simple enhancement* go to:
-https://github.com/arduino/Arduino/issues
-
-* More complex requests and technical discussion should go on the Arduino Developers
-mailing list:
-https://groups.google.com/a/arduino.cc/forum/#!forum/developers
-
-* If you're interested in modifying or extending the Arduino software, we strongly
-suggest discussing your ideas on the Developers mailing list *before* starting
-to work on them. That way you can coordinate with the Arduino Team and others,
-giving your work a higher chance of being integrated into the official release
-https://groups.google.com/a/arduino.cc/forum/#!forum/developers
-
-Installation
-------------
-Detailed instructions for installation in popular operating systems.
-For Linux: https://www.arduino.cc/en/Guide/Linux (see also the Arduino playground page https://playground.arduino.cc/Learning/Linux)
-For macOS X: https://www.arduino.cc/en/Guide/MacOSX
-For Windows: https://www.arduino.cc/en/Guide/Windows
-
-Credits
---------
-Arduino is an open source project, supported by many.
-
-The Arduino team is composed of Massimo Banzi, David Cuartielles, Tom Igoe
-and David A. Mellis.
-
-Arduino uses
-[GNU avr-gcc toolchain](https://gcc.gnu.org/wiki/avr-gcc),
-[GCC ARM Embedded toolchain](https://launchpad.net/gcc-arm-embedded),
-[avr-libc](http://www.nongnu.org/avr-libc/),
-[avrdude](http://www.nongnu.org/avrdude/),
-[bossac](http://www.shumatech.com/web/products/bossa),
-[openOCD](http://openocd.org/)
-and code from [Processing](https://www.processing.org)
-and [Wiring](http://wiring.org.co).
-
-Icon and about image designed by [ToDo](https://www.todo.to.it/)
+
+
+
+**Important Notice**: This repository contains the legacy Arduino IDE 1.x, which is no longer in active development. For the latest features and updates, please visit the [Arduino IDE 2.x](https://github.com/arduino/arduino-ide) repository. If you encounter issues related to the newer IDE, please report them there.
+
+Arduino is an open-source physical computing platform based on a simple I/O board and a development environment that implements the Processing/Wiring language. Arduino can be used to develop stand-alone interactive objects or can be connected to software on your computer (e.g. Flash, Processing and MaxMSP). The boards can be assembled by hand or purchased preassembled; the open-source IDE can be downloaded for free at [https://arduino.cc](https://www.arduino.cc/en/Main/Software).
+
+
+
+## More info at
+
+- [Our website](https://www.arduino.cc/)
+- [The forums](https://forum.arduino.cc/)
+- Follow us on [Twitter](https://twitter.com/arduino)
+- And like us at [Facebook](https://www.facebook.com/official.arduino)
+
+## Bug reports and technical discussions
+
+- To report a *bug* in the software or to request *a simple enhancement*, go to [Github Issues](https://github.com/arduino/Arduino/issues).
+- More complex requests and technical discussions should go on the [Arduino Developers mailing list](https://groups.google.com/a/arduino.cc/forum/#!forum/developers).
+- If you're interested in modifying or extending the Arduino software, we strongly suggest discussing your ideas on the [Developers mailing list](https://groups.google.com/a/arduino.cc/forum/#!forum/developers) *before* starting to work on them. That way you can coordinate with the Arduino Team and others, giving your work a higher chance of being integrated into the official release.
+
+### Security
+
+If you think you found a vulnerability or other security-related bug in this project, please read our [security policy](https://github.com/arduino/Arduino/security/policy) and report the bug to our Security Team 🛡️. Thank you!
+
+e-mail contact: security@arduino.cc
+
+## Installation
+
+Detailed instructions for installation on popular operating systems can be found at:
+
+- [Linux](https://www.arduino.cc/en/Guide/Linux) (see also the [Arduino playground](https://playground.arduino.cc/Learning/Linux))
+- [macOS](https://www.arduino.cc/en/Guide/macOS)
+- [Windows](https://www.arduino.cc/en/Guide/Windows)
+
+## Contents of this repository
+
+This repository contains just the code for the Arduino IDE itself. Originally, it also contained the AVR and SAM Arduino core and libraries (i.e. the code that is compiled as part of a sketch and runs on the actual Arduino device), but those have been moved into their own repositories. They are still automatically downloaded as part of the build process and included in built releases, though.
+
+The repositories for these extra parts can be found here:
+- Non-core specific Libraries are listed under: [Arduino Libraries](https://github.com/arduino-libraries/) (and also a few other places, see `build/build.xml`).
+- The AVR core can be found at: [ArduinoCore-avr](https://github.com/arduino/ArduinoCore-avr).
+- Other cores are not included by default but can be installed through the board manager. Their repositories can also be found under [Arduino GitHub organization](https://github.com/arduino/).
+
+## Building and testing
+
+Instructions for building the IDE and running unit tests can be found on the wiki:
+- [Building Arduino](https://github.com/arduino/Arduino/wiki/Building-Arduino)
+- [Testing Arduino](https://github.com/arduino/Arduino/wiki/Testing-Arduino)
+
+## Credits
+
+Arduino is an open-source project, supported by many. The Arduino team is composed of Massimo Banzi, David Cuartielles, Tom Igoe, and David A. Mellis.
+
+Arduino uses [GNU avr-gcc toolchain](https://gcc.gnu.org/wiki/avr-gcc), [GCC ARM Embedded toolchain](https://launchpad.net/gcc-arm-embedded), [avr-libc](https://www.nongnu.org/avr-libc/), [avrdude](https://www.nongnu.org/avrdude/), [bossac](http://www.shumatech.com/web/products/bossa), [openOCD](http://openocd.org/), and code from [Processing](https://www.processing.org) and [Wiring](http://wiring.org.co).
+
+Icon and about image designed by [ToDo](https://www.todo.to.it/).
diff --git a/app/.classpath b/app/.classpath
index 51172fa7c40..ea9425b4990 100644
--- a/app/.classpath
+++ b/app/.classpath
@@ -4,7 +4,6 @@
-
@@ -30,20 +29,17 @@
-
-
-
+
-
-
+
@@ -53,4 +49,8 @@
+
+
+
+
diff --git a/app/build.xml b/app/build.xml
index cc38670adc6..d2e9ad0069d 100644
--- a/app/build.xml
+++ b/app/build.xml
@@ -1,5 +1,5 @@
-
+
@@ -105,7 +105,20 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -117,9 +130,20 @@
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
@@ -127,6 +151,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/lib/rsyntaxtextarea-BSD.txt b/app/lib/RSyntaxTextArea.License.txt
similarity index 95%
rename from app/lib/rsyntaxtextarea-BSD.txt
rename to app/lib/RSyntaxTextArea.License.txt
index 3a6e638904a..f0f2d4c743e 100644
--- a/app/lib/rsyntaxtextarea-BSD.txt
+++ b/app/lib/RSyntaxTextArea.License.txt
@@ -21,4 +21,4 @@ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/app/lib/commons-io-2.6.jar b/app/lib/commons-io-2.6.jar
new file mode 100644
index 00000000000..00556b119d4
Binary files /dev/null and b/app/lib/commons-io-2.6.jar differ
diff --git a/app/lib/commons-lang3-3.3.2.jar b/app/lib/commons-lang3-3.3.2.jar
deleted file mode 100644
index 2ce08ae99d1..00000000000
Binary files a/app/lib/commons-lang3-3.3.2.jar and /dev/null differ
diff --git a/app/lib/commons-lang3-3.8.1.jar b/app/lib/commons-lang3-3.8.1.jar
new file mode 100644
index 00000000000..2c65ce67d5c
Binary files /dev/null and b/app/lib/commons-lang3-3.8.1.jar differ
diff --git a/app/lib/jackson-module-mrbean-2.9.5.jar b/app/lib/jackson-module-mrbean-2.9.5.jar
deleted file mode 100644
index dc7f5a05721..00000000000
Binary files a/app/lib/jackson-module-mrbean-2.9.5.jar and /dev/null differ
diff --git a/app/lib/jmdns-3.5.3.jar b/app/lib/jmdns-3.5.3.jar
deleted file mode 100644
index d4d9c67f46c..00000000000
Binary files a/app/lib/jmdns-3.5.3.jar and /dev/null differ
diff --git a/app/lib/jmdns-3.5.5.jar b/app/lib/jmdns-3.5.5.jar
new file mode 100644
index 00000000000..a8b65ff2ec7
Binary files /dev/null and b/app/lib/jmdns-3.5.5.jar differ
diff --git a/app/lib/jssc-2.8.0-arduino2.jar b/app/lib/jssc-2.8.0-arduino2.jar
deleted file mode 100644
index a9ec9838921..00000000000
Binary files a/app/lib/jssc-2.8.0-arduino2.jar and /dev/null differ
diff --git a/app/lib/jssc-2.8.0-arduino4.jar b/app/lib/jssc-2.8.0-arduino4.jar
new file mode 100644
index 00000000000..623a3833bce
Binary files /dev/null and b/app/lib/jssc-2.8.0-arduino4.jar differ
diff --git a/app/lib/jtouchbar-1.0.0.jar b/app/lib/jtouchbar-1.0.0.jar
new file mode 100644
index 00000000000..2c473bec881
Binary files /dev/null and b/app/lib/jtouchbar-1.0.0.jar differ
diff --git a/app/lib/jtouchbar.LICENSE.MIT.txt b/app/lib/jtouchbar.LICENSE.MIT.txt
new file mode 100644
index 00000000000..f4fa99707a0
--- /dev/null
+++ b/app/lib/jtouchbar.LICENSE.MIT.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 thizzer.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/app/lib/rsyntaxtextarea-2.6.1.jar b/app/lib/rsyntaxtextarea-2.6.1.jar
deleted file mode 100644
index b834e2d412b..00000000000
Binary files a/app/lib/rsyntaxtextarea-2.6.1.jar and /dev/null differ
diff --git a/app/lib/rsyntaxtextarea-3.0.3-SNAPSHOT.jar b/app/lib/rsyntaxtextarea-3.0.3-SNAPSHOT.jar
new file mode 100644
index 00000000000..e1844494aca
Binary files /dev/null and b/app/lib/rsyntaxtextarea-3.0.3-SNAPSHOT.jar differ
diff --git a/app/src/cc/arduino/contributions/ContributionsSelfCheck.java b/app/src/cc/arduino/contributions/ContributionsSelfCheck.java
index 7812f62ad7f..50e5e8617ea 100644
--- a/app/src/cc/arduino/contributions/ContributionsSelfCheck.java
+++ b/app/src/cc/arduino/contributions/ContributionsSelfCheck.java
@@ -29,32 +29,33 @@
package cc.arduino.contributions;
+import cc.arduino.UpdatableBoardsLibsFakeURLsHandler;
import cc.arduino.contributions.libraries.LibraryInstaller;
import cc.arduino.contributions.libraries.filters.UpdatableLibraryPredicate;
import cc.arduino.contributions.packages.ContributionInstaller;
import cc.arduino.contributions.packages.filters.UpdatablePlatformPredicate;
import cc.arduino.view.NotificationPopup;
-import processing.app.Base;
-import processing.app.BaseNoGui;
-import processing.app.Editor;
-import processing.app.I18n;
+import processing.app.*;
import javax.swing.*;
import javax.swing.event.HyperlinkListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowFocusListener;
+import java.net.URL;
import java.util.TimerTask;
import static processing.app.I18n.tr;
-public class ContributionsSelfCheck extends TimerTask {
+public class ContributionsSelfCheck extends TimerTask implements NotificationPopup.OptionalButtonCallbacks {
private final Base base;
private final HyperlinkListener hyperlinkListener;
private final ContributionInstaller contributionInstaller;
private final LibraryInstaller libraryInstaller;
private final ProgressListener progressListener;
+ private final String boardsManagerURL = "http://boardsmanager/DropdownUpdatableCoresItem";
+ private final String libraryManagerURL = "http://librarymanager/DropdownUpdatableLibrariesItem";
private volatile boolean cancelled;
private volatile NotificationPopup notificationPopup;
@@ -81,13 +82,41 @@ public void run() {
return;
}
- String text;
+ boolean setAccessible = PreferencesData.getBoolean("ide.accessible");
+ final String text;
+ final String button1Name;
+ final String button2Name;
+ String openAnchorBoards = "";
+ String closeAnchorBoards = "";
+ String openAnchorLibraries = "";
+ String closeAnchorLibraries = "";
+
+ // if accessibility mode and board updates are available set the button name and clear the anchors
+ if(setAccessible && updatablePlatforms) {
+ button1Name = tr("Boards");
+ openAnchorBoards = "";
+ closeAnchorBoards = "";
+ }
+ else { // when not accessibility mode or no boards to update no button is needed
+ button1Name = null;
+ }
+
+ // if accessibility mode and libraries updates are available set the button name and clear the anchors
+ if (setAccessible && updatableLibraries) {
+ button2Name = tr("Libraries");
+ openAnchorLibraries = "";
+ closeAnchorLibraries = "";
+ }
+ else { // when not accessibility mode or no libraries to update no button is needed
+ button2Name = null;
+ }
+
if (updatableLibraries && !updatablePlatforms) {
- text = I18n.format(tr("Updates available for some of your {0}libraries{1}"), "", "");
+ text = I18n.format(tr("Updates available for some of your {0}libraries{1}"), openAnchorLibraries, closeAnchorLibraries);
} else if (!updatableLibraries && updatablePlatforms) {
- text = I18n.format(tr("Updates available for some of your {0}boards{1}"), "", "");
+ text = I18n.format(tr("Updates available for some of your {0}boards{1}"), openAnchorBoards, closeAnchorBoards);
} else {
- text = I18n.format(tr("Updates available for some of your {0}boards{1} and {2}libraries{3}"), "", "", "", "");
+ text = I18n.format(tr("Updates available for some of your {0}boards{1} and {2}libraries{3}"), openAnchorBoards, closeAnchorBoards, openAnchorLibraries, closeAnchorLibraries);
}
if (cancelled) {
@@ -96,7 +125,13 @@ public void run() {
SwingUtilities.invokeLater(() -> {
Editor ed = base.getActiveEditor();
- notificationPopup = new NotificationPopup(ed, hyperlinkListener, text);
+ boolean accessibleIde = PreferencesData.getBoolean("ide.accessible");
+ if (accessibleIde) {
+ notificationPopup = new NotificationPopup(ed, hyperlinkListener, text, false, this, button1Name, button2Name);
+ }
+ else { // if not accessible view leave it the same
+ notificationPopup = new NotificationPopup(ed, hyperlinkListener, text);
+ }
if (ed.isFocused()) {
notificationPopup.begin();
return;
@@ -122,6 +157,26 @@ public void windowGainedFocus(WindowEvent evt) {
});
}
+ private void goToManager(String link) {
+ try {
+ ((UpdatableBoardsLibsFakeURLsHandler) hyperlinkListener)
+ .openBoardLibManager(new URL(link));
+ } catch (Exception e) {
+ System.err.println("Error while attempting to open board manager: "
+ + e.getMessage());
+ }
+ }
+
+ // callback for boards button
+ public void onOptionalButton1Callback() {
+ goToManager(boardsManagerURL);
+ }
+
+ // callback for libraries button
+ public void onOptionalButton2Callback() {
+ goToManager(libraryManagerURL);
+ }
+
static boolean checkForUpdatablePlatforms() {
return BaseNoGui.indexer.getPackages().stream()
.flatMap(pack -> pack.getPlatforms().stream())
diff --git a/app/src/cc/arduino/contributions/libraries/ui/ContributedLibraryReleasesComparator.java b/app/src/cc/arduino/contributions/libraries/ui/ContributedLibraryReleasesComparator.java
index b302306523c..11436b2ccfb 100644
--- a/app/src/cc/arduino/contributions/libraries/ui/ContributedLibraryReleasesComparator.java
+++ b/app/src/cc/arduino/contributions/libraries/ui/ContributedLibraryReleasesComparator.java
@@ -32,7 +32,9 @@
import cc.arduino.contributions.libraries.ContributedLibrary;
import cc.arduino.contributions.libraries.ContributedLibraryReleases;
+import java.util.Arrays;
import java.util.Comparator;
+import java.util.List;
public class ContributedLibraryReleasesComparator implements Comparator {
@@ -47,9 +49,11 @@ public int compare(ContributedLibraryReleases o1, ContributedLibraryReleases o2)
ContributedLibrary lib1 = o1.getLatest();
ContributedLibrary lib2 = o2.getLatest();
- if (lib1.getTypes() == null || lib2.getTypes() == null) {
- return compareName(lib1, lib2);
- }
+ List types1 = lib1.getTypes();
+ List types2 = lib2.getTypes();
+ if (types1 == null) types1 = Arrays.asList();
+ if (types2 == null) types2 = Arrays.asList();
+
if (lib1.getTypes().contains(firstType) && lib2.getTypes().contains(firstType)) {
return compareName(lib1, lib2);
}
diff --git a/app/src/cc/arduino/contributions/libraries/ui/ContributedLibraryTableCellEditor.java b/app/src/cc/arduino/contributions/libraries/ui/ContributedLibraryTableCellEditor.java
index 7f46ba7e903..7c2ecff383f 100644
--- a/app/src/cc/arduino/contributions/libraries/ui/ContributedLibraryTableCellEditor.java
+++ b/app/src/cc/arduino/contributions/libraries/ui/ContributedLibraryTableCellEditor.java
@@ -98,33 +98,34 @@ public Component getTableCellEditorComponent(JTable table, Object value,
editorCell.downgradeChooser.addItem(tr("Select version"));
final List notInstalledPrevious = new LinkedList<>();
- final List notIInstalledNewer = new LinkedList<>();
+ final List notInstalledNewer = new LinkedList<>();
notInstalled.stream().forEach(input -> {
if (!mayInstalled.isPresent()
|| VersionComparator.greaterThan(mayInstalled.get(), input)) {
notInstalledPrevious.add(input);
} else {
- notIInstalledNewer.add(input);
+ notInstalledNewer.add(input);
}
});
- notIInstalledNewer.forEach(editorCell.downgradeChooser::addItem);
+ notInstalledNewer.forEach(editorCell.downgradeChooser::addItem);
notInstalledPrevious.forEach(editorCell.downgradeChooser::addItem);
editorCell.downgradeChooser
.setVisible(mayInstalled.isPresent()
&& (!notInstalledPrevious.isEmpty()
- || notIInstalledNewer.size() > 1));
+ || notInstalledNewer.size() > 1));
editorCell.downgradeButton
.setVisible(mayInstalled.isPresent()
&& (!notInstalledPrevious.isEmpty()
- || notIInstalledNewer.size() > 1));
+ || notInstalledNewer.size() > 1));
editorCell.versionToInstallChooser.removeAllItems();
notInstalled.forEach(editorCell.versionToInstallChooser::addItem);
editorCell.versionToInstallChooser
.setVisible(!mayInstalled.isPresent() && notInstalled.size() > 1);
+ editorCell.setForeground(Color.BLACK);
editorCell.setBackground(new Color(218, 227, 227)); // #dae3e3
return editorCell;
}
diff --git a/app/src/cc/arduino/contributions/libraries/ui/ContributedLibraryTableCellJPanel.java b/app/src/cc/arduino/contributions/libraries/ui/ContributedLibraryTableCellJPanel.java
index 99d4b66d23e..a5bb940babc 100644
--- a/app/src/cc/arduino/contributions/libraries/ui/ContributedLibraryTableCellJPanel.java
+++ b/app/src/cc/arduino/contributions/libraries/ui/ContributedLibraryTableCellJPanel.java
@@ -3,21 +3,12 @@
import static processing.app.I18n.format;
import static processing.app.I18n.tr;
-import java.awt.Color;
-import java.awt.Component;
-import java.awt.Dimension;
-import java.awt.Insets;
+import java.awt.*;
import java.util.Optional;
-import javax.swing.Box;
-import javax.swing.BoxLayout;
-import javax.swing.JButton;
-import javax.swing.JComboBox;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.JTable;
-import javax.swing.JTextPane;
+import javax.swing.*;
import javax.swing.border.EmptyBorder;
+import javax.swing.border.TitledBorder;
import javax.swing.event.HyperlinkEvent;
import javax.swing.text.Document;
import javax.swing.text.html.HTMLDocument;
@@ -28,10 +19,12 @@
import cc.arduino.contributions.libraries.ContributedLibraryReleases;
import cc.arduino.contributions.ui.InstallerTableCell;
import processing.app.Base;
+import processing.app.PreferencesData;
import processing.app.Theme;
public class ContributedLibraryTableCellJPanel extends JPanel {
+ final JButton moreInfoButton;
final JButton installButton;
final Component installButtonPlaceholder;
final JComboBox downgradeChooser;
@@ -40,12 +33,22 @@ public class ContributedLibraryTableCellJPanel extends JPanel {
final JPanel buttonsPanel;
final JPanel inactiveButtonsPanel;
final JLabel statusLabel;
+ final JTextPane description;
+ final TitledBorder titledBorder;
+ private final String moreInfoLbl = tr("More info");
public ContributedLibraryTableCellJPanel(JTable parentTable, Object value,
boolean isSelected) {
super();
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
+ // Actual title set below
+ titledBorder = BorderFactory.createTitledBorder("");
+ titledBorder.setTitleFont(getFont().deriveFont(Font.BOLD));
+ setBorder(titledBorder);
+
+ moreInfoButton = new JButton(moreInfoLbl);
+ moreInfoButton.setVisible(false);
installButton = new JButton(tr("Install"));
int width = installButton.getPreferredSize().width;
installButtonPlaceholder = Box.createRigidArea(new Dimension(width, 1));
@@ -72,13 +75,19 @@ public ContributedLibraryTableCellJPanel(JTable parentTable, Object value,
versionToInstallChooser
.setMinimumSize(new Dimension((int)versionToInstallChooser.getPreferredSize().getWidth() + 50, (int)versionToInstallChooser.getPreferredSize().getHeight()));
- makeNewDescription();
+ description = makeNewDescription();
+ add(description);
buttonsPanel = new JPanel();
buttonsPanel.setLayout(new BoxLayout(buttonsPanel, BoxLayout.X_AXIS));
buttonsPanel.setOpaque(false);
buttonsPanel.add(Box.createHorizontalStrut(7));
+ if (PreferencesData.getBoolean("ide.accessible")) {
+ buttonsPanel.add(moreInfoButton);
+ buttonsPanel.add(Box.createHorizontalStrut(5));
+ buttonsPanel.add(Box.createHorizontalStrut(15));
+ }
buttonsPanel.add(downgradeChooser);
buttonsPanel.add(Box.createHorizontalStrut(5));
buttonsPanel.add(downgradeButton);
@@ -111,13 +120,13 @@ public ContributedLibraryTableCellJPanel(JTable parentTable, Object value,
add(Box.createVerticalStrut(15));
ContributedLibraryReleases releases = (ContributedLibraryReleases) value;
- JTextPane description = makeNewDescription();
// FIXME: happens on macosx, don't know why
if (releases == null)
return;
ContributedLibrary selected = releases.getSelected();
+ titledBorder.setTitle(selected.getName());
Optional mayInstalled = releases.getInstalled();
boolean installable, upgradable;
@@ -139,9 +148,9 @@ public ContributedLibraryTableCellJPanel(JTable parentTable, Object value,
installButtonPlaceholder.setVisible(!(installable || upgradable));
String name = selected.getName();
- String author = selected.getAuthor();
- // String maintainer = selectedLib.getMaintainer();
- String website = selected.getWebsite();
+ // String author = selected.getAuthor();
+ String maintainer = selected.getMaintainer();
+ final String website = selected.getWebsite();
String sentence = selected.getSentence();
String paragraph = selected.getParagraph();
// String availableVer = selectedLib.getVersion();
@@ -152,15 +161,15 @@ public ContributedLibraryTableCellJPanel(JTable parentTable, Object value,
String desc = "";
// Library name...
- desc += format("{0}", name);
+// desc += format("{0}", name);
if (mayInstalled.isPresent() && mayInstalled.get().isIDEBuiltIn()) {
desc += " Built-In ";
}
// ...author...
desc += format("", midcolor);
- if (author != null && !author.isEmpty()) {
- desc += format(" by {0}", author);
+ if (maintainer != null && !maintainer.isEmpty()) {
+ desc += format(" by {0}", maintainer);
}
// ...version.
@@ -187,13 +196,14 @@ public ContributedLibraryTableCellJPanel(JTable parentTable, Object value,
desc += format("{0}", paragraph);
desc += " ";
}
- if (author != null && !author.isEmpty()) {
- desc += format("More info", website);
+ if (maintainer != null && !maintainer.isEmpty()) {
+ desc = setButtonOrLink(moreInfoButton, desc, moreInfoLbl, website);
}
desc += "";
description.setText(desc);
- description.setBackground(Color.WHITE);
+ // copy description to accessibility context for screen readers to use
+ description.getAccessibleContext().setAccessibleDescription(desc);
// for modelToView to work, the text area has to be sized. It doesn't
// matter if it's visible or not.
@@ -203,20 +213,29 @@ public ContributedLibraryTableCellJPanel(JTable parentTable, Object value,
InstallerTableCell
.setJTextPaneDimensionToFitContainedText(description,
parentTable.getBounds().width);
+ }
- if (isSelected) {
- setBackground(parentTable.getSelectionBackground());
- setForeground(parentTable.getSelectionForeground());
- } else {
- setBackground(parentTable.getBackground());
- setForeground(parentTable.getForeground());
+ // same function as in ContributedPlatformTableCellJPanel - is there a utils file this can move to?
+ private String setButtonOrLink(JButton button, String desc, String label, String url) {
+ boolean accessibleIDE = PreferencesData.getBoolean("ide.accessible");
+ String retString = desc;
+
+ if (accessibleIDE) {
+ button.setVisible(true);
+ button.addActionListener(e -> {
+ Base.openURL(url);
+ });
}
+ else {
+ // if not accessible IDE, keep link the same EXCEPT that now the link text is translated!
+ retString += format("{1} ", url, label);
+ }
+
+ return retString;
}
+ // TODO Make this a method of Theme
private JTextPane makeNewDescription() {
- if (getComponentCount() > 0) {
- remove(0);
- }
JTextPane description = new JTextPane();
description.setInheritsPopupMenu(true);
Insets margin = description.getMargin();
@@ -242,7 +261,6 @@ private JTextPane makeNewDescription() {
}
});
// description.addKeyListener(new DelegatingKeyListener(parentTable));
- add(description, 0);
return description;
}
@@ -251,4 +269,13 @@ public void setButtonsVisible(boolean enabled) {
buttonsPanel.setVisible(enabled);
inactiveButtonsPanel.setVisible(!enabled);
}
+
+ public void setForeground(Color c) {
+ super.setForeground(c);
+ // The description is not opaque, so copy our foreground color to it.
+ if (description != null)
+ description.setForeground(c);
+ if (titledBorder != null)
+ titledBorder.setTitleColor(c);
+ }
}
diff --git a/app/src/cc/arduino/contributions/libraries/ui/ContributedLibraryTableCellRenderer.java b/app/src/cc/arduino/contributions/libraries/ui/ContributedLibraryTableCellRenderer.java
index bc4b3ffd940..d107f90208a 100644
--- a/app/src/cc/arduino/contributions/libraries/ui/ContributedLibraryTableCellRenderer.java
+++ b/app/src/cc/arduino/contributions/libraries/ui/ContributedLibraryTableCellRenderer.java
@@ -46,6 +46,7 @@ public Component getTableCellRendererComponent(JTable table, Object value,
value, isSelected);
cell.setButtonsVisible(false);
+ cell.setForeground(Color.BLACK);
if (row % 2 == 0) {
cell.setBackground(new Color(236, 241, 241)); // #ecf1f1
} else {
diff --git a/app/src/cc/arduino/contributions/libraries/ui/LibraryManagerUI.java b/app/src/cc/arduino/contributions/libraries/ui/LibraryManagerUI.java
index 7ff1925878c..69ab10006c9 100644
--- a/app/src/cc/arduino/contributions/libraries/ui/LibraryManagerUI.java
+++ b/app/src/cc/arduino/contributions/libraries/ui/LibraryManagerUI.java
@@ -35,12 +35,12 @@
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
-import java.util.function.Predicate;
import javax.swing.Box;
import javax.swing.JComboBox;
@@ -52,6 +52,7 @@
import cc.arduino.contributions.libraries.ContributedLibraryReleases;
import cc.arduino.contributions.libraries.LibraryInstaller;
import cc.arduino.contributions.libraries.LibraryTypeComparator;
+import cc.arduino.contributions.libraries.ui.MultiLibraryInstallDialog.Result;
import cc.arduino.contributions.ui.DropdownItem;
import cc.arduino.contributions.ui.FilteredAbstractTableModel;
import cc.arduino.contributions.ui.InstallerJDialog;
@@ -65,13 +66,16 @@ public class LibraryManagerUI extends InstallerJDialog typeFilter;
@Override
protected FilteredAbstractTableModel createContribModel() {
return new LibrariesIndexTableModel();
}
+ private LibrariesIndexTableModel getContribModel() {
+ return (LibrariesIndexTableModel) contribModel;
+ }
+
@Override
protected TableCellRenderer createCellRenderer() {
return new ContributedLibraryTableCellRenderer();
@@ -85,7 +89,7 @@ protected void onInstall(ContributedLibrary selectedLibrary, Optional selected = (DropdownItem) typeChooser.getSelectedItem();
previousRowAtPoint = -1;
- if (selected != null && typeFilter != selected.getFilterPredicate()) {
- typeFilter = selected.getFilterPredicate();
+ if (selected != null && extraFilter != selected.getFilterPredicate()) {
+ extraFilter = selected.getFilterPredicate();
if (contribTable.getCellEditor() != null) {
contribTable.getCellEditor().stopCellEditing();
}
- updateIndexFilter(filters, categoryFilter.and(typeFilter));
+ updateIndexFilter(filters, categoryFilter.and(extraFilter));
}
}
};
+ private Collection oldCategories = new ArrayList<>();
+ private Collection oldTypes = new ArrayList<>();
+
public void updateUI() {
- DropdownItem previouslySelectedCategory = (DropdownItem) categoryChooser.getSelectedItem();
- DropdownItem previouslySelectedType = (DropdownItem) typeChooser.getSelectedItem();
+ // Check if categories or types have changed
+ Collection categories = BaseNoGui.librariesIndexer.getIndex().getCategories();
+ List types = new LinkedList<>(BaseNoGui.librariesIndexer.getIndex().getTypes());
+ Collections.sort(types, new LibraryTypeComparator());
- categoryChooser.removeActionListener(categoryChooserActionListener);
- typeChooser.removeActionListener(typeChooserActionListener);
+ if (categories.equals(oldCategories) && types.equals(oldTypes)) {
+ return;
+ }
+ oldCategories = categories;
+ oldTypes = types;
// Load categories
categoryFilter = x -> true;
+ categoryChooser.removeActionListener(categoryChooserActionListener);
categoryChooser.removeAllItems();
categoryChooser.addItem(new DropdownAllLibraries());
- Collection categories = BaseNoGui.librariesIndexer.getIndex().getCategories();
for (String category : categories) {
categoryChooser.addItem(new DropdownLibraryOfCategoryItem(category));
}
-
categoryChooser.setEnabled(categoryChooser.getItemCount() > 1);
-
categoryChooser.addActionListener(categoryChooserActionListener);
- if (previouslySelectedCategory != null) {
- categoryChooser.setSelectedItem(previouslySelectedCategory);
- } else {
- categoryChooser.setSelectedIndex(0);
- }
+ categoryChooser.setSelectedIndex(0);
- typeFilter = x -> true;
+ // Load types
+ extraFilter = x -> true;
+ typeChooser.removeActionListener(typeChooserActionListener);
typeChooser.removeAllItems();
typeChooser.addItem(new DropdownAllLibraries());
typeChooser.addItem(new DropdownUpdatableLibrariesItem());
typeChooser.addItem(new DropdownInstalledLibraryItem());
- List types = new LinkedList<>(BaseNoGui.librariesIndexer.getIndex().getTypes());
- Collections.sort(types, new LibraryTypeComparator());
for (String type : types) {
typeChooser.addItem(new DropdownLibraryOfTypeItem(type));
}
typeChooser.setEnabled(typeChooser.getItemCount() > 1);
typeChooser.addActionListener(typeChooserActionListener);
- if (previouslySelectedType != null) {
- typeChooser.setSelectedItem(previouslySelectedType);
- } else {
- typeChooser.setSelectedIndex(0);
- }
+ typeChooser.setSelectedIndex(0);
filterField.setEnabled(contribModel.getRowCount() > 0);
}
@@ -200,8 +201,11 @@ protected void onUpdatePressed() {
try {
setProgressVisible(true, "");
installer.updateIndex(this::setProgress);
- ((LibrariesIndexTableModel) contribModel).update();
onIndexesUpdated();
+ if (contribTable.getCellEditor() != null) {
+ contribTable.getCellEditor().stopCellEditing();
+ }
+ getContribModel().update();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
@@ -213,18 +217,35 @@ protected void onUpdatePressed() {
installerThread.start();
}
- public void onInstallPressed(final ContributedLibrary lib, final Optional mayReplaced) {
+ public void onInstallPressed(final ContributedLibrary lib) {
+ List deps = BaseNoGui.librariesIndexer.getIndex().resolveDependeciesOf(lib);
+ boolean depsInstalled = deps.stream().allMatch(l -> l.getInstalledLibrary().isPresent() || l.getName().equals(lib.getName()));
+ Result installDeps;
+ if (!depsInstalled) {
+ MultiLibraryInstallDialog dialog;
+ dialog = new MultiLibraryInstallDialog(this, lib, deps);
+ dialog.setLocationRelativeTo(this);
+ dialog.setVisible(true);
+ installDeps = dialog.getInstallDepsResult();
+ if (installDeps == Result.CANCEL)
+ return;
+ } else {
+ installDeps = Result.NONE;
+ }
clearErrorMessage();
installerThread = new Thread(() -> {
try {
setProgressVisible(true, tr("Installing..."));
- installer.install(lib, mayReplaced, this::setProgress);
- // TODO: Do a better job in refreshing only the needed element
+ if (installDeps == Result.ALL) {
+ installer.install(deps, this::setProgress);
+ } else {
+ installer.install(lib, this::setProgress);
+ }
+ onIndexesUpdated();
if (contribTable.getCellEditor() != null) {
contribTable.getCellEditor().stopCellEditing();
}
- ((LibrariesIndexTableModel) contribModel).update();
- onIndexesUpdated();
+ getContribModel().update();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
@@ -251,12 +272,11 @@ public void onRemovePressed(final ContributedLibrary lib) {
try {
setProgressVisible(true, tr("Removing..."));
installer.remove(lib, this::setProgress);
- // TODO: Do a better job in refreshing only the needed element
+ onIndexesUpdated();
if (contribTable.getCellEditor() != null) {
contribTable.getCellEditor().stopCellEditing();
}
- ((LibrariesIndexTableModel) contribModel).update();
- onIndexesUpdated();
+ getContribModel().update();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
diff --git a/app/src/cc/arduino/contributions/libraries/ui/MultiLibraryInstallDialog.java b/app/src/cc/arduino/contributions/libraries/ui/MultiLibraryInstallDialog.java
new file mode 100644
index 00000000000..75f7703f430
--- /dev/null
+++ b/app/src/cc/arduino/contributions/libraries/ui/MultiLibraryInstallDialog.java
@@ -0,0 +1,177 @@
+/*
+ * This file is part of Arduino.
+ *
+ * Copyright 2017 Arduino LLC (http://www.arduino.cc/)
+ *
+ * Arduino is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * As a special exception, you may use this file as part of a free software
+ * library without restriction. Specifically, if other files instantiate
+ * templates or use macros or inline functions from this file, or you compile
+ * this file and link it with other files to produce an executable, this
+ * file does not by itself cause the resulting executable to be covered by
+ * the GNU General Public License. This exception does not however
+ * invalidate any other reasons why the executable file might be covered by
+ * the GNU General Public License.
+ */
+
+package cc.arduino.contributions.libraries.ui;
+
+import static processing.app.I18n.format;
+import static processing.app.I18n.tr;
+
+import java.awt.BorderLayout;
+import java.awt.Container;
+import java.awt.Insets;
+import java.awt.Window;
+import java.awt.event.WindowEvent;
+import java.util.List;
+
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JPanel;
+import javax.swing.JTextPane;
+import javax.swing.WindowConstants;
+import javax.swing.border.EmptyBorder;
+import javax.swing.text.Document;
+import javax.swing.text.html.HTMLDocument;
+import javax.swing.text.html.StyleSheet;
+
+import cc.arduino.contributions.libraries.ContributedLibrary;
+import cc.arduino.contributions.libraries.UnavailableContributedLibrary;
+import processing.app.Base;
+import processing.app.Theme;
+
+public class MultiLibraryInstallDialog extends JDialog {
+
+ enum Result {
+ ALL, NONE, CANCEL
+ }
+
+ private Result result = Result.CANCEL;
+
+ public MultiLibraryInstallDialog(Window parent, ContributedLibrary lib,
+ List dependencies) {
+ super(parent, format(tr("Dependencies for library {0}:{1}"), lib.getName(),
+ lib.getParsedVersion()),
+ ModalityType.APPLICATION_MODAL);
+ Container pane = getContentPane();
+ pane.setLayout(new BorderLayout());
+
+ pane.add(Box.createHorizontalStrut(10), BorderLayout.WEST);
+ pane.add(Box.createHorizontalStrut(10), BorderLayout.EAST);
+
+ {
+ JButton cancel = new JButton(tr("Cancel"));
+ cancel.addActionListener(ev -> {
+ result = Result.CANCEL;
+ setVisible(false);
+ });
+
+ JButton all = new JButton(tr("Install all"));
+ all.addActionListener(ev -> {
+ result = Result.ALL;
+ setVisible(false);
+ });
+
+ JButton none = new JButton(format(tr("Install '{0}' only"), lib.getName()));
+ none.addActionListener(ev -> {
+ result = Result.NONE;
+ setVisible(false);
+ });
+
+ Box buttonsBox = Box.createHorizontalBox();
+ buttonsBox.add(all);
+ buttonsBox.add(Box.createHorizontalStrut(5));
+ buttonsBox.add(none);
+ buttonsBox.add(Box.createHorizontalStrut(5));
+ buttonsBox.add(cancel);
+
+ JPanel buttonsPanel = new JPanel();
+ buttonsPanel.setBorder(new EmptyBorder(7, 10, 7, 10));
+ buttonsPanel.setLayout(new BoxLayout(buttonsPanel, BoxLayout.Y_AXIS));
+ buttonsPanel.add(buttonsBox);
+
+ pane.add(buttonsPanel, BorderLayout.SOUTH);
+ }
+
+ {
+ String libName = format("{0}:{1}", lib.getName(),
+ lib.getParsedVersion());
+ String desc = format(tr("The library {0} needs some other library dependencies currently not installed:"),
+ libName);
+ desc += "
";
+ for (ContributedLibrary l : dependencies) {
+ if (l.getName().equals(lib.getName()))
+ continue;
+ if (l.getInstalledLibrary().isPresent())
+ continue;
+ if (l instanceof UnavailableContributedLibrary)
+ continue;
+ desc += format("- {0} ", l.getName());
+ }
+ desc += " ";
+ desc += tr("Would you like to install also all the missing dependencies?");
+
+ JTextPane textArea = makeNewDescription();
+ textArea.setContentType("text/html");
+ textArea.setText(desc);
+
+ JPanel libsList = new JPanel();
+ libsList.setLayout(new BoxLayout(libsList, BoxLayout.Y_AXIS));
+ libsList.add(textArea);
+ libsList.setBorder(new EmptyBorder(7, 7, 7, 7));
+ pane.add(libsList, BorderLayout.NORTH);
+ }
+
+ pack();
+ setResizable(false);
+ setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
+
+ WindowEvent closing = new WindowEvent(this, WindowEvent.WINDOW_CLOSING);
+ Base.registerWindowCloseKeys(getRootPane(), e -> dispatchEvent(closing));
+ }
+
+ // TODO Make this a method of Theme
+ private JTextPane makeNewDescription() {
+ JTextPane description = new JTextPane();
+ description.setInheritsPopupMenu(true);
+ Insets margin = description.getMargin();
+ margin.bottom = 0;
+ description.setMargin(margin);
+ description.setContentType("text/html");
+ Document doc = description.getDocument();
+ if (doc instanceof HTMLDocument) {
+ HTMLDocument html = (HTMLDocument) doc;
+ StyleSheet s = html.getStyleSheet();
+ s.addRule("body { margin: 0; padding: 0;"
+ + "font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;"
+ + "color: black;" + "font-size: " + 15 * Theme.getScale() / 100
+ + "; }");
+ }
+ description.setOpaque(false);
+ description.setBorder(new EmptyBorder(4, 7, 7, 7));
+ description.setHighlighter(null);
+ description.setEditable(false);
+ add(description, 0);
+ return description;
+ }
+
+ public Result getInstallDepsResult() {
+ return result;
+ }
+}
diff --git a/app/src/cc/arduino/contributions/packages/ui/ContributedPlatformReleases.java b/app/src/cc/arduino/contributions/packages/ui/ContributedPlatformReleases.java
index 3545b1ff42b..fc516512d44 100644
--- a/app/src/cc/arduino/contributions/packages/ui/ContributedPlatformReleases.java
+++ b/app/src/cc/arduino/contributions/packages/ui/ContributedPlatformReleases.java
@@ -44,12 +44,14 @@ public class ContributedPlatformReleases {
public final List releases;
public final List versions;
public ContributedPlatform selected = null;
+ public boolean deprecated;
public ContributedPlatformReleases(ContributedPlatform platform) {
packager = platform.getParentPackage();
arch = platform.getArchitecture();
releases = new LinkedList<>();
versions = new LinkedList<>();
+ deprecated = platform.isDeprecated();
add(platform);
}
@@ -65,7 +67,9 @@ public void add(ContributedPlatform platform) {
if (version != null) {
versions.add(version);
}
- selected = getLatest();
+ ContributedPlatform latest = getLatest();
+ selected = latest;
+ deprecated = latest.isDeprecated();
}
public ContributedPlatform getInstalled() {
@@ -89,6 +93,10 @@ public ContributedPlatform getSelected() {
return selected;
}
+ public boolean isDeprecated() {
+ return deprecated;
+ }
+
public void select(ContributedPlatform value) {
for (ContributedPlatform plat : releases) {
if (plat == value) {
diff --git a/app/src/cc/arduino/contributions/packages/ui/ContributedPlatformTableCellEditor.java b/app/src/cc/arduino/contributions/packages/ui/ContributedPlatformTableCellEditor.java
index 26bcb1c9d3b..7fe221fa340 100644
--- a/app/src/cc/arduino/contributions/packages/ui/ContributedPlatformTableCellEditor.java
+++ b/app/src/cc/arduino/contributions/packages/ui/ContributedPlatformTableCellEditor.java
@@ -129,7 +129,8 @@ public Component getTableCellEditorComponent(JTable table, Object _value,
cell.versionToInstallChooser
.setVisible(installed == null && uninstalledReleases.size() > 1);
- cell.update(table, _value, true, !installedBuiltIn.isEmpty());
+ cell.update(table, _value, !installedBuiltIn.isEmpty());
+ cell.setForeground(Color.BLACK);
cell.setBackground(new Color(218, 227, 227)); // #dae3e3
return cell;
}
diff --git a/app/src/cc/arduino/contributions/packages/ui/ContributedPlatformTableCellJPanel.java b/app/src/cc/arduino/contributions/packages/ui/ContributedPlatformTableCellJPanel.java
index b4537ce94cc..19961c1c97f 100644
--- a/app/src/cc/arduino/contributions/packages/ui/ContributedPlatformTableCellJPanel.java
+++ b/app/src/cc/arduino/contributions/packages/ui/ContributedPlatformTableCellJPanel.java
@@ -32,20 +32,11 @@
import static processing.app.I18n.format;
import static processing.app.I18n.tr;
-import java.awt.Color;
-import java.awt.Component;
-import java.awt.Dimension;
-import java.awt.Insets;
-
-import javax.swing.Box;
-import javax.swing.BoxLayout;
-import javax.swing.JButton;
-import javax.swing.JComboBox;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.JTable;
-import javax.swing.JTextPane;
+import java.awt.*;
+
+import javax.swing.*;
import javax.swing.border.EmptyBorder;
+import javax.swing.border.TitledBorder;
import javax.swing.event.HyperlinkEvent;
import javax.swing.text.Document;
import javax.swing.text.html.HTMLDocument;
@@ -57,11 +48,14 @@
import cc.arduino.contributions.packages.ContributedPlatform;
import cc.arduino.contributions.ui.InstallerTableCell;
import processing.app.Base;
+import processing.app.PreferencesData;
import processing.app.Theme;
@SuppressWarnings("serial")
public class ContributedPlatformTableCellJPanel extends JPanel {
+ final JButton moreInfoButton;
+ final JButton onlineHelpButton;
final JButton installButton;
final JButton removeButton;
final Component removeButtonPlaceholder;
@@ -72,13 +66,26 @@ public class ContributedPlatformTableCellJPanel extends JPanel {
final JPanel buttonsPanel;
final JPanel inactiveButtonsPanel;
final JLabel statusLabel;
+ final JTextPane description;
+ final TitledBorder titledBorder;
+ private final String moreInfoLbl = tr("More Info");
+ private final String onlineHelpLbl = tr("Online Help");
public ContributedPlatformTableCellJPanel() {
super();
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
+ // Actual title set by update()
+ titledBorder = BorderFactory.createTitledBorder("");
+ titledBorder.setTitleFont(getFont().deriveFont(Font.BOLD));
+ setBorder(titledBorder);
+
{
installButton = new JButton(tr("Install"));
+ moreInfoButton = new JButton(moreInfoLbl);
+ moreInfoButton.setVisible(false);
+ onlineHelpButton = new JButton(onlineHelpLbl);
+ onlineHelpButton.setVisible(false);
int width = installButton.getPreferredSize().width;
installButtonPlaceholder = Box.createRigidArea(new Dimension(width, 1));
}
@@ -108,13 +115,21 @@ public ContributedPlatformTableCellJPanel() {
versionToInstallChooser
.setMaximumSize(versionToInstallChooser.getPreferredSize());
- makeNewDescription();
+ description = makeNewDescription();
+ add(description);
buttonsPanel = new JPanel();
buttonsPanel.setLayout(new BoxLayout(buttonsPanel, BoxLayout.X_AXIS));
buttonsPanel.setOpaque(false);
buttonsPanel.add(Box.createHorizontalStrut(7));
+ if (PreferencesData.getBoolean("ide.accessible")) { // only add the buttons if needed
+ buttonsPanel.add(onlineHelpButton);
+ buttonsPanel.add(Box.createHorizontalStrut(5));
+ buttonsPanel.add(moreInfoButton);
+ buttonsPanel.add(Box.createHorizontalStrut(5));
+ buttonsPanel.add(Box.createHorizontalStrut(15));
+ }
buttonsPanel.add(downgradeChooser);
buttonsPanel.add(Box.createHorizontalStrut(5));
buttonsPanel.add(downgradeButton);
@@ -149,11 +164,27 @@ public ContributedPlatformTableCellJPanel() {
add(Box.createVerticalStrut(15));
}
- void update(JTable parentTable, Object value, boolean isSelected,
- boolean hasBuiltInRelease) {
- ContributedPlatformReleases releases = (ContributedPlatformReleases) value;
+ // same function as in ContributedLibraryTableCellJPanel - is there a utils file this can move to?
+ private String setButtonOrLink(JButton button, String desc, String label, String url) {
+ boolean accessibleIDE = PreferencesData.getBoolean("ide.accessible");
+ String retString = desc;
- JTextPane description = makeNewDescription();
+ if (accessibleIDE) {
+ button.setVisible(true);
+ button.addActionListener(e -> {
+ Base.openURL(url);
+ });
+ }
+ else {
+ // if not accessible IDE, keep link the same EXCEPT that now the link text is translated!
+ retString += " " + format("{1} ", url, label);
+ }
+
+ return retString;
+ }
+
+ void update(JTable parentTable, Object value, boolean hasBuiltInRelease) {
+ ContributedPlatformReleases releases = (ContributedPlatformReleases) value;
// FIXME: happens on macosx, don't know why
if (releases == null) {
@@ -161,6 +192,7 @@ void update(JTable parentTable, Object value, boolean isSelected,
}
ContributedPlatform selected = releases.getSelected();
+ titledBorder.setTitle(selected.getName());
ContributedPlatform installed = releases.getInstalled();
boolean removable, installable, upgradable;
@@ -186,7 +218,7 @@ void update(JTable parentTable, Object value, boolean isSelected,
removeButtonPlaceholder.setVisible(!removable);
String desc = "";
- desc += "" + selected.getName() + "";
+// desc += "" + selected.getName() + "";
if (installed != null && installed.isBuiltIn()) {
desc += " Built-In ";
}
@@ -200,6 +232,9 @@ void update(JTable parentTable, Object value, boolean isSelected,
+ format(tr("version {0}"), installed.getParsedVersion())
+ " INSTALLED";
}
+ if (releases.isDeprecated()) {
+ desc += " DEPRECATED";
+ }
desc += " ";
desc += tr("Boards included in this package:") + " ";
@@ -216,21 +251,23 @@ void update(JTable parentTable, Object value, boolean isSelected,
} else if (selected.getParentPackage().getHelp() != null) {
help = selected.getParentPackage().getHelp();
}
+
if (help != null) {
String url = help.getOnline();
if (url != null && !url.isEmpty()) {
- desc += " " + format("Online help ", url);
+ desc = setButtonOrLink(onlineHelpButton, desc, onlineHelpLbl, url);
}
}
String url = selected.getParentPackage().getWebsiteURL();
if (url != null && !url.isEmpty()) {
- desc += " " + format("More info", url);
+ desc = setButtonOrLink(moreInfoButton, desc, moreInfoLbl, url);
}
desc += "";
description.setText(desc);
- description.setBackground(Color.WHITE);
+ // copy description to accessibility context for screen readers to use
+ description.getAccessibleContext().setAccessibleDescription(desc);
// for modelToView to work, the text area has to be sized. It doesn't
// matter if it's visible or not.
@@ -240,20 +277,9 @@ void update(JTable parentTable, Object value, boolean isSelected,
int width = parentTable.getBounds().width;
InstallerTableCell.setJTextPaneDimensionToFitContainedText(description,
width);
-
- if (isSelected) {
- setBackground(parentTable.getSelectionBackground());
- setForeground(parentTable.getSelectionForeground());
- } else {
- setBackground(parentTable.getBackground());
- setForeground(parentTable.getForeground());
- }
}
private JTextPane makeNewDescription() {
- if (getComponentCount() > 0) {
- remove(0);
- }
JTextPane description = new JTextPane();
description.setInheritsPopupMenu(true);
Insets margin = description.getMargin();
@@ -277,7 +303,6 @@ private JTextPane makeNewDescription() {
Base.openURL(e.getDescription());
}
});
- add(description, 0);
return description;
}
@@ -288,4 +313,12 @@ public void setButtonsVisible(boolean enabled) {
inactiveButtonsPanel.setVisible(!enabled);
}
+ public void setForeground(Color c) {
+ super.setForeground(c);
+ // The description is not opaque, so copy our foreground color to it.
+ if (description != null)
+ description.setForeground(c);
+ if (titledBorder != null)
+ titledBorder.setTitleColor(c);
+ }
}
diff --git a/app/src/cc/arduino/contributions/packages/ui/ContributedPlatformTableCellRenderer.java b/app/src/cc/arduino/contributions/packages/ui/ContributedPlatformTableCellRenderer.java
index cc4b1d0c186..b6f6aae015c 100644
--- a/app/src/cc/arduino/contributions/packages/ui/ContributedPlatformTableCellRenderer.java
+++ b/app/src/cc/arduino/contributions/packages/ui/ContributedPlatformTableCellRenderer.java
@@ -44,8 +44,9 @@ public Component getTableCellRendererComponent(JTable table, Object value,
int column) {
ContributedPlatformTableCellJPanel cell = new ContributedPlatformTableCellJPanel();
cell.setButtonsVisible(false);
- cell.update(table, value, isSelected, false);
+ cell.update(table, value, false);
+ cell.setForeground(Color.BLACK);
if (row % 2 == 0) {
cell.setBackground(new Color(236, 241, 241)); // #ecf1f1
} else {
diff --git a/app/src/cc/arduino/contributions/packages/ui/ContributionIndexTableModel.java b/app/src/cc/arduino/contributions/packages/ui/ContributionIndexTableModel.java
index f143e33172a..2c9939849bb 100644
--- a/app/src/cc/arduino/contributions/packages/ui/ContributionIndexTableModel.java
+++ b/app/src/cc/arduino/contributions/packages/ui/ContributionIndexTableModel.java
@@ -36,6 +36,7 @@
import processing.app.BaseNoGui;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@@ -47,12 +48,29 @@ public class ContributionIndexTableModel
private final List contributions = new ArrayList<>();
private final String[] columnNames = { "Description" };
private final Class>[] columnTypes = { ContributedPlatform.class };
+ private Predicate filter;
+ private String[] filters;
public void updateIndexFilter(String[] filters,
Predicate filter) {
+ this.filter = filter;
+ this.filters = filters;
+ updateContributions();
+ }
+
+ private void updateContributions() {
contributions.clear();
+
+ // Generate ContributedPlatformReleases from all platform releases
for (ContributedPackage pack : BaseNoGui.indexer.getPackages()) {
for (ContributedPlatform platform : pack.getPlatforms()) {
+ addContribution(platform);
+ }
+ }
+
+ // Filter ContributedPlatformReleases based on search terms
+ contributions.removeIf(releases -> {
+ for (ContributedPlatform platform : releases.releases) {
String compoundTargetSearchText = platform.getName() + "\n"
+ platform.getBoards().stream()
.map(ContributedBoard::getName)
@@ -62,9 +80,25 @@ public void updateIndexFilter(String[] filters,
}
if (!stringContainsAll(compoundTargetSearchText, filters))
continue;
- addContribution(platform);
+ return false;
}
- }
+ return true;
+ });
+
+ // Sort ContributedPlatformReleases and put deprecated platforms to the bottom
+ Collections.sort(contributions, (x,y)-> {
+ if (x.isDeprecated() != y.isDeprecated()) {
+ return x.isDeprecated() ? 1 : -1;
+ }
+ ContributedPlatform x1 = x.getLatest();
+ ContributedPlatform y1 = y.getLatest();
+ int category = (x1.getCategory().equals("Arduino") ? -1 : 0) + (y1.getCategory().equals("Arduino") ? 1 : 0);
+ if (category != 0) {
+ return category;
+ }
+ return x1.getName().compareToIgnoreCase(y1.getName());
+ });
+
fireTableDataChanged();
}
@@ -89,12 +123,12 @@ private boolean stringContainsAll(String string, String set[]) {
private void addContribution(ContributedPlatform platform) {
for (ContributedPlatformReleases contribution : contributions) {
- if (!contribution.shouldContain(platform))
+ if (!contribution.shouldContain(platform)) {
continue;
+ }
contribution.add(platform);
return;
}
-
contributions.add(new ContributedPlatformReleases(platform));
}
@@ -146,6 +180,7 @@ public ContributedPlatform getSelectedRelease(int row) {
}
public void update() {
+ updateContributions();
fireTableDataChanged();
}
diff --git a/app/src/cc/arduino/contributions/packages/ui/ContributionManagerUI.java b/app/src/cc/arduino/contributions/packages/ui/ContributionManagerUI.java
index 6f9c903c3c0..c00e91e9d13 100644
--- a/app/src/cc/arduino/contributions/packages/ui/ContributionManagerUI.java
+++ b/app/src/cc/arduino/contributions/packages/ui/ContributionManagerUI.java
@@ -29,7 +29,6 @@
package cc.arduino.contributions.packages.ui;
-import cc.arduino.contributions.DownloadableContribution;
import cc.arduino.contributions.packages.ContributedPlatform;
import cc.arduino.contributions.packages.ContributionInstaller;
import cc.arduino.contributions.ui.*;
@@ -41,6 +40,7 @@
import javax.swing.table.TableCellRenderer;
import java.awt.*;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
@@ -92,30 +92,28 @@ public ContributionManagerUI(Frame parent, ContributionInstaller installer) {
this.installer = installer;
}
+ private Collection oldCategories = new ArrayList<>();
+
public void updateUI() {
- DropdownItem previouslySelectedCategory = (DropdownItem) categoryChooser
- .getSelectedItem();
+ // Check if categories have changed
+ Collection categories = BaseNoGui.indexer.getCategories();
+ if (categories.equals(oldCategories)) {
+ return;
+ }
+ oldCategories = categories;
categoryChooser.removeActionListener(categoryChooserActionListener);
-
- filterField.setEnabled(getContribModel().getRowCount() > 0);
-
- categoryChooser.addActionListener(categoryChooserActionListener);
-
// Enable categories combo only if there are two or more choices
+ filterField.setEnabled(getContribModel().getRowCount() > 0);
categoryFilter = x -> true;
categoryChooser.removeAllItems();
categoryChooser.addItem(new DropdownAllCoresItem());
categoryChooser.addItem(new DropdownUpdatableCoresItem());
- Collection categories = BaseNoGui.indexer.getCategories();
for (String s : categories) {
categoryChooser.addItem(new DropdownCoreOfCategoryItem(s));
}
- if (previouslySelectedCategory != null) {
- categoryChooser.setSelectedItem(previouslySelectedCategory);
- } else {
- categoryChooser.setSelectedIndex(0);
- }
+ categoryChooser.addActionListener(categoryChooserActionListener);
+ categoryChooser.setSelectedIndex(0);
}
public void setProgress(Progress progress) {
@@ -142,10 +140,12 @@ public void onUpdatePressed() {
installerThread = new Thread(() -> {
try {
setProgressVisible(true, "");
- List downloadedPackageIndexFiles = installer
- .updateIndex(this::setProgress);
- installer.deleteUnknownFiles(downloadedPackageIndexFiles);
+ installer.updateIndex(this::setProgress);
onIndexesUpdated();
+ if (contribTable.getCellEditor() != null) {
+ contribTable.getCellEditor().stopCellEditing();
+ }
+ getContribModel().update();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
@@ -171,6 +171,10 @@ public void onInstallPressed(final ContributedPlatform platformToInstall,
}
errors.addAll(installer.install(platformToInstall, this::setProgress));
onIndexesUpdated();
+ if (contribTable.getCellEditor() != null) {
+ contribTable.getCellEditor().stopCellEditing();
+ }
+ getContribModel().update();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
@@ -209,6 +213,10 @@ public void onRemovePressed(final ContributedPlatform platform,
setProgressVisible(true, tr("Removing..."));
installer.remove(platform);
onIndexesUpdated();
+ if (contribTable.getCellEditor() != null) {
+ contribTable.getCellEditor().stopCellEditing();
+ }
+ getContribModel().update();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
diff --git a/app/src/cc/arduino/contributions/ui/FilterJTextField.java b/app/src/cc/arduino/contributions/ui/FilterJTextField.java
index 9dc7fd8d6b5..83aeba45430 100644
--- a/app/src/cc/arduino/contributions/ui/FilterJTextField.java
+++ b/app/src/cc/arduino/contributions/ui/FilterJTextField.java
@@ -41,6 +41,7 @@ public class FilterJTextField extends JTextField {
private final String filterHint;
private boolean showingHint;
+ private Timer timer;
public FilterJTextField(String hint) {
super(hint);
@@ -48,6 +49,10 @@ public FilterJTextField(String hint) {
showingHint = true;
updateStyle();
+ timer = new Timer(1000, e -> {
+ applyFilter();
+ timer.stop();
+ });
addFocusListener(new FocusListener() {
public void focusLost(FocusEvent focusEvent) {
@@ -68,34 +73,44 @@ public void focusGained(FocusEvent focusEvent) {
getDocument().addDocumentListener(new DocumentListener() {
public void removeUpdate(DocumentEvent e) {
- applyFilter();
+ spawnTimer();
}
public void insertUpdate(DocumentEvent e) {
- applyFilter();
+ spawnTimer();
}
public void changedUpdate(DocumentEvent e) {
- applyFilter();
+
}
});
- }
-
- private String lastFilter = "";
-
- private void applyFilter() {
- String filter = showingHint ? "" : getText();
- filter = filter.toLowerCase();
- // Replace anything but 0-9, a-z, or : with a space
- filter = filter.replaceAll("[^\\x30-\\x39^\\x61-\\x7a^\\x3a]", " ");
+ addActionListener(e -> {
+ if (timer.isRunning()) {
+ timer.stop();
+ }
+ applyFilter();
+ });
+ }
- // Fire event only if the filter is changed
- if (filter.equals(lastFilter))
- return;
+ private void spawnTimer() {
+ if (timer.isRunning()) {
+ timer.stop();
+ }
+ timer.start();
+ }
- lastFilter = filter;
- onFilter(filter.split(" "));
+ public void applyFilter() {
+ String[] filteredText = new String[0];
+ if (!showingHint) {
+ String filter = getText().toLowerCase();
+
+ // Replace anything but 0-9, a-z, or : with a space
+ filter = filter.replaceAll("[^\\x30-\\x39^\\x61-\\x7a^\\x3a]", " ");
+
+ filteredText = filter.split(" ");
+ }
+ onFilter(filteredText);
}
protected void onFilter(String[] strings) {
@@ -112,4 +127,23 @@ private void updateStyle() {
setFont(getFont().deriveFont(Font.PLAIN));
}
}
+
+ @Override
+ public void paste() {
+
+ // Same precondition check as JTextComponent#paste().
+ if (!isEditable() || !isEnabled()) {
+ return;
+ }
+
+ // Disable hint to prevent the focus handler from clearing the pasted text.
+ if (showingHint) {
+ showingHint = false;
+ setText("");
+ updateStyle();
+ }
+
+ // Perform the paste.
+ super.paste();
+ }
}
diff --git a/app/src/cc/arduino/contributions/ui/InstallerJDialog.java b/app/src/cc/arduino/contributions/ui/InstallerJDialog.java
index 3dd864134c4..8abff8f3454 100644
--- a/app/src/cc/arduino/contributions/ui/InstallerJDialog.java
+++ b/app/src/cc/arduino/contributions/ui/InstallerJDialog.java
@@ -45,8 +45,10 @@
import java.awt.event.WindowEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
+import java.awt.event.WindowAdapter;
import java.util.function.Predicate;
+import javax.swing.Action;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
@@ -54,6 +56,7 @@
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
@@ -64,10 +67,10 @@
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
+import javax.swing.text.DefaultEditorKit;
import cc.arduino.contributions.ui.listeners.AbstractKeyListener;
import processing.app.Base;
-import processing.app.Theme;
public abstract class InstallerJDialog extends JDialog {
@@ -78,6 +81,7 @@ public abstract class InstallerJDialog extends JDialog {
protected final FilterJTextField filterField;
protected final JPanel filtersContainer;
// Currently selected category and filters
+ protected Predicate extraFilter = x -> true;
protected Predicate categoryFilter;
protected String[] filters;
protected final String noConnectionErrorMessage;
@@ -129,6 +133,32 @@ protected void onFilter(String[] _filters) {
updateIndexFilter(filters, categoryFilter);
}
};
+ filterField.getAccessibleContext().setAccessibleDescription(tr("Search Filter"));
+
+ // Add cut/copy/paste contextual menu to the search filter input field.
+ JPopupMenu menu = new JPopupMenu();
+
+ Action cut = new DefaultEditorKit.CutAction();
+ cut.putValue(Action.NAME, tr("Cut"));
+ menu.add(cut);
+
+ Action copy = new DefaultEditorKit.CopyAction();
+ copy.putValue(Action.NAME, tr("Copy"));
+ menu.add(copy);
+
+ Action paste = new DefaultEditorKit.PasteAction();
+ paste.putValue(Action.NAME, tr("Paste"));
+ menu.add(paste);
+
+ filterField.setComponentPopupMenu(menu);
+
+ // Focus the filter field when the window opens.
+ addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowOpened(WindowEvent e) {
+ filterField.requestFocus();
+ }
+ });
filtersContainer = new JPanel();
filtersContainer.setLayout(new BoxLayout(filtersContainer, BoxLayout.X_AXIS));
@@ -150,7 +180,6 @@ protected void onFilter(String[] _filters) {
contribTable.setDragEnabled(false);
contribTable.setIntercellSpacing(new Dimension(0, 1));
contribTable.setShowVerticalLines(false);
- contribTable.setSelectionBackground(Theme.getColor("status.notice.bgcolor"));
contribTable.addKeyListener(new AbstractKeyListener() {
@Override
@@ -300,7 +329,6 @@ private void setErrorMessageVisible(boolean visible) {
}
protected final ActionListener categoryChooserActionListener = new ActionListener() {
-
@Override
public void actionPerformed(ActionEvent event) {
DropdownItem selected = (DropdownItem) categoryChooser.getSelectedItem();
@@ -310,7 +338,7 @@ public void actionPerformed(ActionEvent event) {
if (contribTable.getCellEditor() != null) {
contribTable.getCellEditor().stopCellEditing();
}
- updateIndexFilter(filters, categoryFilter);
+ updateIndexFilter(filters, categoryFilter.and(extraFilter));
}
}
};
@@ -320,6 +348,7 @@ public void setFilterText(String filterText) {
listener.focusGained(new FocusEvent(filterField, FocusEvent.FOCUS_GAINED));
}
filterField.setText(filterText);
+ filterField.applyFilter();
}
public void selectDropdownItemByClassName(String dropdownItem) {
diff --git a/app/src/cc/arduino/contributions/ui/ProgressJProgressBar.java b/app/src/cc/arduino/contributions/ui/ProgressJProgressBar.java
index 12b39742fa3..7c946e4993e 100644
--- a/app/src/cc/arduino/contributions/ui/ProgressJProgressBar.java
+++ b/app/src/cc/arduino/contributions/ui/ProgressJProgressBar.java
@@ -38,8 +38,13 @@ public class ProgressJProgressBar extends JProgressBar {
public void setValue(Progress p) {
setValue((int) p.getProgress());
- if (p.getStatus() != null)
+ if (p.getStatus() != null) {
setString(p.getStatus());
+ // copy status to accessibility context for screen readers to use
+ getAccessibleContext().setAccessibleDescription(p.getStatus());
+ // make status focusable so screen readers can get to it
+ setFocusable(true);
+ }
}
}
diff --git a/app/src/cc/arduino/packages/MonitorFactory.java b/app/src/cc/arduino/packages/MonitorFactory.java
index 83d849dd6ef..3be7723b586 100644
--- a/app/src/cc/arduino/packages/MonitorFactory.java
+++ b/app/src/cc/arduino/packages/MonitorFactory.java
@@ -39,7 +39,7 @@ public AbstractMonitor newMonitor(BoardPort port) {
if ("network".equals(port.getProtocol())) {
if ("yes".equals(port.getPrefs().get("ssh_upload"))) {
// the board is SSH capable
- return new NetworkMonitor(port);
+ return new NetworkMonitor(port);
} else {
// SSH not supported, no monitor support
return null;
diff --git a/app/src/cc/arduino/view/NotificationPopup.java b/app/src/cc/arduino/view/NotificationPopup.java
index 2334d6e14bd..2de079c8525 100644
--- a/app/src/cc/arduino/view/NotificationPopup.java
+++ b/app/src/cc/arduino/view/NotificationPopup.java
@@ -36,12 +36,7 @@
import java.awt.Frame;
import java.awt.Image;
import java.awt.Point;
-import java.awt.event.ComponentAdapter;
-import java.awt.event.ComponentEvent;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
-import java.awt.event.WindowAdapter;
-import java.awt.event.WindowEvent;
+import java.awt.event.*;
import java.util.Timer;
import java.util.TimerTask;
@@ -55,22 +50,46 @@
import javax.swing.event.HyperlinkListener;
import cc.arduino.Constants;
+import processing.app.PreferencesData;
import processing.app.Theme;
-public class NotificationPopup extends JDialog {
+import java.awt.event.KeyEvent;
+
+import static processing.app.I18n.tr;
+public class NotificationPopup extends JDialog {
private Timer autoCloseTimer = new Timer(false);
private boolean autoClose = true;
+ private OptionalButtonCallbacks optionalButtonCallbacks;
+
+ public interface OptionalButtonCallbacks {
+ void onOptionalButton1Callback();
+ void onOptionalButton2Callback();
+ }
public NotificationPopup(Frame parent, HyperlinkListener hyperlinkListener,
String message) {
- this(parent, hyperlinkListener, message, true);
+ this(parent, hyperlinkListener, message, true, null, null, null);
}
public NotificationPopup(Frame parent, HyperlinkListener hyperlinkListener,
String message, boolean _autoClose) {
+ this(parent, hyperlinkListener, message, _autoClose, null, null, null);
+ }
+
+ public NotificationPopup(Frame parent, HyperlinkListener hyperlinkListener,
+ String message, boolean _autoClose, OptionalButtonCallbacks listener, String button1Name, String button2Name) {
super(parent, false);
- autoClose = _autoClose;
+
+ if (!PreferencesData.getBoolean("ide.accessible")) {
+ // often auto-close is too fast for users of screen readers, so don't allow it.
+ autoClose = _autoClose;
+ }
+ else {
+ autoClose = false;
+ }
+ optionalButtonCallbacks = listener;
+
setLayout(new FlowLayout());
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
setUndecorated(true);
@@ -90,6 +109,74 @@ public NotificationPopup(Frame parent, HyperlinkListener hyperlinkListener,
text.addHyperlinkListener(hyperlinkListener);
add(text);
+ if (button1Name != null) {
+ JButton optionalButton1 = new JButton(tr(button1Name));
+ MouseAdapter button1Action = new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ if (optionalButtonCallbacks != null) {
+ optionalButtonCallbacks.onOptionalButton1Callback();
+ }
+ }
+ };
+ optionalButton1.addMouseListener(button1Action);
+
+ KeyListener button1Key = new KeyListener() {
+ // Ignore when the key is typed - only act once the key is released
+ public void keyTyped(KeyEvent e) {
+ // do nothing here, wait until the key is released
+ }
+
+ // Ignore when the key is pressed - only act once the key is released
+ public void keyPressed(KeyEvent e) {
+ // do nothing here, wait until the key is released
+ }
+
+ public void keyReleased(KeyEvent e) {
+ int key = e.getKeyCode();
+ if ((key == KeyEvent.VK_ENTER) || (key == KeyEvent.VK_SPACE)) {
+ optionalButtonCallbacks.onOptionalButton1Callback();
+ }
+ }
+ };
+ optionalButton1.addKeyListener(button1Key);
+ add(optionalButton1);
+ }
+
+ if (button2Name != null) {
+ JButton optionalButton2 = new JButton(tr(button2Name));
+ MouseAdapter button2Action = new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ if (optionalButtonCallbacks != null) {
+ optionalButtonCallbacks.onOptionalButton2Callback();
+ }
+ }
+ };
+ optionalButton2.addMouseListener(button2Action);
+
+ KeyListener button2Key = new KeyListener() {
+ // Ignore when the key is typed - only act once the key is released
+ public void keyTyped(KeyEvent e) {
+ // do nothing here, wait until the key is released
+ }
+
+ // Ignore when the key is pressed - only act once the key is released
+ public void keyPressed(KeyEvent e) {
+ // do nothing here, wait until the key is released
+ }
+
+ public void keyReleased(KeyEvent e) {
+ int key = e.getKeyCode();
+ if ((key == KeyEvent.VK_ENTER) || (key == KeyEvent.VK_SPACE)) {
+ optionalButtonCallbacks.onOptionalButton2Callback();
+ }
+ }
+ };
+ optionalButton2.addKeyListener(button2Key);
+ add(optionalButton2);
+ }
+
Image close = Theme.getThemeImage("close", this, scale(22), scale(22));
JButton closeButton = new JButton(new ImageIcon(close));
closeButton.setBorder(null);
@@ -97,6 +184,26 @@ public NotificationPopup(Frame parent, HyperlinkListener hyperlinkListener,
closeButton.setHideActionText(true);
closeButton.setOpaque(false);
closeButton.setBackground(new Color(0, 0, 0, 0));
+ closeButton.getAccessibleContext().setAccessibleDescription(tr("Close"));
+ KeyListener closeKey = new KeyListener() {
+ // Ignore when the key is typed - only act once the key is released
+ public void keyTyped(KeyEvent e) {
+ // do nothing here, wait until the key is released
+ }
+
+ // Ignore when the key is pressed - only act once the key is released
+ public void keyPressed(KeyEvent e) {
+ // do nothing here, wait until the key is released
+ }
+
+ public void keyReleased(KeyEvent e) {
+ int key = e.getKeyCode();
+ if ((key == KeyEvent.VK_ENTER) || (key == KeyEvent.VK_SPACE)) {
+ close();
+ }
+ }
+ };
+ closeButton.addKeyListener(closeKey);
add(closeButton);
MouseAdapter closeOnClick = new MouseAdapter() {
@@ -145,6 +252,7 @@ public void close() {
if (autoClose) {
autoCloseTimer.cancel();
}
+ setModal(false);
dispatchEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSING));
}
@@ -158,5 +266,9 @@ public void run() {
}, Constants.NOTIFICATION_POPUP_AUTOCLOSE_DELAY);
}
setVisible(true);
+ if (PreferencesData.getBoolean("ide.accessible")) {
+ requestFocus();
+ setModal(true);
+ }
}
}
diff --git a/app/src/cc/arduino/view/preferences/Preferences.java b/app/src/cc/arduino/view/preferences/Preferences.java
index 0f8d9803dbc..005d2f83e54 100644
--- a/app/src/cc/arduino/view/preferences/Preferences.java
+++ b/app/src/cc/arduino/view/preferences/Preferences.java
@@ -91,7 +91,7 @@ public Preferences(Window parent, Base base) {
Base.registerWindowCloseKeys(getRootPane(), this::cancelButtonActionPerformed);
- showPrerefencesData();
+ showPreferencesData();
}
/**
@@ -132,10 +132,9 @@ private void initComponents() {
enableCodeFoldingBox = new javax.swing.JCheckBox();
verifyUploadBox = new javax.swing.JCheckBox();
externalEditorBox = new javax.swing.JCheckBox();
- cacheCompiledCore = new javax.swing.JCheckBox();
checkUpdatesBox = new javax.swing.JCheckBox();
- updateExtensionBox = new javax.swing.JCheckBox();
saveVerifyUploadBox = new javax.swing.JCheckBox();
+ accessibleIDEBox = new javax.swing.JCheckBox();
jLabel1 = new javax.swing.JLabel();
jLabel2 = new javax.swing.JLabel();
scaleSpinner = new javax.swing.JSpinner();
@@ -180,7 +179,7 @@ private void initComponents() {
sketchbookLocationLabel.setText(tr("Sketchbook location:"));
sketchbookLocationLabel.setLabelFor(sketchbookLocationField);
-
+
sketchbookLocationField.setColumns(40);
browseButton.setText(I18n.PROMPT_BROWSE);
@@ -193,7 +192,7 @@ public void actionPerformed(java.awt.event.ActionEvent evt) {
comboLanguageLabel.setText(tr("Editor language: "));
requiresRestartLabel.setText(tr(" (requires restart of Arduino)"));
-
+
comboLanguage.getAccessibleContext().setAccessibleName("Editor language (requires restart of Arduino)");
fontSizeLabel.setText(tr("Editor font size: "));
@@ -248,7 +247,7 @@ public void mouseEntered(java.awt.event.MouseEvent evt) {
arduinoNotRunningLabel.setForeground(Color.GRAY);
arduinoNotRunningLabel.setText(tr("(edit only when Arduino is not running)"));
- checkboxesContainer.setLayout(new javax.swing.BoxLayout(checkboxesContainer, javax.swing.BoxLayout.Y_AXIS));
+ checkboxesContainer.setLayout(new GridLayout(0,2));
displayLineNumbersBox.setText(tr("Display line numbers"));
checkboxesContainer.add(displayLineNumbersBox);
@@ -277,18 +276,15 @@ public void mouseEntered(java.awt.event.MouseEvent evt) {
checkboxesContainer.add(externalEditorBox);
- cacheCompiledCore.setText(tr("Aggressively cache compiled core"));
- checkboxesContainer.add(cacheCompiledCore);
-
checkUpdatesBox.setText(tr("Check for updates on startup"));
checkboxesContainer.add(checkUpdatesBox);
- updateExtensionBox.setText(tr("Update sketch files to new extension on save (.pde -> .ino)"));
- checkboxesContainer.add(updateExtensionBox);
-
saveVerifyUploadBox.setText(tr("Save when verifying or uploading"));
checkboxesContainer.add(saveVerifyUploadBox);
+ accessibleIDEBox.setText(tr("Use accessibility features"));
+ checkboxesContainer.add(accessibleIDEBox);
+
jLabel1.setText(tr("Interface scale:"));
jLabel2.setText(tr(" (requires restart of Arduino)"));
@@ -307,7 +303,7 @@ public void itemStateChanged(java.awt.event.ItemEvent evt) {
autoScaleCheckBox.getAccessibleContext().setAccessibleName("Automatic interface scale (requires restart of Arduino");
jLabel3.setText("%");
-
+
comboThemeLabel.setText(tr("Theme: "));
comboTheme.getAccessibleContext().setAccessibleName("Theme (requires restart of Arduino)");
@@ -322,7 +318,7 @@ public void itemStateChanged(java.awt.event.ItemEvent evt) {
.addContainerGap()
.addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(jPanel1Layout.createSequentialGroup()
- .addComponent(sketchbookLocationField, javax.swing.GroupLayout.DEFAULT_SIZE, 553, Short.MAX_VALUE)
+ .addComponent(sketchbookLocationField, javax.swing.GroupLayout.DEFAULT_SIZE, 400, Short.MAX_VALUE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(browseButton))
.addComponent(checkboxesContainer, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
@@ -631,13 +627,13 @@ public void actionPerformed(java.awt.event.ActionEvent evt) {
getContentPane().setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
- .addGap(0, 691, Short.MAX_VALUE)
+ .addGap(0, 800, Short.MAX_VALUE)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(jPanel2, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
- .addGap(0, 580, Short.MAX_VALUE)
+ .addGap(0, 400, Short.MAX_VALUE)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(jPanel2, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
);
@@ -721,6 +717,7 @@ private void autoScaleCheckBoxItemStateChanged(java.awt.event.ItemEvent evt) {//
private javax.swing.JCheckBox autoScaleCheckBox;
private javax.swing.JButton browseButton;
private javax.swing.JCheckBox checkUpdatesBox;
+ private javax.swing.JCheckBox accessibleIDEBox;
private javax.swing.JPanel checkboxesContainer;
private javax.swing.JComboBox comboLanguage;
private javax.swing.JLabel comboLanguageLabel;
@@ -730,7 +727,6 @@ private void autoScaleCheckBoxItemStateChanged(java.awt.event.ItemEvent evt) {//
private javax.swing.JCheckBox enableCodeFoldingBox;
private javax.swing.JButton extendedAdditionalUrlFieldWindow;
private javax.swing.JCheckBox externalEditorBox;
- private javax.swing.JCheckBox cacheCompiledCore;
private javax.swing.JTextField fontSizeField;
private javax.swing.JLabel fontSizeLabel;
private javax.swing.JLabel jLabel1;
@@ -759,7 +755,6 @@ private void autoScaleCheckBoxItemStateChanged(java.awt.event.ItemEvent evt) {//
private javax.swing.JLabel showVerboseLabel;
private javax.swing.JTextField sketchbookLocationField;
private javax.swing.JLabel sketchbookLocationLabel;
- private javax.swing.JCheckBox updateExtensionBox;
private javax.swing.JCheckBox verboseCompilationBox;
private javax.swing.JCheckBox verboseUploadBox;
private javax.swing.JCheckBox verifyUploadBox;
@@ -793,7 +788,7 @@ private void savePreferencesData() {
Language newLanguage = (Language) comboLanguage.getSelectedItem();
PreferencesData.set("editor.languages.current", newLanguage.getIsoCode());
-
+
if (comboTheme.getSelectedIndex() == 0) {
PreferencesData.set("theme.file", "");
} else {
@@ -834,13 +829,9 @@ private void savePreferencesData() {
PreferencesData.setBoolean("editor.external", externalEditorBox.isSelected());
- PreferencesData.setBoolean("compiler.cache_core", cacheCompiledCore.isSelected());
-
PreferencesData.setBoolean("update.check", checkUpdatesBox.isSelected());
- PreferencesData.setBoolean("editor.update_extension", updateExtensionBox.isSelected());
-
- PreferencesData.setBoolean("editor.save_on_verify", saveVerifyUploadBox.isSelected());
+ PreferencesData.setBoolean("ide.accessible", accessibleIDEBox.isSelected());
PreferencesData.set("boardsmanager.additional.urls", additionalBoardsManagerField.getText().replace("\r\n", "\n").replace("\r", "\n").replace("\n", ","));
@@ -849,13 +840,17 @@ private void savePreferencesData() {
PreferencesData.set(Constants.PREF_PROXY_MANUAL_TYPE, manualProxyTypeButtonGroup.getSelection().getActionCommand());
PreferencesData.set(Constants.PREF_PROXY_MANUAL_HOSTNAME, manualProxyHostName.getText());
PreferencesData.set(Constants.PREF_PROXY_MANUAL_PORT, manualProxyPort.getText());
- PreferencesData.set(Constants.PREF_PROXY_MANUAL_USERNAME, manualProxyUsername.getText());
- PreferencesData.set(Constants.PREF_PROXY_MANUAL_PASSWORD, String.valueOf(manualProxyPassword.getPassword()));
- PreferencesData.set(Constants.PREF_PROXY_AUTO_USERNAME, autoProxyUsername.getText());
- PreferencesData.set(Constants.PREF_PROXY_AUTO_PASSWORD, String.valueOf(autoProxyPassword.getPassword()));
+ if (PreferencesData.get(Constants.PREF_PROXY_TYPE).equals(Constants.PROXY_TYPE_MANUAL)) {
+ PreferencesData.set(Constants.PREF_PROXY_USERNAME, manualProxyUsername.getText());
+ PreferencesData.set(Constants.PREF_PROXY_PASSWORD, String.valueOf(manualProxyPassword.getPassword()));
+ }
+ if (PreferencesData.get(Constants.PREF_PROXY_TYPE).equals(Constants.PROXY_TYPE_AUTO)) {
+ PreferencesData.set(Constants.PREF_PROXY_USERNAME, autoProxyUsername.getText());
+ PreferencesData.set(Constants.PREF_PROXY_PASSWORD, String.valueOf(autoProxyPassword.getPassword()));
+ }
}
- private void showPrerefencesData() {
+ private void showPreferencesData() {
sketchbookLocationField.setText(PreferencesData.get("sketchbook.path"));
String currentLanguageISOCode = PreferencesData.get("editor.languages.current");
@@ -864,7 +859,7 @@ private void showPrerefencesData() {
comboLanguage.setSelectedItem(language);
}
}
-
+
String selectedTheme = PreferencesData.get("theme.file", "");
Collection availablethemes = Theme.getAvailablethemes();
comboTheme.addItem(tr("Default theme"));
@@ -906,11 +901,17 @@ private void showPrerefencesData() {
externalEditorBox.setSelected(PreferencesData.getBoolean("editor.external"));
- cacheCompiledCore.setSelected(PreferencesData.get("compiler.cache_core") == null || PreferencesData.getBoolean("compiler.cache_core"));
+ if (PreferencesData.get("compiler.cache_core") == null) {
+ PreferencesData.setBoolean("compiler.cache_core", true);
+ }
checkUpdatesBox.setSelected(PreferencesData.getBoolean("update.check"));
- updateExtensionBox.setSelected(PreferencesData.get("editor.update_extension") == null || PreferencesData.getBoolean("editor.update_extension"));
+ if (PreferencesData.get("editor.update_extension") == null) {
+ PreferencesData.setBoolean("editor.update_extension", true);
+ }
+
+ accessibleIDEBox.setSelected(PreferencesData.getBoolean("ide.accessible"));
saveVerifyUploadBox.setSelected(PreferencesData.getBoolean("editor.save_on_verify"));
@@ -927,16 +928,16 @@ private void showPrerefencesData() {
if (!PreferencesData.get(Constants.PREF_PROXY_PAC_URL, "").isEmpty()) {
autoProxyUsePAC.setSelected(true);
autoProxyPACURL.setText(PreferencesData.get(Constants.PREF_PROXY_PAC_URL));
- autoProxyUsername.setText(PreferencesData.get(Constants.PREF_PROXY_AUTO_USERNAME));
- autoProxyPassword.setText(PreferencesData.get(Constants.PREF_PROXY_AUTO_PASSWORD));
+ autoProxyUsername.setText(PreferencesData.get(Constants.PREF_PROXY_USERNAME));
+ autoProxyPassword.setText(PreferencesData.get(Constants.PREF_PROXY_PASSWORD));
}
} else {
manualProxy.setSelected(true);
manualProxyFieldsSetEnabled(true);
manualProxyHostName.setText(PreferencesData.get(Constants.PREF_PROXY_MANUAL_HOSTNAME));
manualProxyPort.setText(PreferencesData.get(Constants.PREF_PROXY_MANUAL_PORT));
- manualProxyUsername.setText(PreferencesData.get(Constants.PREF_PROXY_MANUAL_USERNAME));
- manualProxyPassword.setText(PreferencesData.get(Constants.PREF_PROXY_MANUAL_PASSWORD));
+ manualProxyUsername.setText(PreferencesData.get(Constants.PREF_PROXY_USERNAME));
+ manualProxyPassword.setText(PreferencesData.get(Constants.PREF_PROXY_PASSWORD));
}
String selectedManualProxyType = PreferencesData.get(Constants.PREF_PROXY_MANUAL_TYPE, Constants.PROXY_MANUAL_TYPE_HTTP);
diff --git a/app/src/processing/app/AbstractMonitor.java b/app/src/processing/app/AbstractMonitor.java
index 52c5b65b56b..b6ba0d7652e 100644
--- a/app/src/processing/app/AbstractMonitor.java
+++ b/app/src/processing/app/AbstractMonitor.java
@@ -17,6 +17,7 @@ public abstract class AbstractMonitor extends JFrame implements ActionListener {
private StringBuffer updateBuffer;
private Timer updateTimer;
+ private Timer portExistsTimer;
private BoardPort boardPort;
@@ -27,6 +28,7 @@ public AbstractMonitor(BoardPort boardPort) {
this.boardPort = boardPort;
addWindowListener(new WindowAdapter() {
+ @Override
public void windowClosing(WindowEvent event) {
try {
closed = true;
@@ -41,6 +43,7 @@ public void windowClosing(WindowEvent event) {
KeyStroke wc = Editor.WINDOW_CLOSE_KEYSTROKE;
getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(wc, "close");
getRootPane().getActionMap().put("close", (new AbstractAction() {
+ @Override
public void actionPerformed(ActionEvent event) {
try {
close();
@@ -71,6 +74,26 @@ public void actionPerformed(ActionEvent event) {
updateTimer = new Timer(33, this); // redraw serial monitor at 30 Hz
updateTimer.start();
+ ActionListener portExists = new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent ae) {
+ try {
+ if (Base.getDiscoveryManager().find(boardPort.getAddress()) == null) {
+ if (!closed) {
+ suspend();
+ }
+ } else {
+ if (closed && !Editor.isUploading()) {
+ resume(boardPort);
+ }
+ }
+ } catch (Exception e) {}
+ }
+ };
+
+ portExistsTimer = new Timer(1000, portExists); // check if the port is still there every second
+ portExistsTimer.start();
+
closed = false;
}
@@ -90,6 +113,11 @@ public void suspend() throws Exception {
close();
}
+ public void dispose() {
+ super.dispose();
+ portExistsTimer.stop();
+ }
+
public void resume(BoardPort boardPort) throws Exception {
setBoardPort(boardPort);
@@ -165,6 +193,7 @@ private synchronized String consumeUpdateBuffer() {
return s;
}
+ @Override
public void actionPerformed(ActionEvent e) {
String s = consumeUpdateBuffer();
if (s.isEmpty()) {
@@ -173,4 +202,13 @@ public void actionPerformed(ActionEvent e) {
message(s);
}
}
+
+ /**
+ * Read and apply new values from the preferences, either because
+ * the app is just starting up, or the user just finished messing
+ * with things in the Preferences window.
+ */
+ public void applyPreferences() {
+ // Empty.
+ };
}
diff --git a/app/src/processing/app/AbstractTextMonitor.java b/app/src/processing/app/AbstractTextMonitor.java
index fdfcfba760a..00eabb20649 100644
--- a/app/src/processing/app/AbstractTextMonitor.java
+++ b/app/src/processing/app/AbstractTextMonitor.java
@@ -3,17 +3,21 @@
import static processing.app.I18n.tr;
import java.awt.BorderLayout;
+import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
+import java.awt.event.KeyListener;
+import java.awt.event.MouseWheelListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.StringTokenizer;
+import javax.swing.Action;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
@@ -21,11 +25,13 @@
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.text.DefaultCaret;
+import javax.swing.text.DefaultEditorKit;
import cc.arduino.packages.BoardPort;
@@ -40,28 +46,35 @@ public abstract class AbstractTextMonitor extends AbstractMonitor {
protected JButton clearButton;
protected JCheckBox autoscrollBox;
protected JCheckBox addTimeStampBox;
- protected JComboBox lineEndings;
- protected JComboBox serialRates;
+ protected JComboBox lineEndings;
+ protected JComboBox serialRates;
- private SimpleDateFormat logDateFormat;
-
public AbstractTextMonitor(BoardPort boardPort) {
super(boardPort);
- logDateFormat = new SimpleDateFormat("HH:mm:ss.SSS -> ");
}
-
+
+ @Override
+ public synchronized void addMouseWheelListener(MouseWheelListener l) {
+ super.addMouseWheelListener(l);
+ textArea.addMouseWheelListener(l);
+ }
+
+ @Override
+ public synchronized void addKeyListener(KeyListener l) {
+ super.addKeyListener(l);
+ textArea.addKeyListener(l);
+ textField.addKeyListener(l);
+ }
+
+ @Override
protected void onCreateWindow(Container mainPane) {
- Font consoleFont = Theme.getFont("console.font");
- Font editorFont = PreferencesData.getFont("editor.font");
- Font font = Theme.scale(new Font(consoleFont.getName(), consoleFont.getStyle(), editorFont.getSize()));
mainPane.setLayout(new BorderLayout());
- textArea = new TextAreaFIFO(8000000);
+ textArea = new TextAreaFIFO(8_000_000);
textArea.setRows(16);
textArea.setColumns(40);
textArea.setEditable(false);
- textArea.setFont(font);
// don't automatically update the caret. that way we can manually decide
// whether or not to do so based on the autoscroll checkbox.
@@ -70,7 +83,7 @@ protected void onCreateWindow(Container mainPane) {
scrollPane = new JScrollPane(textArea);
mainPane.add(scrollPane, BorderLayout.CENTER);
-
+
JPanel upperPane = new JPanel();
upperPane.setLayout(new BoxLayout(upperPane, BoxLayout.X_AXIS));
upperPane.setBorder(new EmptyBorder(4, 4, 4, 4));
@@ -78,11 +91,29 @@ protected void onCreateWindow(Container mainPane) {
textField = new JTextField(40);
// textField is selected every time the window is focused
addWindowFocusListener(new WindowAdapter() {
+ @Override
public void windowGainedFocus(WindowEvent e) {
textField.requestFocusInWindow();
}
});
+ // Add cut/copy/paste contextual menu to the text input field.
+ JPopupMenu menu = new JPopupMenu();
+
+ Action cut = new DefaultEditorKit.CutAction();
+ cut.putValue(Action.NAME, tr("Cut"));
+ menu.add(cut);
+
+ Action copy = new DefaultEditorKit.CopyAction();
+ copy.putValue(Action.NAME, tr("Copy"));
+ menu.add(copy);
+
+ Action paste = new DefaultEditorKit.PasteAction();
+ paste.putValue(Action.NAME, tr("Paste"));
+ menu.add(paste);
+
+ textField.setComponentPopupMenu(menu);
+
sendButton = new JButton(tr("Send"));
clearButton = new JButton(tr("Clear output"));
@@ -106,28 +137,17 @@ public void windowGainedFocus(WindowEvent e) {
minimumSize.setSize(minimumSize.getWidth() / 3, minimumSize.getHeight());
noLineEndingAlert.setMinimumSize(minimumSize);
- lineEndings = new JComboBox(new String[]{tr("No line ending"), tr("Newline"), tr("Carriage return"), tr("Both NL & CR")});
- lineEndings.addActionListener(new ActionListener() {
- public void actionPerformed(ActionEvent event) {
- PreferencesData.setInteger("serial.line_ending", lineEndings.getSelectedIndex());
- noLineEndingAlert.setForeground(pane.getBackground());
- }
- });
- if (PreferencesData.get("serial.line_ending") != null) {
- lineEndings.setSelectedIndex(PreferencesData.getInteger("serial.line_ending"));
- }
- if (PreferencesData.get("serial.show_timestamp") != null) {
- addTimeStampBox.setSelected(PreferencesData.getBoolean("serial.show_timestamp"));
- }
- addTimeStampBox.addActionListener(new ActionListener() {
- public void actionPerformed(ActionEvent e) {
- PreferencesData.setBoolean("serial.show_timestamp", addTimeStampBox.isSelected());
- }
+ lineEndings = new JComboBox<>(new String[]{tr("No line ending"), tr("Newline"), tr("Carriage return"), tr("Both NL & CR")});
+ lineEndings.addActionListener((ActionEvent event) -> {
+ PreferencesData.setInteger("serial.line_ending", lineEndings.getSelectedIndex());
+ noLineEndingAlert.setForeground(pane.getBackground());
});
+ addTimeStampBox.addActionListener((ActionEvent event) ->
+ PreferencesData.setBoolean("serial.show_timestamp", addTimeStampBox.isSelected()));
lineEndings.setMaximumSize(lineEndings.getMinimumSize());
- serialRates = new JComboBox();
+ serialRates = new JComboBox<>();
for (String rate : serialRateStrings) {
serialRates.addItem(rate + " " + tr("baud"));
}
@@ -145,27 +165,41 @@ public void actionPerformed(ActionEvent e) {
pane.add(Box.createRigidArea(new Dimension(8, 0)));
pane.add(clearButton);
+ applyPreferences();
+
mainPane.add(pane, BorderLayout.SOUTH);
}
- protected void onEnableWindow(boolean enable)
- {
- textArea.setEnabled(enable);
- clearButton.setEnabled(enable);
+ @Override
+ protected void onEnableWindow(boolean enable) {
+ // never actually disable textArea, so people can
+ // still select & copy text, even when the port
+ // is closed or disconnected
+ textArea.setEnabled(true);
+ if (enable) {
+ // setting these to null for system default
+ // gives a wrong gray background on Windows
+ // so assume black text on white background
+ textArea.setForeground(Color.BLACK);
+ textArea.setBackground(Color.WHITE);
+ } else {
+ // In theory, UIManager.getDefaults() should
+ // give us the system's colors for disabled
+ // windows. But it doesn't seem to work. :(
+ textArea.setForeground(new Color(64, 64, 64));
+ textArea.setBackground(new Color(238, 238, 238));
+ }
+ textArea.invalidate();
scrollPane.setEnabled(enable);
textField.setEnabled(enable);
sendButton.setEnabled(enable);
- autoscrollBox.setEnabled(enable);
- addTimeStampBox.setEnabled(enable);
- lineEndings.setEnabled(enable);
- serialRates.setEnabled(enable);
}
public void onSendCommand(ActionListener listener) {
textField.addActionListener(listener);
sendButton.addActionListener(listener);
}
-
+
public void onClearCommand(ActionListener listener) {
clearButton.addActionListener(listener);
}
@@ -173,41 +207,59 @@ public void onClearCommand(ActionListener listener) {
public void onSerialRateChange(ActionListener listener) {
serialRates.addActionListener(listener);
}
-
- public void message(final String s) {
- SwingUtilities.invokeLater(new Runnable() {
- // Pre-allocate all objects used for streaming data
- Date t = new Date();
- String now;
- StringBuilder out = new StringBuilder(16384);
- boolean isStartingLine = false;
-
- public void run() {
- if (addTimeStampBox.isSelected()) {
- t.setTime(System.currentTimeMillis());
- now = logDateFormat.format(t);
- out.setLength(0);
-
- StringTokenizer tokenizer = new StringTokenizer(s, "\n", true);
- while (tokenizer.hasMoreTokens()) {
- if (isStartingLine) {
- out.append(now);
- }
- String token = tokenizer.nextToken();
- out.append(token);
- // tokenizer returns "\n" as a single token
- isStartingLine = token.charAt(0) == '\n';
- }
-
- textArea.append(out.toString());
- } else {
- textArea.append(s);
- }
-
- if (autoscrollBox.isSelected()) {
- textArea.setCaretPosition(textArea.getDocument().getLength());
- }
+
+ @Override
+ public void message(String msg) {
+ SwingUtilities.invokeLater(() -> updateTextArea(msg));
+ }
+
+ private static final String LINE_SEPARATOR = "\n";
+ private boolean isStartingLine = true;
+
+ protected void updateTextArea(String msg) {
+ if (addTimeStampBox.isSelected()) {
+ textArea.append(addTimestamps(msg));
+ } else {
+ textArea.append(msg);
+ }
+ if (autoscrollBox.isSelected()) {
+ textArea.setCaretPosition(textArea.getDocument().getLength());
+ }
+ }
+
+ @Override
+ public void applyPreferences() {
+
+ // Apply font.
+ Font consoleFont = Theme.getFont("console.font");
+ Font editorFont = PreferencesData.getFont("editor.font");
+ textArea.setFont(Theme.scale(new Font(
+ consoleFont.getName(), consoleFont.getStyle(), editorFont.getSize())));
+
+ // Apply line endings.
+ if (PreferencesData.get("serial.line_ending") != null) {
+ lineEndings.setSelectedIndex(PreferencesData.getInteger("serial.line_ending"));
+ }
+
+ // Apply timestamp visibility.
+ if (PreferencesData.get("serial.show_timestamp") != null) {
+ addTimeStampBox.setSelected(PreferencesData.getBoolean("serial.show_timestamp"));
+ }
+ }
+
+ private String addTimestamps(String text) {
+ String now = new SimpleDateFormat("HH:mm:ss.SSS -> ").format(new Date());
+ final StringBuilder sb = new StringBuilder(text.length() + now.length());
+ StringTokenizer tokenizer = new StringTokenizer(text, LINE_SEPARATOR, true);
+ while (tokenizer.hasMoreTokens()) {
+ if (isStartingLine) {
+ sb.append(now);
}
- });
+ String token = tokenizer.nextToken();
+ sb.append(token);
+ // tokenizer returns "\n" as a single token
+ isStartingLine = token.equals(LINE_SEPARATOR);
+ }
+ return sb.toString();
}
}
diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java
index 26c7f67f4c8..cdac3059f6d 100644
--- a/app/src/processing/app/Base.java
+++ b/app/src/processing/app/Base.java
@@ -27,7 +27,10 @@
import cc.arduino.UpdatableBoardsLibsFakeURLsHandler;
import cc.arduino.UploaderUtils;
import cc.arduino.contributions.*;
-import cc.arduino.contributions.libraries.*;
+import cc.arduino.contributions.libraries.ContributedLibrary;
+import cc.arduino.contributions.libraries.LibrariesIndexer;
+import cc.arduino.contributions.libraries.LibraryInstaller;
+import cc.arduino.contributions.libraries.LibraryOfSameTypeComparator;
import cc.arduino.contributions.libraries.ui.LibraryManagerUI;
import cc.arduino.contributions.packages.ContributedPlatform;
import cc.arduino.contributions.packages.ContributionInstaller;
@@ -35,15 +38,13 @@
import cc.arduino.contributions.packages.ui.ContributionManagerUI;
import cc.arduino.files.DeleteFilesOnShutdown;
import cc.arduino.packages.DiscoveryManager;
+import cc.arduino.packages.Uploader;
import cc.arduino.view.Event;
import cc.arduino.view.JMenuUtils;
import cc.arduino.view.SplashScreenHelper;
-
+import com.github.zafarkhaja.semver.Version;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.commons.lang3.StringUtils;
-
-import com.github.zafarkhaja.semver.Version;
-
import processing.app.debug.TargetBoard;
import processing.app.debug.TargetPackage;
import processing.app.debug.TargetPlatform;
@@ -65,9 +66,9 @@
import java.awt.*;
import java.awt.event.*;
import java.io.*;
-import java.util.*;
import java.util.List;
import java.util.Timer;
+import java.util.*;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -140,7 +141,7 @@ static public void main(String args[]) throws Exception {
if (OSUtils.isMacOS()) {
System.setProperty("apple.laf.useScreenMenuBar",
String.valueOf(!System.getProperty("os.version").startsWith("10.13")
- || com.apple.eawt.Application.getApplication().isAboutMenuItemPresent()));
+ || isMacOsAboutMenuItemPresent()));
ThinkDifferent.init();
}
@@ -153,6 +154,11 @@ static public void main(String args[]) throws Exception {
}
}
+ @SuppressWarnings("deprecation")
+ public static boolean isMacOsAboutMenuItemPresent() {
+ return com.apple.eawt.Application.getApplication().isAboutMenuItemPresent();
+ }
+
static public void initLogger() {
Handler consoleHandler = new ConsoleLogger();
consoleHandler.setLevel(Level.ALL);
@@ -265,6 +271,8 @@ public Base(String[] args) throws Exception {
splash.splashText(tr("Initializing packages..."));
BaseNoGui.initPackages();
+ parser.getUploadPort().ifPresent(BaseNoGui::selectSerialPort);
+
splash.splashText(tr("Preparing boards..."));
if (!isCommandLine()) {
@@ -282,8 +290,9 @@ public Base(String[] args) throws Exception {
pdeKeywords = new PdeKeywords();
pdeKeywords.reload();
- contributionInstaller = new ContributionInstaller(BaseNoGui.getPlatform(), new GPGDetachedSignatureVerifier());
- libraryInstaller = new LibraryInstaller(BaseNoGui.getPlatform());
+ final GPGDetachedSignatureVerifier gpgDetachedSignatureVerifier = new GPGDetachedSignatureVerifier();
+ contributionInstaller = new ContributionInstaller(BaseNoGui.getPlatform(), gpgDetachedSignatureVerifier);
+ libraryInstaller = new LibraryInstaller(BaseNoGui.getPlatform(), gpgDetachedSignatureVerifier);
parser.parseArgumentsPhase2();
@@ -297,11 +306,10 @@ public Base(String[] args) throws Exception {
if (parser.isInstallBoard()) {
ContributionsIndexer indexer = new ContributionsIndexer(
BaseNoGui.getSettingsFolder(), BaseNoGui.getHardwareFolder(),
- BaseNoGui.getPlatform(), new GPGDetachedSignatureVerifier());
+ BaseNoGui.getPlatform(), gpgDetachedSignatureVerifier);
ProgressListener progressListener = new ConsoleProgressListener();
- List downloadedPackageIndexFiles = contributionInstaller.updateIndex(progressListener);
- contributionInstaller.deleteUnknownFiles(downloadedPackageIndexFiles);
+ contributionInstaller.updateIndex(progressListener);
indexer.parseIndex();
indexer.syncWithFilesystem();
@@ -380,7 +388,7 @@ public Base(String[] args) throws Exception {
library, mayInstalled.get().getParsedVersion())));
libraryInstaller.remove(mayInstalled.get(), progressListener);
} else {
- libraryInstaller.install(selected, mayInstalled, progressListener);
+ libraryInstaller.install(selected, progressListener);
}
}
@@ -611,7 +619,12 @@ private int[] retrieveSketchLocation(String index) {
.get("last.sketch" + index + ".location");
if (locationStr == null)
return defaultEditorLocation();
- return PApplet.parseInt(PApplet.split(locationStr, ','));
+
+ int location[] = PApplet.parseInt(PApplet.split(locationStr, ','));
+ if (location[0] > screen.width || location[1] > screen.height)
+ return defaultEditorLocation();
+
+ return location;
}
protected void storeRecentSketches(SketchController sketch) {
@@ -760,7 +773,20 @@ protected File createNewUntitled() throws IOException {
if (!newbieFile.createNewFile()) {
throw new IOException();
}
- FileUtils.copyFile(new File(getContentFile("examples"), "01.Basics" + File.separator + "BareMinimum" + File.separator + "BareMinimum.ino"), newbieFile);
+
+ // Initialize the pde file with the BareMinimum sketch.
+ // Apply user-defined tab settings.
+ String sketch = FileUtils.readFileToString(
+ new File(getContentFile("examples"), "01.Basics" + File.separator
+ + "BareMinimum" + File.separator + "BareMinimum.ino"));
+ String currentTab = " ";
+ String newTab = (PreferencesData.getBoolean("editor.tabs.expand")
+ ? StringUtils.repeat(" ",
+ PreferencesData.getInteger("editor.tabs.size"))
+ : "\t");
+ sketch = sketch.replaceAll(
+ "(?<=(^|\n)(" + currentTab + "){0,50})" + currentTab, newTab);
+ FileUtils.writeStringToFile(newbieFile, sketch);
return newbieFile;
}
@@ -924,46 +950,23 @@ public void actionPerformed(ActionEvent actionEvent) {
* @return true if succeeded in closing, false if canceled.
*/
public boolean handleClose(Editor editor) {
- // Check if modified
-// boolean immediate = editors.size() == 1;
- if (!editor.checkModified()) {
- return false;
- }
if (editors.size() == 1) {
- storeScreenDimensions();
- storeSketches();
-
- // This will store the sketch count as zero
- editors.remove(editor);
- try {
- Editor.serialMonitor.close();
- } catch (Exception e) {
- //ignore
+ if (!handleQuit()) {
+ return false;
}
- rebuildRecentSketchesMenuItems();
-
- // Save out the current prefs state
- PreferencesData.save();
-
- // Since this wasn't an actual Quit event, call System.exit()
- System.exit(0);
-
+ // Everything called after handleQuit will only affect OSX
+ editor.setVisible(false);
+ editors.remove(editor);
} else {
// More than one editor window open,
// proceed with closing the current window.
+ // Check if modified
+ if (!editor.checkModified()) {
+ return false;
+ }
editor.setVisible(false);
editor.dispose();
-// for (int i = 0; i < editorCount; i++) {
-// if (editor == editors[i]) {
-// for (int j = i; j < editorCount-1; j++) {
-// editors[j] = editors[j+1];
-// }
-// editorCount--;
-// // Set to null so that garbage collection occurs
-// editors[editorCount] = null;
-// }
-// }
editors.remove(editor);
}
return true;
@@ -986,11 +989,19 @@ public boolean handleQuit() {
// ignore
}
+ // kill uploader (if still alive)
+ UploaderUtils uploaderInstance = new UploaderUtils();
+ Uploader uploader = uploaderInstance.getUploaderByPreferences(false);
+ if (uploader != null && Uploader.programmerPid != null && Uploader.programmerPid.isAlive()) {
+ // kill the stuck programmer
+ Uploader.programmerPid.destroyForcibly();
+ }
+
if (handleQuitEach()) {
// Save out the current prefs state
PreferencesData.save();
- if (!OSUtils.hasMacOSStyleMenus()) {
+ if (!OSUtils.isMacOS()) {
// If this was fired from the menu or an AppleEvent (the Finder),
// then Mac OS X will send the terminate signal itself.
System.exit(0);
@@ -1302,6 +1313,7 @@ public void rebuildExamplesMenu(JMenu menu) {
if (!sketchbookIncompatibleLibs.isEmpty()) {
sketchbookIncompatibleLibs.sort();
JMenu incompatible = new JMenu(tr("INCOMPATIBLE"));
+ MenuScroller.setScrollerFor(incompatible);
menu.add(incompatible);
for (UserLibrary lib : sketchbookIncompatibleLibs) {
addSketchesSubmenu(incompatible, lib);
@@ -1425,8 +1437,9 @@ public void actionPerformed(ActionEvent actionevent) {
String filterText = "";
String dropdownItem = "";
if (actionevent instanceof Event) {
- filterText = ((Event) actionevent).getPayload().get("filterText").toString();
- dropdownItem = ((Event) actionevent).getPayload().get("dropdownItem").toString();
+ Event e = ((Event) actionevent);
+ filterText = e.getPayload().get("filterText").toString();
+ dropdownItem = e.getPayload().get("dropdownItem").toString();
}
try {
openBoardsManager(filterText, dropdownItem);
@@ -1446,41 +1459,42 @@ public void actionPerformed(ActionEvent actionevent) {
boardMenu.add(new JSeparator());
// Generate custom menus for all platforms
- Set customMenusTitles = new LinkedHashSet<>();
for (TargetPackage targetPackage : BaseNoGui.packages.values()) {
for (TargetPlatform targetPlatform : targetPackage.platforms()) {
- customMenusTitles.addAll(targetPlatform.getCustomMenus().values());
+ for (String customMenuTitle : targetPlatform.getCustomMenus().values()) {
+ JMenu customMenu = new JMenu(tr(customMenuTitle));
+ customMenu.putClientProperty("platform", getPlatformUniqueId(targetPlatform));
+ customMenu.putClientProperty("removeOnWindowDeactivation", true);
+ boardsCustomMenus.add(customMenu);
+ MenuScroller.setScrollerFor(customMenu);
+ }
}
}
- for (String customMenuTitle : customMenusTitles) {
- JMenu customMenu = new JMenu(tr(customMenuTitle));
- customMenu.putClientProperty("removeOnWindowDeactivation", true);
- boardsCustomMenus.add(customMenu);
- }
List menuItemsToClickAfterStartup = new LinkedList<>();
ButtonGroup boardsButtonGroup = new ButtonGroup();
Map buttonGroupsMap = new HashMap<>();
+ List platformMenus = new ArrayList<>();
+
// Cycle through all packages
- boolean first = true;
for (TargetPackage targetPackage : BaseNoGui.packages.values()) {
// For every package cycle through all platform
for (TargetPlatform targetPlatform : targetPackage.platforms()) {
- // Add a separator from the previous platform
- if (!first)
- boardMenu.add(new JSeparator());
- first = false;
-
// Add a title for each platform
String platformLabel = targetPlatform.getPreferences().get("name");
- if (platformLabel != null && !targetPlatform.getBoards().isEmpty()) {
- JMenuItem menuLabel = new JMenuItem(tr(platformLabel));
- menuLabel.setEnabled(false);
- boardMenu.add(menuLabel);
- }
+ if (platformLabel == null)
+ platformLabel = targetPackage.getId() + "-" + targetPlatform.getId();
+
+ // add an hint that this core lives in sketchbook
+ if (targetPlatform.isInSketchbook())
+ platformLabel += " (in sketchbook)";
+
+ JMenu platformBoardsMenu = new JMenu(platformLabel);
+ MenuScroller.setScrollerFor(platformBoardsMenu);
+ platformMenus.add(platformBoardsMenu);
// Cycle through all boards of this platform
for (TargetBoard board : targetPlatform.getBoards().values()) {
@@ -1489,14 +1503,40 @@ public void actionPerformed(ActionEvent actionevent) {
JMenuItem item = createBoardMenusAndCustomMenus(boardsCustomMenus, menuItemsToClickAfterStartup,
buttonGroupsMap,
board, targetPlatform, targetPackage);
- boardMenu.add(item);
+ platformBoardsMenu.add(item);
boardsButtonGroup.add(item);
}
}
}
+ platformMenus.sort((x,y) -> x.getText().compareToIgnoreCase(y.getText()));
+
+ JMenuItem firstBoardItem = null;
+ if (platformMenus.size() == 1) {
+ // When just one platform exists, add the board items directly,
+ // rather than using a submenu
+ for (Component boardItem : platformMenus.get(0).getMenuComponents()) {
+ boardMenu.add(boardItem);
+ if (firstBoardItem == null)
+ firstBoardItem = (JMenuItem)boardItem;
+ }
+ } else {
+ // For multiple platforms, use submenus
+ for (JMenu platformMenu : platformMenus) {
+ if (firstBoardItem == null && platformMenu.getItemCount() > 0)
+ firstBoardItem = platformMenu.getItem(0);
+ boardMenu.add(platformMenu);
+ }
+ }
+
+ if (firstBoardItem == null) {
+ throw new IllegalStateException("No available boards");
+ }
+
+ // If there is no current board yet (first startup, or selected
+ // board no longer defined), select first available board.
if (menuItemsToClickAfterStartup.isEmpty()) {
- menuItemsToClickAfterStartup.add(selectFirstEnabledMenuItem(boardMenu));
+ menuItemsToClickAfterStartup.add(firstBoardItem);
}
for (JMenuItem menuItemToClick : menuItemsToClickAfterStartup) {
@@ -1505,6 +1545,10 @@ public void actionPerformed(ActionEvent actionevent) {
}
}
+ private String getPlatformUniqueId(TargetPlatform platform) {
+ return platform.getId() + "_" + platform.getFolder();
+ }
+
private JRadioButtonMenuItem createBoardMenusAndCustomMenus(
final List boardsCustomMenus, List menuItemsToClickAfterStartup,
Map buttonGroupsMap,
@@ -1528,6 +1572,7 @@ public void actionPerformed(ActionEvent actionevent) {
onBoardOrPortChange();
rebuildImportMenu(Editor.importMenu);
rebuildExamplesMenu(Editor.examplesMenu);
+ rebuildProgrammerMenu();
}
};
action.putValue("b", board);
@@ -1542,7 +1587,7 @@ public void actionPerformed(ActionEvent actionevent) {
PreferencesMap customMenus = targetPlatform.getCustomMenus();
for (final String menuId : customMenus.keySet()) {
String title = customMenus.get(menuId);
- JMenu menu = getBoardCustomMenu(tr(title));
+ JMenu menu = getBoardCustomMenu(tr(title), getPlatformUniqueId(targetPlatform));
if (board.hasMenu(menuId)) {
PreferencesMap boardCustomMenu = board.getMenuLabels(menuId);
@@ -1550,11 +1595,16 @@ public void actionPerformed(ActionEvent actionevent) {
@SuppressWarnings("serial")
Action subAction = new AbstractAction(tr(boardCustomMenu.get(customMenuOption))) {
public void actionPerformed(ActionEvent e) {
- PreferencesData.set("custom_" + menuId, ((TargetBoard) getValue("board")).getId() + "_" + getValue("custom_menu_option"));
+ PreferencesData.set("custom_" + menuId, ((List) getValue("board")).get(0).getId() + "_" + getValue("custom_menu_option"));
onBoardOrPortChange();
}
};
- subAction.putValue("board", board);
+ List boards = (List) subAction.getValue("board");
+ if (boards == null) {
+ boards = new ArrayList<>();
+ }
+ boards.add(board);
+ subAction.putValue("board", boards);
subAction.putValue("custom_menu_option", customMenuOption);
if (!buttonGroupsMap.containsKey(menuId)) {
@@ -1582,7 +1632,9 @@ private void filterVisibilityOfSubsequentBoardMenus(List boardsCustomMenu
JMenu menu = boardsCustomMenus.get(i);
for (int m = 0; m < menu.getItemCount(); m++) {
JMenuItem menuItem = menu.getItem(m);
- menuItem.setVisible(menuItem.getAction().getValue("board").equals(board));
+ for (TargetBoard t_board : (List)menuItem.getAction().getValue("board")) {
+ menuItem.setVisible(t_board.equals(board));
+ }
}
menu.setVisible(ifThereAreVisibleItemsOn(menu));
@@ -1605,9 +1657,9 @@ private static boolean ifThereAreVisibleItemsOn(JMenu menu) {
return false;
}
- private JMenu getBoardCustomMenu(String label) throws Exception {
+ private JMenu getBoardCustomMenu(String label, String platformUniqueId) throws Exception {
for (JMenu menu : boardsCustomMenus) {
- if (label.equals(menu.getText())) {
+ if (label.equals(menu.getText()) && menu.getClientProperty("platform").equals(platformUniqueId)) {
return menu;
}
}
@@ -1639,40 +1691,50 @@ private static JMenuItem selectVisibleSelectedOrFirstMenuItem(JMenu menu) {
throw new IllegalStateException("Menu has no enabled items");
}
- private static JMenuItem selectFirstEnabledMenuItem(JMenu menu) {
- for (int i = 1; i < menu.getItemCount(); i++) {
- JMenuItem item = menu.getItem(i);
- if (item != null && item.isEnabled()) {
- return item;
+ public void rebuildProgrammerMenu() {
+ programmerMenus = new LinkedList<>();
+ ButtonGroup group = new ButtonGroup();
+
+ TargetBoard board = BaseNoGui.getTargetBoard();
+ if (board != null) {
+ TargetPlatform boardPlatform = board.getContainerPlatform();
+ TargetPlatform corePlatform = null;
+
+ String core = board.getPreferences().get("build.core");
+ if (core != null && core.contains(":")) {
+ String[] split = core.split(":", 2);
+ corePlatform = BaseNoGui.getCurrentTargetPlatformFromPackage(split[0]);
}
+
+ addProgrammersForPlatform(boardPlatform, programmerMenus, group);
+ if (corePlatform != null)
+ addProgrammersForPlatform(corePlatform, programmerMenus, group);
}
- throw new IllegalStateException("Menu has no enabled items");
- }
- public void rebuildProgrammerMenu() {
- programmerMenus = new LinkedList<>();
+ if (programmerMenus.isEmpty()) {
+ JMenuItem item = new JMenuItem(tr("No programmers available for this board"));
+ item.setEnabled(false);
+ programmerMenus.add(item);
+ }
+ }
- ButtonGroup group = new ButtonGroup();
- for (TargetPackage targetPackage : BaseNoGui.packages.values()) {
- for (TargetPlatform targetPlatform : targetPackage.platforms()) {
- for (String programmer : targetPlatform.getProgrammers().keySet()) {
- String id = targetPackage.getId() + ":" + programmer;
+ public void addProgrammersForPlatform(TargetPlatform platform, List menus, ButtonGroup group) {
+ for (String programmer : platform.getProgrammers().keySet()) {
+ String id = platform.getContainerPackage().getId() + ":" + programmer;
- @SuppressWarnings("serial")
- AbstractAction action = new AbstractAction(targetPlatform.getProgrammer(programmer).get("name")) {
- public void actionPerformed(ActionEvent actionevent) {
- PreferencesData.set("programmer", "" + getValue("id"));
- }
- };
- action.putValue("id", id);
- JMenuItem item = new JRadioButtonMenuItem(action);
- if (PreferencesData.get("programmer").equals(id)) {
- item.setSelected(true);
- }
- group.add(item);
- programmerMenus.add(item);
+ @SuppressWarnings("serial")
+ AbstractAction action = new AbstractAction(platform.getProgrammer(programmer).get("name")) {
+ public void actionPerformed(ActionEvent actionevent) {
+ PreferencesData.set("programmer", "" + getValue("id"));
}
+ };
+ action.putValue("id", id);
+ JMenuItem item = new JRadioButtonMenuItem(action);
+ if (PreferencesData.get("programmer").equals(id)) {
+ item.setSelected(true);
}
+ group.add(item);
+ menus.add(item);
}
}
@@ -1701,19 +1763,12 @@ public int compare(File file, File file2) {
});
boolean ifound = false;
-
for (File subfolder : files) {
- if (FileUtils.isSCCSOrHiddenFile(subfolder)) {
- continue;
- }
-
- if (!subfolder.isDirectory()) continue;
-
- if (addSketchesSubmenu(menu, subfolder.getName(), subfolder)) {
+ if (!FileUtils.isSCCSOrHiddenFile(subfolder) && subfolder.isDirectory()
+ && addSketchesSubmenu(menu, subfolder.getName(), subfolder)) {
ifound = true;
}
}
-
return ifound;
}
@@ -1887,6 +1942,75 @@ public void handleFontSizeChange(int change) {
getEditors().forEach(Editor::applyPreferences);
}
+ /**
+ * Adds a {@link MouseWheelListener} and {@link KeyListener} to the given
+ * component that will make "CTRL scroll" and "CTRL +/-"
+ * (with optional SHIFT for +) increase/decrease the editor text size.
+ * This method is equivalent to calling
+ * {@link #addEditorFontResizeMouseWheelListener(Component)} and
+ * {@link #addEditorFontResizeKeyListener(Component)} on the given component.
+ * Note that this also affects components that use the editor font settings.
+ * @param comp - The component to add the listener to.
+ */
+ public void addEditorFontResizeListeners(Component comp) {
+ addEditorFontResizeMouseWheelListener(comp);
+ addEditorFontResizeKeyListener(comp);
+ }
+
+ /**
+ * Adds a {@link MouseWheelListener} to the given component that will
+ * make "CTRL scroll" increase/decrease the editor text size.
+ * When CTRL is not pressed while scrolling, mouse wheel events are passed
+ * on to the parent of the given component.
+ * Note that this also affects components that use the editor font settings.
+ * @param comp - The component to add the listener to.
+ */
+ public void addEditorFontResizeMouseWheelListener(Component comp) {
+ comp.addMouseWheelListener(e -> {
+ if (e.isControlDown()) {
+ if (e.getWheelRotation() < 0) {
+ this.handleFontSizeChange(1);
+ } else {
+ this.handleFontSizeChange(-1);
+ }
+ } else {
+ if (e.getComponent() != null && e.getComponent().getParent() != null) {
+ e.getComponent().getParent().dispatchEvent(e);
+ }
+ }
+ });
+ }
+
+ /**
+ * Adds a {@link KeyListener} to the given component that will make "CTRL +/-"
+ * (with optional SHIFT for +) increase/decrease the editor text size.
+ * Note that this also affects components that use the editor font settings.
+ * @param comp - The component to add the listener to.
+ */
+ public void addEditorFontResizeKeyListener(Component comp) {
+ comp.addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyPressed(KeyEvent e) {
+ if (e.getModifiersEx() == KeyEvent.CTRL_DOWN_MASK
+ || e.getModifiersEx() == (KeyEvent.CTRL_DOWN_MASK
+ | KeyEvent.SHIFT_DOWN_MASK)) {
+ switch (e.getKeyCode()) {
+ case KeyEvent.VK_PLUS:
+ case KeyEvent.VK_EQUALS:
+ Base.this.handleFontSizeChange(1);
+ break;
+ case KeyEvent.VK_MINUS:
+ if (!e.isShiftDown()) {
+ Base.this.handleFontSizeChange(-1);
+ }
+ break;
+ default:
+ }
+ }
+ }
+ });
+ }
+
public List getBoardsCustomMenus() {
return boardsCustomMenus;
}
@@ -2039,60 +2163,6 @@ static public void registerWindowCloseKeys(JRootPane root,
// .................................................................
- static public void showReference(String filename) {
- showReference("reference/www.arduino.cc/en", filename);
- }
-
- static public void showReference(String prefix, String filename) {
- File referenceFolder = getContentFile(prefix);
- File referenceFile = new File(referenceFolder, filename);
- if (!referenceFile.exists())
- referenceFile = new File(referenceFolder, filename + ".html");
-
- if(referenceFile.exists()){
- openURL(referenceFile.getAbsolutePath());
- }else{
- showWarning(tr("Problem Opening URL"), format(tr("Could not open the URL\n{0}"), referenceFile), null);
- }
- }
-
- public static void showEdisonGettingStarted() {
- showReference("reference/Edison_help_files", "ArduinoIDE_guide_edison");
- }
-
- static public void showArduinoGettingStarted() {
- if (OSUtils.isMacOS()) {
- showReference("Guide/MacOSX");
- } else if (OSUtils.isWindows()) {
- showReference("Guide/Windows");
- } else {
- openURL("http://www.arduino.cc/playground/Learning/Linux");
- }
- }
-
- static public void showReference() {
- showReference("Reference/HomePage");
- }
-
-
- static public void showEnvironment() {
- showReference("Guide/Environment");
- }
-
-
- static public void showTroubleshooting() {
- showReference("Guide/Troubleshooting");
- }
-
-
- static public void showFAQ() {
- showReference("Main/FAQ");
- }
-
-
- // .................................................................
-
-
/**
* "No cookie for you" type messages. Nothing fatal or all that
* much of a bummer, but something to notify the user about.
diff --git a/app/src/processing/app/CommandHistory.java b/app/src/processing/app/CommandHistory.java
new file mode 100644
index 00000000000..cae3c2fc498
--- /dev/null
+++ b/app/src/processing/app/CommandHistory.java
@@ -0,0 +1,167 @@
+package processing.app;
+
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+/**
+ * Keeps track of command history in console-like applications.
+ * @author P.J.S. Kools
+ */
+public class CommandHistory {
+
+ private final LinkedList commandHistory = new LinkedList();
+ private final int maxHistorySize;
+ private ListIterator iterator = null;
+ private boolean iteratorAsc;
+
+ /**
+ * Create a new {@link CommandHistory}.
+ * @param maxHistorySize - The max command history size.
+ */
+ public CommandHistory(int maxHistorySize) {
+ this.maxHistorySize = (maxHistorySize < 0 ? 0 : maxHistorySize);
+ this.commandHistory.addLast(""); // Current command placeholder.
+ }
+
+ /**
+ * Adds the given command to the history and resets the history traversal
+ * position to the latest command. If the latest command in the history is
+ * equal to the given command, it will not be added to the history.
+ * If the max history size is exceeded, the oldest command will be removed
+ * from the history.
+ * @param command - The command to add.
+ */
+ public void addCommand(String command) {
+ if (this.maxHistorySize == 0) {
+ return;
+ }
+
+ // Remove 'current' command.
+ this.commandHistory.removeLast();
+
+ // Add new command if it differs from the latest command.
+ if (this.commandHistory.isEmpty()
+ || !this.commandHistory.getLast().equals(command)) {
+
+ // Remove oldest command if max history size is exceeded.
+ if (this.commandHistory.size() >= this.maxHistorySize) {
+ this.commandHistory.removeFirst();
+ }
+
+ // Add new command and reset 'current' command.
+ this.commandHistory.addLast(command);
+ }
+
+ // Re-add 'current' command and reset command iterator.
+ this.commandHistory.addLast(""); // Current command placeholder.
+ this.iterator = null;
+ }
+
+ /**
+ * Gets whether a next (more recent) command is available in the history.
+ * @return {@code true} if a next command is available,
+ * returns {@code false} otherwise.
+ */
+ public boolean hasNextCommand() {
+ if (this.iterator == null) {
+ return false;
+ }
+ if (!this.iteratorAsc) {
+ this.iterator.next(); // Current command, ascending.
+ this.iteratorAsc = true;
+ }
+ return this.iterator.hasNext();
+ }
+
+ /**
+ * Gets the next (more recent) command from the history.
+ * @return The next command or {@code null} if no next command is available.
+ */
+ public String getNextCommand() {
+
+ // Return null if there is no next command available.
+ if (!this.hasNextCommand()) {
+ return null;
+ }
+
+ // Get next command.
+ String next = this.iterator.next();
+
+ // Reset 'current' command when at the end of the list.
+ if (this.iterator.nextIndex() == this.commandHistory.size()) {
+ this.iterator.set(""); // Reset 'current' command.
+ }
+ return next;
+ }
+
+ /**
+ * Gets whether a previous (older) command is available in the history.
+ * @return {@code true} if a previous command is available,
+ * returns {@code false} otherwise.
+ */
+ public boolean hasPreviousCommand() {
+ if (this.iterator == null) {
+ return this.commandHistory.size() > 1;
+ }
+ if (this.iteratorAsc) {
+ this.iterator.previous(); // Current command, descending.
+ this.iteratorAsc = false;
+ }
+ return this.iterator.hasPrevious();
+ }
+
+ /**
+ * Gets the previous (older) command from the history.
+ * When this method is called while the most recent command in the history is
+ * selected, this will store the current command as temporary latest command
+ * so that {@link #getNextCommand()} will return it once. This temporary
+ * latest command gets reset when this case occurs again or when
+ * {@link #addCommand(String)} is invoked.
+ * @param currentCommand - The current unexecuted command.
+ * @return The previous command or {@code null} if no previous command is
+ * available.
+ */
+ public String getPreviousCommand(String currentCommand) {
+
+ // Return null if there is no previous command available.
+ if (!this.hasPreviousCommand()) {
+ return null;
+ }
+
+ // Store current unexecuted command and create iterator if not traversing.
+ if (this.iterator == null) {
+ this.iterator =
+ this.commandHistory.listIterator(this.commandHistory.size());
+ this.iterator.previous(); // Last element, descending.
+ this.iteratorAsc = false;
+ }
+
+ // Store current unexecuted command if on 'current' index.
+ if (this.iterator.nextIndex() == this.commandHistory.size() - 1) {
+ this.iterator.set(currentCommand == null ? "" : currentCommand);
+ }
+
+ // Return the previous command.
+ return this.iterator.previous();
+ }
+
+ /**
+ * Resets the history location to the most recent command.
+ * @returns The latest unexecuted command as stored by
+ * {@link #getPreviousCommand(String)} or an empty string if no such command
+ * was set.
+ */
+ public String resetHistoryLocation() {
+ this.iterator = null;
+ return this.commandHistory.set(this.commandHistory.size() - 1, "");
+ }
+
+ /**
+ * Clears the command history.
+ */
+ public void clear() {
+ this.iterator = null;
+ this.commandHistory.clear();
+ this.commandHistory.addLast(""); // Current command placeholder.
+ }
+}
diff --git a/app/src/processing/app/Editor.java b/app/src/processing/app/Editor.java
index 9500ddd695a..a307bde8fbb 100644
--- a/app/src/processing/app/Editor.java
+++ b/app/src/processing/app/Editor.java
@@ -35,7 +35,6 @@
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
@@ -47,13 +46,14 @@
import java.io.FileFilter;
import java.io.FilenameFilter;
import java.io.IOException;
+import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.URL;
import java.net.URLClassLoader;
+import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
-import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
@@ -83,6 +83,8 @@
import javax.swing.event.MenuListener;
import javax.swing.text.BadLocationException;
+import org.fife.ui.rsyntaxtextarea.folding.FoldManager;
+
import com.jcraft.jsch.JSchException;
import cc.arduino.CompilerProgressListener;
@@ -133,17 +135,21 @@ public boolean test(SketchController controller) {
}
}
- private static class ShouldSaveReadOnly implements Predicate {
+ private static class CanExportInSketchFolder
+ implements Predicate {
@Override
- public boolean test(SketchController sketch) {
- return sketch.isReadOnly();
+ public boolean test(SketchController controller) {
+ if (controller.isReadOnly()) {
+ return false;
+ }
+ if (controller.getSketch().isModified()) {
+ return PreferencesData.getBoolean("editor.save_on_verify");
+ }
+ return true;
}
}
- private final static List BOARD_PROTOCOLS_ORDER = Arrays.asList("serial", "network");
- private final static List BOARD_PROTOCOLS_ORDER_TRANSLATIONS = Arrays.asList(tr("Serial ports"), tr("Network ports"));
-
final Base base;
// otherwise, if the window is resized with the message label
@@ -176,7 +182,7 @@ public boolean test(SketchController sketch) {
private int numTools = 0;
- public boolean avoidMultipleOperations = false;
+ static public boolean avoidMultipleOperations = false;
private final EditorToolbar toolbar;
// these menus are shared so that they needn't be rebuilt for all windows
@@ -214,7 +220,7 @@ public boolean test(SketchController sketch) {
private JMenuItem saveAsMenuItem;
//boolean presenting;
- private boolean uploading;
+ static private boolean uploading;
// undo fellers
private JMenuItem undoItem;
@@ -226,8 +232,8 @@ public boolean test(SketchController sketch) {
Runnable presentHandler;
private Runnable runAndSaveHandler;
private Runnable presentAndSaveHandler;
- Runnable exportHandler;
- private Runnable exportAppHandler;
+ private UploadHandler uploadHandler;
+ private UploadHandler uploadUsingProgrammerHandler;
private Runnable timeoutUploadHandler;
private Map internalToolCache = new HashMap();
@@ -310,7 +316,7 @@ public void windowDeactivated(WindowEvent e) {
status = new EditorStatus(this);
consolePanel.add(status, BorderLayout.NORTH);
- console = new EditorConsole();
+ console = new EditorConsole(base);
console.setName("console");
// windows puts an ugly border on this guy
console.setBorder(null);
@@ -490,6 +496,9 @@ public void applyPreferences() {
tab.applyPreferences();
}
console.applyPreferences();
+ if (serialMonitor != null) {
+ serialMonitor.applyPreferences();
+ }
}
@@ -666,11 +675,12 @@ private void buildSketchMenu(JMenu sketchMenu) {
item = newJMenuItemAlt(tr("Export compiled Binary"), 'S');
item.addActionListener(event -> {
- if (new ShouldSaveReadOnly().test(sketchController) && !handleSave(true)) {
+ if (!(new CanExportInSketchFolder().test(sketchController))) {
System.out.println(tr("Export canceled, changes must first be saved."));
return;
}
- handleRun(false, new ShouldSaveReadOnly(), presentAndSaveHandler, runAndSaveHandler);
+ handleRun(false, new CanExportInSketchFolder(), presentAndSaveHandler, runAndSaveHandler);
+
});
sketchMenu.add(item);
@@ -750,6 +760,20 @@ private JMenu buildToolsMenu() {
toolsMenu.add(item);
toolsMenu.addMenuListener(new StubMenuListener() {
+ public JMenuItem getSelectedItemRecursive(JMenu menu) {
+ int count = menu.getItemCount();
+ for (int i=0; i < count; i++) {
+ JMenuItem item = menu.getItem(i);
+
+ if ((item instanceof JMenu))
+ item = getSelectedItemRecursive((JMenu)item);
+
+ if (item != null && item.isSelected())
+ return item;
+ }
+ return null;
+ }
+
public void menuSelected(MenuEvent e) {
//System.out.println("Tools menu selected.");
populatePortMenu();
@@ -761,15 +785,9 @@ public void menuSelected(MenuEvent e) {
String basename = name;
int index = name.indexOf(':');
if (index > 0) basename = name.substring(0, index);
- String sel = null;
- int count = menu.getItemCount();
- for (int i=0; i < count; i++) {
- JMenuItem item = menu.getItem(i);
- if (item != null && item.isSelected()) {
- sel = item.getText();
- if (sel != null) break;
- }
- }
+
+ JMenuItem item = getSelectedItemRecursive(menu);
+ String sel = item != null ? item.getText() : null;
if (sel == null) {
if (!name.equals(basename)) menu.setText(basename);
} else {
@@ -983,21 +1001,6 @@ private void addInternalTools(JMenu menu) {
}
- class SerialMenuListener implements ActionListener {
-
- private final String serialPort;
-
- public SerialMenuListener(String serialPort) {
- this.serialPort = serialPort;
- }
-
- public void actionPerformed(ActionEvent e) {
- selectSerialPort(serialPort);
- base.onBoardOrPortChange();
- }
-
- }
-
private void selectSerialPort(String name) {
if(portMenu == null) {
System.out.println(tr("serialMenu is null"));
@@ -1021,22 +1024,20 @@ private void selectSerialPort(String name) {
//System.out.println(item.getLabel());
BaseNoGui.selectSerialPort(name);
- if (serialMonitor != null) {
- try {
+ try {
+ boolean reopenMonitor = ((serialMonitor != null && serialMonitor.isVisible()) ||
+ serialPlotter != null && serialPlotter.isVisible());
+ if (serialMonitor != null) {
serialMonitor.close();
- serialMonitor.setVisible(false);
- } catch (Exception e) {
- // ignore
}
- }
-
- if (serialPlotter != null) {
- try {
+ if (serialPlotter != null) {
serialPlotter.close();
- serialPlotter.setVisible(false);
- } catch (Exception e) {
- // ignore
}
+ if (reopenMonitor) {
+ handleSerial();
+ }
+ } catch (Exception e) {
+ // ignore
}
onBoardOrPortChange();
@@ -1045,8 +1046,34 @@ private void selectSerialPort(String name) {
//System.out.println("set to " + get("serial.port"));
}
+ class BoardPortJCheckBoxMenuItem extends JCheckBoxMenuItem {
+ private BoardPort port;
+
+ public BoardPortJCheckBoxMenuItem(BoardPort port) {
+ super();
+ this.port = port;
+ setText(toString());
+ addActionListener(e -> {
+ selectSerialPort(port.getAddress());
+ base.onBoardOrPortChange();
+ });
+ }
+
+ @Override
+ public String toString() {
+ // This is required for serialPrompt()
+ String label = port.getLabel();
+ if (port.getBoardName() != null && !port.getBoardName().isEmpty()) {
+ label += " (" + port.getBoardName() + ")";
+ }
+ return label;
+ }
+ }
private void populatePortMenu() {
+ final List PROTOCOLS_ORDER = Arrays.asList("serial", "network");
+ final List PROTOCOLS_LABELS = Arrays.asList(tr("Serial ports"), tr("Network ports"));
+
portMenu.removeAll();
String selectedPort = PreferencesData.get("serial.port");
@@ -1055,36 +1082,48 @@ private void populatePortMenu() {
ports = platform.filterPorts(ports, PreferencesData.getBoolean("serial.ports.showall"));
- Collections.sort(ports, new Comparator() {
- @Override
- public int compare(BoardPort o1, BoardPort o2) {
- return BOARD_PROTOCOLS_ORDER.indexOf(o1.getProtocol()) - BOARD_PROTOCOLS_ORDER.indexOf(o2.getProtocol());
- }
+ ports.stream() //
+ .filter(port -> port.getProtocolLabel() == null || port.getProtocolLabel().isEmpty())
+ .forEach(port -> {
+ int labelIdx = PROTOCOLS_ORDER.indexOf(port.getProtocol());
+ if (labelIdx != -1) {
+ port.setProtocolLabel(PROTOCOLS_LABELS.get(labelIdx));
+ } else {
+ port.setProtocolLabel(port.getProtocol());
+ }
+ });
+
+ Collections.sort(ports, (port1, port2) -> {
+ String pr1 = port1.getProtocol();
+ String pr2 = port2.getProtocol();
+ int prIdx1 = PROTOCOLS_ORDER.contains(pr1) ? PROTOCOLS_ORDER.indexOf(pr1) : 999;
+ int prIdx2 = PROTOCOLS_ORDER.contains(pr2) ? PROTOCOLS_ORDER.indexOf(pr2) : 999;
+ int r = prIdx1 - prIdx2;
+ if (r != 0)
+ return r;
+ r = port1.getProtocolLabel().compareTo(port2.getProtocolLabel());
+ if (r != 0)
+ return r;
+ return port1.getAddress().compareTo(port2.getAddress());
});
- String lastProtocol = null;
- String lastProtocolTranslated;
+ String lastProtocol = "";
+ String lastProtocolLabel = "";
for (BoardPort port : ports) {
- if (lastProtocol == null || !port.getProtocol().equals(lastProtocol)) {
- if (lastProtocol != null) {
+ if (!port.getProtocol().equals(lastProtocol) || !port.getProtocolLabel().equals(lastProtocolLabel)) {
+ if (!lastProtocol.isEmpty()) {
portMenu.addSeparator();
}
lastProtocol = port.getProtocol();
-
- if (BOARD_PROTOCOLS_ORDER.indexOf(port.getProtocol()) != -1) {
- lastProtocolTranslated = BOARD_PROTOCOLS_ORDER_TRANSLATIONS.get(BOARD_PROTOCOLS_ORDER.indexOf(port.getProtocol()));
- } else {
- lastProtocolTranslated = port.getProtocol();
- }
- JMenuItem lastProtocolMenuItem = new JMenuItem(tr(lastProtocolTranslated));
- lastProtocolMenuItem.setEnabled(false);
- portMenu.add(lastProtocolMenuItem);
+ lastProtocolLabel = port.getProtocolLabel();
+ JMenuItem item = new JMenuItem(tr(lastProtocolLabel));
+ item.setEnabled(false);
+ portMenu.add(item);
}
String address = port.getAddress();
- String label = port.getLabel();
- JCheckBoxMenuItem item = new JCheckBoxMenuItem(label, address.equals(selectedPort));
- item.addActionListener(new SerialMenuListener(address));
+ BoardPortJCheckBoxMenuItem item = new BoardPortJCheckBoxMenuItem(port);
+ item.setSelected(address.equals(selectedPort));
portMenu.add(item);
}
@@ -1097,61 +1136,33 @@ private JMenu buildHelpMenu() {
menu.setMnemonic(KeyEvent.VK_H);
JMenuItem item = new JMenuItem(tr("Getting Started"));
- item.addActionListener(event -> Base.showArduinoGettingStarted());
+ item.addActionListener(event -> Base.openURL("https://www.arduino.cc/en/Guide"));
menu.add(item);
item = new JMenuItem(tr("Environment"));
- item.addActionListener(event -> Base.showEnvironment());
+ item.addActionListener(event -> Base.openURL("https://www.arduino.cc/en/Guide/Environment"));
menu.add(item);
item = new JMenuItem(tr("Troubleshooting"));
- item.addActionListener(event -> Base.showTroubleshooting());
+ item.addActionListener(event -> Base.openURL("https://support.arduino.cc/hc/en-us"));
menu.add(item);
item = new JMenuItem(tr("Reference"));
- item.addActionListener(event -> Base.showReference());
- menu.add(item);
-
- menu.addSeparator();
-
- item = new JMenuItem(tr("Galileo Help"));
- item.setEnabled(false);
- menu.add(item);
-
- item = new JMenuItem(tr("Getting Started"));
- item.addActionListener(event -> Base.showReference("reference/Galileo_help_files", "ArduinoIDE_guide_galileo"));
- menu.add(item);
-
- item = new JMenuItem(tr("Troubleshooting"));
- item.addActionListener(event -> Base.showReference("reference/Galileo_help_files", "Guide_Troubleshooting_Galileo"));
- menu.add(item);
-
- menu.addSeparator();
-
- item = new JMenuItem(tr("Edison Help"));
- item.setEnabled(false);
- menu.add(item);
-
- item = new JMenuItem(tr("Getting Started"));
- item.addActionListener(event -> Base.showReference("reference/Edison_help_files", "ArduinoIDE_guide_edison"));
- menu.add(item);
-
- item = new JMenuItem(tr("Troubleshooting"));
- item.addActionListener(event -> Base.showReference("reference/Edison_help_files", "Guide_Troubleshooting_Edison"));
+ item.addActionListener(event -> Base.openURL("https://www.arduino.cc/reference/en/"));
menu.add(item);
menu.addSeparator();
item = newJMenuItemShift(tr("Find in Reference"), 'F');
- item.addActionListener(event -> handleFindReference(event));
+ item.addActionListener(event -> handleFindReference(getCurrentTab().getCurrentKeyword()));
menu.add(item);
item = new JMenuItem(tr("Frequently Asked Questions"));
- item.addActionListener(event -> Base.showFAQ());
+ item.addActionListener(event -> Base.openURL("https://support.arduino.cc/hc/en-us"));
menu.add(item);
item = new JMenuItem(tr("Visit Arduino.cc"));
- item.addActionListener(event -> Base.openURL(tr("http://www.arduino.cc/")));
+ item.addActionListener(event -> Base.openURL("https://www.arduino.cc/"));
menu.add(item);
// macosx already has its own about menu
@@ -1241,12 +1252,14 @@ private JMenu buildEditMenu() {
JMenuItem increaseFontSizeItem = newJMenuItem(tr("Increase Font Size"), KeyEvent.VK_PLUS);
increaseFontSizeItem.addActionListener(event -> base.handleFontSizeChange(1));
menu.add(increaseFontSizeItem);
- // Add alternative shortcut "CTRL SHIFT =" for keyboards that haven't the "+" key
- // in the base layer. This workaround covers all the keyboards that have the "+"
- // key available as "SHIFT =" that seems to be very common.
+ // Many keyboards have '+' and '=' on the same key. Allowing "CTRL +",
+ // "CTRL SHIFT +" and "CTRL =" covers the generally expected behavior.
KeyStroke ctrlShiftEq = KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, SHORTCUT_KEY_MASK | ActionEvent.SHIFT_MASK);
menu.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ctrlShiftEq, "IncreaseFontSize");
+ KeyStroke ctrlEq = KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, SHORTCUT_KEY_MASK);
+ menu.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ctrlEq, "IncreaseFontSize");
menu.getActionMap().put("IncreaseFontSize", new AbstractAction() {
+ @Override
public void actionPerformed(ActionEvent e) {
base.handleFontSizeChange(1);
}
@@ -1376,8 +1389,10 @@ private void resetHandlers() {
presentHandler = new BuildHandler(true);
runAndSaveHandler = new BuildHandler(false, true);
presentAndSaveHandler = new BuildHandler(true, true);
- exportHandler = new DefaultExportHandler();
- exportAppHandler = new DefaultExportAppHandler();
+ uploadHandler = new UploadHandler();
+ uploadHandler.setUsingProgrammer(false);
+ uploadUsingProgrammerHandler = new UploadHandler();
+ uploadUsingProgrammerHandler.setUsingProgrammer(true);
timeoutUploadHandler = new TimeoutUploadHandler();
}
@@ -1437,8 +1452,10 @@ public void selectTab(final int index) {
// This must be run in the GUI thread
SwingUtilities.invokeLater(() -> {
codePanel.removeAll();
- codePanel.add(tabs.get(index), BorderLayout.CENTER);
- tabs.get(index).requestFocusInWindow(); // get the caret blinking
+ EditorTab selectedTab = tabs.get(index);
+ codePanel.add(selectedTab, BorderLayout.CENTER);
+ selectedTab.applyPreferences();
+ selectedTab.requestFocusInWindow(); // get the caret blinking
// For some reason, these are needed. Revalidate says it should be
// automatically called when components are added or removed, but without
// it, the component switched to is not displayed. repaint() is needed to
@@ -1540,20 +1557,25 @@ protected void removeTab(SketchFile file) throws IOException {
tabs.remove(index);
}
+
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
- void handleFindReference(ActionEvent e) {
- String text = getCurrentTab().getCurrentKeyword();
+ void handleFindReference(String text) {
String referenceFile = base.getPdeKeywords().getReference(text);
+ String q;
if (referenceFile == null) {
- statusNotice(I18n.format(tr("No reference available for \"{0}\""), text));
+ q = text;
+ } else if (referenceFile.startsWith("Serial_")) {
+ q = referenceFile.substring(7);
} else {
- if (referenceFile.startsWith("Serial_")) {
- Base.showReference("Serial/" + referenceFile.substring("Serial_".length()));
- } else {
- Base.showReference("Reference/" + referenceFile);
- }
+ q = referenceFile;
+ }
+ try {
+ Base.openURL("https://www.arduino.cc/search?tab=&q="
+ + URLEncoder.encode(q, "UTF-8"));
+ } catch (UnsupportedEncodingException e) {
+ e.printStackTrace();
}
}
@@ -1636,8 +1658,17 @@ public void removeAllLineHighlights() {
}
public void addLineHighlight(int line) throws BadLocationException {
- getCurrentTab().getTextArea().addLineHighlight(line, new Color(1, 0, 0, 0.2f));
- getCurrentTab().getTextArea().setCaretPosition(getCurrentTab().getTextArea().getLineStartOffset(line));
+ SketchTextArea textArea = getCurrentTab().getTextArea();
+ FoldManager foldManager = textArea.getFoldManager();
+ if (foldManager.isLineHidden(line)) {
+ for (int i = 0; i < foldManager.getFoldCount(); i++) {
+ if (foldManager.getFold(i).containsLine(line)) {
+ foldManager.getFold(i).setCollapsed(false);
+ }
+ }
+ }
+ textArea.addLineHighlight(line, new Color(1, 0, 0, 0.2f));
+ textArea.setCaretPosition(textArea.getLineStartOffset(line));
}
@@ -1953,32 +1984,30 @@ public boolean handleSaveAs() {
private boolean serialPrompt() {
- int count = portMenu.getItemCount();
- Object[] names = new Object[count];
- for (int i = 0; i < count; i++) {
- names[i] = portMenu.getItem(i).getText();
- }
-
- // FIXME: This is horribly unreadable
- String result = (String)
- JOptionPane.showInputDialog(this,
- I18n.format(
- tr("Serial port {0} not found.\n" +
- "Retry the upload with another serial port?"),
- PreferencesData.get("serial.port")
- ),
- "Serial port not found",
- JOptionPane.PLAIN_MESSAGE,
- null,
- names,
- 0);
- if (result == null) return false;
- selectSerialPort(result);
+ List items = new ArrayList<>();
+ for (int i = 0; i < portMenu.getItemCount(); i++) {
+ if (portMenu.getItem(i) instanceof BoardPortJCheckBoxMenuItem)
+ items.add((BoardPortJCheckBoxMenuItem) portMenu.getItem(i));
+ }
+
+ String port = PreferencesData.get("serial.port");
+ String title;
+ if (port == null || port.isEmpty()) {
+ title = tr("Serial port not selected.");
+ } else {
+ title = I18n.format(tr("Serial port {0} not found."), port);
+ }
+ String question = tr("Retry the upload with another serial port?");
+ BoardPortJCheckBoxMenuItem result = (BoardPortJCheckBoxMenuItem) JOptionPane
+ .showInputDialog(this, title + "\n" + question, title,
+ JOptionPane.PLAIN_MESSAGE, null, items.toArray(), 0);
+ if (result == null)
+ return false;
+ result.doClick();
base.onBoardOrPortChange();
return true;
}
-
/**
* Called by Sketch → Export.
* Handles calling the export() function on sketch, and
@@ -2007,14 +2036,20 @@ synchronized public void handleExport(final boolean usingProgrammer) {
avoidMultipleOperations = true;
new Thread(timeoutUploadHandler).start();
- new Thread(usingProgrammer ? exportAppHandler : exportHandler).start();
+ new Thread(usingProgrammer ? uploadUsingProgrammerHandler : uploadHandler).start();
}
- // DAM: in Arduino, this is upload
- class DefaultExportHandler implements Runnable {
- public void run() {
+ class UploadHandler implements Runnable {
+ boolean usingProgrammer = false;
+
+ public void setUsingProgrammer(boolean usingProgrammer) {
+ this.usingProgrammer = usingProgrammer;
+ }
+ public void run() {
try {
+ uploading = true;
+
removeAllLineHighlights();
if (serialMonitor != null) {
serialMonitor.suspend();
@@ -2023,16 +2058,20 @@ public void run() {
serialPlotter.suspend();
}
- uploading = true;
-
- boolean success = sketchController.exportApplet(false);
+ boolean success = sketchController.exportApplet(usingProgrammer);
if (success) {
statusNotice(tr("Done uploading."));
}
} catch (SerialNotFoundException e) {
- if (portMenu.getItemCount() == 0) statusError(e);
- else if (serialPrompt()) run();
- else statusNotice(tr("Upload canceled."));
+ if (portMenu.getItemCount() == 0) {
+ statusError(tr("Serial port not selected."));
+ } else {
+ if (serialPrompt()) {
+ run();
+ } else {
+ statusNotice(tr("Upload canceled."));
+ }
+ }
} catch (PreferencesMapException e) {
statusError(I18n.format(
tr("Error while uploading: missing '{0}' configuration parameter"),
@@ -2059,9 +2098,18 @@ public void run() {
}
}
+ static public boolean isUploading() {
+ return uploading;
+ }
+
private void resumeOrCloseSerialMonitor() {
// Return the serial monitor window to its initial state
if (serialMonitor != null) {
+ try {
+ Thread.sleep(200);
+ } catch (InterruptedException e) {
+ // noop
+ }
BoardPort boardPort = BaseNoGui.getDiscoveryManager().find(PreferencesData.get("serial.port"));
long sleptFor = 0;
while (boardPort == null && sleptFor < MAX_TIME_AWAITING_FOR_RESUMING_SERIAL_MONITOR) {
@@ -2108,55 +2156,6 @@ private void resumeOrCloseSerialPlotter() {
}
}
- // DAM: in Arduino, this is upload (with verbose output)
- class DefaultExportAppHandler implements Runnable {
- public void run() {
-
- try {
- if (serialMonitor != null) {
- serialMonitor.suspend();
- }
- if (serialPlotter != null) {
- serialPlotter.suspend();
- }
-
- uploading = true;
-
- boolean success = sketchController.exportApplet(true);
- if (success) {
- statusNotice(tr("Done uploading."));
- }
- } catch (SerialNotFoundException e) {
- if (portMenu.getItemCount() == 0) statusError(e);
- else if (serialPrompt()) run();
- else statusNotice(tr("Upload canceled."));
- } catch (PreferencesMapException e) {
- statusError(I18n.format(
- tr("Error while uploading: missing '{0}' configuration parameter"),
- e.getMessage()));
- } catch (RunnerException e) {
- //statusError("Error during upload.");
- //e.printStackTrace();
- status.unprogress();
- statusError(e);
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- avoidMultipleOperations = false;
- populatePortMenu();
- }
- status.unprogress();
- uploading = false;
- //toolbar.clear();
- toolbar.deactivateExport();
-
- resumeOrCloseSerialMonitor();
- resumeOrCloseSerialPlotter();
-
- base.onBoardOrPortChange();
- }
- }
-
class TimeoutUploadHandler implements Runnable {
public void run() {
@@ -2219,6 +2218,7 @@ public void handleSerial() {
return;
}
+ base.addEditorFontResizeListeners(serialMonitor);
Base.setIcon(serialMonitor);
// If currently uploading, disable the monitor (it will be later
@@ -2388,6 +2388,8 @@ private void handleBurnBootloader() {
SwingUtilities.invokeLater(() -> statusError(tr("Error while burning bootloader.")));
// error message will already be visible
}
+ } catch (SerialNotFoundException e) {
+ SwingUtilities.invokeLater(() -> statusError(tr("Error while burning bootloader: please select a serial port.")));
} catch (PreferencesMapException e) {
SwingUtilities.invokeLater(() -> {
statusError(I18n.format(
@@ -2419,9 +2421,9 @@ private void handleBoardInfo() {
for (BoardPort port : ports) {
if (port.getAddress().equals(selectedPort)) {
label = port.getBoardName();
- vid = port.getVID();
- pid = port.getPID();
- iserial = port.getISerial();
+ vid = port.getPrefs().get("vid");
+ pid = port.getPrefs().get("pid");
+ iserial = port.getPrefs().get("iserial");
protocol = port.getProtocol();
found = true;
break;
@@ -2591,12 +2593,7 @@ private void statusEmpty() {
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
protected void onBoardOrPortChange() {
- Map boardPreferences = BaseNoGui.getBoardPreferences();
- if (boardPreferences != null)
- lineStatus.setBoardName(boardPreferences.get("name"));
- else
- lineStatus.setBoardName("-");
- lineStatus.setSerialPort(PreferencesData.get("serial.port"));
+ lineStatus.updateBoardAndPort();
lineStatus.repaint();
}
diff --git a/app/src/processing/app/EditorConsole.java b/app/src/processing/app/EditorConsole.java
index f656798f0cb..3942908a1da 100644
--- a/app/src/processing/app/EditorConsole.java
+++ b/app/src/processing/app/EditorConsole.java
@@ -27,6 +27,8 @@
import javax.swing.text.*;
import java.awt.*;
import java.io.PrintStream;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import static processing.app.Theme.scale;
@@ -37,20 +39,21 @@ public class EditorConsole extends JScrollPane {
private static ConsoleOutputStream out;
private static ConsoleOutputStream err;
+ private int startOfLine = 0;
+ private int insertPosition = 0;
- private static synchronized void init(SimpleAttributeSet outStyle, PrintStream outStream, SimpleAttributeSet errStyle, PrintStream errStream) {
- if (out != null) {
- return;
- }
+ // Regex for linesplitting, see insertString for comments.
+ private static final Pattern newLinePattern = Pattern.compile("([^\r\n]*)([\r\n]*\n)?(\r+)?");
- out = new ConsoleOutputStream(outStyle, outStream);
- System.setOut(new PrintStream(out, true));
+ public static synchronized void setCurrentEditorConsole(EditorConsole console) {
+ if (out == null) {
+ out = new ConsoleOutputStream(console.stdOutStyle, System.out);
+ System.setOut(new PrintStream(out, true));
- err = new ConsoleOutputStream(errStyle, errStream);
- System.setErr(new PrintStream(err, true));
- }
+ err = new ConsoleOutputStream(console.stdErrStyle, System.err);
+ System.setErr(new PrintStream(err, true));
+ }
- public static void setCurrentEditorConsole(EditorConsole console) {
out.setCurrentEditorConsole(console);
err.setCurrentEditorConsole(console);
}
@@ -61,7 +64,7 @@ public static void setCurrentEditorConsole(EditorConsole console) {
private SimpleAttributeSet stdOutStyle;
private SimpleAttributeSet stdErrStyle;
- public EditorConsole() {
+ public EditorConsole(Base base) {
document = new DefaultStyledDocument();
consoleTextPane = new JTextPane(document);
@@ -109,24 +112,64 @@ public EditorConsole() {
setPreferredSize(new Dimension(100, (height * lines)));
setMinimumSize(new Dimension(100, (height * lines)));
- EditorConsole.init(stdOutStyle, System.out, stdErrStyle, System.err);
+ // Add font size adjustment listeners.
+ if (base != null)
+ base.addEditorFontResizeListeners(consoleTextPane);
}
public void applyPreferences() {
+
+ // Update the console text pane font from the preferences.
Font consoleFont = Theme.getFont("console.font");
Font editorFont = PreferencesData.getFont("editor.font");
Font actualFont = new Font(consoleFont.getName(), consoleFont.getStyle(), scale(editorFont.getSize()));
+ AttributeSet stdOutStyleOld = stdOutStyle.copyAttributes();
+ AttributeSet stdErrStyleOld = stdErrStyle.copyAttributes();
StyleConstants.setFontSize(stdOutStyle, actualFont.getSize());
StyleConstants.setFontSize(stdErrStyle, actualFont.getSize());
- out.setAttibutes(stdOutStyle);
- err.setAttibutes(stdErrStyle);
+ // Re-insert console text with the new preferences if there were changes.
+ // This assumes that the document has single-child paragraphs (default).
+ if (!stdOutStyle.isEqual(stdOutStyleOld) || !stdErrStyle.isEqual(stdOutStyleOld)) {
+ if (out != null)
+ out.setAttibutes(stdOutStyle);
+ if (err != null)
+ err.setAttibutes(stdErrStyle);
+
+ int start;
+ for (int end = document.getLength() - 1; end >= 0; end = start - 1) {
+ Element elem = document.getParagraphElement(end);
+ start = elem.getStartOffset();
+ AttributeSet attrs = elem.getElement(0).getAttributes();
+ AttributeSet newAttrs;
+ if (attrs.isEqual(stdErrStyleOld)) {
+ newAttrs = stdErrStyle;
+ } else if (attrs.isEqual(stdOutStyleOld)) {
+ newAttrs = stdOutStyle;
+ } else {
+ continue;
+ }
+ try {
+ String text = document.getText(start, end - start);
+ document.remove(start, end - start);
+ document.insertString(start, text, newAttrs);
+ } catch (BadLocationException e) {
+ // Should only happen when text is async removed (through clear()).
+ // Accept this case, but throw an error when text could mess up.
+ if (document.getLength() != 0) {
+ throw new Error(e);
+ }
+ }
+ }
+ }
}
public void clear() {
try {
document.remove(0, document.getLength());
+ startOfLine = 0;
+ insertPosition = 0;
} catch (BadLocationException e) {
// ignore the error otherwise this will cause an infinite loop
// maybe not a good idea in the long run?
@@ -142,14 +185,53 @@ public boolean isEmpty() {
return document.getLength() == 0;
}
- public void insertString(String line, SimpleAttributeSet attributes) throws BadLocationException {
- line = line.replace("\r\n", "\n").replace("\r", "\n");
- int offset = document.getLength();
- document.insertString(offset, line, attributes);
+ public void insertString(String str, SimpleAttributeSet attributes) throws BadLocationException {
+ // Separate the string into content, newlines and lone carriage
+ // returns.
+ //
+ // Doing so allows lone CRs to move the insertPosition back to the
+ // start of the line to allow overwriting the most recent line (e.g.
+ // for a progress bar). Any CR or NL that are immediately followed
+ // by another NL are bunched together for efficiency, since these
+ // can just be inserted into the document directly and still be
+ // correct.
+ //
+ // The regex is written so it will necessarily match any string
+ // completely if applied repeatedly. This is important because any
+ // part not matched would be silently dropped.
+ Matcher m = newLinePattern.matcher(str);
+
+ while (m.find()) {
+ String content = m.group(1);
+ String newlines = m.group(2);
+ String crs = m.group(3);
+
+ // Replace (or append if at end of the document) the content first
+ int replaceLength = Math.min(content.length(), document.getLength() - insertPosition);
+ document.replace(insertPosition, replaceLength, content, attributes);
+ insertPosition += content.length();
+
+ // Then insert any newlines, but always at the end of the document
+ // e.g. if insertPosition is halfway a line, do not delete
+ // anything, just add the newline(s) at the end).
+ if (newlines != null) {
+ document.insertString(document.getLength(), newlines, attributes);
+ insertPosition = document.getLength();
+ startOfLine = insertPosition;
+ }
+
+ // Then, for any CRs not followed by newlines, move insertPosition
+ // to the start of the line. Note that if a newline follows before
+ // any content in the next call to insertString, it will be added
+ // at the end of the document anyway, as expected.
+ if (crs != null) {
+ insertPosition = startOfLine;
+ }
+ }
}
public String getText() {
- return consoleTextPane.getText().trim();
+ return consoleTextPane.getText();
}
}
diff --git a/app/src/processing/app/EditorHeader.java b/app/src/processing/app/EditorHeader.java
index 25c09a8dfaa..c5695cf8abd 100644
--- a/app/src/processing/app/EditorHeader.java
+++ b/app/src/processing/app/EditorHeader.java
@@ -73,6 +73,7 @@ public class EditorHeader extends JComponent {
static final int PIECE_WIDTH = scale(4);
static final int PIECE_HEIGHT = scale(33);
+ static final int TAB_HEIGHT = scale(27);
// value for the size bars, buttons, etc
// TODO: Should be a Theme value?
@@ -270,8 +271,8 @@ public void paintComponent(Graphics screen) {
int textLeft = contentLeft + (pieceWidth - textWidth) / 2;
g.setColor(textColor[state]);
- int baseline = (sizeH + fontAscent) / 2;
- //g.drawString(sketch.code[i].name, textLeft, baseline);
+ int tabMarginTop = sizeH - TAB_HEIGHT;
+ int baseline = tabMarginTop + ((TAB_HEIGHT + fontAscent) / 2) ;
g.drawString(text, textLeft, baseline);
g.drawImage(pieces[state][RIGHT], x, 0, null);
diff --git a/app/src/processing/app/EditorLineStatus.java b/app/src/processing/app/EditorLineStatus.java
index 7d4e80b6577..f768b597c28 100644
--- a/app/src/processing/app/EditorLineStatus.java
+++ b/app/src/processing/app/EditorLineStatus.java
@@ -51,8 +51,7 @@ public class EditorLineStatus extends JComponent {
String text = "";
String name = "";
- String serialport = "";
- String serialnumber = "";
+ String port = "";
public EditorLineStatus() {
background = Theme.getColor("linestatus.bgcolor");
@@ -92,13 +91,8 @@ public void set(int newStart, int newStop) {
public void paintComponent(Graphics graphics) {
Graphics2D g = Theme.setupGraphics2D(graphics);
- if (name.isEmpty() && serialport.isEmpty()) {
- PreferencesMap boardPreferences = BaseNoGui.getBoardPreferences();
- if (boardPreferences != null)
- setBoardName(boardPreferences.get("name"));
- else
- setBoardName("-");
- setSerialPort(PreferencesData.get("serial.port"));
+ if (name.isEmpty() && port.isEmpty()) {
+ updateBoardAndPort();
}
g.setColor(background);
Dimension size = getSize();
@@ -110,11 +104,17 @@ public void paintComponent(Graphics graphics) {
g.drawString(text, scale(6), baseline);
g.setColor(messageForeground);
- String tmp = I18n.format(tr("{0} on {1}"), name, serialport);
-
- Rectangle2D bounds = g.getFontMetrics().getStringBounds(tmp, null);
-
- g.drawString(tmp, size.width - (int) bounds.getWidth() - RESIZE_IMAGE_SIZE,
+
+ String statusText;
+ if (port != null && !port.isEmpty()) {
+ statusText = I18n.format(tr("{0} on {1}"), name, port);
+ } else {
+ statusText = name;
+ }
+
+ Rectangle2D bounds = g.getFontMetrics().getStringBounds(statusText, null);
+
+ g.drawString(statusText, size.width - (int) bounds.getWidth() - RESIZE_IMAGE_SIZE,
baseline);
if (OSUtils.isMacOS()) {
@@ -126,12 +126,8 @@ public void setBoardName(String name) {
this.name = name;
}
- public void setSerialPort(String serialport) {
- this.serialport = serialport;
- }
-
- public void setSerialNumber(String serialnumber) {
- this.serialnumber = serialnumber;
+ public void setPort(String port) {
+ this.port = port;
}
public Dimension getPreferredSize() {
@@ -145,4 +141,13 @@ public Dimension getMinimumSize() {
public Dimension getMaximumSize() {
return scale(new Dimension(3000, height));
}
+
+ public void updateBoardAndPort() {
+ PreferencesMap boardPreferences = BaseNoGui.getBoardPreferences();
+ if (boardPreferences != null)
+ setBoardName(boardPreferences.get("name"));
+ else
+ setBoardName("-");
+ setPort(PreferencesData.get("serial.port"));
+ }
}
diff --git a/app/src/processing/app/EditorTab.java b/app/src/processing/app/EditorTab.java
index 8c5fb86a7d8..59bfe3c77d7 100644
--- a/app/src/processing/app/EditorTab.java
+++ b/app/src/processing/app/EditorTab.java
@@ -30,9 +30,8 @@
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
-import java.awt.event.MouseWheelListener;
-import java.awt.event.MouseWheelEvent;
-
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
import java.io.IOException;
import javax.swing.Action;
@@ -68,7 +67,7 @@
/**
* Single tab, editing a single file, in the main window.
*/
-public class EditorTab extends JPanel implements SketchFile.TextStorage, MouseWheelListener {
+public class EditorTab extends JPanel implements SketchFile.TextStorage {
protected Editor editor;
protected SketchTextArea textarea;
protected RTextScrollPane scrollPane;
@@ -110,7 +109,7 @@ public EditorTab(Editor editor, SketchFile file, String contents)
file.setStorage(this);
applyPreferences();
add(scrollPane, BorderLayout.CENTER);
- textarea.addMouseWheelListener(this);
+ editor.base.addEditorFontResizeMouseWheelListener(textarea);
}
private RSyntaxDocument createDocument(String contents) {
@@ -177,23 +176,20 @@ private SketchTextArea createTextArea(RSyntaxDocument document)
editor.lineStatus.set(lineStart, lineEnd);
});
+ textArea.addFocusListener(new FocusListener() {
+ public void focusGained(FocusEvent e) {
+ Element root = textArea.getDocument().getDefaultRootElement();
+ int lineStart = root.getElementIndex(textArea.getCaret().getMark());
+ int lineEnd = root.getElementIndex(textArea.getCaret().getDot());
+ editor.lineStatus.set(lineStart, lineEnd);
+ };
+ public void focusLost(FocusEvent e) {};
+ });
ToolTipManager.sharedInstance().registerComponent(textArea);
configurePopupMenu(textArea);
return textArea;
}
-
- public void mouseWheelMoved(MouseWheelEvent e) {
- if (e.isControlDown()) {
- if (e.getWheelRotation() < 0) {
- editor.base.handleFontSizeChange(1);
- } else {
- editor.base.handleFontSizeChange(-1);
- }
- } else {
- e.getComponent().getParent().dispatchEvent(e);
- }
- }
private void configurePopupMenu(final SketchTextArea textarea){
@@ -251,7 +247,7 @@ public void actionPerformed(ActionEvent e) {
menu.add(item);
final JMenuItem referenceItem = new JMenuItem(tr("Find in Reference"));
- referenceItem.addActionListener(editor::handleFindReference);
+ referenceItem.addActionListener(ev -> editor.handleFindReference(getCurrentKeyword()));
menu.add(referenceItem);
final JMenuItem openURLItem = new JMenuItem(tr("Open URL"));
@@ -446,6 +442,9 @@ public void setText(String what) {
} finally {
caret.setUpdatePolicy(policy);
}
+ // A trick to force textarea to recalculate the bracket matching rectangle.
+ // In the worst case scenario, this should be ineffective.
+ textarea.setLineWrap(textarea.getLineWrap());
}
/**
diff --git a/app/src/processing/app/EditorToolbar.java b/app/src/processing/app/EditorToolbar.java
index 00da0f37ac7..a2a9b804e70 100644
--- a/app/src/processing/app/EditorToolbar.java
+++ b/app/src/processing/app/EditorToolbar.java
@@ -23,11 +23,25 @@
package processing.app;
+import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.event.MouseInputListener;
+
+import com.thizzer.jtouchbar.JTouchBar;
+import com.thizzer.jtouchbar.item.TouchBarItem;
+import com.thizzer.jtouchbar.item.view.TouchBarButton;
+
+import cc.arduino.contributions.VersionComparator;
+import processing.app.helpers.OSUtils;
+
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
import static processing.app.I18n.tr;
import static processing.app.Theme.scale;
@@ -48,7 +62,7 @@ public class EditorToolbar extends JComponent implements MouseInputListener, Key
* Titles for each button when the shift key is pressed.
*/
private static final String[] titleShift = {
- tr("Verify"), tr("Upload Using Programmer"), tr("New"), tr("Open"), tr("Save As..."), tr("Serial Monitor")
+ tr("Verify"), tr("Upload Using Programmer"), tr("New"), tr("Open"), tr("Save As..."), tr("Serial Plotter")
};
private static final int BUTTON_COUNT = title.length;
@@ -92,10 +106,13 @@ public class EditorToolbar extends JComponent implements MouseInputListener, Key
private final Color bgcolor;
private static Image[][] buttonImages;
+ private static com.thizzer.jtouchbar.common.Image[][] touchBarImages;
private int currentRollover;
private JPopupMenu popup;
private final JMenu menu;
+ private JTouchBar touchBar;
+ private TouchBarButton[] touchBarButtons;
private int buttonCount;
private int[] state = new int[BUTTON_COUNT];
@@ -133,10 +150,60 @@ public EditorToolbar(Editor editor, JMenu menu) {
statusFont = Theme.getFont("buttons.status.font");
statusColor = Theme.getColor("buttons.status.color");
+ if (OSUtils.isMacOS() && VersionComparator.greaterThanOrEqual(OSUtils.version(), "10.12")) {
+ editor.addWindowListener(new WindowAdapter() {
+ public void windowActivated(WindowEvent e) {
+ if (touchBar == null) {
+ buildTouchBar();
+
+ touchBar.show(editor);
+ }
+ }
+ });
+ }
+
addMouseListener(this);
addMouseMotionListener(this);
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(this);
}
+
+ private void buildTouchBar() {
+ if (touchBarImages == null) {
+ loadTouchBarImages();
+ }
+
+ touchBar = new JTouchBar();
+ touchBarButtons = new TouchBarButton[BUTTON_COUNT];
+ touchBar.setCustomizationIdentifier("Arduino");
+
+ for (int i = 0; i < BUTTON_COUNT; i++) {
+ final int selection = i;
+
+ // add spacers before NEW and SERIAL buttons
+ if (i == NEW) {
+ touchBar.addItem(new TouchBarItem(TouchBarItem.NSTouchBarItemIdentifierFixedSpaceSmall));
+ } else if (i == SERIAL) {
+ touchBar.addItem(new TouchBarItem(TouchBarItem.NSTouchBarItemIdentifierFlexibleSpace));
+ }
+
+ touchBarButtons[i] = new TouchBarButton();
+ touchBarButtons[i].setImage(touchBarImages[i][ROLLOVER]);
+ touchBarButtons[i].setAction(event -> {
+ // Run event handler later to prevent hanging if a dialog needs to be open
+ EventQueue.invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ handleSelectionPressed(selection);
+ }
+ });
+ });
+
+ TouchBarItem touchBarItem = new TouchBarItem(title[i], touchBarButtons[i], true);
+ touchBarItem.setCustomizationLabel(title[i]);
+
+ touchBar.addItem(touchBarItem);
+ }
+ }
private void loadButtons() {
Image allButtons = Theme.getThemeImage("buttons", this,
@@ -157,6 +224,36 @@ private void loadButtons() {
}
}
}
+
+ private void loadTouchBarImages() {
+ Image allButtonsRetina = Theme.getThemeImage("buttons", this,
+ BUTTON_IMAGE_SIZE * BUTTON_COUNT * 2,
+ BUTTON_IMAGE_SIZE * 3 * 2);
+ touchBarImages = new com.thizzer.jtouchbar.common.Image[BUTTON_COUNT][3];
+
+ for (int i = 0; i < BUTTON_COUNT; i++) {
+ for (int state = 0; state < 3; state++) {
+ BufferedImage image = new BufferedImage(BUTTON_WIDTH * 2, BUTTON_HEIGHT * 2,
+ BufferedImage.TYPE_INT_ARGB);
+ Graphics g = image.getGraphics();
+
+ int offset = (BUTTON_IMAGE_SIZE * 2 - BUTTON_WIDTH * 2) / 2;
+ g.drawImage(allButtonsRetina, -(i * BUTTON_IMAGE_SIZE * 2) - offset,
+ (-2 + state) * BUTTON_IMAGE_SIZE * 2, null);
+
+ // convert the image to a PNG to display on the touch bar
+ ByteArrayOutputStream pngStream = new ByteArrayOutputStream();
+
+ try {
+ ImageIO.write(image, "PNG", pngStream);
+
+ touchBarImages[i][state] = new com.thizzer.jtouchbar.common.Image(pngStream.toByteArray());
+ } catch (IOException e) {
+ // ignore errors
+ }
+ }
+ }
+ }
@Override
public void paintComponent(Graphics screen) {
@@ -305,6 +402,15 @@ private void setState(int slot, int newState, boolean updateAfter) {
if (updateAfter) {
repaint();
}
+
+ if (touchBarButtons != null) {
+ if (newState == INACTIVE) {
+ // use ROLLOVER state when INACTIVE
+ newState = ROLLOVER;
+ }
+
+ touchBarButtons[slot].setImage(touchBarImages[slot][newState]);
+ }
}
@@ -339,6 +445,20 @@ public void mousePressed(MouseEvent e) {
if (sel == -1) return;
currentRollover = -1;
+ handleSelectionPressed(sel, e.isShiftDown(), x, y);
+ }
+
+ public void mouseClicked(MouseEvent e) {
+ }
+
+ public void mouseReleased(MouseEvent e) {
+ }
+
+ private void handleSelectionPressed(int sel) {
+ handleSelectionPressed(sel, false, 0, 0);
+ }
+
+ private void handleSelectionPressed(int sel, boolean isShiftDown, int x, int y) {
switch (sel) {
case RUN:
if (!editor.avoidMultipleOperations) {
@@ -347,10 +467,10 @@ public void mousePressed(MouseEvent e) {
}
break;
-// case STOP:
-// editor.handleStop();
-// break;
-//
+// case STOP:
+// editor.handleStop();
+// break;
+//
case OPEN:
popup = menu.getPopupMenu();
popup.show(EditorToolbar.this, x, y);
@@ -365,7 +485,7 @@ public void mousePressed(MouseEvent e) {
break;
case SAVE:
- if (e.isShiftDown()) {
+ if (isShiftDown) {
editor.handleSaveAs();
} else {
editor.handleSave(false);
@@ -375,12 +495,16 @@ public void mousePressed(MouseEvent e) {
case EXPORT:
// launch a timeout timer which can reenable to upload button functionality an
if (!editor.avoidMultipleOperations) {
- editor.handleExport(e.isShiftDown());
+ editor.handleExport(isShiftDown);
}
break;
case SERIAL:
- editor.handleSerial();
+ if (isShiftDown) {
+ editor.handlePlotter();
+ } else {
+ editor.handleSerial();
+ }
break;
default:
@@ -388,15 +512,6 @@ public void mousePressed(MouseEvent e) {
}
}
-
- public void mouseClicked(MouseEvent e) {
- }
-
-
- public void mouseReleased(MouseEvent e) {
- }
-
-
/**
* Set a particular button to be active.
*/
diff --git a/app/src/processing/app/SerialMonitor.java b/app/src/processing/app/SerialMonitor.java
index 45adbd7d54b..b2656ca653d 100644
--- a/app/src/processing/app/SerialMonitor.java
+++ b/app/src/processing/app/SerialMonitor.java
@@ -21,9 +21,10 @@
import cc.arduino.packages.BoardPort;
import processing.app.legacy.PApplet;
-import java.awt.*;
+import java.awt.Color;
import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
import static processing.app.I18n.tr;
@@ -33,17 +34,21 @@ public class SerialMonitor extends AbstractTextMonitor {
private Serial serial;
private int serialRate;
+ private static final int COMMAND_HISTORY_SIZE = 100;
+ private final CommandHistory commandHistory =
+ new CommandHistory(COMMAND_HISTORY_SIZE);
+
public SerialMonitor(BoardPort port) {
super(port);
serialRate = PreferencesData.getInteger("serial.debug_rate");
serialRates.setSelectedItem(serialRate + " " + tr("baud"));
- onSerialRateChange(new ActionListener() {
- public void actionPerformed(ActionEvent event) {
- String wholeString = (String) serialRates.getSelectedItem();
- String rateString = wholeString.substring(0, wholeString.indexOf(' '));
- serialRate = Integer.parseInt(rateString);
- PreferencesData.set("serial.debug_rate", rateString);
+ onSerialRateChange((ActionEvent event) -> {
+ String wholeString = (String) serialRates.getSelectedItem();
+ String rateString = wholeString.substring(0, wholeString.indexOf(' '));
+ serialRate = Integer.parseInt(rateString);
+ PreferencesData.set("serial.debug_rate", rateString);
+ if (serial != null) {
try {
close();
Thread.sleep(100); // Wait for serial port to properly close
@@ -56,16 +61,41 @@ public void actionPerformed(ActionEvent event) {
}
});
- onSendCommand(new ActionListener() {
- public void actionPerformed(ActionEvent e) {
- send(textField.getText());
- textField.setText("");
- }
+ onSendCommand((ActionEvent event) -> {
+ String command = textField.getText();
+ send(command);
+ commandHistory.addCommand(command);
+ textField.setText("");
});
-
- onClearCommand(new ActionListener() {
- public void actionPerformed(ActionEvent e) {
- textArea.setText("");
+
+ onClearCommand((ActionEvent event) -> textArea.setText(""));
+
+ // Add key listener to UP, DOWN, ESC keys for command history traversal.
+ textField.addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyPressed(KeyEvent e) {
+ switch (e.getKeyCode()) {
+
+ // Select previous command.
+ case KeyEvent.VK_UP:
+ if (commandHistory.hasPreviousCommand()) {
+ textField.setText(
+ commandHistory.getPreviousCommand(textField.getText()));
+ }
+ break;
+
+ // Select next command.
+ case KeyEvent.VK_DOWN:
+ if (commandHistory.hasNextCommand()) {
+ textField.setText(commandHistory.getNextCommand());
+ }
+ break;
+
+ // Reset history location, restoring the last unexecuted command.
+ case KeyEvent.VK_ESCAPE:
+ textField.setText(commandHistory.resetHistoryLocation());
+ break;
+ }
}
});
}
@@ -93,6 +123,7 @@ private void send(String s) {
}
}
+ @Override
public void open() throws Exception {
super.open();
@@ -106,13 +137,13 @@ protected void message(char buff[], int n) {
};
}
+ @Override
public void close() throws Exception {
super.close();
if (serial != null) {
int[] location = getPlacement();
String locationStr = PApplet.join(PApplet.str(location), ",");
PreferencesData.set("last.serial.location", locationStr);
- textArea.setText("");
serial.dispose();
serial = null;
}
diff --git a/app/src/processing/app/SerialPlotter.java b/app/src/processing/app/SerialPlotter.java
index 363753749fe..81b21cd7860 100644
--- a/app/src/processing/app/SerialPlotter.java
+++ b/app/src/processing/app/SerialPlotter.java
@@ -26,8 +26,12 @@
import java.util.ArrayList;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
+import javax.swing.text.DefaultEditorKit;
import java.awt.*;
+import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
@@ -40,12 +44,18 @@ public class SerialPlotter extends AbstractMonitor {
private Serial serial;
private int serialRate, xCount;
+ private JLabel noLineEndingAlert;
+ private JTextField textField;
+ private JButton sendButton;
+ private JComboBox lineEndings;
+
private ArrayList graphs;
private final static int BUFFER_CAPACITY = 500;
private static class Graph {
public CircularBuffer buffer;
private Color color;
+ public String label;
public Graph(int id) {
buffer = new CircularBuffer(BUFFER_CAPACITY);
@@ -185,12 +195,24 @@ public void paintComponent(Graphics g1) {
g.setTransform(AffineTransform.getTranslateInstance(xOffset, 0));
float xstep = (float) (bounds.width - xOffset - xPadding) / (float) BUFFER_CAPACITY;
- int legendLength = graphs.size() * 10 + (graphs.size() - 1) * 3;
+ // draw legend
+ int legendXOffset = 0;
for(int i = 0; i < graphs.size(); ++i) {
graphs.get(i).paint(g, xstep, minY, maxY, rangeY, bounds.height);
- if(graphs.size() > 1) {
- g.fillRect(bounds.width - (xOffset + legendLength + 10) + i * 13, 10, 10, 10);
+ if(graphs.size() > 1) {
+ //draw legend rectangle
+ g.fillRect(10 + legendXOffset, 10, 10, 10);
+ legendXOffset += 13;
+ //draw label
+ g.setColor(boundsColor);
+ String s = graphs.get(i).label;
+ if(s != null && s.length() > 0) {
+ Rectangle2D fBounds = fm.getStringBounds(s, g);
+ int sWidth = (int)fBounds.getWidth();
+ g.drawString(s, 10 + legendXOffset, 10 + (int)fBounds.getHeight() /2);
+ legendXOffset += sWidth + 3;
+ }
}
}
}
@@ -220,12 +242,14 @@ public SerialPlotter(BoardPort port) {
String rateString = wholeString.substring(0, wholeString.indexOf(' '));
serialRate = Integer.parseInt(rateString);
PreferencesData.set("serial.debug_rate", rateString);
- try {
- close();
- Thread.sleep(100); // Wait for serial port to properly close
- open();
- } catch (Exception e) {
- // ignore
+ if (serial != null) {
+ try {
+ close();
+ Thread.sleep(100); // Wait for serial port to properly close
+ open();
+ } catch (Exception e) {
+ // ignore
+ }
}
});
@@ -254,10 +278,111 @@ protected void onCreateWindow(Container mainPane) {
pane.add(serialRates);
mainPane.add(pane, BorderLayout.SOUTH);
+
+ textField = new JTextField(40);
+ // textField is selected every time the window is focused
+ addWindowFocusListener(new WindowAdapter() {
+ @Override
+ public void windowGainedFocus(WindowEvent e) {
+ textField.requestFocusInWindow();
+ }
+ });
+
+ // Add cut/copy/paste contextual menu to the text input field.
+ JPopupMenu menu = new JPopupMenu();
+
+ Action cut = new DefaultEditorKit.CutAction();
+ cut.putValue(Action.NAME, tr("Cut"));
+ menu.add(cut);
+
+ Action copy = new DefaultEditorKit.CopyAction();
+ copy.putValue(Action.NAME, tr("Copy"));
+ menu.add(copy);
+
+ Action paste = new DefaultEditorKit.PasteAction();
+ paste.putValue(Action.NAME, tr("Paste"));
+ menu.add(paste);
+
+ textField.setComponentPopupMenu(menu);
+
+ sendButton = new JButton(tr("Send"));
+
+ JPanel lowerPane = new JPanel();
+ lowerPane.setLayout(new BoxLayout(lowerPane, BoxLayout.X_AXIS));
+ lowerPane.setBorder(new EmptyBorder(4, 4, 4, 4));
+
+ noLineEndingAlert = new JLabel(I18n.format(tr("You've pressed {0} but nothing was sent. Should you select a line ending?"), tr("Send")));
+ noLineEndingAlert.setToolTipText(noLineEndingAlert.getText());
+ noLineEndingAlert.setForeground(pane.getBackground());
+ Dimension minimumSize = new Dimension(noLineEndingAlert.getMinimumSize());
+ minimumSize.setSize(minimumSize.getWidth() / 3, minimumSize.getHeight());
+ noLineEndingAlert.setMinimumSize(minimumSize);
+
+
+ lineEndings = new JComboBox(new String[]{tr("No line ending"), tr("Newline"), tr("Carriage return"), tr("Both NL & CR")});
+ lineEndings.addActionListener((ActionEvent event) -> {
+ PreferencesData.setInteger("serial.line_ending", lineEndings.getSelectedIndex());
+ noLineEndingAlert.setForeground(pane.getBackground());
+ });
+ lineEndings.setMaximumSize(lineEndings.getMinimumSize());
+
+ lowerPane.add(textField);
+ lowerPane.add(Box.createRigidArea(new Dimension(4, 0)));
+ lowerPane.add(sendButton);
+
+ pane.add(lowerPane);
+ pane.add(noLineEndingAlert);
+ pane.add(Box.createRigidArea(new Dimension(8, 0)));
+ pane.add(lineEndings);
+
+ applyPreferences();
+
+ onSendCommand((ActionEvent event) -> {
+ send(textField.getText());
+ textField.setText("");
+ });
+
+ }
+
+ private void send(String string) {
+ String s = string;
+ if (serial != null) {
+ switch (lineEndings.getSelectedIndex()) {
+ case 1:
+ s += "\n";
+ break;
+ case 2:
+ s += "\r";
+ break;
+ case 3:
+ s += "\r\n";
+ break;
+ default:
+ break;
+ }
+ if ("".equals(s) && lineEndings.getSelectedIndex() == 0 && !PreferencesData.has("runtime.line.ending.alert.notified")) {
+ noLineEndingAlert.setForeground(Color.RED);
+ PreferencesData.set("runtime.line.ending.alert.notified", "true");
+ }
+ serial.write(s);
+ }
+ }
+
+ public void onSendCommand(ActionListener listener) {
+ textField.addActionListener(listener);
+ sendButton.addActionListener(listener);
+ }
+
+ public void applyPreferences() {
+ // Apply line endings.
+ if (PreferencesData.get("serial.line_ending") != null) {
+ lineEndings.setSelectedIndex(PreferencesData.getInteger("serial.line_ending"));
+ }
}
protected void onEnableWindow(boolean enable) {
- serialRates.setEnabled(enable);
+ textField.setEnabled(enable);
+ sendButton.setEnabled(enable);
}
private void onSerialRateChange(ActionListener listener) {
@@ -276,23 +401,70 @@ public void message(final String s) {
messageBuffer.delete(0, linebreak + 1);
line = line.trim();
+ if (line.length() == 0) {
+ // the line only contained trimmable characters
+ continue;
+ }
String[] parts = line.split("[, \t]+");
if(parts.length == 0) {
continue;
}
int validParts = 0;
+ int validLabels = 0;
for(int i = 0; i < parts.length; ++i) {
- try {
- double value = Double.valueOf(parts[i]);
+ Double value = null;
+ String label = null;
+
+ // column formated name value pair
+ if(parts[i].contains(":")) {
+ // get label
+ String[] subString = parts[i].split("[:]+");
+
+ if(subString.length > 0) {
+ int labelLength = subString[0].length();
+
+ if(labelLength > 32) {
+ labelLength = 32;
+ }
+ label = subString[0].substring(0, labelLength);
+ } else {
+ label = "";
+ }
+
+ if(subString.length > 1) {
+ parts[i] = subString[1];
+ } else {
+ parts[i] = "";
+ }
+ }
+
+ try {
+ value = Double.valueOf(parts[i]);
+ } catch (NumberFormatException e) {
+ // ignored
+ }
+ //CSV header
+ if(label == null && value == null) {
+ label = parts[i];
+ }
+
+ if(value != null) {
if(validParts >= graphs.size()) {
graphs.add(new Graph(validParts));
}
graphs.get(validParts).buffer.add(value);
validParts++;
- } catch (NumberFormatException e) {
- // ignore
}
+ if(label != null) {
+ if(validLabels >= graphs.size()) {
+ graphs.add(new Graph(validLabels));
+ }
+ graphs.get(validLabels).label = label;
+ validLabels++;
+ }
+ if(validParts > validLabels) validLabels = validParts;
+ else if(validLabels > validParts) validParts = validLabels;
}
}
diff --git a/app/src/processing/app/SketchController.java b/app/src/processing/app/SketchController.java
index 392422c5923..ce9e468cc68 100644
--- a/app/src/processing/app/SketchController.java
+++ b/app/src/processing/app/SketchController.java
@@ -674,7 +674,7 @@ private File saveSketchInTempFolder() throws IOException {
FileUtils.copy(sketch.getFolder(), tempFolder);
for (SketchFile file : Stream.of(sketch.getFiles()).filter(SketchFile::isModified).collect(Collectors.toList())) {
- Files.write(Paths.get(tempFolder.getAbsolutePath(), file.getFileName()), file.getProgram().getBytes());
+ Files.write(Paths.get(tempFolder.getAbsolutePath(), file.getFileName()), file.getProgram().getBytes("UTF-8"));
}
return Paths.get(tempFolder.getAbsolutePath(), sketch.getPrimaryFile().getFileName()).toFile();
@@ -709,10 +709,6 @@ private boolean upload(String suggestedClassName, boolean usingProgrammer) throw
UploaderUtils uploaderInstance = new UploaderUtils();
Uploader uploader = uploaderInstance.getUploaderByPreferences(false);
- if (uploader == null) {
- editor.statusError(tr("Please select a Port before Upload"));
- return false;
- }
EditorConsole.setCurrentEditorConsole(editor.console);
diff --git a/app/src/processing/app/UpdateCheck.java b/app/src/processing/app/UpdateCheck.java
index 39c555069a1..4c736e60413 100644
--- a/app/src/processing/app/UpdateCheck.java
+++ b/app/src/processing/app/UpdateCheck.java
@@ -51,7 +51,7 @@
*/
public class UpdateCheck implements Runnable {
Base base;
- String downloadURL = tr("http://www.arduino.cc/latest.txt");
+ String downloadURL = "https://www.arduino.cc/latest.txt";
static final long ONE_DAY = 24 * 60 * 60 * 1000;
@@ -66,14 +66,14 @@ public UpdateCheck(Base base) {
public void run() {
//System.out.println("checking for updates...");
- // generate a random id in case none exists yet
- Random r = new Random();
- long id = r.nextLong();
-
+ long id;
String idString = PreferencesData.get("update.id");
if (idString != null) {
id = Long.parseLong(idString);
} else {
+ // generate a random id in case none exists yet
+ Random r = new Random();
+ id = r.nextLong();
PreferencesData.set("update.id", String.valueOf(id));
}
@@ -116,7 +116,7 @@ public void run() {
options,
options[0]);
if (result == JOptionPane.YES_OPTION) {
- Base.openURL(tr("http://www.arduino.cc/en/Main/Software"));
+ Base.openURL("https://www.arduino.cc/en/software");
}
}
}
diff --git a/app/src/processing/app/macosx/ThinkDifferent.java b/app/src/processing/app/macosx/ThinkDifferent.java
index e946bdc0fd7..590196ace95 100644
--- a/app/src/processing/app/macosx/ThinkDifferent.java
+++ b/app/src/processing/app/macosx/ThinkDifferent.java
@@ -23,6 +23,8 @@
package processing.app.macosx;
import com.apple.eawt.*;
+import com.apple.eawt.AppEvent.AppReOpenedEvent;
+
import processing.app.Base;
import processing.app.Editor;
@@ -45,6 +47,20 @@ public class ThinkDifferent {
static public void init() {
Application application = Application.getApplication();
+
+ application.addAppEventListener(new AppReOpenedListener() {
+ @Override
+ public void appReOpened(AppReOpenedEvent aroe) {
+ try {
+ if (Base.INSTANCE.getEditors().size() == 0) {
+ Base.INSTANCE.handleNew();
+ }
+ } catch (Exception e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ });
application.setAboutHandler(new AboutHandler() {
@Override
public void handleAbout(AppEvent.AboutEvent aboutEvent) {
diff --git a/app/src/processing/app/syntax/SketchTextArea.java b/app/src/processing/app/syntax/SketchTextArea.java
index ce74a3f1f8f..ba10bfc7af3 100644
--- a/app/src/processing/app/syntax/SketchTextArea.java
+++ b/app/src/processing/app/syntax/SketchTextArea.java
@@ -89,14 +89,11 @@ public SketchTextArea(RSyntaxDocument document, PdeKeywords pdeKeywords) throws
public void setKeywords(PdeKeywords keywords) {
pdeKeywords = keywords;
- setLinkGenerator(new DocLinkGenerator(pdeKeywords));
}
private void installFeatures() throws IOException {
setTheme(PreferencesData.get("editor.syntax_theme", "default"));
- setLinkGenerator(new DocLinkGenerator(pdeKeywords));
-
setSyntaxEditingStyle(SYNTAX_STYLE_CPLUSPLUS);
}
@@ -175,48 +172,6 @@ public void getTextLine(int line, Segment segment) {
}
}
- private static class DocLinkGenerator implements LinkGenerator {
-
- private final PdeKeywords pdeKeywords;
-
- public DocLinkGenerator(PdeKeywords pdeKeywords) {
- this.pdeKeywords = pdeKeywords;
- }
-
- @Override
- public LinkGeneratorResult isLinkAtOffset(RSyntaxTextArea textArea, final int offs) {
- Token token = textArea.modelToToken(offs);
- if (token == null) {
- return null;
- }
-
- String reference = pdeKeywords.getReference(token.getLexeme());
-
- if (reference != null || (token.getType() == TokenTypes.DATA_TYPE || token.getType() == TokenTypes.VARIABLE || token.getType() == TokenTypes.FUNCTION)) {
-
- return new LinkGeneratorResult() {
-
- @Override
- public int getSourceOffset() {
- return offs;
- }
-
- @Override
- public HyperlinkEvent execute() {
-
- LOG.fine("Open Reference: " + reference);
-
- Base.showReference("Reference/" + reference);
-
- return null;
- }
- };
- }
-
- return null;
- }
- }
-
/**
* Handles http hyperlinks.
diff --git a/app/src/processing/app/tools/MenuScroller.java b/app/src/processing/app/tools/MenuScroller.java
index 3523ec7ceca..d934a4583ca 100644
--- a/app/src/processing/app/tools/MenuScroller.java
+++ b/app/src/processing/app/tools/MenuScroller.java
@@ -3,6 +3,8 @@
*/
package processing.app.tools;
+import processing.app.PreferencesData;
+
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
@@ -14,6 +16,7 @@
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.event.KeyEvent;
+import java.util.Arrays;
/**
* A class that provides scrolling capabilities to a long menu dropdown or
@@ -31,6 +34,7 @@ public class MenuScroller {
private JPopupMenu menu;
private Component[] menuItems;
+ private Component[] allMenuItems;
private MenuScrollItem upItem;
private MenuScrollItem downItem;
private final MenuScrollListener menuListener = new MenuScrollListener();
@@ -537,7 +541,8 @@ public void popupMenuCanceled(PopupMenuEvent e) {
}
private void setMenuItems() {
- menuItems = menu.getComponents();
+ allMenuItems = menu.getComponents();
+ menuItems = Arrays.stream(allMenuItems).filter(x -> x.isVisible()).toArray(Component[]::new);
if (keepVisibleIndex >= topFixedCount
&& keepVisibleIndex <= menuItems.length - bottomFixedCount
&& (keepVisibleIndex > firstIndex + scrollCount
@@ -552,7 +557,7 @@ private void setMenuItems() {
private void restoreMenuItems() {
menu.removeAll();
- for (Component component : menuItems) {
+ for (Component component : allMenuItems) {
menu.add(component);
}
}
@@ -567,6 +572,43 @@ public MenuScrollTimer(final int increment, int interval) {
public void actionPerformed(ActionEvent e) {
firstIndex += increment * accelerator;
refreshMenu();
+ if (PreferencesData.getBoolean("ide.accessible")) {
+ // If the user has chosen to use accessibility features, it means that they are using a screen reader
+ // to assist them in development of their project. This scroller is very unfriendly toward screen readers
+ // because it does not tell the user that it is scrolling through the board options, and it does not read
+ // the name of the boards as they scroll by. It is possible that the desired board will never become
+ // accessible.
+ // Because this scroller is quite nice for the sighted user, the idea here is to continue to use the
+ // scroller, but to fool it into scrolling one item at a time for accessible features users so that the
+ // screen readers work well, too.
+ // It's not the prettiest of code, but it works.
+ String itemClassName;
+ int keyEvent;
+
+ // The blind user likely used an arrow key to get to the scroller. Determine which arrow key
+ // so we can send an event for the opposite arrow key. This fools the scroller into scrolling
+ // a single item. Get the class name of the new item while we're here
+ if (increment > 0) {
+ itemClassName = menuItems[firstIndex + scrollCount - 1].getClass().getName();
+ keyEvent = KeyEvent.VK_UP;
+ }
+ else {
+ itemClassName = menuItems[firstIndex].getClass().getName();
+ keyEvent = KeyEvent.VK_DOWN;
+ }
+
+ // Use the class name to check if the next item is a separator. If it is, just let it scroll on like
+ // normal, otherwise move the cursor back with the opposite key event to the new item so that item is read
+ // by a screen reader and the user can use their arrow keys to navigate the list one item at a time
+ if (!itemClassName.equals(JSeparator.class.getName()) ) {
+ KeyboardFocusManager manager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
+ Component comp = manager.getFocusOwner();
+ KeyEvent event = new KeyEvent(comp,
+ KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0,
+ keyEvent, KeyEvent.CHAR_UNDEFINED);
+ comp.dispatchEvent(event);
+ }
+ }
}
});
}
diff --git a/app/test/cc/arduino/contributions/GzippedJsonDownloaderTest.java b/app/test/cc/arduino/contributions/GzippedJsonDownloaderTest.java
index e1e231acd3a..0892c361516 100644
--- a/app/test/cc/arduino/contributions/GzippedJsonDownloaderTest.java
+++ b/app/test/cc/arduino/contributions/GzippedJsonDownloaderTest.java
@@ -4,10 +4,11 @@
import cc.arduino.utils.MultiStepProgress;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.module.mrbean.MrBeanModule;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
+
+import processing.app.BaseNoGui;
import processing.app.helpers.FileUtils;
import java.io.File;
@@ -38,11 +39,13 @@ public void tearDown() throws Exception {
@Test
public void testJsonDownload() throws Exception {
- new GZippedJsonDownloader(downloader, new URL("http://downloads.arduino.cc/libraries/library_index.json"), new URL("http://downloads.arduino.cc/libraries/library_index.json.gz")).download(tempFile, new MultiStepProgress(1), "", new NoopProgressListener());
+ BaseNoGui.initPlatform();
+ new GZippedJsonDownloader(downloader, new URL("http://downloads.arduino.cc/libraries/library_index.json"),
+ new URL("http://downloads.arduino.cc/libraries/library_index.json.gz"))
+ .download(tempFile, new MultiStepProgress(1), "", new NoopProgressListener(), true);
InputStream indexIn = new FileInputStream(tempFile);
ObjectMapper mapper = new ObjectMapper();
- mapper.registerModule(new MrBeanModule());
mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
mapper.configure(DeserializationFeature.EAGER_DESERIALIZER_FETCH, true);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
diff --git a/app/test/cc/arduino/contributions/JsonDownloaderTest.java b/app/test/cc/arduino/contributions/JsonDownloaderTest.java
index ebf3c0913ce..1315fe223a0 100644
--- a/app/test/cc/arduino/contributions/JsonDownloaderTest.java
+++ b/app/test/cc/arduino/contributions/JsonDownloaderTest.java
@@ -4,10 +4,11 @@
import cc.arduino.utils.MultiStepProgress;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.module.mrbean.MrBeanModule;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
+
+import processing.app.BaseNoGui;
import processing.app.helpers.FileUtils;
import java.io.File;
@@ -38,11 +39,12 @@ public void tearDown() throws Exception {
@Test
public void testJsonDownload() throws Exception {
- new JsonDownloader(downloader, new URL("http://downloads.arduino.cc/libraries/library_index.json")).download(tempFile, new MultiStepProgress(1), "", new NoopProgressListener());
+ BaseNoGui.initPlatform();
+ new JsonDownloader(downloader, new URL("http://downloads.arduino.cc/libraries/library_index.json"))
+ .download(tempFile, new MultiStepProgress(1), "", new NoopProgressListener(), true);
InputStream indexIn = new FileInputStream(tempFile);
ObjectMapper mapper = new ObjectMapper();
- mapper.registerModule(new MrBeanModule());
mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
mapper.configure(DeserializationFeature.EAGER_DESERIALIZER_FETCH, true);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
diff --git a/app/test/cc/arduino/contributions/UpdatableLibraryTest.java b/app/test/cc/arduino/contributions/UpdatableLibraryTest.java
index e06c12710bf..0dab3531cd1 100644
--- a/app/test/cc/arduino/contributions/UpdatableLibraryTest.java
+++ b/app/test/cc/arduino/contributions/UpdatableLibraryTest.java
@@ -36,8 +36,7 @@ public void testUpdatableLibrary() throws Exception {
LibrariesIndexer indexer = new LibrariesIndexer(index_SD_only);
BaseNoGui.librariesIndexer = indexer;
indexer.parseIndex();
- indexer.setLibrariesFolders(folders);
- indexer.rescanLibraries();
+ indexer.setLibrariesFoldersAndRescan(folders);
ContributedLibrary sdLib = indexer.getIndex().getInstalled("SD").get();
assertTrue("SD lib is installed", sdLib.isLibraryInstalled());
@@ -46,7 +45,7 @@ public void testUpdatableLibrary() throws Exception {
assertTrue(ContributionsSelfCheck.checkForUpdatableLibraries());
folders.add(new UserLibraryFolder(SD121, Location.SKETCHBOOK));
- indexer.setLibrariesFolders(folders);
+ indexer.setLibrariesFoldersAndRescan(folders);
sdLib = indexer.getIndex().getInstalled("SD").get();
assertTrue("SD lib is installed", sdLib.isLibraryInstalled());
@@ -63,8 +62,7 @@ public void testUpdatableLibraryWithBundled() throws Exception {
LibrariesIndexer indexer = new LibrariesIndexer(index_Bridge_only);
BaseNoGui.librariesIndexer = indexer;
indexer.parseIndex();
- indexer.setLibrariesFolders(folders);
- indexer.rescanLibraries();
+ indexer.setLibrariesFoldersAndRescan(folders);
ContributedLibrary l = indexer.getIndex().getInstalled("Bridge").get();
assertTrue("Bridge lib is installed", l.isLibraryInstalled());
@@ -73,7 +71,7 @@ public void testUpdatableLibraryWithBundled() throws Exception {
assertTrue(ContributionsSelfCheck.checkForUpdatableLibraries());
folders.add(new UserLibraryFolder(Bridge170, Location.SKETCHBOOK));
- indexer.setLibrariesFolders(folders);
+ indexer.setLibrariesFoldersAndRescan(folders);
l = indexer.getIndex().getInstalled("Bridge").get();
assertTrue("Bridge lib is installed", l.isLibraryInstalled());
diff --git a/app/test/cc/arduino/contributions/VersionHelperTest.java b/app/test/cc/arduino/contributions/VersionHelperTest.java
index fecbcb8a8cb..de7463da16d 100644
--- a/app/test/cc/arduino/contributions/VersionHelperTest.java
+++ b/app/test/cc/arduino/contributions/VersionHelperTest.java
@@ -30,28 +30,38 @@
package cc.arduino.contributions;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Optional;
import org.junit.Test;
+import com.github.zafarkhaja.semver.Version;
+
public class VersionHelperTest {
+ public void assertOptionalEquals(String expected, Optional value) {
+ assertTrue(value.isPresent());
+ assertEquals(expected, value.get().toString());
+ }
+
@Test
public void testVersions() throws Exception {
- assertEquals("1.0.0", VersionHelper.valueOf("1.0.0").toString());
- assertEquals("1.0.0", VersionHelper.valueOf("1.0").toString());
- assertEquals("1.0.0", VersionHelper.valueOf("1").toString());
- assertEquals("1.0.0-abc", VersionHelper.valueOf("1.0.0-abc").toString());
- assertEquals("1.0.0-abc", VersionHelper.valueOf("1.0-abc").toString());
- assertEquals("1.0.0-abc", VersionHelper.valueOf("1-abc").toString());
- assertEquals("1.0.0+abc", VersionHelper.valueOf("1.0.0+abc").toString());
- assertEquals("1.0.0+abc", VersionHelper.valueOf("1.0+abc").toString());
- assertEquals("1.0.0+abc", VersionHelper.valueOf("1+abc").toString());
- assertEquals("1.0.0-def+abc", VersionHelper.valueOf("1.0.0-def+abc").toString());
- assertEquals("1.0.0-def+abc", VersionHelper.valueOf("1.0-def+abc").toString());
- assertEquals("1.0.0-def+abc", VersionHelper.valueOf("1-def+abc").toString());
- assertEquals("1.0.0+def-abc", VersionHelper.valueOf("1.0.0+def-abc").toString());
- assertEquals("1.0.0+def-abc", VersionHelper.valueOf("1.0+def-abc").toString());
- assertEquals("1.0.0+def-abc", VersionHelper.valueOf("1+def-abc").toString());
+ assertOptionalEquals("1.0.0", VersionHelper.valueOf("1.0.0"));
+ assertOptionalEquals("1.0.0", VersionHelper.valueOf("1.0"));
+ assertOptionalEquals("1.0.0", VersionHelper.valueOf("1"));
+ assertOptionalEquals("1.0.0-abc", VersionHelper.valueOf("1.0.0-abc"));
+ assertOptionalEquals("1.0.0-abc", VersionHelper.valueOf("1.0-abc"));
+ assertOptionalEquals("1.0.0-abc", VersionHelper.valueOf("1-abc"));
+ assertOptionalEquals("1.0.0+abc", VersionHelper.valueOf("1.0.0+abc"));
+ assertOptionalEquals("1.0.0+abc", VersionHelper.valueOf("1.0+abc"));
+ assertOptionalEquals("1.0.0+abc", VersionHelper.valueOf("1+abc"));
+ assertOptionalEquals("1.0.0-def+abc", VersionHelper.valueOf("1.0.0-def+abc"));
+ assertOptionalEquals("1.0.0-def+abc", VersionHelper.valueOf("1.0-def+abc"));
+ assertOptionalEquals("1.0.0-def+abc", VersionHelper.valueOf("1-def+abc"));
+ assertOptionalEquals("1.0.0+def-abc", VersionHelper.valueOf("1.0.0+def-abc"));
+ assertOptionalEquals("1.0.0+def-abc", VersionHelper.valueOf("1.0+def-abc"));
+ assertOptionalEquals("1.0.0+def-abc", VersionHelper.valueOf("1+def-abc"));
}
}
diff --git a/app/test/cc/arduino/net/CustomProxySelectorTest.java b/app/test/cc/arduino/net/CustomProxySelectorTest.java
index 005f5cce822..411f1aa2366 100644
--- a/app/test/cc/arduino/net/CustomProxySelectorTest.java
+++ b/app/test/cc/arduino/net/CustomProxySelectorTest.java
@@ -83,8 +83,8 @@ public void testProxyPACHTTP() throws Exception {
public void testProxyPACHTTPWithLogin() throws Exception {
preferences.put(Constants.PREF_PROXY_TYPE, Constants.PROXY_TYPE_AUTO);
preferences.put(Constants.PREF_PROXY_PAC_URL, CustomProxySelectorTest.class.getResource("proxy_http.pac").toExternalForm());
- preferences.put(Constants.PREF_PROXY_AUTO_USERNAME, "auto");
- preferences.put(Constants.PREF_PROXY_AUTO_PASSWORD, "autopassword");
+ preferences.put(Constants.PREF_PROXY_USERNAME, "auto");
+ preferences.put(Constants.PREF_PROXY_PASSWORD, "autopassword");
CustomProxySelector proxySelector = new CustomProxySelector(preferences);
Proxy proxy = proxySelector.getProxyFor(uri);
@@ -154,8 +154,8 @@ public void testManualProxyWithLogin() throws Exception {
preferences.put(Constants.PREF_PROXY_MANUAL_TYPE, Constants.PROXY_MANUAL_TYPE_HTTP);
preferences.put(Constants.PREF_PROXY_MANUAL_HOSTNAME, "localhost");
preferences.put(Constants.PREF_PROXY_MANUAL_PORT, "8080");
- preferences.put(Constants.PREF_PROXY_MANUAL_USERNAME, "username");
- preferences.put(Constants.PREF_PROXY_MANUAL_PASSWORD, "pwd");
+ preferences.put(Constants.PREF_PROXY_USERNAME, "username");
+ preferences.put(Constants.PREF_PROXY_PASSWORD, "pwd");
CustomProxySelector proxySelector = new CustomProxySelector(preferences);
Proxy proxy = proxySelector.getProxyFor(uri);
diff --git a/app/test/cc/arduino/packages/uploaders/MergeSketchWithUploaderTest.java b/app/test/cc/arduino/packages/uploaders/MergeSketchWithUploaderTest.java
index 384aa169deb..2f048b2a48b 100644
--- a/app/test/cc/arduino/packages/uploaders/MergeSketchWithUploaderTest.java
+++ b/app/test/cc/arduino/packages/uploaders/MergeSketchWithUploaderTest.java
@@ -35,18 +35,29 @@
import processing.app.helpers.FileUtils;
import java.io.File;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.List;
import static org.junit.Assert.assertEquals;
public class MergeSketchWithUploaderTest {
private File sketch;
+ private File bootloader;
@Before
public void setup() throws Exception {
- File originalSketch = new File(MergeSketchWithUploaderTest.class.getResource("/sketch.hex").getFile());
+ File originalSketch = getResourceFile("/sketch.hex");
sketch = new File(System.getProperty("java.io.tmpdir"), "sketch.hex");
FileUtils.copyFile(originalSketch, sketch);
+ removeCariageReturns(sketch);
+
+ File originalBootloader = getResourceFile("/optiboot_atmega328.hex");
+ bootloader = new File(System.getProperty("java.io.tmpdir"), "optiboot_atmega328.hex");
+ FileUtils.copyFile(originalBootloader, bootloader);
+ removeCariageReturns(bootloader);
}
@After
@@ -57,11 +68,24 @@ public void removeTmpFile() {
@Test
public void shouldMergeWithOptiboot() throws Exception {
assertEquals(11720, sketch.length());
+ assertEquals(1432, bootloader.length());
- File bootloader = new File(MergeSketchWithUploaderTest.class.getResource("/optiboot_atmega328.hex").getFile());
+ File bootloader = getResourceFile("/optiboot_atmega328.hex");
new MergeSketchWithBooloader().merge(sketch, bootloader);
assertEquals(13140, sketch.length());
}
+ private static File getResourceFile(String resourcePath) throws Exception {
+ return new File(URLDecoder.decode(
+ MergeSketchWithUploaderTest.class.getResource(resourcePath).getFile(), "UTF-8"));
+ }
+ private static void removeCariageReturns(File file) throws Exception {
+ List lines = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8);
+ StringBuilder contentBuilder = new StringBuilder();
+ for(String line : lines) {
+ contentBuilder.append(line).append('\n');
+ }
+ Files.write(file.toPath(), contentBuilder.toString().getBytes(StandardCharsets.UTF_8));
+ }
}
diff --git a/app/test/processing/app/AbstractGUITest.java b/app/test/processing/app/AbstractGUITest.java
index d1db60d98aa..efc68c1df84 100644
--- a/app/test/processing/app/AbstractGUITest.java
+++ b/app/test/processing/app/AbstractGUITest.java
@@ -29,42 +29,36 @@
package processing.app;
-import cc.arduino.files.DeleteFilesOnShutdown;
+import javax.swing.JPopupMenu;
+
import org.fest.swing.edt.FailOnThreadViolationRepaintManager;
import org.fest.swing.edt.GuiActionRunner;
import org.fest.swing.edt.GuiQuery;
import org.junit.After;
import org.junit.Before;
-import processing.app.helpers.ArduinoFrameFixture;
-import processing.app.helpers.FileUtils;
-import javax.swing.*;
-import java.util.Random;
+import processing.app.helpers.ArduinoFrameFixture;
-public abstract class AbstractGUITest {
+public abstract class AbstractGUITest extends AbstractWithPreferencesTest {
protected ArduinoFrameFixture window;
@Before
public void startUpTheIDE() throws Exception {
+ // This relies on AbstractWithPreferencesTest to set up the
+ // non-gui-specific stuff.
+
System.setProperty("mrj.version", "whynot"); //makes sense only on osx. See https://github.com/alexruiz/fest-swing-1.x/issues/2#issuecomment-86532042
- Runtime.getRuntime().addShutdownHook(new Thread(DeleteFilesOnShutdown.INSTANCE));
FailOnThreadViolationRepaintManager.install();
- BaseNoGui.initPlatform();
- BaseNoGui.getPlatform().init();
- PreferencesData.init(null);
JPopupMenu.setDefaultLightWeightPopupEnabled(false);
- Theme.init();
BaseNoGui.getPlatform().setLookAndFeel();
- Base.untitledFolder = FileUtils.createTempFolder("untitled" + new Random().nextInt(Integer.MAX_VALUE), ".tmp");
- DeleteFilesOnShutdown.add(Base.untitledFolder);
window = GuiActionRunner.execute(new GuiQuery() {
@Override
protected ArduinoFrameFixture executeInEDT() throws Throwable {
- return new ArduinoFrameFixture(new Base(new String[0]).editors.get(0));
+ return new ArduinoFrameFixture(createBase().editors.get(0));
}
});
}
diff --git a/app/test/processing/app/AbstractWithPreferencesTest.java b/app/test/processing/app/AbstractWithPreferencesTest.java
index f0d2f3a2b07..1075aebda4f 100644
--- a/app/test/processing/app/AbstractWithPreferencesTest.java
+++ b/app/test/processing/app/AbstractWithPreferencesTest.java
@@ -29,26 +29,88 @@
package processing.app;
-import cc.arduino.files.DeleteFilesOnShutdown;
+import static org.junit.Assert.assertEquals;
import org.junit.Before;
+import org.junit.After;
+
import processing.app.helpers.FileUtils;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
import java.util.Random;
+import java.util.List;
+import java.util.LinkedList;
public abstract class AbstractWithPreferencesTest {
+ /**
+ * Files or directories that will be deleted after each test.
+ * Subclasses can add files here in @Test or @Before functions.
+ */
+ protected List deleteAfter = new LinkedList();
+ protected File preferencesFile;
@Before
public void init() throws Exception {
- Runtime.getRuntime().addShutdownHook(new Thread(DeleteFilesOnShutdown.INSTANCE));
+ File settingsDir = Files.createTempDirectory("arduino_test_settings").toFile();
+ deleteAfter.add(settingsDir);
+
+ preferencesFile = new File(settingsDir, "preferences.txt");
+ File sketchbookDir = new File(settingsDir, "sketchbook");
+ sketchbookDir.mkdir();
+
BaseNoGui.initPlatform();
BaseNoGui.getPlatform().init();
- PreferencesData.init(null);
+
+ PreferencesData.init(preferencesFile);
+ // Do not read anything from e.g. ~/.arduino15
+ PreferencesData.set("settings.path", settingsDir.toString());
+ // Do not read or write the default ~/Arduino sketchbook
+ PreferencesData.set("sketchbook.path", sketchbookDir.toString());
+ // Do not perform any update checks
+ PreferencesData.set("update.check", sketchbookDir.toString());
+ // Write the defaults, with these changes to file. This allows them
+ // to be reloaded when creating a Base instance (see getBaseArgs()
+ // below).
+ PreferencesData.save();
+
Theme.init();
BaseNoGui.initPackages();
Base.untitledFolder = FileUtils.createTempFolder("untitled" + new Random().nextInt(Integer.MAX_VALUE), ".tmp");
- DeleteFilesOnShutdown.add(Base.untitledFolder);
+ deleteAfter.add(Base.untitledFolder);
}
+ /**
+ * Returns arguments to be passed to the Base constructor or on the
+ * commandline to set up the created dummy environment.
+ */
+ protected String[] getBaseArgs() {
+ return new String[] {
+ // Preferences are loaded (using --preferences-file) before
+ // processing any other commandline options (e.g. --pref), so only
+ // use --preferences-file here. Also, this does not affect the
+ // "action" mode, for tests that require the GUI to be loaded.
+ "--preferences-file", preferencesFile.toString(),
+ };
+ }
+
+ /**
+ * Creates a new instance of Base. Always use this rather than calling
+ * it directly, to ensure the right settings are used.
+ */
+ protected Base createBase() throws Exception {
+ Base base = new Base(getBaseArgs());
+ // Doublecheck that the right preferencesFile was loaded
+ assertEquals(preferencesFile, PreferencesData.preferencesFile);
+ return base;
+ }
+
+ @After
+ public void cleanup() throws IOException {
+ for (File f : deleteAfter)
+ FileUtils.recursiveDelete(f);
+ deleteAfter = new LinkedList();
+ }
}
diff --git a/app/test/processing/app/CommandLineTest.java b/app/test/processing/app/CommandLineTest.java
index bf8eb8904c1..92c0dfec83e 100644
--- a/app/test/processing/app/CommandLineTest.java
+++ b/app/test/processing/app/CommandLineTest.java
@@ -32,22 +32,32 @@
import static org.junit.Assert.*;
import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Arrays;
import org.apache.commons.compress.utils.IOUtils;
import org.fest.assertions.Assertions;
-import org.junit.Before;
+import org.junit.BeforeClass;
import org.junit.Test;
import processing.app.helpers.OSUtils;
import processing.app.helpers.PreferencesMap;
-public class CommandLineTest {
+/**
+ * This extends AbstractWithPreferencesTest which initializes part of
+ * the internal Arduino structures. Most of that is not required, but it
+ * also conveniently sets up a settings directory and preferences file,
+ * which we can use here (through getBaseArgs()).
+ */
+public class CommandLineTest extends AbstractWithPreferencesTest {
- File buildPath;
- File arduinoPath;
+ private static File buildPath;
+ private static File arduinoPath;
- @Before
- public void findBuildPaths() throws Exception {
+ @BeforeClass
+ public static void findBuildPaths() throws Exception {
buildPath = new File(System.getProperty("user.dir"));
while (!new File(buildPath, "build").isDirectory()) {
buildPath = buildPath.getParentFile();
@@ -72,58 +82,60 @@ public void findBuildPaths() throws Exception {
System.out.println("found arduino: " + arduinoPath);
}
- @Test
- public void testCommandLineBuildWithRelativePath() throws Exception {
+ public Process runArduino(boolean output, boolean success, File wd, String[] extraArgs) throws IOException, InterruptedException {
Runtime rt = Runtime.getRuntime();
- File wd = new File(buildPath, "build/shared/examples/01.Basics/Blink/");
- Process pr = rt
- .exec(arduinoPath + " --board arduino:avr:uno --verify Blink.ino", null,
- wd);
- IOUtils.copy(pr.getInputStream(), System.out);
+
+ List args = new ArrayList();
+ args.add(arduinoPath.getAbsolutePath());
+ args.addAll(Arrays.asList(getBaseArgs()));
+ args.addAll(Arrays.asList(extraArgs));
+
+ System.out.println("Running: " + String.join(" ", args));
+
+ Process pr = rt.exec(args.toArray(new String[0]), null, wd);
+ if (output) {
+ IOUtils.copy(pr.getInputStream(), System.out);
+ IOUtils.copy(pr.getErrorStream(), System.out);
+ }
pr.waitFor();
- assertEquals(0, pr.exitValue());
+ if (success)
+ assertEquals(0, pr.exitValue());
+ return pr;
+ }
+
+ @Test
+ public void testCommandLineBuildWithRelativePath() throws Exception {
+ File wd = new File(buildPath, "app/testdata/sketches/Blink/");
+ runArduino(true, true, wd, new String[] {
+ "--board", "arduino:avr:uno",
+ "--verify", "Blink.ino",
+ });
}
@Test
public void testCommandLinePreferencesSave() throws Exception {
- Runtime rt = Runtime.getRuntime();
File prefFile = File.createTempFile("test_pref", ".txt");
prefFile.deleteOnExit();
- Process pr = rt.exec(new String[] {
- arduinoPath.getAbsolutePath(),
+ runArduino(true, true, null, new String[] {
"--save-prefs",
"--preferences-file", prefFile.getAbsolutePath(),
- "--get-pref", // avoids starting the GUI
+ "--version", // avoids starting the GUI
});
- IOUtils.copy(pr.getInputStream(), System.out);
- IOUtils.copy(pr.getErrorStream(), System.out);
- pr.waitFor();
- assertEquals(0, pr.exitValue());
- pr = rt.exec(new String[] {
- arduinoPath.getAbsolutePath(),
+ runArduino(true, true, null, new String[] {
"--pref", "test_pref=xxx",
"--preferences-file", prefFile.getAbsolutePath(),
});
- IOUtils.copy(pr.getInputStream(), System.out);
- IOUtils.copy(pr.getErrorStream(), System.out);
- pr.waitFor();
- assertEquals(0, pr.exitValue());
PreferencesMap prefs = new PreferencesMap(prefFile);
assertNull("preference should not be saved", prefs.get("test_pref"));
- pr = rt.exec(new String[] {
- arduinoPath.getAbsolutePath(),
+ runArduino(true, true, null, new String[] {
"--pref", "test_pref=xxx",
"--preferences-file", prefFile.getAbsolutePath(),
"--save-prefs",
});
- IOUtils.copy(pr.getInputStream(), System.out);
- IOUtils.copy(pr.getErrorStream(), System.out);
- pr.waitFor();
- assertEquals(0, pr.exitValue());
prefs = new PreferencesMap(prefFile);
assertEquals("preference should be saved", "xxx", prefs.get("test_pref"));
@@ -131,32 +143,23 @@ public void testCommandLinePreferencesSave() throws Exception {
@Test
public void testCommandLineVersion() throws Exception {
- Runtime rt = Runtime.getRuntime();
- Process pr = rt.exec(new String[]{
- arduinoPath.getAbsolutePath(),
+ Process pr = runArduino(false, true, null, new String[] {
"--version",
});
- pr.waitFor();
- Assertions.assertThat(pr.exitValue())
- .as("Process will finish with exit code 0 in --version")
- .isEqualTo(0);
Assertions.assertThat(new String(IOUtils.toByteArray(pr.getInputStream())))
- .matches("Arduino: \\d+\\.\\d+\\.\\d+.*");
+ .matches("Arduino: \\d+\\.\\d+\\.\\d+.*\r?\n");
}
@Test
public void testCommandLineMultipleAction() throws Exception {
- Runtime rt = Runtime.getRuntime();
- Process pr = rt.exec(new String[]{
- arduinoPath.getAbsolutePath(),
+ Process pr = runArduino(true, false, null, new String[] {
"--version",
"--verify",
});
- pr.waitFor();
Assertions.assertThat(pr.exitValue())
- .as("Multiple Action will be rejected")
- .isEqualTo(3);
+ .as("Multiple Action will be rejected")
+ .isEqualTo(3);
}
}
diff --git a/app/test/processing/app/DefaultTargetTest.java b/app/test/processing/app/DefaultTargetTest.java
index 37819c84cff..24767bee30d 100644
--- a/app/test/processing/app/DefaultTargetTest.java
+++ b/app/test/processing/app/DefaultTargetTest.java
@@ -57,7 +57,7 @@ public void testDefaultTarget() throws Exception {
PreferencesData.set("board", "unreal_board");
// should not raise an exception
- new Base(new String[0]);
+ createBase();
// skip test if no target platforms are available
Assume.assumeNotNull(BaseNoGui.getTargetPlatform());
diff --git a/app/test/processing/app/EditorConsoleTest.java b/app/test/processing/app/EditorConsoleTest.java
new file mode 100644
index 00000000000..308523ce6ef
--- /dev/null
+++ b/app/test/processing/app/EditorConsoleTest.java
@@ -0,0 +1,155 @@
+/*
+ * This file is part of Arduino.
+ *
+ * Copyright 2020 Arduino LLC (http://www.arduino.cc/)
+ *
+ * Arduino is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * As a special exception, you may use this file as part of a free software
+ * library without restriction. Specifically, if other files instantiate
+ * templates or use macros or inline functions from this file, or you compile
+ * this file and link it with other files to produce an executable, this
+ * file does not by itself cause the resulting executable to be covered by
+ * the GNU General Public License. This exception does not however
+ * invalidate any other reasons why the executable file might be covered by
+ * the GNU General Public License.
+ */
+
+package processing.app;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class EditorConsoleTest extends AbstractWithPreferencesTest {
+ private EditorConsole console;
+
+ @Before
+ public void createConsole() {
+ console = new EditorConsole(null);
+ }
+
+ public String escapeString(String input) {
+ // This escapes backslashes, newlines and carriage returns, to get
+ // more readable assertion failures.
+ return input.replace("\\", "\\\\").replace("\n", "\\n").replace("\r", "\\r");
+ }
+
+ public void assertOutput(String output) {
+ assertEquals(escapeString(output), escapeString(console.getText()));
+ }
+
+ @Test
+ public void testHelloWorld() throws Exception {
+ console.insertString("Hello, world!", null);
+
+ assertOutput("Hello, world!");
+ }
+
+ @Test
+ public void testCrNlHandling() throws Exception {
+ // Do some basic tests with \r\n
+ console.insertString("abc\r\ndef", null);
+ assertOutput("abc\r\ndef");
+
+ console.insertString("xyz", null);
+ assertOutput("abc\r\ndefxyz");
+
+ console.insertString("000\r\n123", null);
+ assertOutput("abc\r\ndefxyz000\r\n123");
+
+ console.insertString("\r\n", null);
+ assertOutput("abc\r\ndefxyz000\r\n123\r\n");
+ }
+
+ @Test
+ public void testNlHandling() throws Exception {
+ // Basic tests, but with just \n
+ console.insertString("abc\ndef", null);
+ assertOutput("abc\ndef");
+
+ console.insertString("xyz", null);
+ assertOutput("abc\ndefxyz");
+
+ console.insertString("000\n123", null);
+ assertOutput("abc\ndefxyz000\n123");
+
+ console.insertString("\n", null);
+ assertOutput("abc\ndefxyz000\n123\n");
+ }
+
+ @Test
+ public void testCrHandling() throws Exception {
+ // Then test that single \r clears the current line
+ console.clear();
+ console.insertString("abc\rdef", null);
+ assertOutput("def");
+
+ // A single \r at the end is not added to the document
+ console.insertString("\r", null);
+ assertOutput("def");
+
+ // Nor are multiple \r at the end
+ console.insertString("\r\r\r", null);
+ assertOutput("def");
+
+ // But it does clear the line on the next write
+ console.insertString("123", null);
+ assertOutput("123");
+
+ // Same when combined with some data
+ console.insertString("\r456\r\r", null);
+ assertOutput("456");
+
+ console.insertString("000", null);
+ assertOutput("000");
+
+ // Then add a newline so preceding data is kept
+ console.insertString("\r\nxxx\r", null);
+ assertOutput("000\r\nxxx");
+
+ // But data after the newline is removed
+ console.insertString("yyy", null);
+ assertOutput("000\r\nyyy");
+
+ // When a \r\n is split across inserts, it becomes a lone \n
+ console.insertString("\r", null);
+ assertOutput("000\r\nyyy");
+ console.insertString("\n", null);
+ assertOutput("000\r\nyyy\n");
+ }
+
+ @Test
+ public void testCrPartialOverwrite() throws Exception {
+ console.insertString("abcdef\r", null);
+ assertOutput("abcdef");
+
+ console.insertString("123", null);
+ assertOutput("123def");
+
+ console.insertString("4", null);
+ assertOutput("1234ef");
+
+ console.insertString("\r\n56", null);
+ assertOutput("1234ef\r\n56");
+ }
+
+ @Test
+ public void testTogether() throws Exception {
+ console.insertString("abc\n123456\rdef\rx\r\nyyy\nzzz\r999", null);
+ assertOutput("abc\nxef456\r\nyyy\n999");
+ }
+}
diff --git a/app/test/processing/app/HittingEscapeOnCloseConfirmationDialogTest.java b/app/test/processing/app/HittingEscapeOnCloseConfirmationDialogTest.java
index 83897b1963c..00539b15b29 100644
--- a/app/test/processing/app/HittingEscapeOnCloseConfirmationDialogTest.java
+++ b/app/test/processing/app/HittingEscapeOnCloseConfirmationDialogTest.java
@@ -29,16 +29,18 @@
package processing.app;
+import static org.junit.Assert.assertEquals;
+import static processing.app.I18n.tr;
+
+import java.awt.event.KeyEvent;
+
import org.fest.swing.core.KeyPressInfo;
+import org.fest.swing.core.matcher.DialogMatcher;
import org.fest.swing.finder.WindowFinder;
import org.fest.swing.fixture.DialogFixture;
import org.junit.Test;
-import processing.app.helpers.SketchTextAreaFixture;
-import javax.swing.*;
-import java.awt.event.KeyEvent;
-
-import static org.junit.Assert.assertEquals;
+import processing.app.helpers.SketchTextAreaFixture;
public class HittingEscapeOnCloseConfirmationDialogTest extends AbstractGUITest {
@@ -49,7 +51,8 @@ public void shouldJustCloseTheDialog() throws Exception {
window.close();
- DialogFixture dialog = WindowFinder.findDialog(JDialog.class).using(window.robot);
+ DialogMatcher matcher = DialogMatcher.withTitle(tr("Close")).andShowing();
+ DialogFixture dialog = WindowFinder.findDialog(matcher).using(window.robot);
dialog.pressAndReleaseKey(KeyPressInfo.keyCode(KeyEvent.VK_ESCAPE));
EditorConsole console = (EditorConsole) window.scrollPane("console").component();
diff --git a/app/test/processing/app/helpers/StringUtilsTest.java b/app/test/processing/app/SerialTest.java
similarity index 64%
rename from app/test/processing/app/helpers/StringUtilsTest.java
rename to app/test/processing/app/SerialTest.java
index 1ddbf51b35e..63280811e24 100644
--- a/app/test/processing/app/helpers/StringUtilsTest.java
+++ b/app/test/processing/app/SerialTest.java
@@ -1,6 +1,8 @@
/*
* This file is part of Arduino.
*
+ * Copyright 2020 Arduino LLC (http://www.arduino.cc/)
+ *
* Arduino is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
@@ -23,20 +25,34 @@
* the GNU General Public License. This exception does not however
* invalidate any other reasons why the executable file might be covered by
* the GNU General Public License.
- *
- * Copyright 2015 Arduino LLC (http://www.arduino.cc/)
*/
-package processing.app.helpers;
+package processing.app;
+
+import static org.junit.Assert.assertEquals;
import org.junit.Test;
-import static org.junit.Assert.assertEquals;
+public class SerialTest {
+ class NullSerial extends Serial {
+ public NullSerial() throws SerialException {
+ super("none", 0, 'n', 0, 0, false, false);
+ }
+
+ @Override
+ protected void message(char[] chars, int length) {
+ output += new String(chars, 0, length);
+ }
-public class StringUtilsTest {
+ String output = "";
+ }
@Test
- public void shouldJoinAnArray() {
- assertEquals("1 - 2 - 3", StringUtils.join(new String[]{"1", "2", "3"}, " - "));
+ public void testSerialUTF8Decoder() throws Exception {
+ NullSerial s = new NullSerial();
+ // https://github.com/arduino/Arduino/issues/9808
+ String testdata = "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789°0123456789";
+ s.processSerialEvent(testdata.getBytes());
+ assertEquals(s.output, testdata);
}
}
diff --git a/app/test/processing/app/UpdateTextAreaActionTest.java b/app/test/processing/app/UpdateTextAreaActionTest.java
new file mode 100644
index 00000000000..b32ea1850be
--- /dev/null
+++ b/app/test/processing/app/UpdateTextAreaActionTest.java
@@ -0,0 +1,91 @@
+package processing.app;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import cc.arduino.packages.BoardPort;
+import processing.app.helpers.PreferencesMap;
+
+public class UpdateTextAreaActionTest {
+
+ private static final String TIMESTAMP_REGEX = "\\d\\d:\\d\\d:\\d\\d.\\d\\d\\d";
+
+ class DummyTextMonitor extends AbstractTextMonitor {
+ public DummyTextMonitor(BoardPort boardPort) {
+ super(boardPort);
+ }
+ }
+
+ @Before
+ public void setup() {
+ PreferencesData.defaults = new PreferencesMap();
+ PreferencesData.set("editor.font", "Monospaced,plain,12");
+ PreferencesData.set("gui.scale", "100");
+ Theme.defaults = new PreferencesMap();
+ Theme.table.put("console.font", "Monospaced,plain,12");
+ }
+
+ @Test
+ public void noTimestampAdded() {
+ DummyTextMonitor textMon = new DummyTextMonitor(new BoardPort());
+ textMon.addTimeStampBox.setSelected(false);
+
+ textMon.updateTextArea("line1\nline2\r\nline3");
+ assertThat(textMon.textArea.getText()).matches("line1\nline2\r\nline3");
+ }
+
+ @Test
+ public void all3LinesHaveTimestampAdded() {
+ DummyTextMonitor textMon = new DummyTextMonitor(new BoardPort());
+ textMon.addTimeStampBox.setSelected(true);
+
+ textMon.updateTextArea("line1\nline2\r\nline3");
+ assertThat(textMon.textArea.getText())
+ .matches(TIMESTAMP_REGEX + " -> line1\\n" + //
+ TIMESTAMP_REGEX + " -> line2\\r\\n" + //
+ TIMESTAMP_REGEX + " -> line3");
+ }
+
+ @Test
+ public void emptyLinesHaveTimestampToo() {
+ DummyTextMonitor textMon = new DummyTextMonitor(new BoardPort());
+ textMon.addTimeStampBox.setSelected(true);
+
+ textMon.updateTextArea("line_1\n\nline_2");
+ assertThat(textMon.textArea.getText())
+ .matches(TIMESTAMP_REGEX + " -> line_1\\n" + //
+ TIMESTAMP_REGEX + " -> \\n" + //
+ TIMESTAMP_REGEX + " -> line_2");
+ }
+
+ @Test
+ public void newLinesAreRememberedWhenNewBufferIsUsed() {
+ DummyTextMonitor textMon = new DummyTextMonitor(new BoardPort());
+ textMon.addTimeStampBox.setSelected(true);
+
+ textMon.updateTextArea("no newline");
+ assertThat(textMon.textArea.getText())
+ .matches(TIMESTAMP_REGEX + " -> no newline");
+
+ textMon.updateTextArea(" more text");
+ assertThat(textMon.textArea.getText())
+ .matches(TIMESTAMP_REGEX + " -> no newline more text");
+
+ textMon.updateTextArea("\n");
+ assertThat(textMon.textArea.getText())
+ .matches(TIMESTAMP_REGEX + " -> no newline more text\n");
+
+ textMon.updateTextArea("\n");
+ assertThat(textMon.textArea.getText())
+ .matches(TIMESTAMP_REGEX + " -> no newline more text\n" + //
+ TIMESTAMP_REGEX + " -> \n");
+
+ textMon.updateTextArea("third line");
+ assertThat(textMon.textArea.getText())
+ .matches(TIMESTAMP_REGEX + " -> no newline more text\n" + //
+ TIMESTAMP_REGEX + " -> \n" + //
+ TIMESTAMP_REGEX + " -> third line");
+ }
+}
\ No newline at end of file
diff --git a/app/test/processing/app/debug/TargetPlatformStub.java b/app/test/processing/app/debug/TargetPlatformStub.java
index a8048c514a4..59b655f96f5 100644
--- a/app/test/processing/app/debug/TargetPlatformStub.java
+++ b/app/test/processing/app/debug/TargetPlatformStub.java
@@ -99,4 +99,10 @@ public TargetBoard getBoard(String boardId) {
public TargetPackage getContainerPackage() {
return targetPackage;
}
+
+ @Override
+ public boolean isInSketchbook() {
+ // TODO Auto-generated method stub
+ return false;
+ }
}
diff --git a/app/testdata/libraries/Bridge_1.6.3/Bridge/examples/HttpClient/HttpClient.ino b/app/testdata/libraries/Bridge_1.6.3/Bridge/examples/HttpClient/HttpClient.ino
index 47a37c3f2eb..b0a6c21ffbe 100644
--- a/app/testdata/libraries/Bridge_1.6.3/Bridge/examples/HttpClient/HttpClient.ino
+++ b/app/testdata/libraries/Bridge_1.6.3/Bridge/examples/HttpClient/HttpClient.ino
@@ -37,7 +37,7 @@ void loop() {
HttpClient client;
// Make a HTTP request:
- client.get("http://www.arduino.cc/asciilogo.txt");
+ client.get("http://arduino.tips/asciilogo.txt");
// if there are incoming bytes available
// from the server, read them and print them:
diff --git a/build/shared/examples/01.Basics/Blink/Blink.ino b/app/testdata/sketches/Blink/Blink.ino
similarity index 100%
rename from build/shared/examples/01.Basics/Blink/Blink.ino
rename to app/testdata/sketches/Blink/Blink.ino
diff --git a/arduino-core/.classpath b/arduino-core/.classpath
index 2f200cc0713..3f0d53aaaf6 100644
--- a/arduino-core/.classpath
+++ b/arduino-core/.classpath
@@ -5,10 +5,9 @@
-
+
-
@@ -16,13 +15,14 @@
-
-
+
+
+
diff --git a/arduino-core/lib/commons-io-2.6.jar b/arduino-core/lib/commons-io-2.6.jar
new file mode 100644
index 00000000000..00556b119d4
Binary files /dev/null and b/arduino-core/lib/commons-io-2.6.jar differ
diff --git a/arduino-core/lib/commons-lang3-3.3.2.jar b/arduino-core/lib/commons-lang3-3.3.2.jar
deleted file mode 100644
index 2ce08ae99d1..00000000000
Binary files a/arduino-core/lib/commons-lang3-3.3.2.jar and /dev/null differ
diff --git a/arduino-core/lib/commons-lang3-3.8.1.jar b/arduino-core/lib/commons-lang3-3.8.1.jar
new file mode 100644
index 00000000000..2c65ce67d5c
Binary files /dev/null and b/arduino-core/lib/commons-lang3-3.8.1.jar differ
diff --git a/arduino-core/lib/jackson-module-mrbean-2.9.5.jar b/arduino-core/lib/jackson-module-mrbean-2.9.5.jar
deleted file mode 100644
index dc7f5a05721..00000000000
Binary files a/arduino-core/lib/jackson-module-mrbean-2.9.5.jar and /dev/null differ
diff --git a/arduino-core/lib/jmdns-3.5.3.jar b/arduino-core/lib/jmdns-3.5.3.jar
deleted file mode 100644
index d4d9c67f46c..00000000000
Binary files a/arduino-core/lib/jmdns-3.5.3.jar and /dev/null differ
diff --git a/arduino-core/lib/jmdns-3.5.5.jar b/arduino-core/lib/jmdns-3.5.5.jar
new file mode 100644
index 00000000000..a8b65ff2ec7
Binary files /dev/null and b/arduino-core/lib/jmdns-3.5.5.jar differ
diff --git a/arduino-core/lib/jssc-2.8.0-arduino2.jar b/arduino-core/lib/jssc-2.8.0-arduino2.jar
deleted file mode 100644
index a9ec9838921..00000000000
Binary files a/arduino-core/lib/jssc-2.8.0-arduino2.jar and /dev/null differ
diff --git a/arduino-core/lib/jssc-2.8.0-arduino4.jar b/arduino-core/lib/jssc-2.8.0-arduino4.jar
new file mode 100644
index 00000000000..623a3833bce
Binary files /dev/null and b/arduino-core/lib/jssc-2.8.0-arduino4.jar differ
diff --git a/arduino-core/src/cc/arduino/Compiler.java b/arduino-core/src/cc/arduino/Compiler.java
index ad7a964b466..5ad4484d98d 100644
--- a/arduino-core/src/cc/arduino/Compiler.java
+++ b/arduino-core/src/cc/arduino/Compiler.java
@@ -101,8 +101,8 @@ public class Compiler implements MessageConsumer {
tr("Couldn't determine program size: {0}");
tr("Global variables use {0} bytes ({2}%%) of dynamic memory, leaving {3} bytes for local variables. Maximum is {1} bytes.");
tr("Global variables use {0} bytes of dynamic memory.");
- tr("Sketch too big; see http://www.arduino.cc/en/Guide/Troubleshooting#size for tips on reducing it.");
- tr("Not enough memory; see http://www.arduino.cc/en/Guide/Troubleshooting#size for tips on reducing your footprint.");
+ tr("Sketch too big; see https://support.arduino.cc/hc/en-us/articles/360013825179 for tips on reducing it.");
+ tr("Not enough memory; see https://support.arduino.cc/hc/en-us/articles/360013825179 for tips on reducing your footprint.");
tr("Low memory available, stability problems may occur.");
tr("An error occurred while verifying the sketch");
tr("An error occurred while verifying/uploading the sketch");
@@ -134,7 +134,7 @@ enum BuilderAction {
}
}
- private static final Pattern ERROR_FORMAT = Pattern.compile("(.+\\.\\w+):(\\d+)(:\\d+)*:\\s*(fatal)?\\s*error:\\s*(.*)\\s*", Pattern.MULTILINE | Pattern.DOTALL);
+ private static final Pattern ERROR_FORMAT = Pattern.compile("(.+\\.\\w+):(\\d+)(:\\d+)*:\\s*((fatal)?\\s*error:\\s*)(.*)\\s*", Pattern.MULTILINE | Pattern.DOTALL);
private final File pathToSketch;
private final Sketch sketch;
@@ -405,7 +405,7 @@ private void runRecipe(String recipe, PreferencesMap prefs) throws RunnerExcepti
String[] cmdArray;
String cmd = prefs.getOrExcept(recipe);
try {
- cmdArray = StringReplacer.formatAndSplit(cmd, dict, true);
+ cmdArray = StringReplacer.formatAndSplit(cmd, dict);
} catch (Exception e) {
throw new RunnerException(e);
}
@@ -516,16 +516,14 @@ public void message(String s) {
if (pieces != null) {
String msg = "";
- int errorIdx = pieces.length - 1;
- String error = pieces[errorIdx];
String filename = pieces[1];
int line = PApplet.parseInt(pieces[2]);
- int col;
- if (errorIdx > 3) {
+ int col = -1;
+ if (pieces[3] != null) {
col = PApplet.parseInt(pieces[3].substring(1));
- } else {
- col = -1;
}
+ String errorPrefix = pieces[4];
+ String error = pieces[6];
if (error.trim().equals("SPI.h: No such file or directory")) {
error = tr("Please import the SPI library from the Sketch > Import Library menu.");
@@ -585,11 +583,8 @@ public void message(String s) {
String fileName = ex.getCodeFile().getPrettyName();
int lineNum = ex.getCodeLine() + 1;
int colNum = ex.getCodeColumn();
- if (colNum != -1) {
- s = fileName + ":" + lineNum + ":" + colNum + ": error: " + error + msg;
- } else {
- s = fileName + ":" + lineNum + ": error: " + error + msg;
- }
+ String column = (colNum != -1) ? (":" + colNum) : "";
+ s = fileName + ":" + lineNum + column + ": " + errorPrefix + error + msg;
}
if (ex != null) {
diff --git a/arduino-core/src/cc/arduino/Constants.java b/arduino-core/src/cc/arduino/Constants.java
index 67e3a4f82d3..20858ea99f3 100644
--- a/arduino-core/src/cc/arduino/Constants.java
+++ b/arduino-core/src/cc/arduino/Constants.java
@@ -37,6 +37,7 @@ public class Constants {
public static final String PREF_REMOVE_PLACEHOLDER = "___REMOVE___";
public static final String PREF_BOARDS_MANAGER_ADDITIONAL_URLS = "boardsmanager.additional.urls";
public static final String PREF_CONTRIBUTIONS_TRUST_ALL = "contributions.trust.all";
+ public static final String ALLOW_INSECURE_PACKAGES = "allow_insecure_packages";
public static final String DEFAULT_INDEX_FILE_NAME = "package_index.json";
public static final String BUNDLED_INDEX_FILE_NAME = "package_index_bundled.json";
@@ -57,10 +58,8 @@ public class Constants {
public static final String PREF_PROXY_PAC_URL = "proxy.pac.url";
public static final String PREF_PROXY_MANUAL_HOSTNAME = "proxy.manual.hostname";
public static final String PREF_PROXY_MANUAL_PORT = "proxy.manual.port";
- public static final String PREF_PROXY_MANUAL_USERNAME = "proxy.manual.username";
- public static final String PREF_PROXY_MANUAL_PASSWORD = "proxy.manual.password";
- public static final String PREF_PROXY_AUTO_USERNAME = "proxy.manual.username";
- public static final String PREF_PROXY_AUTO_PASSWORD = "proxy.manual.password";
+ public static final String PREF_PROXY_USERNAME = "proxy.manual.username";
+ public static final String PREF_PROXY_PASSWORD = "proxy.manual.password";
public static final String PACKAGE_INDEX_URL;
public static final String LIBRARY_INDEX_URL;
diff --git a/arduino-core/src/cc/arduino/UploaderUtils.java b/arduino-core/src/cc/arduino/UploaderUtils.java
index 108d7c137b3..875f41d7676 100644
--- a/arduino-core/src/cc/arduino/UploaderUtils.java
+++ b/arduino-core/src/cc/arduino/UploaderUtils.java
@@ -35,7 +35,7 @@
import processing.app.BaseNoGui;
import processing.app.PreferencesData;
import processing.app.Sketch;
-import processing.app.debug.TargetPlatform;
+import processing.app.debug.TargetBoard;
import java.util.LinkedList;
import java.util.List;
@@ -45,19 +45,14 @@
public class UploaderUtils {
public Uploader getUploaderByPreferences(boolean noUploadPort) {
- TargetPlatform target = BaseNoGui.getTargetPlatform();
- String board = PreferencesData.get("board");
-
BoardPort boardPort = null;
if (!noUploadPort) {
String port = PreferencesData.get("serial.port");
- if (port == null || port.isEmpty()) {
- return null;
- }
boardPort = BaseNoGui.getDiscoveryManager().find(port);
}
- return new UploaderFactory().newUploader(target.getBoards().get(board), boardPort, noUploadPort);
+ TargetBoard board = BaseNoGui.getTargetBoard();
+ return new UploaderFactory().newUploader(board, boardPort, noUploadPort);
}
public boolean upload(Sketch data, Uploader uploader, String suggestedClassName, boolean usingProgrammer, boolean noUploadPort, List warningsAccumulator) throws Exception {
diff --git a/arduino-core/src/cc/arduino/contributions/DownloadableContributionVersionComparator.java b/arduino-core/src/cc/arduino/contributions/DownloadableContributionVersionComparator.java
index b7f9959a475..f500a9e4186 100644
--- a/arduino-core/src/cc/arduino/contributions/DownloadableContributionVersionComparator.java
+++ b/arduino-core/src/cc/arduino/contributions/DownloadableContributionVersionComparator.java
@@ -33,15 +33,9 @@
public class DownloadableContributionVersionComparator implements Comparator {
- private final VersionComparator versionComparator;
-
- public DownloadableContributionVersionComparator() {
- versionComparator = new VersionComparator();
- }
-
@Override
public int compare(DownloadableContribution lib1, DownloadableContribution lib2) {
- return versionComparator.compare(lib1.getParsedVersion(), lib2.getParsedVersion());
+ return VersionComparator.compareTo(lib1.getParsedVersion(), lib2.getParsedVersion());
}
diff --git a/arduino-core/src/cc/arduino/contributions/DownloadableContributionsDownloader.java b/arduino-core/src/cc/arduino/contributions/DownloadableContributionsDownloader.java
index 3157514f876..620152abf83 100644
--- a/arduino-core/src/cc/arduino/contributions/DownloadableContributionsDownloader.java
+++ b/arduino-core/src/cc/arduino/contributions/DownloadableContributionsDownloader.java
@@ -30,34 +30,40 @@
package cc.arduino.contributions;
import cc.arduino.utils.FileHash;
+import cc.arduino.utils.MultiStepProgress;
import cc.arduino.utils.Progress;
import cc.arduino.utils.network.FileDownloader;
+import org.apache.commons.io.FilenameUtils;
+import processing.app.BaseNoGui;
+import processing.app.PreferencesData;
import java.io.File;
import java.net.URL;
-import java.nio.file.Files;
-import java.nio.file.LinkOption;
-import java.nio.file.Path;
-import java.nio.file.Paths;
+import java.nio.file.*;
+import java.util.Collection;
import static processing.app.I18n.format;
import static processing.app.I18n.tr;
public class DownloadableContributionsDownloader {
-
private final File stagingFolder;
public DownloadableContributionsDownloader(File _stagingFolder) {
stagingFolder = _stagingFolder;
}
- public File download(DownloadableContribution contribution, Progress progress, final String statusText, ProgressListener progressListener) throws Exception {
- return download(contribution, progress, statusText, progressListener, false);
+ public File download(DownloadableContribution contribution, Progress progress, final String statusText, ProgressListener progressListener, boolean allowCache) throws Exception {
+ return download(contribution, progress, statusText, progressListener, false, allowCache);
}
- public File download(DownloadableContribution contribution, Progress progress, final String statusText, ProgressListener progressListener, boolean noResume) throws Exception {
+ public File download(DownloadableContribution contribution, Progress progress, final String statusText, ProgressListener progressListener, boolean noResume, boolean allowCache) throws Exception {
URL url = new URL(contribution.getUrl());
- Path outputFile = Paths.get(stagingFolder.getAbsolutePath(), contribution.getArchiveFileName());
+ // Filter out paths from file name
+ String filename = new File(contribution.getArchiveFileName()).getName();
+ Path outputFile = Paths.get(stagingFolder.getAbsolutePath(), filename).normalize();
+ if (outputFile.toFile().isDirectory()) {
+ throw new Exception(format("Can't download {0}: invalid filename or exinsting directory", contribution.getArchiveFileName()));
+ }
// Ensure the existence of staging folder
Files.createDirectories(stagingFolder.toPath());
@@ -70,7 +76,7 @@ public File download(DownloadableContribution contribution, Progress progress, f
while (true) {
// Need to download or resume downloading?
if (!Files.isRegularFile(outputFile, LinkOption.NOFOLLOW_LINKS) || (Files.size(outputFile) < contribution.getSize())) {
- download(url, outputFile.toFile(), progress, statusText, progressListener, noResume);
+ download(url, outputFile.toFile(), progress, statusText, progressListener, noResume, allowCache);
downloaded = true;
}
@@ -116,12 +122,12 @@ private boolean hasChecksum(DownloadableContribution contribution) {
return algo != null && !algo.isEmpty();
}
- public void download(URL url, File tmpFile, Progress progress, String statusText, ProgressListener progressListener) throws Exception {
- download(url, tmpFile, progress, statusText, progressListener, false);
+ public void download(URL url, File tmpFile, Progress progress, String statusText, ProgressListener progressListener, boolean allowCache) throws Exception {
+ download(url, tmpFile, progress, statusText, progressListener, false, allowCache);
}
- public void download(URL url, File tmpFile, Progress progress, String statusText, ProgressListener progressListener, boolean noResume) throws Exception {
- FileDownloader downloader = new FileDownloader(url, tmpFile);
+ public void download(URL url, File tmpFile, Progress progress, String statusText, ProgressListener progressListener, boolean noResume, boolean allowCache) throws Exception {
+ final FileDownloader downloader = new FileDownloader(url, tmpFile, allowCache);
downloader.addObserver((o, arg) -> {
FileDownloader me = (FileDownloader) o;
String msg = "";
@@ -140,4 +146,81 @@ public void download(URL url, File tmpFile, Progress progress, String statusText
}
}
+ public void downloadIndexAndSignature(MultiStepProgress progress, URL packageIndexUrl, ProgressListener progressListener, SignatureVerifier signatureVerifier) throws Exception {
+ // Extract the file name from the url
+ final String indexFileName = FilenameUtils.getName(packageIndexUrl.getPath());
+ final File packageIndex = BaseNoGui.indexer.getIndexFile(indexFileName);
+
+ final String statusText = tr("Downloading platforms index...");
+
+ // Create temp files
+ final File packageIndexTemp = File.createTempFile(indexFileName, ".tmp");
+ try {
+ // Download package index
+ download(packageIndexUrl, packageIndexTemp, progress, statusText, progressListener, true, true);
+ final URL signatureUrl = new URL(packageIndexUrl.toString() + ".sig");
+
+ if (verifyDomain(packageIndexUrl)) {
+ if (checkSignature(progress, signatureUrl, progressListener, signatureVerifier, statusText, packageIndexTemp)) {
+ Files.move(packageIndexTemp.toPath(), packageIndex.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ } else {
+ FileDownloader.invalidateFiles(packageIndexUrl, signatureUrl);
+ }
+ } else {
+ // Move the package index to the destination when the signature is not necessary
+ Files.move(packageIndexTemp.toPath(), packageIndex.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ }
+ } catch (Exception e) {
+ throw e;
+ } finally {
+ // Delete useless temp file
+ Files.deleteIfExists(packageIndexTemp.toPath());
+ }
+ }
+
+ public boolean verifyDomain(URL url) {
+ final Collection domain = PreferencesData.
+ getCollection("http.signature_verify_domains");
+ if (domain.size() == 0) {
+ // Default domain
+ domain.add("downloads.arduino.cc");
+ }
+ if (domain.contains(url.getHost())) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public boolean checkSignature(MultiStepProgress progress, URL signatureUrl, ProgressListener progressListener, SignatureVerifier signatureVerifier, String statusText, File fileToVerify) throws Exception {
+ // Signature file name
+ final String signatureFileName = FilenameUtils.getName(signatureUrl.getPath());
+ final File packageIndexSignature = BaseNoGui.indexer.getIndexFile(signatureFileName);
+ final File packageIndexSignatureTemp = File.createTempFile(signatureFileName, ".tmp");
+
+ try {
+ // Download signature
+ download(signatureUrl, packageIndexSignatureTemp, progress, statusText, progressListener, true);
+
+ if (PreferencesData.areInsecurePackagesAllowed()) {
+ Files.move(packageIndexSignatureTemp.toPath(), packageIndexSignature.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ return true;
+ }
+
+ // Verify the signature before move the files
+ final boolean signatureVerified = signatureVerifier.isSigned(fileToVerify, packageIndexSignatureTemp);
+ if (signatureVerified) {
+ // Move if the signature is ok
+ Files.move(packageIndexSignatureTemp.toPath(), packageIndexSignature.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ } else {
+ System.err.println(format(tr("{0} file signature verification failed. File ignored."), signatureUrl.toString()));
+ }
+ return signatureVerified;
+ } catch (Exception e) {
+ throw e;
+ } finally {
+ Files.deleteIfExists(packageIndexSignatureTemp.toPath());
+ }
+ }
+
}
diff --git a/arduino-core/src/cc/arduino/contributions/GZippedJsonDownloader.java b/arduino-core/src/cc/arduino/contributions/GZippedJsonDownloader.java
index 6b6f3812327..8a717dcf26c 100644
--- a/arduino-core/src/cc/arduino/contributions/GZippedJsonDownloader.java
+++ b/arduino-core/src/cc/arduino/contributions/GZippedJsonDownloader.java
@@ -29,13 +29,16 @@
package cc.arduino.contributions;
+import cc.arduino.Constants;
import cc.arduino.utils.Progress;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.apache.commons.compress.compressors.gzip.GzipUtils;
import org.apache.commons.compress.utils.IOUtils;
+import org.apache.commons.io.FilenameUtils;
import java.io.*;
import java.net.URL;
+import java.nio.file.Files;
public class GZippedJsonDownloader {
@@ -49,18 +52,22 @@ public GZippedJsonDownloader(DownloadableContributionsDownloader downloader, URL
this.gzippedUrl = gzippedUrl;
}
- public void download(File tmpFile, Progress progress, String statusText, ProgressListener progressListener) throws Exception {
+ public void download(File tmpFile, Progress progress, String statusText, ProgressListener progressListener, boolean allowCache) throws Exception {
+ File gzipTmpFile = null;
try {
- File gzipTmpFile = new File(tmpFile.getParentFile(), GzipUtils.getCompressedFilename(tmpFile.getName()));
+ String tmpFileName = FilenameUtils.getName(new URL(Constants.LIBRARY_INDEX_URL_GZ).getPath());
+ gzipTmpFile = File.createTempFile(tmpFileName, GzipUtils.getCompressedFilename(tmpFile.getName()));
// remove eventual leftovers from previous downloads
- if (gzipTmpFile.exists()) {
- gzipTmpFile.delete();
- }
- new JsonDownloader(downloader, gzippedUrl).download(gzipTmpFile, progress, statusText, progressListener);
+ Files.deleteIfExists(gzipTmpFile.toPath());
+
+ new JsonDownloader(downloader, gzippedUrl).download(gzipTmpFile, progress, statusText, progressListener, allowCache);
decompress(gzipTmpFile, tmpFile);
- gzipTmpFile.delete();
} catch (Exception e) {
- new JsonDownloader(downloader, url).download(tmpFile, progress, statusText, progressListener);
+ new JsonDownloader(downloader, url).download(tmpFile, progress, statusText, progressListener, allowCache);
+ } finally {
+ if (gzipTmpFile != null) {
+ Files.deleteIfExists(gzipTmpFile.toPath());
+ }
}
}
diff --git a/arduino-core/src/cc/arduino/contributions/JsonDownloader.java b/arduino-core/src/cc/arduino/contributions/JsonDownloader.java
index 88f9e7783f1..5b932d08064 100644
--- a/arduino-core/src/cc/arduino/contributions/JsonDownloader.java
+++ b/arduino-core/src/cc/arduino/contributions/JsonDownloader.java
@@ -44,9 +44,9 @@ public JsonDownloader(DownloadableContributionsDownloader downloader, URL url) {
this.url = url;
}
- public void download(File tmpFile, Progress progress, String statusText, ProgressListener progressListener) throws Exception {
+ public void download(File tmpFile, Progress progress, String statusText, ProgressListener progressListener, boolean allowCache) throws Exception {
try {
- downloader.download(url, tmpFile, progress, statusText, progressListener);
+ downloader.download(url, tmpFile, progress, statusText, progressListener, allowCache);
} catch (InterruptedException e) {
// Download interrupted... just exit
}
diff --git a/arduino-core/src/cc/arduino/contributions/SignatureVerificationFailedException.java b/arduino-core/src/cc/arduino/contributions/SignatureVerificationFailedException.java
deleted file mode 100644
index 77136cb46cb..00000000000
--- a/arduino-core/src/cc/arduino/contributions/SignatureVerificationFailedException.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * This file is part of Arduino.
- *
- * Copyright 2015 Arduino LLC (http://www.arduino.cc/)
- *
- * Arduino is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
- *
- * As a special exception, you may use this file as part of a free software
- * library without restriction. Specifically, if other files instantiate
- * templates or use macros or inline functions from this file, or you compile
- * this file and link it with other files to produce an executable, this
- * file does not by itself cause the resulting executable to be covered by
- * the GNU General Public License. This exception does not however
- * invalidate any other reasons why the executable file might be covered by
- * the GNU General Public License.
- */
-
-package cc.arduino.contributions;
-
-import processing.app.I18n;
-
-import static processing.app.I18n.tr;
-
-@SuppressWarnings("serial")
-public class SignatureVerificationFailedException extends Exception {
-
- public SignatureVerificationFailedException(String filename) {
- super(I18n.format(tr("{0} file signature verification failed"), filename));
- }
-
- public SignatureVerificationFailedException(String filename, Throwable cause) {
- super(I18n.format(tr("{0} file signature verification failed"), filename), cause);
- }
-}
diff --git a/arduino-core/src/cc/arduino/contributions/SignatureVerifier.java b/arduino-core/src/cc/arduino/contributions/SignatureVerifier.java
index 6e2a80626b1..a4ea7a7ba53 100644
--- a/arduino-core/src/cc/arduino/contributions/SignatureVerifier.java
+++ b/arduino-core/src/cc/arduino/contributions/SignatureVerifier.java
@@ -50,6 +50,15 @@ public boolean isSigned(File indexFile) {
}
}
+ public boolean isSigned(File indexFile, File signature) {
+ try {
+ return verify(indexFile, signature, new File(BaseNoGui.getContentFile("lib"), "public.gpg.key"));
+ } catch (Exception e) {
+ BaseNoGui.showWarning(e.getMessage(), e.getMessage(), e);
+ return false;
+ }
+ }
+
protected abstract boolean verify(File signedFile, File signature, File publicKey) throws IOException;
}
diff --git a/arduino-core/src/cc/arduino/contributions/VersionComparator.java b/arduino-core/src/cc/arduino/contributions/VersionComparator.java
index ba0ebb639c7..af6a1fdb376 100644
--- a/arduino-core/src/cc/arduino/contributions/VersionComparator.java
+++ b/arduino-core/src/cc/arduino/contributions/VersionComparator.java
@@ -62,6 +62,10 @@ public static boolean greaterThan(String a, String b) {
return compareTo(a, b) > 0;
}
+ public static boolean greaterThanOrEqual(String a, String b) {
+ return compareTo(a, b) >= 0;
+ }
+
public static String max(String a, String b) {
return greaterThan(a, b) ? a : b;
}
@@ -73,4 +77,8 @@ public static ContributedLibrary max(ContributedLibrary a, ContributedLibrary b)
public static boolean greaterThan(ContributedLibrary a, ContributedLibrary b) {
return greaterThan(a.getParsedVersion(), b.getParsedVersion());
}
+
+ public static int compareTo(ContributedLibrary a, ContributedLibrary b) {
+ return compareTo(a.getParsedVersion(), b.getParsedVersion());
+ }
}
diff --git a/arduino-core/src/cc/arduino/contributions/VersionHelper.java b/arduino-core/src/cc/arduino/contributions/VersionHelper.java
index caf98c8f120..bead8d46ebf 100644
--- a/arduino-core/src/cc/arduino/contributions/VersionHelper.java
+++ b/arduino-core/src/cc/arduino/contributions/VersionHelper.java
@@ -65,4 +65,7 @@ public static Optional valueOf(String ver) {
}
}
+ public static int compare(String a, String b) {
+ return valueOf(a).get().compareTo(valueOf(b).get());
+ }
}
diff --git a/arduino-core/src/cc/arduino/contributions/libraries/ContributedLibrary.java b/arduino-core/src/cc/arduino/contributions/libraries/ContributedLibrary.java
index 3aa1198882c..603b46909b3 100644
--- a/arduino-core/src/cc/arduino/contributions/libraries/ContributedLibrary.java
+++ b/arduino-core/src/cc/arduino/contributions/libraries/ContributedLibrary.java
@@ -32,40 +32,70 @@
import cc.arduino.contributions.DownloadableContribution;
import processing.app.I18n;
import processing.app.packages.UserLibrary;
+import static processing.app.I18n.tr;
import java.util.Comparator;
+import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
-import static processing.app.I18n.tr;
+import cc.arduino.contributions.VersionHelper;
+
+public class ContributedLibrary extends DownloadableContribution {
+
+ private String url;
+ private String version;
+ private String checksum;
+ private long size;
+ private String archiveFileName;
+ private String name;
+ private String maintainer;
+ private String author;
+ private String website;
+ private String category;
+ private String licence;
+ private String paragraph;
+ private String sentence;
+ private ArrayList architectures;
+ private ArrayList types;
+ private ArrayList dependencies;
+ private ArrayList providesIncludes;
+
+ public String getUrl() { return url; }
+
+ public String getVersion() { return version; }
+
+ public String getChecksum() { return checksum; }
-public abstract class ContributedLibrary extends DownloadableContribution {
+ public long getSize() { return size; }
- public abstract String getName();
+ public String getArchiveFileName() { return archiveFileName; }
- public abstract String getMaintainer();
+ public String getName() { return name; }
- public abstract String getAuthor();
+ public String getMaintainer() { return maintainer; }
- public abstract String getWebsite();
+ public String getAuthor() { return author; }
- public abstract String getCategory();
+ public String getWebsite() { return website; }
- public abstract void setCategory(String category);
+ public String getCategory() { return category; }
- public abstract String getLicense();
+ public void setCategory(String category) { this.category = category; }
- public abstract String getParagraph();
+ public String getLicense() { return licence; }
- public abstract String getSentence();
+ public String getParagraph() { return paragraph; }
- public abstract List getArchitectures();
+ public String getSentence() { return sentence; }
- public abstract List getTypes();
+ public List getArchitectures() { return architectures; }
- public abstract List getRequires();
+ public List getTypes() { return types; }
- public abstract List getProvidesIncludes();
+ public List getDependencies() { return dependencies; }
+
+ public List getProvidesIncludes() { return providesIncludes; }
public static final Comparator CASE_INSENSITIVE_ORDER = (o1, o2) -> o1.getName().compareToIgnoreCase(o2.getName());
@@ -145,8 +175,8 @@ public String info() {
}
res += "\n";
res += " requires :\n";
- if (getRequires() != null)
- for (ContributedLibraryReference r : getRequires()) {
+ if (getDependencies() != null)
+ for (ContributedLibraryDependency r : getDependencies()) {
res += " " + r;
}
res += "\n";
@@ -166,7 +196,7 @@ public boolean equals(Object obj) {
String thisVersion = getParsedVersion();
String otherVersion = other.getParsedVersion();
- boolean versionEquals = (thisVersion != null && otherVersion != null
+ boolean versionEquals = (thisVersion != null
&& thisVersion.equals(otherVersion));
// Important: for legacy libs, versions are null. Two legacy libs must
@@ -176,9 +206,18 @@ public boolean equals(Object obj) {
String thisName = getName();
String otherName = other.getName();
-
- boolean nameEquals = thisName == null || otherName == null || thisName.equals(otherName);
+ boolean nameEquals = thisName != null && thisName.equals(otherName);
return versionEquals && nameEquals;
}
+
+ public boolean isBefore(ContributedLibrary other) {
+ return VersionHelper.compare(getVersion(), other.getVersion()) < 0;
+ }
+
+ @Override
+ public int hashCode() {
+ String hashingData = "CONTRIBUTEDLIB" + getName() + getVersion();
+ return hashingData.hashCode();
+ }
}
diff --git a/arduino-core/src/cc/arduino/contributions/libraries/ContributedLibraryReference.java b/arduino-core/src/cc/arduino/contributions/libraries/ContributedLibraryDependency.java
similarity index 85%
rename from arduino-core/src/cc/arduino/contributions/libraries/ContributedLibraryReference.java
rename to arduino-core/src/cc/arduino/contributions/libraries/ContributedLibraryDependency.java
index f4edd57327f..4da5ba6f75b 100644
--- a/arduino-core/src/cc/arduino/contributions/libraries/ContributedLibraryReference.java
+++ b/arduino-core/src/cc/arduino/contributions/libraries/ContributedLibraryDependency.java
@@ -29,16 +29,17 @@
package cc.arduino.contributions.libraries;
-public abstract class ContributedLibraryReference {
+public class ContributedLibraryDependency {
- public abstract String getName();
+ private String name;
+ private String version;
- public abstract String getMaintainer();
+ public String getName() { return name; }
- public abstract String getVersion();
+ public String getVersion() { return version; }
@Override
public String toString() {
- return getName() + " " + getVersion() + " (" + getMaintainer() + ")";
+ return getName() + " " + getVersion();
}
}
diff --git a/arduino-core/src/cc/arduino/contributions/libraries/LibrariesIndex.java b/arduino-core/src/cc/arduino/contributions/libraries/LibrariesIndex.java
index 7998525a152..02ff0475cfa 100644
--- a/arduino-core/src/cc/arduino/contributions/libraries/LibrariesIndex.java
+++ b/arduino-core/src/cc/arduino/contributions/libraries/LibrariesIndex.java
@@ -29,6 +29,7 @@
package cc.arduino.contributions.libraries;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
@@ -37,9 +38,15 @@
import java.util.Optional;
import java.util.stream.Collectors;
-public abstract class LibrariesIndex {
+import cc.arduino.contributions.VersionComparator;
- public abstract List getLibraries();
+public class LibrariesIndex {
+
+ private ArrayList list = new ArrayList<>();
+
+ public List getLibraries() {
+ return list;
+ }
public List find(final String name) {
return getLibraries().stream() //
@@ -98,4 +105,78 @@ public Optional getInstalled(String name) {
ContributedLibraryReleases rel = new ContributedLibraryReleases(find(name));
return rel.getInstalled();
}
+
+ public List resolveDependeciesOf(ContributedLibrary library) {
+ List solution = new ArrayList<>();
+ solution.add(library);
+ if (resolveDependeciesOf(solution, library)) {
+ return solution;
+ } else {
+ return null;
+ }
+ }
+
+ public boolean resolveDependeciesOf(List solution,
+ ContributedLibrary library) {
+ List requirements = library.getDependencies();
+ if (requirements == null) {
+ // No deps for this library, great!
+ return true;
+ }
+
+ for (ContributedLibraryDependency dep : requirements) {
+
+ // If the current solution already contains this dependency, skip over
+ boolean alreadyInSolution = solution.stream()
+ .anyMatch(l -> l.getName().equals(dep.getName()));
+ if (alreadyInSolution)
+ continue;
+
+ // Generate possible matching dependencies
+ List possibleDeps = findMatchingDependencies(dep);
+
+ // If there are no dependencies available add as "missing" lib
+ if (possibleDeps.isEmpty()) {
+ solution.add(new UnavailableContributedLibrary(dep));
+ continue;
+ }
+
+ // Pick the installed version if available
+ ContributedLibrary selected;
+ Optional installed = possibleDeps.stream()
+ .filter(l -> l.getInstalledLibrary().isPresent()).findAny();
+ if (installed.isPresent()) {
+ selected = installed.get();
+ } else {
+ // otherwise pick the latest version
+ selected = possibleDeps.stream().reduce(VersionComparator::max).get();
+ }
+
+ // Add dependency to the solution and process recursively
+ solution.add(selected);
+ if (!resolveDependeciesOf(solution, selected)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private List findMatchingDependencies(ContributedLibraryDependency dep) {
+ List available = find(dep.getName());
+ if (dep.getVersion() == null || dep.getVersion().isEmpty())
+ return available;
+
+ // XXX: The following part is actually never reached. The use of version
+ // constraints requires a much complex backtracking algorithm, the following
+ // is just a draft placeholder.
+
+// List match = available.stream()
+// // TODO: add more complex version comparators (> >= < <= ~ 1.0.* 1.*...)
+// .filter(candidate -> candidate.getParsedVersion()
+// .equals(dep.getVersionRequired()))
+// .collect(Collectors.toList());
+// return match;
+
+ return available;
+ }
}
diff --git a/arduino-core/src/cc/arduino/contributions/libraries/LibrariesIndexer.java b/arduino-core/src/cc/arduino/contributions/libraries/LibrariesIndexer.java
index c43a4a09423..57460fc19e1 100644
--- a/arduino-core/src/cc/arduino/contributions/libraries/LibrariesIndexer.java
+++ b/arduino-core/src/cc/arduino/contributions/libraries/LibrariesIndexer.java
@@ -36,7 +36,6 @@
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.module.mrbean.MrBeanModule;
import org.apache.commons.compress.utils.IOUtils;
import processing.app.BaseNoGui;
import processing.app.I18n;
@@ -76,7 +75,7 @@ public LibrariesIndexer(File preferencesFolder) {
}
public void parseIndex() throws IOException {
- index = new EmptyLibrariesIndex(); // Fallback
+ index = new LibrariesIndex(); // Fallback
if (!indexFile.exists()) {
return;
@@ -92,7 +91,6 @@ private void parseIndex(File file) throws IOException {
try {
indexIn = new FileInputStream(file);
ObjectMapper mapper = new ObjectMapper();
- mapper.registerModule(new MrBeanModule());
mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
mapper.configure(DeserializationFeature.EAGER_DESERIALIZER_FETCH, true);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
@@ -116,7 +114,11 @@ private void parseIndex(File file) throws IOException {
}
public void setLibrariesFolders(List folders) {
- librariesFolders = folders;
+ this.librariesFolders = folders;
+ }
+
+ public void setLibrariesFoldersAndRescan(List folders) {
+ setLibrariesFolders(folders);
rescanLibraries();
}
diff --git a/arduino-core/src/cc/arduino/contributions/libraries/LibraryInstaller.java b/arduino-core/src/cc/arduino/contributions/libraries/LibraryInstaller.java
index 4b4fb7f7dbb..64a15e7b2b3 100644
--- a/arduino-core/src/cc/arduino/contributions/libraries/LibraryInstaller.java
+++ b/arduino-core/src/cc/arduino/contributions/libraries/LibraryInstaller.java
@@ -31,10 +31,13 @@
import cc.arduino.Constants;
import cc.arduino.contributions.DownloadableContributionsDownloader;
+import cc.arduino.contributions.GPGDetachedSignatureVerifier;
import cc.arduino.contributions.GZippedJsonDownloader;
import cc.arduino.contributions.ProgressListener;
import cc.arduino.utils.ArchiveExtractor;
import cc.arduino.utils.MultiStepProgress;
+import cc.arduino.utils.network.FileDownloader;
+import org.apache.commons.io.FilenameUtils;
import processing.app.BaseNoGui;
import processing.app.I18n;
import processing.app.Platform;
@@ -43,16 +46,21 @@
import java.io.File;
import java.io.IOException;
import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Optional;
import static processing.app.I18n.tr;
public class LibraryInstaller {
-
private final Platform platform;
+ private final GPGDetachedSignatureVerifier signatureVerifier;
- public LibraryInstaller(Platform platform) {
+ public LibraryInstaller(Platform platform, GPGDetachedSignatureVerifier signatureVerifier) {
this.platform = platform;
+ this.signatureVerifier = signatureVerifier;
}
public synchronized void updateIndex(ProgressListener progressListener) throws Exception {
@@ -61,48 +69,88 @@ public synchronized void updateIndex(ProgressListener progressListener) throws E
DownloadableContributionsDownloader downloader = new DownloadableContributionsDownloader(BaseNoGui.librariesIndexer.getStagingFolder());
// Step 1: Download index
File outputFile = BaseNoGui.librariesIndexer.getIndexFile();
- File tmpFile = new File(outputFile.getAbsolutePath() + ".tmp");
+ // Create temp files
+ String signatureFileName = FilenameUtils.getName(new URL(Constants.LIBRARY_INDEX_URL).getPath());
+ File libraryIndexTemp = File.createTempFile(signatureFileName, ".tmp");
+ final URL libraryURL = new URL(Constants.LIBRARY_INDEX_URL);
+ final URL libraryGzURL = new URL(Constants.LIBRARY_INDEX_URL_GZ);
+ final String statusText = tr("Downloading libraries index...");
try {
- GZippedJsonDownloader gZippedJsonDownloader = new GZippedJsonDownloader(downloader, new URL(Constants.LIBRARY_INDEX_URL), new URL(Constants.LIBRARY_INDEX_URL_GZ));
- gZippedJsonDownloader.download(tmpFile, progress, tr("Downloading libraries index..."), progressListener);
+ GZippedJsonDownloader gZippedJsonDownloader = new GZippedJsonDownloader(downloader, libraryURL, libraryGzURL);
+ gZippedJsonDownloader.download(libraryIndexTemp, progress, statusText, progressListener, true);
} catch (InterruptedException e) {
// Download interrupted... just exit
return;
}
progress.stepDone();
- // TODO: Check downloaded index
-
- // Replace old index with the updated one
- if (outputFile.exists())
- outputFile.delete();
- if (!tmpFile.renameTo(outputFile))
- throw new Exception(tr("An error occurred while updating libraries index!"));
+ URL signatureUrl = new URL(libraryURL.toString() + ".sig");
+ if (downloader.verifyDomain(signatureUrl)) {
+ if (downloader.checkSignature(progress, signatureUrl, progressListener, signatureVerifier, statusText, libraryIndexTemp)) {
+ // Replace old index with the updated one
+ if (libraryIndexTemp.length() > 0) {
+ Files.move(libraryIndexTemp.toPath(), outputFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ }
+ } else {
+ FileDownloader.invalidateFiles(libraryGzURL, libraryURL, signatureUrl);
+ }
+ }
// Step 2: Parse index
BaseNoGui.librariesIndexer.parseIndex();
// Step 3: Rescan index
rescanLibraryIndex(progress, progressListener);
+
+ }
+
+ public void install(ContributedLibrary lib, ProgressListener progressListener) throws Exception {
+ ArrayList libs = new ArrayList<>();
+ libs.add(lib);
+ install(libs, progressListener);
+ }
+
+ public synchronized void install(List libs, ProgressListener progressListener) throws Exception {
+ MultiStepProgress progress = new MultiStepProgress(3 * libs.size() + 1);
+
+ for (ContributedLibrary lib : libs) {
+ // Do install library (3 steps)
+ performInstall(lib, progressListener, progress);
+ }
+
+ // Rescan index (1 step)
+ rescanLibraryIndex(progress, progressListener);
}
- public synchronized void install(ContributedLibrary lib, Optional mayReplacedLib, ProgressListener progressListener) throws Exception {
+ private void performInstall(ContributedLibrary lib, ProgressListener progressListener, MultiStepProgress progress) throws Exception {
if (lib.isLibraryInstalled()) {
System.out.println(I18n.format(tr("Library is already installed: {0}:{1}"), lib.getName(), lib.getParsedVersion()));
return;
}
- DownloadableContributionsDownloader downloader = new DownloadableContributionsDownloader(BaseNoGui.librariesIndexer.getStagingFolder());
+ File libsFolder = BaseNoGui.getSketchbookLibrariesFolder().folder;
+ File destFolder = new File(libsFolder, lib.getName().replaceAll(" ", "_"));
- final MultiStepProgress progress = new MultiStepProgress(3);
+ // Check if we are replacing an already installed lib
+ LibrariesIndex index = BaseNoGui.librariesIndexer.getIndex();
+ Optional replacedLib = index.find(lib.getName()).stream() //
+ .filter(l -> l.getInstalledLibrary().isPresent()) //
+ .filter(l -> l.getInstalledLibrary().get().getInstalledFolder().equals(destFolder)) //
+ .findAny();
+ if (!replacedLib.isPresent() && destFolder.exists()) {
+ System.out.println(I18n.format(tr("Library {0} is already installed in: {1}"), lib.getName(), destFolder));
+ return;
+ }
+ DownloadableContributionsDownloader downloader = new DownloadableContributionsDownloader(BaseNoGui.librariesIndexer.getStagingFolder());
// Step 1: Download library
try {
- downloader.download(lib, progress, I18n.format(tr("Downloading library: {0}"), lib.getName()), progressListener);
+ downloader.download(lib, progress, I18n.format(tr("Downloading library: {0}"), lib.getName()), progressListener, false);
} catch (InterruptedException e) {
// Download interrupted... just exit
return;
}
+ progress.stepDone();
// TODO: Extract to temporary folders and move to the final destination only
// once everything is successfully unpacked. If the operation fails remove
@@ -111,7 +159,6 @@ public synchronized void install(ContributedLibrary lib, Optional getArchitectures() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public List getTypes() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public List getDependencies() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public String getUrl() {
+ return "";
+ }
+
+ @Override
+ public String getVersion() {
+ return version;
+ }
+
+ @Override
+ public String getChecksum() {
+ return "";
+ }
+
+ @Override
+ public long getSize() {
+ return 0;
+ }
+
+ @Override
+ public String getArchiveFileName() {
+ return "";
+ }
+
+ @Override
+ public String toString() {
+ return "!" + super.toString();
+ }
+
+ @Override
+ public List getProvidesIncludes() {
+ return new ArrayList<>();
+ }
+}
diff --git a/arduino-core/src/cc/arduino/contributions/packages/ContributedBoard.java b/arduino-core/src/cc/arduino/contributions/packages/ContributedBoard.java
index 7017ced5d10..c29110376bc 100644
--- a/arduino-core/src/cc/arduino/contributions/packages/ContributedBoard.java
+++ b/arduino-core/src/cc/arduino/contributions/packages/ContributedBoard.java
@@ -29,8 +29,9 @@
package cc.arduino.contributions.packages;
-public interface ContributedBoard {
+public class ContributedBoard {
- String getName();
+ private String name;
+ public String getName() { return name; }
}
diff --git a/arduino-core/src/cc/arduino/contributions/packages/ContributedHelp.java b/arduino-core/src/cc/arduino/contributions/packages/ContributedHelp.java
index a8f998f668b..2156f5c48c9 100644
--- a/arduino-core/src/cc/arduino/contributions/packages/ContributedHelp.java
+++ b/arduino-core/src/cc/arduino/contributions/packages/ContributedHelp.java
@@ -29,8 +29,9 @@
package cc.arduino.contributions.packages;
-public abstract class ContributedHelp {
+public class ContributedHelp {
- public abstract String getOnline();
+ private String online;
+ public String getOnline() { return online; }
}
diff --git a/arduino-core/src/cc/arduino/contributions/packages/ContributedPackage.java b/arduino-core/src/cc/arduino/contributions/packages/ContributedPackage.java
index 507a63be11d..8b260527ae5 100644
--- a/arduino-core/src/cc/arduino/contributions/packages/ContributedPackage.java
+++ b/arduino-core/src/cc/arduino/contributions/packages/ContributedPackage.java
@@ -29,25 +29,33 @@
package cc.arduino.contributions.packages;
+import java.util.ArrayList;
import java.util.List;
-public abstract class ContributedPackage {
+public class ContributedPackage {
- public abstract String getName();
+ private String name;
+ private String maintainer;
+ private String websiteURL;
+ private String email;
+ private ArrayList platforms = new ArrayList();
+ private ArrayList tools = new ArrayList();
+ private ContributedHelp help;
+ private boolean trusted;
- public abstract String getMaintainer();
+ public String getName() { return name; }
- public abstract String getWebsiteURL();
+ public String getMaintainer() { return maintainer; }
- public abstract String getEmail();
+ public String getWebsiteURL() { return websiteURL; }
- public abstract List getPlatforms();
+ public String getEmail() { return email; }
- public abstract List getTools();
+ public List getPlatforms() { return platforms; }
- public abstract ContributedHelp getHelp();
+ public List getTools() { return tools; }
- private boolean trusted;
+ public ContributedHelp getHelp() { return help; }
public ContributedPlatform findPlatform(String architecture, String version) {
if (architecture == null || version == null) {
diff --git a/arduino-core/src/cc/arduino/contributions/packages/ContributedPlatform.java b/arduino-core/src/cc/arduino/contributions/packages/ContributedPlatform.java
index 3149aea1e2f..6c11a4cc26b 100644
--- a/arduino-core/src/cc/arduino/contributions/packages/ContributedPlatform.java
+++ b/arduino-core/src/cc/arduino/contributions/packages/ContributedPlatform.java
@@ -29,32 +29,67 @@
package cc.arduino.contributions.packages;
-import cc.arduino.contributions.DownloadableContribution;
-import com.fasterxml.jackson.annotation.JsonIgnore;
-
import java.io.File;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
-public abstract class ContributedPlatform extends DownloadableContribution {
+import cc.arduino.contributions.DownloadableContribution;
- public abstract String getName();
+public class ContributedPlatform extends DownloadableContribution {
+
+ private String url;
+ private String version;
+ private long size;
+ private String archiveFileName;
+ private String name;
+ private String category;
+ private String architecture;
+ private String checksum;
+ private ArrayList toolsDependencies = new ArrayList<>();
+ private ArrayList boards = new ArrayList<>();
+ private ContributedHelp help;
+ private boolean installed;
+ private File installedFolder;
+ private boolean builtIn;
+ private Map resolvedToolReferences;
+ private ContributedPackage parentPackage;
+ private boolean deprecated;
- public abstract String getCategory();
+ @Override
+ public String getUrl() { return url; }
- public abstract void setCategory(String category);
+ @Override
+ public String getVersion() { return version; }
- public abstract String getArchitecture();
+ @Override
+ public long getSize() { return size; }
@Override
- public abstract String getChecksum();
+ public String getArchiveFileName() { return archiveFileName; }
- public abstract List getToolsDependencies();
+ public String getName() { return name; }
- public abstract List getBoards();
+ public String getCategory() { return category; }
- public abstract ContributedHelp getHelp();
+ public void setCategory(String category) { this.category = category; }
- private boolean installed;
+ public String getArchitecture() { return architecture; }
+
+ @Override
+ public String getChecksum() { return checksum; }
+
+ public List getToolsDependencies() { return toolsDependencies; }
+
+ public List getBoards() { return boards; }
+
+ public ContributedHelp getHelp() { return help; }
public boolean isInstalled() {
return installed;
@@ -64,8 +99,6 @@ public void setInstalled(boolean installed) {
this.installed = installed;
}
- private File installedFolder;
-
public File getInstalledFolder() {
return installedFolder;
}
@@ -74,8 +107,6 @@ public void setInstalledFolder(File installedFolder) {
this.installedFolder = installedFolder;
}
- private boolean builtIn;
-
public boolean isBuiltIn() {
return builtIn;
}
@@ -90,10 +121,6 @@ public void setBuiltIn(boolean builtIn) {
return px - py;
};
- private Map resolvedToolReferences;
-
- private ContributedPackage parentPackage;
-
public List getResolvedTools() {
return new LinkedList<>(resolvedToolReferences.values());
}
@@ -131,6 +158,14 @@ public void setParentPackage(ContributedPackage parentPackage) {
this.parentPackage = parentPackage;
}
+ public boolean isDeprecated() {
+ return deprecated;
+ }
+
+ public void setDeprecated(boolean deprecated) {
+ this.deprecated = deprecated;
+ }
+
@Override
public String toString() {
return getParsedVersion();
diff --git a/arduino-core/src/cc/arduino/contributions/packages/ContributedTool.java b/arduino-core/src/cc/arduino/contributions/packages/ContributedTool.java
index cafeb9aef84..915dc28489b 100644
--- a/arduino-core/src/cc/arduino/contributions/packages/ContributedTool.java
+++ b/arduino-core/src/cc/arduino/contributions/packages/ContributedTool.java
@@ -33,17 +33,24 @@
import processing.app.Platform;
import java.io.File;
+import java.util.ArrayList;
import java.util.List;
-public abstract class ContributedTool {
+public class ContributedTool {
- public abstract String getName();
+ private String name;
+ private String version;
+ private ArrayList systems = new ArrayList();
+ private boolean installed;
+ private File installedFolder;
+ private boolean builtIn;
+ private ContributedPackage contributedPackage;
- public abstract String getVersion();
+ public String getName() { return name; }
- public abstract List getSystems();
+ public String getVersion() { return version; }
- private boolean installed;
+ public List getSystems() { return systems; }
public boolean isInstalled() {
return installed;
@@ -53,8 +60,6 @@ public void setInstalled(boolean installed) {
this.installed = installed;
}
- private File installedFolder;
-
public File getInstalledFolder() {
return installedFolder;
}
@@ -63,8 +68,6 @@ public void setInstalledFolder(File installedFolder) {
this.installedFolder = installedFolder;
}
- private boolean builtIn;
-
public boolean isBuiltIn() {
return builtIn;
}
@@ -73,8 +76,6 @@ public void setBuiltIn(boolean builtIn) {
this.builtIn = builtIn;
}
- private ContributedPackage contributedPackage;
-
public ContributedPackage getPackage() {
return contributedPackage;
}
diff --git a/arduino-core/src/cc/arduino/contributions/packages/ContributedToolReference.java b/arduino-core/src/cc/arduino/contributions/packages/ContributedToolReference.java
index 7d86f234f23..3faf0cbbd31 100644
--- a/arduino-core/src/cc/arduino/contributions/packages/ContributedToolReference.java
+++ b/arduino-core/src/cc/arduino/contributions/packages/ContributedToolReference.java
@@ -31,13 +31,17 @@
import java.util.Collection;
-public abstract class ContributedToolReference {
+public class ContributedToolReference {
- public abstract String getName();
+ private String name;
+ private String version;
+ private String packager;
- public abstract String getVersion();
+ public String getName() { return name; }
- public abstract String getPackager();
+ public String getVersion() { return version; }
+
+ public String getPackager() { return packager; }
public ContributedTool resolve(Collection packages) {
for (ContributedPackage pack : packages) {
diff --git a/arduino-core/src/cc/arduino/contributions/packages/ContributionInstaller.java b/arduino-core/src/cc/arduino/contributions/packages/ContributionInstaller.java
index c75d1352a8c..1540ce54c40 100644
--- a/arduino-core/src/cc/arduino/contributions/packages/ContributionInstaller.java
+++ b/arduino-core/src/cc/arduino/contributions/packages/ContributionInstaller.java
@@ -62,7 +62,6 @@
import static processing.app.I18n.tr;
public class ContributionInstaller {
-
private final Platform platform;
private final SignatureVerifier signatureVerifier;
@@ -98,7 +97,7 @@ public synchronized List install(ContributedPlatform contributedPlatform
// Download all
try {
// Download platform
- downloader.download(contributedPlatform, progress, tr("Downloading boards definitions."), progressListener);
+ downloader.download(contributedPlatform, progress, tr("Downloading boards definitions."), progressListener, false);
progress.stepDone();
// Download tools
@@ -106,7 +105,7 @@ public synchronized List install(ContributedPlatform contributedPlatform
for (ContributedTool tool : tools) {
String msg = format(tr("Downloading tools ({0}/{1})."), i, tools.size());
i++;
- downloader.download(tool.getDownloadableContribution(platform), progress, msg, progressListener);
+ downloader.download(tool.getDownloadableContribution(platform), progress, msg, progressListener, false);
progress.stepDone();
}
} catch (InterruptedException e) {
@@ -122,10 +121,10 @@ public synchronized List install(ContributedPlatform contributedPlatform
// all the temporary folders and abort installation.
List> resolvedToolReferences = contributedPlatform
- .getResolvedToolReferences().entrySet().stream()
- .filter((entry) -> !entry.getValue().isInstalled()
- || entry.getValue().isBuiltIn())
- .collect(Collectors.toList());
+ .getResolvedToolReferences().entrySet().stream()
+ .filter((entry) -> !entry.getValue().isInstalled()
+ || entry.getValue().isBuiltIn())
+ .collect(Collectors.toList());
int i = 1;
for (Map.Entry entry : resolvedToolReferences) {
@@ -141,7 +140,7 @@ public synchronized List install(ContributedPlatform contributedPlatform
assert toolContrib.getDownloadedFile() != null;
new ArchiveExtractor(platform).extract(toolContrib.getDownloadedFile(), destFolder.toFile(), 1);
try {
- findAndExecutePostInstallScriptIfAny(destFolder.toFile(), contributedPlatform.getParentPackage().isTrusted(), PreferencesData.getBoolean(Constants.PREF_CONTRIBUTIONS_TRUST_ALL));
+ findAndExecutePostInstallScriptIfAny(destFolder.toFile(), contributedPlatform.getParentPackage().isTrusted(), PreferencesData.areInsecurePackagesAllowed());
} catch (IOException e) {
errors.add(tr("Error running post install script"));
}
@@ -160,7 +159,7 @@ public synchronized List install(ContributedPlatform contributedPlatform
contributedPlatform.setInstalled(true);
contributedPlatform.setInstalledFolder(destFolder);
try {
- findAndExecutePostInstallScriptIfAny(destFolder, contributedPlatform.getParentPackage().isTrusted(), PreferencesData.getBoolean(Constants.PREF_CONTRIBUTIONS_TRUST_ALL));
+ findAndExecutePostInstallScriptIfAny(destFolder, contributedPlatform.getParentPackage().isTrusted(), PreferencesData.areInsecurePackagesAllowed());
} catch (IOException e) {
e.printStackTrace();
errors.add(tr("Error running post install script"));
@@ -240,7 +239,7 @@ public synchronized List remove(ContributedPlatform contributedPlatform)
}
List errors = new LinkedList<>();
try {
- findAndExecutePreUninstallScriptIfAny(contributedPlatform.getInstalledFolder(), contributedPlatform.getParentPackage().isTrusted(), PreferencesData.getBoolean(Constants.PREF_CONTRIBUTIONS_TRUST_ALL));
+ findAndExecutePreUninstallScriptIfAny(contributedPlatform.getInstalledFolder(), contributedPlatform.getParentPackage().isTrusted(), PreferencesData.areInsecurePackagesAllowed());
} catch (IOException e) {
errors.add(tr("Error running post install script"));
}
@@ -278,76 +277,27 @@ public synchronized List remove(ContributedPlatform contributedPlatform)
return errors;
}
- public synchronized List updateIndex(ProgressListener progressListener) throws Exception {
+ public synchronized void updateIndex(ProgressListener progressListener) {
MultiStepProgress progress = new MultiStepProgress(1);
- List downloadedPackageIndexFilesAccumulator = new LinkedList<>();
- downloadIndexAndSignature(progress, downloadedPackageIndexFilesAccumulator, Constants.PACKAGE_INDEX_URL, progressListener);
+ final DownloadableContributionsDownloader downloader = new DownloadableContributionsDownloader(BaseNoGui.indexer.getStagingFolder());
- Set packageIndexURLs = new HashSet<>();
- String additionalURLs = PreferencesData.get(Constants.PREF_BOARDS_MANAGER_ADDITIONAL_URLS, "");
- if (!"".equals(additionalURLs)) {
- packageIndexURLs.addAll(Arrays.asList(additionalURLs.split(",")));
- }
+ final Set packageIndexURLs = new HashSet<>(
+ PreferencesData.getCollection(Constants.PREF_BOARDS_MANAGER_ADDITIONAL_URLS)
+ );
+ packageIndexURLs.add(Constants.PACKAGE_INDEX_URL);
- for (String packageIndexURL : packageIndexURLs) {
+ for (String packageIndexURLString : packageIndexURLs) {
try {
- downloadIndexAndSignature(progress, downloadedPackageIndexFilesAccumulator, packageIndexURL, progressListener);
+ // Extract the file name from the URL
+ final URL packageIndexURL = new URL(packageIndexURLString);
+
+ downloader.downloadIndexAndSignature(progress, packageIndexURL, progressListener, signatureVerifier);
} catch (Exception e) {
System.err.println(e.getMessage());
}
}
progress.stepDone();
-
- return downloadedPackageIndexFilesAccumulator;
- }
-
- private void downloadIndexAndSignature(MultiStepProgress progress, List downloadedPackagedIndexFilesAccumulator, String packageIndexUrl, ProgressListener progressListener) throws Exception {
- File packageIndex = download(progress, packageIndexUrl, progressListener);
- downloadedPackagedIndexFilesAccumulator.add(packageIndex.getName());
- try {
- File packageIndexSignature = download(progress, packageIndexUrl + ".sig", progressListener);
- boolean signatureVerified = signatureVerifier.isSigned(packageIndex);
- if (signatureVerified) {
- downloadedPackagedIndexFilesAccumulator.add(packageIndexSignature.getName());
- } else {
- downloadedPackagedIndexFilesAccumulator.remove(packageIndex.getName());
- Files.delete(packageIndex.toPath());
- Files.delete(packageIndexSignature.toPath());
- System.err.println(I18n.format(tr("{0} file signature verification failed. File ignored."), packageIndexUrl));
- }
- } catch (Exception e) {
- //ignore errors
- }
- }
-
- private File download(MultiStepProgress progress, String packageIndexUrl, ProgressListener progressListener) throws Exception {
- String statusText = tr("Downloading platforms index...");
- URL url = new URL(packageIndexUrl);
- String[] urlPathParts = url.getFile().split("/");
- File outputFile = BaseNoGui.indexer.getIndexFile(urlPathParts[urlPathParts.length - 1]);
- File tmpFile = new File(outputFile.getAbsolutePath() + ".tmp");
- DownloadableContributionsDownloader downloader = new DownloadableContributionsDownloader(BaseNoGui.indexer.getStagingFolder());
- boolean noResume = true;
- downloader.download(url, tmpFile, progress, statusText, progressListener, noResume);
-
- Files.deleteIfExists(outputFile.toPath());
- Files.move(tmpFile.toPath(), outputFile.toPath());
-
- return outputFile;
- }
-
- public synchronized void deleteUnknownFiles(List downloadedPackageIndexFiles) throws IOException {
- File preferencesFolder = BaseNoGui.indexer.getIndexFile(".").getParentFile();
- File[] additionalPackageIndexFiles = preferencesFolder.listFiles(new PackageIndexFilenameFilter(Constants.DEFAULT_INDEX_FILE_NAME));
- if (additionalPackageIndexFiles == null) {
- return;
- }
- for (File additionalPackageIndexFile : additionalPackageIndexFiles) {
- if (!downloadedPackageIndexFiles.contains(additionalPackageIndexFile.getName())) {
- Files.delete(additionalPackageIndexFile.toPath());
- }
- }
}
}
diff --git a/arduino-core/src/cc/arduino/contributions/packages/ContributionsIndex.java b/arduino-core/src/cc/arduino/contributions/packages/ContributionsIndex.java
index 4ca65863939..6b86b0fb234 100644
--- a/arduino-core/src/cc/arduino/contributions/packages/ContributionsIndex.java
+++ b/arduino-core/src/cc/arduino/contributions/packages/ContributionsIndex.java
@@ -38,9 +38,10 @@
import java.util.List;
import java.util.stream.Collectors;
-public abstract class ContributionsIndex {
+public class ContributionsIndex {
- public abstract List getPackages();
+ private ArrayList packages = new ArrayList<>();
+ public List getPackages() { return packages; }
public ContributedPackage findPackage(String packageName) {
for (ContributedPackage pack : getPackages()) {
diff --git a/arduino-core/src/cc/arduino/contributions/packages/ContributionsIndexer.java b/arduino-core/src/cc/arduino/contributions/packages/ContributionsIndexer.java
index a1ca10929d9..b0db6ca1981 100644
--- a/arduino-core/src/cc/arduino/contributions/packages/ContributionsIndexer.java
+++ b/arduino-core/src/cc/arduino/contributions/packages/ContributionsIndexer.java
@@ -31,14 +31,14 @@
import cc.arduino.Constants;
import cc.arduino.contributions.DownloadableContribution;
-import cc.arduino.contributions.SignatureVerificationFailedException;
import cc.arduino.contributions.SignatureVerifier;
-
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.module.mrbean.MrBeanModule;
import org.apache.commons.compress.utils.IOUtils;
+import org.apache.commons.io.FilenameUtils;
+
+import processing.app.BaseNoGui;
import processing.app.Platform;
import processing.app.PreferencesData;
import processing.app.debug.TargetPackage;
@@ -51,6 +51,8 @@
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;
@@ -73,7 +75,7 @@ public ContributionsIndexer(File preferencesFolder, File builtInHardwareFolder,
this.builtInHardwareFolder = builtInHardwareFolder;
this.platform = platform;
this.signatureVerifier = signatureVerifier;
- index = new EmptyContributionIndex();
+ index = new ContributionsIndex();
packagesFolder = new File(preferencesFolder, "packages");
stagingFolder = new File(preferencesFolder, "staging" + File.separator + "packages");
}
@@ -87,30 +89,35 @@ public void parseIndex() throws Exception {
File defaultIndexFile = getIndexFile(Constants.DEFAULT_INDEX_FILE_NAME);
if (defaultIndexFile.exists()) {
// Check main index signature
- if (!PreferencesData.getBoolean("allow_insecure_packages") && !signatureVerifier.isSigned(defaultIndexFile)) {
- throw new SignatureVerificationFailedException(Constants.DEFAULT_INDEX_FILE_NAME);
+ if (signatureVerifier.isSigned(defaultIndexFile)) {
+ mergeContributions(defaultIndexFile);
+ } else if (PreferencesData.areInsecurePackagesAllowed()) {
+ System.err.println(format(tr("Warning: forced trusting untrusted contributions")));
+ mergeContributions(defaultIndexFile);
+ } else {
+ BaseNoGui
+ .showWarning(Constants.DEFAULT_INDEX_FILE_NAME,
+ tr("A package index has an invalid signature and needs to be updated.\n"
+ + "Please open the Board Manager from the menu\n"
+ + "\n" //
+ + " Tools -> Board -> Board Manager\n"
+ + "\nto update it"),
+ null);
}
-
- mergeContributions(defaultIndexFile);
}
// Set main and bundled indexes as trusted
index.getPackages().forEach(pack -> pack.setTrusted(true));
// Overlay 3rd party indexes
- File[] indexFiles = preferencesFolder.listFiles(new TestPackageIndexFilenameFilter(new PackageIndexFilenameFilter(Constants.DEFAULT_INDEX_FILE_NAME)));
-
- if (indexFiles != null) {
- for (File indexFile : indexFiles) {
- try {
- mergeContributions(indexFile);
- } catch (JsonProcessingException e) {
- System.err.println(format(tr("Skipping contributed index file {0}, parsing error occured:"), indexFile));
- System.err.println(e);
- }
+ List indexFiles = get3rdPartyIndexFiles();
+ for (File indexFile : indexFiles) {
+ try {
+ mergeContributions(indexFile);
+ } catch (JsonProcessingException e) {
+ System.err.println(format(tr("Skipping contributed index file {0}, parsing error occured:"), indexFile));
+ System.err.println(e);
}
- } else {
- System.err.println(format(tr("Error reading package indexes folder: {0}\n(maybe a permission problem?)"), preferencesFolder));
}
// Fill tools and toolsDependency cross references
@@ -137,13 +144,41 @@ public void parseIndex() throws Exception {
index.fillCategories();
}
+ private List get3rdPartyIndexFiles() {
+ List indexFiles = new ArrayList<>();
+ for (String urlString : PreferencesData.getCollection(Constants.PREF_BOARDS_MANAGER_ADDITIONAL_URLS)) {
+ URL url;
+ try {
+ url = new URL(urlString);
+ String filename = FilenameUtils.getName(url.getPath());
+ indexFiles.add(getIndexFile(filename));
+ } catch (MalformedURLException e) {
+ System.err.println(format(tr("Malformed Additional Board Manager URL '{0}': {1}"), urlString, e.getMessage()));
+ }
+ }
+
+ File[] testIndexFiles = preferencesFolder.listFiles((dir, name) -> {
+ if (!new File(dir, name).isFile())
+ return false;
+ if (!name.startsWith("test_package_") || !name.endsWith("_index.json"))
+ return false;
+ return true;
+ });
+ if (testIndexFiles == null) {
+ System.err.println(
+ format(tr("Error reading package indexes folder: {0}\n(maybe a permission problem?)"), preferencesFolder));
+ }
+ indexFiles.addAll(Arrays.asList(testIndexFiles));
+ return indexFiles;
+ }
+
private void mergeContributions(File indexFile) throws IOException {
if (!indexFile.exists())
return;
ContributionsIndex contributionsIndex = parseIndex(indexFile);
boolean signed = signatureVerifier.isSigned(indexFile);
- boolean trustall = PreferencesData.getBoolean(Constants.PREF_CONTRIBUTIONS_TRUST_ALL);
+ boolean trustall = PreferencesData.areInsecurePackagesAllowed();
for (ContributedPackage contributedPackage : contributionsIndex.getPackages()) {
contributedPackage.setTrusted(signed || trustall);
@@ -198,7 +233,6 @@ private ContributionsIndex parseIndex(File indexFile) throws IOException {
try {
inputStream = new FileInputStream(indexFile);
ObjectMapper mapper = new ObjectMapper();
- mapper.registerModule(new MrBeanModule());
mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
mapper.configure(DeserializationFeature.EAGER_DESERIALIZER_FETCH, true);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
@@ -215,7 +249,7 @@ public void syncWithFilesystem() throws IOException {
}
private void syncBuiltInHardware() throws IOException {
- if (index == null) {
+ if (index == null || builtInHardwareFolder == null) {
return;
}
for (File folder : builtInHardwareFolder.listFiles(ONLY_DIRS)) {
diff --git a/arduino-core/src/cc/arduino/contributions/packages/EmptyContributionIndex.java b/arduino-core/src/cc/arduino/contributions/packages/EmptyContributionIndex.java
deleted file mode 100644
index 200dce3c2fe..00000000000
--- a/arduino-core/src/cc/arduino/contributions/packages/EmptyContributionIndex.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * This file is part of Arduino.
- *
- * Copyright 2014 Arduino LLC (http://www.arduino.cc/)
- *
- * Arduino is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
- *
- * As a special exception, you may use this file as part of a free software
- * library without restriction. Specifically, if other files instantiate
- * templates or use macros or inline functions from this file, or you compile
- * this file and link it with other files to produce an executable, this
- * file does not by itself cause the resulting executable to be covered by
- * the GNU General Public License. This exception does not however
- * invalidate any other reasons why the executable file might be covered by
- * the GNU General Public License.
- */
-
-package cc.arduino.contributions.packages;
-
-import java.util.ArrayList;
-import java.util.List;
-
-class EmptyContributionIndex extends ContributionsIndex {
- List packs = new ArrayList<>();
-
- @Override
- public List getPackages() {
- return packs;
- }
-}
\ No newline at end of file
diff --git a/arduino-core/src/cc/arduino/contributions/packages/HostDependentDownloadableContribution.java b/arduino-core/src/cc/arduino/contributions/packages/HostDependentDownloadableContribution.java
index cb7137d8638..2b692a54fb8 100644
--- a/arduino-core/src/cc/arduino/contributions/packages/HostDependentDownloadableContribution.java
+++ b/arduino-core/src/cc/arduino/contributions/packages/HostDependentDownloadableContribution.java
@@ -32,9 +32,26 @@
import cc.arduino.contributions.DownloadableContribution;
import processing.app.Platform;
-public abstract class HostDependentDownloadableContribution extends DownloadableContribution {
+public class HostDependentDownloadableContribution extends DownloadableContribution {
- public abstract String getHost();
+ private String url;
+ private String version;
+ private String checksum;
+ private long size;
+ private String archiveFileName;
+ private String host;
+
+ public String getUrl() { return url; }
+
+ public String getVersion() { return version; }
+
+ public String getChecksum() { return checksum; }
+
+ public long getSize() { return size; }
+
+ public String getArchiveFileName() { return archiveFileName; }
+
+ public String getHost() { return host; }
@Override
public String toString() {
diff --git a/arduino-core/src/cc/arduino/contributions/packages/PackageIndexFilenameFilter.java b/arduino-core/src/cc/arduino/contributions/packages/PackageIndexFilenameFilter.java
deleted file mode 100644
index bfc016750a5..00000000000
--- a/arduino-core/src/cc/arduino/contributions/packages/PackageIndexFilenameFilter.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * This file is part of Arduino.
- *
- * Copyright 2015 Arduino LLC (http://www.arduino.cc/)
- *
- * Arduino is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
- *
- * As a special exception, you may use this file as part of a free software
- * library without restriction. Specifically, if other files instantiate
- * templates or use macros or inline functions from this file, or you compile
- * this file and link it with other files to produce an executable, this
- * file does not by itself cause the resulting executable to be covered by
- * the GNU General Public License. This exception does not however
- * invalidate any other reasons why the executable file might be covered by
- * the GNU General Public License.
- */
-
-package cc.arduino.contributions.packages;
-
-import java.io.File;
-import java.io.FilenameFilter;
-
-public class PackageIndexFilenameFilter implements FilenameFilter {
-
- private final String defaultPackageIndexFileName;
-
- public PackageIndexFilenameFilter(String defaultPackageIndexFileName) {
- this.defaultPackageIndexFileName = defaultPackageIndexFileName;
- }
-
- @Override
- public boolean accept(File file, String name) {
- return new File(file, name).isFile() && !defaultPackageIndexFileName.equals(name) && name.startsWith("package_") && name.endsWith("_index.json");
- }
-}
diff --git a/arduino-core/src/cc/arduino/contributions/packages/TestPackageIndexFilenameFilter.java b/arduino-core/src/cc/arduino/contributions/packages/TestPackageIndexFilenameFilter.java
deleted file mode 100644
index fdcd08384bb..00000000000
--- a/arduino-core/src/cc/arduino/contributions/packages/TestPackageIndexFilenameFilter.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * This file is part of Arduino.
- *
- * Copyright 2015 Arduino LLC (http://www.arduino.cc/)
- *
- * Arduino is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
- *
- * As a special exception, you may use this file as part of a free software
- * library without restriction. Specifically, if other files instantiate
- * templates or use macros or inline functions from this file, or you compile
- * this file and link it with other files to produce an executable, this
- * file does not by itself cause the resulting executable to be covered by
- * the GNU General Public License. This exception does not however
- * invalidate any other reasons why the executable file might be covered by
- * the GNU General Public License.
- */
-
-package cc.arduino.contributions.packages;
-
-import java.io.File;
-import java.io.FilenameFilter;
-
-public class TestPackageIndexFilenameFilter implements FilenameFilter {
-
- private final FilenameFilter parent;
-
- public TestPackageIndexFilenameFilter(FilenameFilter parent) {
- this.parent = parent;
- }
-
- public TestPackageIndexFilenameFilter() {
- this(null);
- }
-
- @Override
- public boolean accept(File file, String name) {
- boolean result = false;
- if (parent != null) {
- result = parent.accept(file, name);
- }
- result = result || (new File(file, name).isFile() && name.startsWith("test_package_") && name.endsWith("_index.json"));
- return result;
- }
-}
diff --git a/arduino-core/src/cc/arduino/net/CustomProxySelector.java b/arduino-core/src/cc/arduino/net/CustomProxySelector.java
index 6bb29879e10..32a0894e89e 100644
--- a/arduino-core/src/cc/arduino/net/CustomProxySelector.java
+++ b/arduino-core/src/cc/arduino/net/CustomProxySelector.java
@@ -75,7 +75,7 @@ public Proxy getProxyFor(URI uri) throws IOException, ScriptException, NoSuchMet
}
private Proxy pacProxy(String pac, URI uri) throws IOException, ScriptException, NoSuchMethodException {
- setAuthenticator(preferences.get(Constants.PREF_PROXY_AUTO_USERNAME), preferences.get(Constants.PREF_PROXY_AUTO_PASSWORD));
+ setAuthenticator(preferences.get(Constants.PREF_PROXY_USERNAME), preferences.get(Constants.PREF_PROXY_PASSWORD));
URLConnection urlConnection = new URL(pac).openConnection();
urlConnection.connect();
@@ -141,7 +141,7 @@ private URL toUrl(URI uri) {
}
private Proxy manualProxy() {
- setAuthenticator(preferences.get(Constants.PREF_PROXY_MANUAL_USERNAME), preferences.get(Constants.PREF_PROXY_MANUAL_PASSWORD));
+ setAuthenticator(preferences.get(Constants.PREF_PROXY_USERNAME), preferences.get(Constants.PREF_PROXY_PASSWORD));
Proxy.Type type = Proxy.Type.valueOf(preferences.get(Constants.PREF_PROXY_MANUAL_TYPE));
return new Proxy(type, new InetSocketAddress(preferences.get(Constants.PREF_PROXY_MANUAL_HOSTNAME), Integer.valueOf(preferences.get(Constants.PREF_PROXY_MANUAL_PORT))));
}
diff --git a/arduino-core/src/cc/arduino/packages/BoardPort.java b/arduino-core/src/cc/arduino/packages/BoardPort.java
index 0e85ffe135d..397c0818a49 100644
--- a/arduino-core/src/cc/arduino/packages/BoardPort.java
+++ b/arduino-core/src/cc/arduino/packages/BoardPort.java
@@ -29,22 +29,36 @@
package cc.arduino.packages;
+import processing.app.BaseNoGui;
+import processing.app.debug.TargetBoard;
+import processing.app.debug.TargetPackage;
+import processing.app.debug.TargetPlatform;
import processing.app.helpers.PreferencesMap;
public class BoardPort {
- private String address;
- private String protocol;
+ private String address; // unique name for this port, used by Preferences
+ private String protocol; // how to communicate, used for Ports menu sections
+ private String protocolLabel; // protocol extended name to display on GUI
private String boardName;
- private String vid;
- private String pid;
- private String iserial;
- private String label;
- private final PreferencesMap prefs;
- private boolean online;
+ private String label; // friendly name shown in Ports menu
+ private final PreferencesMap identificationPrefs; // data to match with boards.txt
+ private final PreferencesMap prefs; // "vendorId", "productId", "serialNumber"
+ private boolean online; // used by SerialBoardsLister (during upload??)
public BoardPort() {
this.prefs = new PreferencesMap();
+ this.identificationPrefs = new PreferencesMap();
+ }
+
+ public BoardPort(BoardPort bp) {
+ prefs = new PreferencesMap(bp.prefs);
+ identificationPrefs = new PreferencesMap(bp.identificationPrefs);
+ address = bp.address;
+ protocol = bp.protocol;
+ boardName = bp.boardName;
+ label = bp.label;
+ online = bp.online;
}
public String getAddress() {
@@ -63,6 +77,14 @@ public void setProtocol(String protocol) {
this.protocol = protocol;
}
+ public String getProtocolLabel() {
+ return protocolLabel;
+ }
+
+ public void setProtocolLabel(String protocolLabel) {
+ this.protocolLabel = protocolLabel;
+ }
+
public String getBoardName() {
return boardName;
}
@@ -75,6 +97,10 @@ public PreferencesMap getPrefs() {
return prefs;
}
+ public PreferencesMap getIdentificationPrefs() {
+ return identificationPrefs;
+ }
+
public void setLabel(String label) {
this.label = label;
}
@@ -91,28 +117,80 @@ public boolean isOnline() {
return online;
}
- public void setVIDPID(String vid, String pid) {
- this.vid = vid;
- this.pid = pid;
- }
-
- public String getVID() {
- return vid;
- }
-
- public String getPID() {
- return pid;
- }
-
- public void setISerial(String iserial) {
- this.iserial = iserial;
- }
- public String getISerial() {
- return iserial;
- }
-
@Override
public String toString() {
- return this.address+"_"+this.vid+"_"+this.pid;
+ return this.address;
+ }
+
+ public String toCompleteString() {
+ return this.address + "_" + this.getPrefs().get("vid") + "_" + this.getPrefs().get("pid");
+ }
+
+ // Search for the board which matches identificationPrefs.
+ // If found, boardName is set to the name from boards.txt
+ // and the board is returned. If not found, null is returned.
+ public TargetBoard searchMatchingBoard() {
+ if (identificationPrefs == null || identificationPrefs.isEmpty()) return null;
+ for (TargetPackage targetPackage : BaseNoGui.packages.values()) {
+ for (TargetPlatform targetPlatform : targetPackage.getPlatforms().values()) {
+ for (TargetBoard board : targetPlatform.getBoards().values()) {
+ if (matchesBoard(board)) {
+ setBoardName(board.getName());
+ return board;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ public boolean matchesBoard(TargetBoard board) {
+ PreferencesMap identificationProps = getIdentificationPrefs();
+ PreferencesMap boardProps = board.getPreferences();
+
+ String wildMatcher = identificationProps.get(".");
+ if (wildMatcher != null) {
+ if (wildMatcher.equals(board.getId())) {
+ return true;
+ }
+ if (wildMatcher.equals(board.getFQBN())) {
+ return true;
+ }
+ }
+
+ // Identification properties are defined in boards.txt with a ".N" suffix
+ // for example:
+ //
+ // uno.name=Arduino/Genuino Uno
+ // uno.vid.0=0x2341
+ // uno.pid.0=0x0043
+ // uno.vid.1=0x2341
+ // uno.pid.1=0x0001
+ // uno.vid.2=0x2A03
+ // uno.pid.2=0x0043
+ // uno.vid.3=0x2341
+ // uno.pid.3=0x0243
+ //
+ // so we must search starting from suffix ".0" and increasing until we
+ // found a match or the board has no more identification properties defined
+
+ for (int suffix = 0;; suffix++) {
+ boolean found = true;
+ for (String prop : identificationProps.keySet()) {
+ String value = identificationProps.get(prop);
+ prop += "." + suffix;
+ if (!boardProps.containsKey(prop)) {
+ return false;
+ }
+ if (!value.equalsIgnoreCase(boardProps.get(prop))) {
+ found = false;
+ break;
+ }
+ }
+ if (found) {
+ return true;
+ }
+ }
}
+
}
diff --git a/arduino-core/src/cc/arduino/packages/DiscoveryManager.java b/arduino-core/src/cc/arduino/packages/DiscoveryManager.java
index b1ec50d85c4..21876ffc47e 100644
--- a/arduino-core/src/cc/arduino/packages/DiscoveryManager.java
+++ b/arduino-core/src/cc/arduino/packages/DiscoveryManager.java
@@ -29,13 +29,22 @@
package cc.arduino.packages;
-import cc.arduino.packages.discoverers.NetworkDiscovery;
-import cc.arduino.packages.discoverers.SerialDiscovery;
+import static processing.app.I18n.format;
+import static processing.app.I18n.tr;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
+import java.io.File;
-import static processing.app.I18n.tr;
+import cc.arduino.packages.discoverers.PluggableDiscovery;
+import cc.arduino.packages.discoverers.serial.SerialDiscovery;
+import cc.arduino.packages.discoverers.NetworkDiscovery;
+import processing.app.PreferencesData;
+import processing.app.debug.TargetPackage;
+import processing.app.debug.TargetPlatform;
+import processing.app.helpers.PreferencesMap;
+import processing.app.helpers.StringReplacer;
public class DiscoveryManager {
@@ -43,17 +52,57 @@ public class DiscoveryManager {
private final SerialDiscovery serialDiscoverer = new SerialDiscovery();
private final NetworkDiscovery networkDiscoverer = new NetworkDiscovery();
- public DiscoveryManager() {
+// private final Map packages;
+
+ public DiscoveryManager(Map packages) {
+// this.packages = packages;
+
discoverers = new ArrayList<>();
discoverers.add(serialDiscoverer);
discoverers.add(networkDiscoverer);
+ // Search for discoveries in installed packages
+ for (TargetPackage targetPackage : packages.values()) {
+ for (TargetPlatform platform: targetPackage.getPlatforms().values()) {
+ //System.out.println("installed: "+platform);
+ PreferencesMap prefs = platform.getPreferences().subTree("discovery");
+ PreferencesMap pathPrefs = new PreferencesMap();
+ File platformFolder = platform.getFolder();
+ pathPrefs.put("runtime.platform.path", platformFolder.getAbsolutePath());
+ pathPrefs.put("runtime.hardware.path", platformFolder.getParentFile().getAbsolutePath());
+ for (String discoveryName : prefs.firstLevelMap().keySet()) {
+ PreferencesMap discoveryPrefs = prefs.subTree(discoveryName);
+
+ String pattern = discoveryPrefs.get("pattern");
+ if (pattern == null) {
+ System.out.println(format(tr("No recipes defined for discovery '{0}'"),discoveryName));
+ continue;
+ }
+ try {
+ if (PreferencesData.getBoolean("discovery.debug")) {
+ System.out.println("found discovery: " + discoveryName + " -> " + pattern);
+ System.out.println("with pathnames -> " + pathPrefs);
+ System.out.println("with preferencess -> " + discoveryPrefs);
+ }
+ pattern = StringReplacer.replaceFromMapping(pattern, PreferencesData.getMap());
+ pattern = StringReplacer.replaceFromMapping(pattern, pathPrefs);
+ String[] cmd = StringReplacer.formatAndSplit(pattern, discoveryPrefs);
+ discoverers.add(new PluggableDiscovery(discoveryName, cmd));
+ } catch (Exception e) {
+ if (PreferencesData.getBoolean("discovery.debug")) {
+ System.out.println(format(tr("Could not start discovery '{0}': {1}"), discoveryName, e.getMessage()));
+ }
+ }
+ }
+ }
+ }
+
// Start all discoverers
for (Discovery d : discoverers) {
try {
new Thread(d).start();
} catch (Exception e) {
- System.err.println(tr("Error starting discovery method: ") + d.getClass());
+ System.err.println(tr("Error starting discovery method: ") + d.toString());
e.printStackTrace();
}
}
diff --git a/arduino-core/src/cc/arduino/packages/Uploader.java b/arduino-core/src/cc/arduino/packages/Uploader.java
index 54b5c7abd1e..0847027e2ac 100644
--- a/arduino-core/src/cc/arduino/packages/Uploader.java
+++ b/arduino-core/src/cc/arduino/packages/Uploader.java
@@ -37,35 +37,35 @@
import processing.app.debug.MessageConsumer;
import processing.app.debug.MessageSiphon;
import processing.app.helpers.ProcessUtils;
-import processing.app.helpers.StringUtils;
import java.io.File;
-import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
+import org.apache.commons.lang3.StringUtils;
+
import static processing.app.I18n.tr;
public abstract class Uploader implements MessageConsumer {
- private static final List STRINGS_TO_SUPPRESS;
- private static final List AVRDUDE_PROBLEMS;
+ private static final String[] STRINGS_TO_SUPPRESS;
+ private static final String[] AVRDUDE_PROBLEMS;
static {
- STRINGS_TO_SUPPRESS = Arrays.asList("Connecting to programmer:",
+ STRINGS_TO_SUPPRESS = new String[] {"Connecting to programmer:",
"Found programmer: Id = \"CATERIN\"; type = S",
"Software Version = 1.0; No Hardware Version given.",
"Programmer supports auto addr increment.",
"Programmer supports buffered memory access with buffersize=128 bytes.",
- "Programmer supports the following devices:", "Device code: 0x44");
+ "Programmer supports the following devices:", "Device code: 0x44"};
- AVRDUDE_PROBLEMS = Arrays.asList("Programmer is not responding",
+ AVRDUDE_PROBLEMS = new String[] {"Programmer is not responding",
"programmer is not responding",
"protocol error", "avrdude: ser_open(): can't open device",
"avrdude: ser_drain(): read error",
"avrdude: ser_send(): write error",
- "avrdude: error: buffered memory access not supported.");
+ "avrdude: error: buffered memory access not supported."};
}
protected final boolean verbose;
@@ -106,7 +106,7 @@ public String getAuthorizationKey() {
}
// static field for last executed programmer process ID
- static protected Process programmerPid;
+ static public Process programmerPid;
protected boolean executeUploadCommand(Collection command) throws Exception {
return executeUploadCommand(command.toArray(new String[command.size()]));
@@ -155,7 +155,7 @@ public String getFailureMessage() {
@Override
public void message(String s) {
// selectively suppress a bunch of avrdude output for AVR109/Caterina that should already be quelled but isn't
- if (!verbose && StringUtils.stringContainsOneOf(s, STRINGS_TO_SUPPRESS)) {
+ if (!verbose && StringUtils.containsAny(s, STRINGS_TO_SUPPRESS)) {
s = "";
}
@@ -175,8 +175,8 @@ public void message(String s) {
error = tr("Device is not responding, check the right serial port is selected or RESET the board right before exporting");
return;
}
- if (StringUtils.stringContainsOneOf(s, AVRDUDE_PROBLEMS)) {
- error = tr("Problem uploading to board. See http://www.arduino.cc/en/Guide/Troubleshooting#upload for suggestions.");
+ if (StringUtils.containsAny(s, AVRDUDE_PROBLEMS)) {
+ error = tr("Problem uploading to board. See https://support.arduino.cc/hc/en-us/sections/360003198300 for suggestions.");
return;
}
if (s.contains("Expected signature")) {
diff --git a/arduino-core/src/cc/arduino/packages/discoverers/NetworkDiscovery.java b/arduino-core/src/cc/arduino/packages/discoverers/NetworkDiscovery.java
index 71395669052..8619a92f0fd 100644
--- a/arduino-core/src/cc/arduino/packages/discoverers/NetworkDiscovery.java
+++ b/arduino-core/src/cc/arduino/packages/discoverers/NetworkDiscovery.java
@@ -113,15 +113,12 @@ public void serviceResolved(ServiceEvent serviceEvent) {
String label = name + " at " + address;
if (board != null && BaseNoGui.packages != null) {
String boardName = BaseNoGui.getPlatform().resolveDeviceByBoardID(BaseNoGui.packages, board);
- if (boardName != null) {
- label += " (" + boardName + ")";
- }
+ port.setBoardName(boardName);
} else if (description != null) {
label += " (" + description + ")";
}
port.setAddress(address);
- port.setBoardName(name);
port.setProtocol("network");
port.setLabel(label);
@@ -165,7 +162,7 @@ public void stop() {
@Override
public List listDiscoveredBoards() {
- synchronized (reachableBoardPorts) {
+ synchronized (reachableBoardPorts) {
return getBoardPortsDiscoveredWithJmDNS();
}
}
@@ -179,8 +176,8 @@ public List listDiscoveredBoards(boolean complete) {
public void setReachableBoardPorts(List newReachableBoardPorts) {
synchronized (reachableBoardPorts) {
- this.reachableBoardPorts.clear();
- this.reachableBoardPorts.addAll(newReachableBoardPorts);
+ reachableBoardPorts.clear();
+ reachableBoardPorts.addAll(newReachableBoardPorts);
}
}
diff --git a/arduino-core/src/cc/arduino/packages/discoverers/PluggableDiscovery.java b/arduino-core/src/cc/arduino/packages/discoverers/PluggableDiscovery.java
new file mode 100644
index 00000000000..3f7202fa6a9
--- /dev/null
+++ b/arduino-core/src/cc/arduino/packages/discoverers/PluggableDiscovery.java
@@ -0,0 +1,293 @@
+/*
+ * This file is part of Arduino.
+ *
+ * Arduino is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * As a special exception, you may use this file as part of a free software
+ * library without restriction. Specifically, if other files instantiate
+ * templates or use macros or inline functions from this file, or you compile
+ * this file and link it with other files to produce an executable, this
+ * file does not by itself cause the resulting executable to be covered by
+ * the GNU General Public License. This exception does not however
+ * invalidate any other reasons why the executable file might be covered by
+ * the GNU General Public License.
+ *
+ * Copyright 2018 Arduino SA (http://www.arduino.cc/)
+ */
+
+package cc.arduino.packages.discoverers;
+
+import static processing.app.I18n.format;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import cc.arduino.packages.BoardPort;
+import cc.arduino.packages.Discovery;
+import processing.app.PreferencesData;
+
+public class PluggableDiscovery implements Discovery {
+
+ private final String discoveryName;
+ private final String[] cmd;
+ private final List portList = new ArrayList<>();
+ private Process program=null;
+ private Thread pollingThread;
+
+ private void debug(String x) {
+ if (PreferencesData.getBoolean("discovery.debug"))
+ System.out.println(discoveryName + ": " + x);
+ }
+
+ public PluggableDiscovery(String discoveryName, String[] cmd) {
+ this.cmd = cmd;
+ this.discoveryName = discoveryName;
+ }
+
+ @Override
+ public void run() {
+ // this method is started as a new thread, it will constantly listen
+ // to the discovery tool and keep track of the discovered ports
+ try {
+ start();
+ InputStream input = program.getInputStream();
+ JsonFactory factory = new JsonFactory();
+ JsonParser parser = factory.createParser(input);
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.setVisibility(PropertyAccessor.ALL, Visibility.NONE);
+ mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
+ mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+ while (program != null && program.isAlive()) {
+ JsonNode tree = mapper.readTree(parser);
+ if (tree == null) {
+ if (program != null && program.isAlive()) {
+ System.err.println(format("{0}: Invalid json message", discoveryName));
+ }
+ break;
+ }
+ debug("Received json: " + tree);
+
+ processJsonNode(mapper, tree);
+ }
+ debug("thread exit normally");
+ } catch (InterruptedException e) {
+ debug("thread exit by interrupt");
+ e.printStackTrace();
+ } catch (Exception e) {
+ debug("thread exit other exception");
+ e.printStackTrace();
+ }
+ try {
+ stop();
+ } catch (Exception e) {
+ }
+ }
+
+ private void processJsonNode(ObjectMapper mapper, JsonNode node) {
+ JsonNode eventTypeNode = node.get("eventType");
+ if (eventTypeNode == null) {
+ System.err.println(format("{0}: Invalid message, missing eventType", discoveryName));
+ return;
+ }
+
+ switch (eventTypeNode.asText()) {
+ case "error":
+ try {
+ PluggableDiscoveryMessage msg = mapper.treeToValue(node, PluggableDiscoveryMessage.class);
+ debug("error: " + msg.getMessage());
+ if (msg.getMessage().contains("START_SYNC")) {
+ startPolling();
+ }
+ } catch (JsonProcessingException e) {
+ e.printStackTrace();
+ }
+ return;
+
+ case "list":
+ JsonNode portsNode = node.get("ports");
+ if (portsNode == null) {
+ System.err.println(format("{0}: Invalid message, missing ports list", discoveryName));
+ return;
+ }
+ if (!portsNode.isArray()) {
+ System.err.println(format("{0}: Invalid message, ports list should be an array", discoveryName));
+ return;
+ }
+
+ synchronized (portList) {
+ portList.clear();
+ }
+ portsNode.forEach(portNode -> {
+ BoardPort port = mapJsonNodeToBoardPort(mapper, node);
+ if (port != null) {
+ addOrUpdate(port);
+ }
+ });
+ return;
+
+ // Messages for SYNC updates
+
+ case "add":
+ BoardPort addedPort = mapJsonNodeToBoardPort(mapper, node);
+ if (addedPort != null) {
+ addOrUpdate(addedPort);
+ }
+ return;
+
+ case "remove":
+ BoardPort removedPort = mapJsonNodeToBoardPort(mapper, node);
+ if (removedPort != null) {
+ remove(removedPort);
+ }
+ return;
+
+ default:
+ debug("Invalid event: " + eventTypeNode.asText());
+ return;
+ }
+ }
+
+ private BoardPort mapJsonNodeToBoardPort(ObjectMapper mapper, JsonNode node) {
+ try {
+ BoardPort port = mapper.treeToValue(node.get("port"), BoardPort.class);
+ // if no label, use address
+ if (port.getLabel() == null || port.getLabel().isEmpty()) {
+ port.setLabel(port.getAddress());
+ }
+ port.searchMatchingBoard();
+ return port;
+ } catch (JsonProcessingException e) {
+ System.err.println(format("{0}: Invalid BoardPort message", discoveryName));
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ @Override
+ public void start() throws Exception {
+ try {
+ debug("Starting: " + StringUtils.join(cmd, " "));
+ program = Runtime.getRuntime().exec(cmd);
+ } catch (Exception e) {
+ program = null;
+ return;
+ }
+ debug("START_SYNC");
+ write("START_SYNC\n");
+ pollingThread = null;
+ }
+
+ private void startPolling() {
+ // Discovery tools not supporting START_SYNC require a periodic
+ // LIST command. A second thread is created to send these
+ // commands, while the run() thread above listens for the
+ // discovery tool output.
+ debug("START");
+ write("START\n");
+ Thread pollingThread = new Thread() {
+ public void run() {
+ try {
+ while (program != null && program.isAlive()) {
+ debug("LIST");
+ write("LIST\n");
+ sleep(2500);
+ }
+ } catch (Exception e) {
+ }
+ }
+ };
+ pollingThread.start();
+ }
+
+ @Override
+ public void stop() throws Exception {
+ if (pollingThread != null) {
+ pollingThread.interrupt();
+ pollingThread = null;
+ }
+ write("STOP\n");
+ if (program != null) {
+ program.destroy();
+ program = null;
+ }
+ }
+
+ private void write(String command) {
+ if (program != null && program.isAlive()) {
+ OutputStream out = program.getOutputStream();
+ try {
+ out.write(command.getBytes());
+ out.flush();
+ } catch (Exception e) {
+ }
+ }
+ }
+
+ private void addOrUpdate(BoardPort port) {
+ String address = port.getAddress();
+ if (address == null)
+ return; // address required for "add" & "remove"
+
+ synchronized (portList) {
+ // if address already on the list, discard old info
+ portList.removeIf(bp -> address.equals(bp.getAddress()));
+ portList.add(port);
+ }
+ }
+
+ private void remove(BoardPort port) {
+ String address = port.getAddress();
+ if (address == null)
+ return; // address required for "add" & "remove"
+ synchronized (portList) {
+ portList.removeIf(bp -> address.equals(bp.getAddress()));
+ }
+ }
+
+ @Override
+ public List listDiscoveredBoards() {
+ synchronized (portList) {
+ return new ArrayList<>(portList);
+ }
+ }
+
+ @Override
+ public List listDiscoveredBoards(boolean complete) {
+ // XXX: parameter "complete "is really needed?
+ // should be checked on all existing discoveries
+ synchronized (portList) {
+ return new ArrayList<>(portList);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return discoveryName;
+ }
+}
diff --git a/arduino-core/src/cc/arduino/contributions/libraries/EmptyLibrariesIndex.java b/arduino-core/src/cc/arduino/packages/discoverers/PluggableDiscoveryMessage.java
similarity index 77%
rename from arduino-core/src/cc/arduino/contributions/libraries/EmptyLibrariesIndex.java
rename to arduino-core/src/cc/arduino/packages/discoverers/PluggableDiscoveryMessage.java
index f85ab46ef3c..3a377d0643f 100644
--- a/arduino-core/src/cc/arduino/contributions/libraries/EmptyLibrariesIndex.java
+++ b/arduino-core/src/cc/arduino/packages/discoverers/PluggableDiscoveryMessage.java
@@ -1,8 +1,6 @@
/*
* This file is part of Arduino.
*
- * Copyright 2016 Arduino LLC (http://www.arduino.cc/)
- *
* Arduino is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
@@ -25,20 +23,21 @@
* the GNU General Public License. This exception does not however
* invalidate any other reasons why the executable file might be covered by
* the GNU General Public License.
+ *
+ * Copyright 2018 Arduino SA (http://www.arduino.cc/)
*/
-package cc.arduino.contributions.libraries;
-
-import java.util.ArrayList;
-import java.util.List;
+package cc.arduino.packages.discoverers;
-public class EmptyLibrariesIndex extends LibrariesIndex {
+public class PluggableDiscoveryMessage {
+ private String eventType; // "add", "remove", "error"
+ private String message; // optional message, e.g. "START_SYNC not supported"
- private List list = new ArrayList<>();
-
- @Override
- public List getLibraries() {
- return list;
+ public String getEventType() {
+ return eventType;
}
+ public String getMessage() {
+ return message;
+ }
}
diff --git a/arduino-core/src/cc/arduino/packages/discoverers/SerialDiscovery.java b/arduino-core/src/cc/arduino/packages/discoverers/SerialDiscovery.java
deleted file mode 100644
index 4de78552c97..00000000000
--- a/arduino-core/src/cc/arduino/packages/discoverers/SerialDiscovery.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * This file is part of Arduino.
- *
- * Arduino is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
- *
- * As a special exception, you may use this file as part of a free software
- * library without restriction. Specifically, if other files instantiate
- * templates or use macros or inline functions from this file, or you compile
- * this file and link it with other files to produce an executable, this
- * file does not by itself cause the resulting executable to be covered by
- * the GNU General Public License. This exception does not however
- * invalidate any other reasons why the executable file might be covered by
- * the GNU General Public License.
- *
- * Copyright 2013 Arduino LLC (http://www.arduino.cc/)
- */
-
-package cc.arduino.packages.discoverers;
-
-import cc.arduino.packages.BoardPort;
-import cc.arduino.packages.Discovery;
-import cc.arduino.packages.discoverers.serial.SerialBoardsLister;
-
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Timer;
-
-public class SerialDiscovery implements Discovery, Runnable {
-
- private Timer serialBoardsListerTimer;
- private final List serialBoardPorts;
- private SerialBoardsLister serialBoardsLister = new SerialBoardsLister(this);
-
- public SerialDiscovery() {
- this.serialBoardPorts = new LinkedList<>();
- }
-
- @Override
- public List listDiscoveredBoards() {
- return getSerialBoardPorts(false);
- }
-
- @Override
- public List listDiscoveredBoards(boolean complete) {
- return getSerialBoardPorts(complete);
- }
-
- private List getSerialBoardPorts(boolean complete) {
- if (complete) {
- return new LinkedList<>(serialBoardPorts);
- }
- List onlineBoardPorts = new LinkedList<>();
- for (BoardPort port : serialBoardPorts) {
- if (port.isOnline() == true) {
- onlineBoardPorts.add(port);
- }
- }
- return onlineBoardPorts;
- }
-
- public void setSerialBoardPorts(List newSerialBoardPorts) {
- serialBoardPorts.clear();
- serialBoardPorts.addAll(newSerialBoardPorts);
- }
-
- public void forceRefresh() {
- serialBoardsLister.retriggerDiscovery(false);
- }
-
- public void setUploadInProgress(boolean param) {
- serialBoardsLister.uploadInProgress = param;
- }
-
- public void pausePolling(boolean param) { serialBoardsLister.pausePolling = param;}
-
- @Override
- public void run() {
- start();
- }
-
- @Override
- public void start() {
- this.serialBoardsListerTimer = new Timer(SerialBoardsLister.class.getName());
- serialBoardsLister.start(serialBoardsListerTimer);
- }
-
- @Override
- public void stop() {
- this.serialBoardsListerTimer.purge();
- }
-}
diff --git a/arduino-core/src/cc/arduino/packages/discoverers/serial/SerialBoardsLister.java b/arduino-core/src/cc/arduino/packages/discoverers/serial/SerialDiscovery.java
similarity index 53%
rename from arduino-core/src/cc/arduino/packages/discoverers/serial/SerialBoardsLister.java
rename to arduino-core/src/cc/arduino/packages/discoverers/serial/SerialDiscovery.java
index d055a921ab4..fc86c950ecd 100644
--- a/arduino-core/src/cc/arduino/packages/discoverers/serial/SerialBoardsLister.java
+++ b/arduino-core/src/cc/arduino/packages/discoverers/serial/SerialDiscovery.java
@@ -30,75 +30,121 @@
package cc.arduino.packages.discoverers.serial;
import cc.arduino.packages.BoardPort;
-import cc.arduino.packages.discoverers.SerialDiscovery;
+import cc.arduino.packages.Discovery;
import processing.app.BaseNoGui;
import processing.app.Platform;
import processing.app.debug.TargetBoard;
+import processing.app.helpers.BoardCloudResolver;
import java.util.*;
-public class SerialBoardsLister extends TimerTask {
+public class SerialDiscovery implements Discovery, Runnable {
- private final SerialDiscovery serialDiscovery;
- private final List boardPorts = new LinkedList<>();
- private List oldPorts = new LinkedList<>();
+ private Timer serialBoardsListerTimer;
+ private final List serialBoardPorts = new ArrayList<>();
+ private final List boardPorts = new ArrayList<>();
+ private final List oldPorts = new ArrayList<>();
public boolean uploadInProgress = false;
public boolean pausePolling = false;
- private BoardPort oldUploadBoardPort = null;
+ private final BoardCloudResolver boardCloudResolver = new BoardCloudResolver();
- public SerialBoardsLister(SerialDiscovery serialDiscovery) {
- this.serialDiscovery = serialDiscovery;
+
+ @Override
+ public List listDiscoveredBoards() {
+ return listDiscoveredBoards(false);
+ }
+
+ @Override
+ public synchronized List listDiscoveredBoards(boolean complete) {
+ if (complete) {
+ return new ArrayList<>(serialBoardPorts);
+ }
+ List onlineBoardPorts = new ArrayList<>();
+ for (BoardPort port : serialBoardPorts) {
+ if (port.isOnline() == true) {
+ onlineBoardPorts.add(port);
+ }
+ }
+ return onlineBoardPorts;
+ }
+
+ public synchronized void setSerialBoardPorts(List newSerialBoardPorts) {
+ serialBoardPorts.clear();
+ serialBoardPorts.addAll(newSerialBoardPorts);
}
- public void start(Timer timer) {
- timer.schedule(this, 0, 1000);
+ public void setUploadInProgress(boolean param) {
+ uploadInProgress = param;
}
- public synchronized void retriggerDiscovery(boolean polled) {
+ public void pausePolling(boolean param) {
+ pausePolling = param;
+ }
+
+ @Override
+ public void run() {
+ start();
+ }
+
+ @Override
+ public void start() {
+ serialBoardsListerTimer = new Timer(SerialDiscovery.class.getName());
+ serialBoardsListerTimer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ if (BaseNoGui.packages != null && !pausePolling) {
+ forceRefresh();
+ }
+ }
+ }, 0, 1000);
+ }
+
+ @Override
+ public void stop() {
+ serialBoardsListerTimer.cancel();
+ }
+
+ public synchronized void forceRefresh() {
Platform platform = BaseNoGui.getPlatform();
if (platform == null) {
return;
}
- if (polled && pausePolling) {
- return;
- }
-
List ports = platform.listSerials();
if (ports.equals(oldPorts)) {
return;
}
- // if (updating) {}
- // a port will disappear, another will appear
- // use this information to "merge" the boards
- // updating must be signaled by SerialUpload class
-
oldPorts.clear();
oldPorts.addAll(ports);
+ // set unreachable ports offline
for (BoardPort board : boardPorts) {
- if (ports.contains(board.toString())) {
- if (board.isOnline()) {
- ports.remove(ports.indexOf(board.toString()));
- }
- } else {
- if (uploadInProgress && board.isOnline()) {
- oldUploadBoardPort = board;
- }
+ if (!ports.contains(board.toCompleteString())) {
board.setOnlineStatus(false);
}
}
+ // add information for newly added ports
for (String newPort : ports) {
- String[] parts = newPort.split("_");
+ // if port has been already discovered bring it back online
+ BoardPort oldBoardPort = boardPorts.stream() //
+ .filter(bp -> bp.toCompleteString().equalsIgnoreCase(newPort)) //
+ .findAny().orElse(null);
+ if (oldBoardPort != null) {
+ oldBoardPort.setOnlineStatus(true);
+ continue;
+ }
+ // Otherwise build a BoardPort object out of it and add it to
+ // to the known boardPorts
+
+ String[] parts = newPort.split("_");
if (parts.length < 3) {
// something went horribly wrong
continue;
}
-
if (parts.length > 3) {
// port name with _ in it (like CP2102 on OSX)
for (int i = 1; i < (parts.length-2); i++) {
@@ -110,76 +156,37 @@ public synchronized void retriggerDiscovery(boolean polled) {
String port = parts[0];
- Map boardData = platform.resolveDeviceByVendorIdProductId(port, BaseNoGui.packages);
-
- BoardPort boardPort = null;
- boolean updatingInfos = false;
- int i = 0;
- // create new board or update existing
- for (BoardPort board : boardPorts) {
- if (board.toString().equals(newPort)) {
- updatingInfos = true;
- boardPort = boardPorts.get(i);
- break;
- }
- i++;
- }
- if (!updatingInfos) {
- boardPort = new BoardPort();
- }
+ BoardPort boardPort = new BoardPort();
+ boardPorts.add(boardPort);
boardPort.setAddress(port);
boardPort.setProtocol("serial");
boardPort.setOnlineStatus(true);
+ boardPort.setLabel(port);
- String label = port;
-
+ Map boardData = platform.resolveDeviceByVendorIdProductId(port, BaseNoGui.packages);
if (boardData != null) {
boardPort.getPrefs().put("vid", boardData.get("vid").toString());
boardPort.getPrefs().put("pid", boardData.get("pid").toString());
- boardPort.setVIDPID(parts[1], parts[2]);
String iserial = boardData.get("iserial").toString();
- if (iserial.length() >= 10) {
- boardPort.getPrefs().put("iserial", iserial);
- boardPort.setISerial(iserial);
- }
- if (uploadInProgress && oldUploadBoardPort!=null) {
- oldUploadBoardPort.getPrefs().put("iserial", iserial);
- oldUploadBoardPort.setISerial(iserial);
- }
+ boardPort.getPrefs().put("iserial", iserial);
TargetBoard board = (TargetBoard) boardData.get("board");
if (board != null) {
String boardName = board.getName();
- if (boardName != null) {
- label += " (" + boardName + ")";
- }
boardPort.setBoardName(boardName);
}
+ } else if (!parts[1].equals("0000")) {
+ boardPort.getPrefs().put("vid", parts[1]);
+ boardPort.getPrefs().put("pid", parts[2]);
+ // ask Cloud API to match the board with known VID/PID pair
+ boardCloudResolver.getBoardBy(parts[1], parts[2]);
} else {
- if (!parts[1].equals("0000")) {
- boardPort.setVIDPID(parts[1], parts[2]);
- // ask Cloud API to match the board with known VID/PID pair
- platform.getBoardWithMatchingVidPidFromCloud(parts[1], parts[2]);
- } else {
- boardPort.setVIDPID("0000", "0000");
- boardPort.setISerial("");
- }
- }
-
- boardPort.setLabel(label);
- if (!updatingInfos) {
- boardPorts.add(boardPort);
+ boardPort.getPrefs().put("vid", "0000");
+ boardPort.getPrefs().put("pid", "0000");
+ boardPort.getPrefs().put("iserial", "");
}
}
- serialDiscovery.setSerialBoardPorts(boardPorts);
- }
-
- @Override
- public void run() {
- if (BaseNoGui.packages == null) {
- return;
- }
- retriggerDiscovery(true);
+ setSerialBoardPorts(boardPorts);
}
}
diff --git a/arduino-core/src/cc/arduino/packages/ssh/SSHConfigFileSetup.java b/arduino-core/src/cc/arduino/packages/ssh/SSHConfigFileSetup.java
index 2f2e53cff44..5d4cb66cf7e 100644
--- a/arduino-core/src/cc/arduino/packages/ssh/SSHConfigFileSetup.java
+++ b/arduino-core/src/cc/arduino/packages/ssh/SSHConfigFileSetup.java
@@ -46,7 +46,6 @@ public SSHConfigFileSetup(SSHClientSetupChainRing nextChainRing) {
@Override
public Session setup(BoardPort port, JSch jSch) throws JSchException, IOException {
String ipAddress = port.getAddress();
- String hostname = port.getBoardName().contains(".local") ? port.getBoardName() : port.getBoardName() + ".local";
File sshFolder = new File(System.getProperty("user.home"), ".ssh");
File sshConfig = new File(sshFolder, "config");
@@ -62,7 +61,7 @@ public Session setup(BoardPort port, JSch jSch) throws JSchException, IOExceptio
jSch.setConfigRepository(new OpenSSHConfigWrapper(configRepository, ipAddress));
- return jSch.getSession(hostname);
+ return jSch.getSession(ipAddress);
}
public static class OpenSSHConfigWrapper implements ConfigRepository {
diff --git a/arduino-core/src/cc/arduino/packages/uploaders/GenericNetworkUploader.java b/arduino-core/src/cc/arduino/packages/uploaders/GenericNetworkUploader.java
index ab7667787dc..0cf2ba04be5 100644
--- a/arduino-core/src/cc/arduino/packages/uploaders/GenericNetworkUploader.java
+++ b/arduino-core/src/cc/arduino/packages/uploaders/GenericNetworkUploader.java
@@ -95,7 +95,7 @@ public boolean uploadUsingPreferences(File sourcePath, String buildPath, String
pattern = prefs.get("upload.network_pattern");
if(pattern == null)
pattern = prefs.getOrExcept("upload.pattern");
- String[] cmd = StringReplacer.formatAndSplit(pattern, prefs, true);
+ String[] cmd = StringReplacer.formatAndSplit(pattern, prefs);
uploadResult = executeUploadCommand(cmd);
} catch (RunnerException e) {
throw e;
diff --git a/arduino-core/src/cc/arduino/packages/uploaders/MergeSketchWithBooloader.java b/arduino-core/src/cc/arduino/packages/uploaders/MergeSketchWithBooloader.java
index a6f34b265d2..bbab2bd7b09 100644
--- a/arduino-core/src/cc/arduino/packages/uploaders/MergeSketchWithBooloader.java
+++ b/arduino-core/src/cc/arduino/packages/uploaders/MergeSketchWithBooloader.java
@@ -29,19 +29,19 @@
package cc.arduino.packages.uploaders;
-import processing.app.helpers.FileUtils;
-
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
import java.util.List;
public class MergeSketchWithBooloader {
public void merge(File sketch, File bootloader) throws IOException {
- List mergedSketch = FileUtils.readFileToListOfStrings(sketch);
+ List mergedSketch = Files.readAllLines(sketch.toPath(), StandardCharsets.UTF_8);
mergedSketch.remove(mergedSketch.size() - 1);
- mergedSketch.addAll(FileUtils.readFileToListOfStrings(bootloader));
+ mergedSketch.addAll(Files.readAllLines(bootloader.toPath(), StandardCharsets.UTF_8));
FileWriter writer = null;
try {
diff --git a/arduino-core/src/cc/arduino/packages/uploaders/SSHUploader.java b/arduino-core/src/cc/arduino/packages/uploaders/SSHUploader.java
index b3152a75539..fb0eb3ffbd5 100644
--- a/arduino-core/src/cc/arduino/packages/uploaders/SSHUploader.java
+++ b/arduino-core/src/cc/arduino/packages/uploaders/SSHUploader.java
@@ -44,18 +44,23 @@
import processing.app.helpers.PreferencesMap;
import processing.app.helpers.PreferencesMapException;
import processing.app.helpers.StringReplacer;
-import processing.app.helpers.StringUtils;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
+
+import org.apache.commons.lang3.StringUtils;
import static processing.app.I18n.tr;
public class SSHUploader extends Uploader {
- private static final List FILES_NOT_TO_COPY = Arrays.asList(".DS_Store", ".Trash", "Thumbs.db", "__MACOSX");
+ private static final Set FILES_NOT_TO_COPY =
+ Collections.unmodifiableSet(new HashSet(Arrays.asList(".DS_Store", ".Trash", "Thumbs.db", "__MACOSX")));
private final BoardPort port;
@@ -165,7 +170,7 @@ private boolean runUploadTool(SSH ssh, PreferencesMap prefs) throws Exception {
}
String pattern = prefs.getOrExcept("upload.pattern");
- String command = StringUtils.join(StringReplacer.formatAndSplit(pattern, prefs, true), " ");
+ String command = StringUtils.join(StringReplacer.formatAndSplit(pattern, prefs), " ");
if (verbose) {
System.out.println(command);
}
@@ -223,7 +228,7 @@ private void recursiveSCP(File from, SCP scp) throws IOException {
}
for (File file : files) {
- if (!StringUtils.stringContainsOneOf(file.getName(), FILES_NOT_TO_COPY)) {
+ if (!FILES_NOT_TO_COPY.contains(file.getName())) {
if (file.isDirectory() && file.canExecute()) {
scp.startFolder(file.getName());
recursiveSCP(file, scp);
diff --git a/arduino-core/src/cc/arduino/packages/uploaders/SerialUploader.java b/arduino-core/src/cc/arduino/packages/uploaders/SerialUploader.java
index cd5e285c415..96ba383aceb 100644
--- a/arduino-core/src/cc/arduino/packages/uploaders/SerialUploader.java
+++ b/arduino-core/src/cc/arduino/packages/uploaders/SerialUploader.java
@@ -41,6 +41,7 @@
import processing.app.debug.RunnerException;
import processing.app.debug.TargetPlatform;
import processing.app.helpers.PreferencesMap;
+import processing.app.helpers.PreferencesMapException;
import processing.app.helpers.StringReplacer;
import java.io.File;
@@ -105,17 +106,11 @@ public boolean uploadUsingPreferences(File sourcePath, String buildPath, String
else
prefs.put("upload.verify", prefs.get("upload.params.noverify", ""));
- boolean uploadResult;
try {
- String pattern = prefs.getOrExcept("upload.pattern");
- String[] cmd = StringReplacer.formatAndSplit(pattern, prefs, true);
- uploadResult = executeUploadCommand(cmd);
- } catch (Exception e) {
- throw new RunnerException(e);
+ return runCommand("upload.pattern", prefs);
} finally {
BaseNoGui.getDiscoveryManager().getSerialDiscoverer().pausePolling(false);
}
- return uploadResult;
}
// need to do a little dance for Leonardo and derivatives:
@@ -124,13 +119,10 @@ public boolean uploadUsingPreferences(File sourcePath, String buildPath, String
// this wait a moment for the bootloader to enumerate. On Windows, also must
// deal with the fact that the COM port number changes from bootloader to
// sketch.
- String t = prefs.get("upload.use_1200bps_touch");
- boolean doTouch = t != null && t.equals("true");
-
- t = prefs.get("upload.wait_for_upload_port");
- boolean waitForUploadPort = (t != null) && t.equals("true");
+ boolean doTouch = prefs.getBoolean("upload.use_1200bps_touch");
+ boolean waitForUploadPort = prefs.getBoolean("upload.wait_for_upload_port");
- String userSelectedUploadPort = prefs.getOrExcept("serial.port");
+ String userSelectedUploadPort = prefs.get("serial.port", "");
String actualUploadPort = null;
if (doTouch) {
@@ -180,7 +172,7 @@ public boolean uploadUsingPreferences(File sourcePath, String buildPath, String
Thread.sleep(100);
}
- BoardPort boardPort = BaseNoGui.getDiscoveryManager().find(PreferencesData.get("serial.port"));
+ BoardPort boardPort = BaseNoGui.getDiscoveryManager().find(PreferencesData.get("serial.port", ""));
try {
prefs.put("serial.port.iserial", boardPort.getPrefs().getOrExcept("iserial"));
} catch (Exception e) {
@@ -202,20 +194,11 @@ public boolean uploadUsingPreferences(File sourcePath, String buildPath, String
boolean uploadResult;
try {
- String pattern = prefs.getOrExcept("upload.pattern");
- String[] cmd = StringReplacer.formatAndSplit(pattern, prefs, true);
- uploadResult = executeUploadCommand(cmd);
- } catch (RunnerException e) {
- throw e;
- } catch (Exception e) {
- throw new RunnerException(e);
+ uploadResult = runCommand("upload.pattern", prefs);
} finally {
BaseNoGui.getDiscoveryManager().getSerialDiscoverer().pausePolling(false);
}
- BaseNoGui.getDiscoveryManager().getSerialDiscoverer().setUploadInProgress(false);
- BaseNoGui.getDiscoveryManager().getSerialDiscoverer().pausePolling(false);
-
String finalUploadPort = null;
if (uploadResult && doTouch) {
try {
@@ -224,37 +207,35 @@ public boolean uploadUsingPreferences(File sourcePath, String buildPath, String
// sketch serial port reconnects (or timeout after a few seconds if the
// sketch port never comes back). Doing this saves users from accidentally
// opening Serial Monitor on the soon-to-be-orphaned bootloader port.
- Thread.sleep(1000);
- long started = System.currentTimeMillis();
- while (System.currentTimeMillis() - started < 2000) {
- List portList = Serial.list();
- if (portList.contains(userSelectedUploadPort)) {
- finalUploadPort = userSelectedUploadPort;
- break;
- }
- Thread.sleep(250);
- }
+
+ // Reuse waitForUploadPort for this task, but this time we are simply waiting
+ // for one port to reappear. If no port reappears before the timeout, actualUploadPort is selected
+ finalUploadPort = waitForUploadPort(actualUploadPort, Serial.list(), false, 2000);
}
- } catch (InterruptedException ex) {
+ } catch (RunnerException ex) {
// noop
}
}
- if (finalUploadPort == null) {
- finalUploadPort = actualUploadPort;
- }
if (finalUploadPort == null) {
finalUploadPort = userSelectedUploadPort;
}
BaseNoGui.selectSerialPort(finalUploadPort);
+ BaseNoGui.getDiscoveryManager().getSerialDiscoverer().setUploadInProgress(false);
+ BaseNoGui.getDiscoveryManager().getSerialDiscoverer().pausePolling(false);
+
return uploadResult;
}
private String waitForUploadPort(String uploadPort, List before) throws InterruptedException, RunnerException {
+ return waitForUploadPort(uploadPort, before, verbose, 10000);
+ }
+
+ private String waitForUploadPort(String uploadPort, List before, boolean verbose, int timeout) throws InterruptedException, RunnerException {
// Wait for a port to appear on the list
int elapsed = 0;
- while (elapsed < 10000) {
+ while (elapsed < timeout) {
List now = Serial.list();
List diff = new ArrayList<>(now);
diff.removeAll(before);
@@ -331,21 +312,7 @@ private boolean uploadUsingProgrammer(String buildPath, String className) throws
else
prefs.put("program.verify", prefs.get("program.params.noverify", ""));
- try {
- // if (prefs.get("program.disable_flushing") == null
- // || prefs.get("program.disable_flushing").toLowerCase().equals("false"))
- // {
- // flushSerialBuffer();
- // }
-
- String pattern = prefs.getOrExcept("program.pattern");
- String[] cmd = StringReplacer.formatAndSplit(pattern, prefs, true);
- return executeUploadCommand(cmd);
- } catch (RunnerException e) {
- throw e;
- } catch (Exception e) {
- throw new RunnerException(e);
- }
+ return runCommand("program.pattern", prefs);
}
@Override
@@ -402,13 +369,27 @@ public boolean burnBootloader() throws Exception {
new LoadVIDPIDSpecificPreferences().load(prefs);
- String pattern = prefs.getOrExcept("erase.pattern");
- String[] cmd = StringReplacer.formatAndSplit(pattern, prefs, true);
- if (!executeUploadCommand(cmd))
+ if (!runCommand("erase.pattern", prefs))
return false;
- pattern = prefs.getOrExcept("bootloader.pattern");
- cmd = StringReplacer.formatAndSplit(pattern, prefs, true);
- return executeUploadCommand(cmd);
+ return runCommand("bootloader.pattern", prefs);
+ }
+
+ private boolean runCommand(String patternKey, PreferencesMap prefs) throws Exception, RunnerException {
+ try {
+ String pattern = prefs.getOrExcept(patternKey);
+ StringReplacer.checkIfRequiredKeyIsMissingOrExcept("serial.port", pattern, prefs);
+ String[] cmd = StringReplacer.formatAndSplit(pattern, prefs);
+ return executeUploadCommand(cmd);
+ } catch (RunnerException e) {
+ throw e;
+ } catch (PreferencesMapException e) {
+ if (e.getMessage().equals("serial.port")) {
+ throw new SerialNotFoundException(e);
+ }
+ throw e;
+ } catch (Exception e) {
+ throw new RunnerException(e);
+ }
}
}
diff --git a/arduino-core/src/cc/arduino/utils/ArchiveExtractor.java b/arduino-core/src/cc/arduino/utils/ArchiveExtractor.java
index 6d1e4ba3193..c22f7da156c 100644
--- a/arduino-core/src/cc/arduino/utils/ArchiveExtractor.java
+++ b/arduino-core/src/cc/arduino/utils/ArchiveExtractor.java
@@ -36,6 +36,7 @@
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
+import org.apache.commons.compress.compressors.xz.XZCompressorInputStream;
import org.apache.commons.compress.utils.IOUtils;
import processing.app.I18n;
import processing.app.Platform;
@@ -98,6 +99,8 @@ public void extract(File archiveFile, File destFolder, int stripPath, boolean ov
in = new ZipArchiveInputStream(new FileInputStream(archiveFile));
} else if (archiveFile.getName().endsWith("tar.gz")) {
in = new TarArchiveInputStream(new GzipCompressorInputStream(new FileInputStream(archiveFile)));
+ } else if (archiveFile.getName().endsWith("tar.xz")) {
+ in = new TarArchiveInputStream(new XZCompressorInputStream(new FileInputStream(archiveFile)));
} else if (archiveFile.getName().endsWith("tar")) {
in = new TarArchiveInputStream(new FileInputStream(archiveFile));
} else {
diff --git a/arduino-core/src/cc/arduino/utils/network/CacheControl.java b/arduino-core/src/cc/arduino/utils/network/CacheControl.java
new file mode 100644
index 00000000000..a34fde7be3b
--- /dev/null
+++ b/arduino-core/src/cc/arduino/utils/network/CacheControl.java
@@ -0,0 +1,119 @@
+/*
+ * This file is part of Arduino.
+ *
+ * Copyright 2019 Arduino LLC (http://www.arduino.cc/)
+ *
+ * Arduino is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * As a special exception, you may use this file as part of a free software
+ * library without restriction. Specifically, if other files instantiate
+ * templates or use macros or inline functions from this file, or you compile
+ * this file and link it with other files to produce an executable, this
+ * file does not by itself cause the resulting executable to be covered by
+ * the GNU General Public License. This exception does not however
+ * invalidate any other reasons why the executable file might be covered by
+ * the GNU General Public License.
+ */
+
+
+package cc.arduino.utils.network;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class CacheControl {
+
+
+ // see org.apache.abdera.protocol.util.CacheControlUtil
+ private static final Pattern PATTERN
+ = Pattern.compile("\\s*([\\w\\-]+)\\s*(=)?\\s*(\\-?\\d+|\\\"([^\"\\\\]*(\\\\.[^\"\\\\]*)*)+\\\")?\\s*");
+
+ private int maxAge = -1;
+
+ private boolean isMustRevalidate = false;
+
+ private boolean isNoCache = false;
+
+ private boolean isNoStore = false;
+
+
+ public static CacheControl valueOf(String value) {
+ CacheControl cc = new CacheControl();
+
+ if (value != null) {
+ Matcher matcher = PATTERN.matcher(value);
+ while (matcher.find()) {
+ switch (matcher.group(1).toLowerCase()) {
+ case "max-age":
+ cc.setMaxAge(Integer.parseInt(matcher.group(3)));
+ break;
+ case "must-revalidate":
+ cc.setMustRevalidate(true);
+ break;
+ case "no-cache":
+ cc.setNoCache(true);
+ break;
+ case "no-store":
+ cc.setNoStore(true);
+ break;
+ default: //ignore
+ }
+ }
+ }
+ return cc;
+ }
+
+ public void setMaxAge(int maxAge) {
+ this.maxAge = maxAge;
+ }
+
+ public int getMaxAge() {
+ return maxAge;
+ }
+
+ public boolean isMustRevalidate() {
+ return isMustRevalidate;
+ }
+
+ public void setMustRevalidate(boolean mustRevalidate) {
+ isMustRevalidate = mustRevalidate;
+ }
+
+ public boolean isNoCache() {
+ return isNoCache;
+ }
+
+ public void setNoCache(boolean noCache) {
+ isNoCache = noCache;
+ }
+
+ public boolean isNoStore() {
+ return isNoStore;
+ }
+
+ public void setNoStore(boolean noStore) {
+ isNoStore = noStore;
+ }
+
+ @Override
+ public String toString() {
+ return "CacheControl{" +
+ "maxAge=" + maxAge +
+ ", isMustRevalidate=" + isMustRevalidate +
+ ", isNoCache=" + isNoCache +
+ ", isNoStore=" + isNoStore +
+ '}';
+ }
+}
diff --git a/arduino-core/src/cc/arduino/utils/network/FileDownloader.java b/arduino-core/src/cc/arduino/utils/network/FileDownloader.java
index 8e25cc9227c..78c2cced8fa 100644
--- a/arduino-core/src/cc/arduino/utils/network/FileDownloader.java
+++ b/arduino-core/src/cc/arduino/utils/network/FileDownloader.java
@@ -29,27 +29,25 @@
package cc.arduino.utils.network;
-import cc.arduino.net.CustomProxySelector;
-import org.apache.commons.codec.binary.Base64;
import org.apache.commons.compress.utils.IOUtils;
+import processing.app.helpers.FileUtils;
-import processing.app.BaseNoGui;
-import processing.app.PreferencesData;
-
+import javax.script.ScriptException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
-import java.net.Proxy;
import java.net.SocketTimeoutException;
+import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
+import java.util.Arrays;
import java.util.Observable;
+import java.util.Optional;
public class FileDownloader extends Observable {
-
public enum Status {
CONNECTING, //
CONNECTION_TIMEOUT_ERROR, //
@@ -66,17 +64,15 @@ public enum Status {
private final URL downloadUrl;
private final File outputFile;
- private InputStream stream = null;
+ private final boolean allowCache;
private Exception error;
- private String userAgent;
-
- public FileDownloader(URL url, File file) {
- downloadUrl = url;
- outputFile = file;
- downloaded = 0;
- initialSize = 0;
- userAgent = "ArduinoIDE/" + BaseNoGui.VERSION_NAME + " Java/"
- + System.getProperty("java.version");
+
+ public FileDownloader(URL url, File file, boolean allowCache) {
+ this.downloadUrl = url;
+ this.outputFile = file;
+ this.allowCache = allowCache;
+ this.downloaded = 0;
+ this.initialSize = 0;
}
public long getInitialSize() {
@@ -121,9 +117,6 @@ public void setStatus(Status status) {
notifyObservers();
}
- public void download() throws InterruptedException {
- download(false);
- }
public void download(boolean noResume) throws InterruptedException {
if ("file".equals(downloadUrl.getProtocol())) {
@@ -143,68 +136,107 @@ private void saveLocalFile() {
}
}
+ public static void invalidateFiles(URL... filesUrl) {
+ // For each file delete the file cached if exist
+ Arrays.stream(filesUrl).forEach(url -> {
+ try {
+ FileDownloaderCache.getFileCached(url).ifPresent(fileCached -> {
+ try {
+ fileCached.invalidateCache();
+ } catch (Exception e) {
+ System.err.println("Error invalidating cached file " + fileCached.getLocalPath() + " that comes from "
+ + fileCached.getRemoteURL() + ": " + e.getMessage());
+ }
+ });
+ } catch (URISyntaxException | NoSuchMethodException | ScriptException | IOException e) {
+ System.err.println("Fail to get the file cached during the file invalidation" + e.getMessage());
+ }
+ });
+ }
+
private void downloadFile(boolean noResume) throws InterruptedException {
- RandomAccessFile file = null;
try {
- // Open file and seek to the end of it
- file = new RandomAccessFile(outputFile, "rw");
- initialSize = file.length();
-
- if (noResume && initialSize > 0) {
- // delete file and restart downloading
- Files.delete(outputFile.toPath());
- initialSize = 0;
- }
+ setStatus(Status.CONNECTING);
- file.seek(initialSize);
+ final Optional fileCachedOpt = FileDownloaderCache.getFileCached(downloadUrl, allowCache);
+ if (fileCachedOpt.isPresent()) {
+ final FileDownloaderCache.FileCached fileCached = fileCachedOpt.get();
- setStatus(Status.CONNECTING);
+ final Optional fileFromCache = getFileCached(fileCached);
+ if (fileCached.isNotChange() && fileFromCache.isPresent()) {
+ // Copy the cached file in the destination file
+ FileUtils.copyFile(fileFromCache.get(), outputFile);
+ } else {
+ openConnectionAndFillTheFile(noResume);
- Proxy proxy = new CustomProxySelector(PreferencesData.getMap()).getProxyFor(downloadUrl.toURI());
- if ("true".equals(System.getProperty("DEBUG"))) {
- System.err.println("Using proxy " + proxy);
+ fileCached.updateCacheFile(outputFile);
+ }
+ } else {
+ openConnectionAndFillTheFile(noResume);
}
+ setStatus(Status.COMPLETE);
- HttpURLConnection connection = (HttpURLConnection) downloadUrl.openConnection(proxy);
- connection.setRequestProperty("User-agent", userAgent);
- if (downloadUrl.getUserInfo() != null) {
- String auth = "Basic " + new String(new Base64().encode(downloadUrl.getUserInfo().getBytes()));
- connection.setRequestProperty("Authorization", auth);
- }
+ } catch (InterruptedException e) {
+ setStatus(Status.CANCELLED);
+ // lets InterruptedException go up to the caller
+ throw e;
- connection.setRequestProperty("Range", "bytes=" + initialSize + "-");
- connection.setConnectTimeout(5000);
- setDownloaded(0);
+ } catch (SocketTimeoutException e) {
+ setStatus(Status.CONNECTION_TIMEOUT_ERROR);
+ setError(e);
- // Connect
- connection.connect();
- int resp = connection.getResponseCode();
+ } catch (Exception e) {
+ setStatus(Status.ERROR);
+ setError(e);
+ }
+
+ }
- if (resp == HttpURLConnection.HTTP_MOVED_PERM || resp == HttpURLConnection.HTTP_MOVED_TEMP) {
- URL newUrl = new URL(connection.getHeaderField("Location"));
+ private Optional getFileCached(FileDownloaderCache.FileCached fileCached) {
+ try {
+ final Optional fileFromCache = fileCached.getFileFromCache();
+ if (fileFromCache.isPresent()) {
+ return fileFromCache;
+ }
+ } catch (Exception e) {
+ // Cannot get the file from the cache, download a new one
+ }
+ return Optional.empty();
+ }
- proxy = new CustomProxySelector(PreferencesData.getMap()).getProxyFor(newUrl.toURI());
+ private void openConnectionAndFillTheFile(boolean noResume) throws Exception {
+ initialSize = outputFile.length();
+ if (noResume && initialSize > 0) {
+ // delete file and restart downloading
+ Files.deleteIfExists(outputFile.toPath());
+ initialSize = 0;
+ }
- // open the new connnection again
- connection = (HttpURLConnection) newUrl.openConnection(proxy);
- connection.setRequestProperty("User-agent", userAgent);
- if (downloadUrl.getUserInfo() != null) {
- String auth = "Basic " + new String(new Base64().encode(downloadUrl.getUserInfo().getBytes()));
- connection.setRequestProperty("Authorization", auth);
- }
+ final HttpURLConnection connection = new HttpConnectionManager(downloadUrl)
+ .makeConnection((c) -> setDownloaded(0));
+ final int resp = connection.getResponseCode();
- connection.setRequestProperty("Range", "bytes=" + initialSize + "-");
- connection.setConnectTimeout(5000);
+ if (resp < 200 || resp >= 300) {
+ Files.deleteIfExists(outputFile.toPath());
+ throw new IOException("Received invalid http status code from server: " + resp);
+ }
- connection.connect();
- resp = connection.getResponseCode();
- }
+ RandomAccessFile randomAccessOutputFile = null;
+ try {
+ // Open file and seek to the end of it
+ randomAccessOutputFile = new RandomAccessFile(outputFile, "rw");
+ randomAccessOutputFile.seek(initialSize);
+ readStreamCopyTo(randomAccessOutputFile, connection);
+ } finally {
+ IOUtils.closeQuietly(randomAccessOutputFile);
+ }
- if (resp < 200 || resp >= 300) {
- throw new IOException("Received invalid http status code from server: " + resp);
- }
+ }
+ private void readStreamCopyTo(RandomAccessFile randomAccessOutputFile, HttpURLConnection connection) throws Exception {
+ InputStream stream = null;
+ try {
// Check for valid content length.
long len = connection.getContentLength();
if (len >= 0) {
@@ -212,20 +244,19 @@ private void downloadFile(boolean noResume) throws InterruptedException {
}
setStatus(Status.DOWNLOADING);
- synchronized (this) {
- stream = connection.getInputStream();
- }
- byte buffer[] = new byte[10240];
+ stream = connection.getInputStream();
+
+ byte[] buffer = new byte[10240];
while (status == Status.DOWNLOADING) {
int read = stream.read(buffer);
if (read == -1)
break;
- file.write(buffer, 0, read);
+ randomAccessOutputFile.write(buffer, 0, read);
setDownloaded(getDownloaded() + read);
if (Thread.interrupted()) {
- file.close();
+ randomAccessOutputFile.close();
throw new InterruptedException();
}
}
@@ -234,26 +265,8 @@ private void downloadFile(boolean noResume) throws InterruptedException {
if (getDownloaded() < getDownloadSize())
throw new Exception("Incomplete download");
}
- setStatus(Status.COMPLETE);
- } catch (InterruptedException e) {
- setStatus(Status.CANCELLED);
- // lets InterruptedException go up to the caller
- throw e;
-
- } catch (SocketTimeoutException e) {
- setStatus(Status.CONNECTION_TIMEOUT_ERROR);
- setError(e);
-
- } catch (Exception e) {
- setStatus(Status.ERROR);
- setError(e);
-
} finally {
- IOUtils.closeQuietly(file);
-
- synchronized (this) {
- IOUtils.closeQuietly(stream);
- }
+ IOUtils.closeQuietly(stream);
}
}
diff --git a/arduino-core/src/cc/arduino/utils/network/FileDownloaderCache.java b/arduino-core/src/cc/arduino/utils/network/FileDownloaderCache.java
new file mode 100644
index 00000000000..a529a241d0f
--- /dev/null
+++ b/arduino-core/src/cc/arduino/utils/network/FileDownloaderCache.java
@@ -0,0 +1,399 @@
+/*
+ * This file is part of Arduino.
+ *
+ * Copyright 2019 Arduino LLC (http://www.arduino.cc/)
+ *
+ * Arduino is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * As a special exception, you may use this file as part of a free software
+ * library without restriction. Specifically, if other files instantiate
+ * templates or use macros or inline functions from this file, or you compile
+ * this file and link it with other files to produce an executable, this
+ * file does not by itself cause the resulting executable to be covered by
+ * the GNU General Public License. This exception does not however
+ * invalidate any other reasons why the executable file might be covered by
+ * the GNU General Public License.
+ */
+
+
+package cc.arduino.utils.network;
+
+import cc.arduino.utils.FileHash;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import processing.app.BaseNoGui;
+import processing.app.PreferencesData;
+import processing.app.helpers.FileUtils;
+
+import javax.script.ScriptException;
+import java.io.File;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.ProtocolException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.NoSuchAlgorithmException;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public class FileDownloaderCache {
+ private final static String CACHE_ENABLE_PREFERENCE_KEY = "cache.enable";
+ private final static Map cachedFiles = Collections
+ .synchronizedMap(new HashMap<>());
+ private final static String cacheFolder;
+ private static boolean enableCache;
+
+ static {
+ enableCache = Boolean.valueOf(PreferencesData.get(CACHE_ENABLE_PREFERENCE_KEY, "true"));
+ PreferencesData.set(CACHE_ENABLE_PREFERENCE_KEY, Boolean.toString(enableCache));
+
+ final File settingsFolder;
+ settingsFolder = BaseNoGui.getSettingsFolder();
+ if (settingsFolder != null) {
+ cacheFolder = Paths.get(settingsFolder.getPath(), "cache")
+ .toString();
+ } else {
+ enableCache = false;
+ cacheFolder = null;
+ }
+ final Path pathCacheInfo = getCachedInfoPath();
+ try {
+ if (Files.exists(pathCacheInfo)) {
+ ObjectMapper mapper = new ObjectMapper();
+ final JsonNode jsonNode = mapper.readTree(pathCacheInfo.toFile());
+
+ // Read the files array
+ TypeReference> typeRef = new TypeReference>() {
+ };
+ final List files = mapper
+ .readValue(mapper.treeAsTokens(jsonNode.get("files")), typeRef);
+
+ // Update the map with the remote url as a key and the file cache info as a value
+ cachedFiles.putAll(Collections
+ .synchronizedMap(files
+ .stream()
+ .filter(FileCached::exists)
+ .collect(Collectors.toMap(FileCached::getRemoteURL, Function.identity()))
+ )
+ );
+
+ }
+ } catch (Exception e) {
+ System.err.println("Cannot initialized the cache: " + e.getMessage());
+ }
+ }
+
+ public static Optional getFileCached(final URL remoteURL)
+ throws URISyntaxException, NoSuchMethodException, ScriptException, IOException {
+ return getFileCached(remoteURL, true);
+ }
+
+ public static Optional getFileCached(final URL remoteURL, boolean enableCache)
+ throws URISyntaxException, NoSuchMethodException, ScriptException, IOException {
+ // Return always and empty file if the cache is not enable
+ if (!(enableCache && FileDownloaderCache.enableCache)) {
+ return Optional.empty();
+ }
+ final String[] splitPath = remoteURL.getPath().split("/");
+ if (splitPath.length == 0) {
+ return Optional.empty();
+ }
+ // Create the path where the cached file should exist
+ final Deque addFirstRemoteURL = new LinkedList<>(Arrays.asList(splitPath));
+ addFirstRemoteURL.addFirst(remoteURL.getHost());
+ final Path cacheFilePath = Paths.get(cacheFolder, addFirstRemoteURL.toArray(new String[0]));
+
+ // Take from the cache the file info or build from scratch
+ final FileCached fileCached = Optional.ofNullable(cachedFiles.get(remoteURL.toString()))
+ .orElseGet(() -> new FileCached(remoteURL.toString(), cacheFilePath.toString()));
+
+ // If the file is change of the cache is disable run the HEAD request to check if the file is changed
+ if (fileCached.isExpire() || !fileCached.exists()) {
+ // Update remote etag and cache control header
+ final Optional fileCachedInfoUpdated =
+ FileDownloaderCache.updateCacheInfo(remoteURL, (remoteETagClean, cacheControl) -> {
+ // Check cache control data
+ if (cacheControl.isNoCache() || cacheControl.isMustRevalidate() || cacheControl.isNoStore()) {
+ return Optional.empty();
+ }
+ final FileCached fileCachedUpdateETag = new FileCached(
+ remoteURL.toString(),
+ cacheFilePath.toString(),
+ fileCached.eTag,
+ remoteETagClean, // Set the lastETag
+ fileCached.md5,
+ cacheControl // Set the new cache control
+ );
+ cachedFiles.put(remoteURL.toString(), fileCachedUpdateETag);
+ return Optional.of(fileCachedUpdateETag);
+ });
+ FileDownloaderCache.updateCacheFilesInfo();
+ return fileCachedInfoUpdated;
+ }
+ return Optional.of(fileCached);
+ }
+
+ private static Optional updateCacheInfo(URL remoteURL, BiFunction> getNewFile)
+ throws URISyntaxException, NoSuchMethodException, ScriptException,
+ IOException {
+ // Update the headers of the cached file
+ final HttpURLConnection headRequest = new HttpConnectionManager(remoteURL).makeConnection((connection) -> {
+ try {
+ connection.setRequestMethod("HEAD");
+ } catch (ProtocolException e) {
+ System.err.println(e.getMessage());
+ }
+ });
+ final int responseCode = headRequest.getResponseCode();
+ headRequest.disconnect();
+ // Something bad is happening return a conservative true to try to download the file
+ if (responseCode < 200 || responseCode >= 300) {
+ // if something bad happened
+ return Optional.empty();
+ }
+ // Get all the useful headers
+ String remoteETag = headRequest.getHeaderField("ETag");
+ String cacheControlHeader = headRequest.getHeaderField("Cache-Control");
+ if (remoteETag != null && cacheControlHeader != null) {
+ final String remoteETagClean = remoteETag.trim().replace("\"", "");
+ final CacheControl cacheControl = CacheControl.valueOf(cacheControlHeader);
+ return getNewFile.apply(remoteETagClean, cacheControl);
+ }
+ // the head request do not return the ETag or the Cache-Control
+ return Optional.empty();
+ }
+
+ private synchronized static void updateCacheFilesInfo() throws IOException {
+ ObjectMapper mapper = new ObjectMapper();
+ // Generate a pretty json
+ mapper.enable(SerializationFeature.INDENT_OUTPUT);
+ final ObjectNode objectNode = mapper.createObjectNode();
+ // Generate a json {"files":[...{files_info}...]}
+ objectNode.putArray("files").addAll(
+ cachedFiles.values().stream()
+ .map((v) -> mapper.convertValue(v, JsonNode.class))
+ .collect(Collectors.toList()));
+ // Create the path Arduino15/cache
+ Path cachedFileInfo = getCachedInfoPath();
+ if (Files.notExists(cachedFileInfo)) {
+ Files.createDirectories(cachedFileInfo.getParent());
+ }
+ // Write to cache.json
+ mapper.writeValue(cachedFileInfo.toFile(), objectNode);
+ }
+
+ private static Path getCachedInfoPath() {
+ return Paths.get(cacheFolder, "cache.json");
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ static class FileCached {
+ private final String remoteURL;
+ private final String localPath;
+ private final String eTag;
+ private final String lastETag;
+ private final String md5;
+ private final String createdAt;
+ private final CacheControl cacheControl;
+
+ FileCached() {
+ this.remoteURL = null;
+ this.localPath = null;
+ lastETag = null;
+ eTag = null;
+ md5 = null;
+ createdAt = null;
+ cacheControl = null;
+ }
+
+ FileCached(String remoteURL, String localPath) {
+ this.remoteURL = remoteURL;
+ this.localPath = localPath;
+ lastETag = null;
+ eTag = null;
+ md5 = null;
+ createdAt = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME);
+ cacheControl = null;
+ }
+
+ public FileCached(String remoteURL, String localPath, String eTag, String lastETag, String md5, CacheControl cacheControl) {
+ this.remoteURL = remoteURL;
+ this.localPath = localPath;
+ this.eTag = eTag;
+ this.lastETag = lastETag;
+ this.md5 = md5;
+ this.createdAt = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME);
+ this.cacheControl = cacheControl;
+ }
+
+ @JsonIgnore
+ public boolean isExpire() {
+ // Check if the file is expire
+ final LocalDateTime now = LocalDateTime.now();
+ return this.getExpiresTime().isBefore(now) || this.getExpiresTime().isEqual(now);
+ }
+
+ @JsonIgnore
+ public boolean isNotChange() {
+ return !isChange();
+ }
+
+ @JsonIgnore
+ public boolean isChange() {
+ // Check if the file is expire
+ boolean isChanged = false;
+ if (isExpire()) {
+ isChanged = true;
+ }
+
+ if (lastETag != null && !lastETag.equals(eTag)) {
+ // Different ETag means that the file is changed
+ isChanged = true;
+ }
+ return isChanged;
+ }
+
+ @JsonIgnore
+ public boolean exists() {
+ return localPath != null && Files.exists(Paths.get(localPath));
+ }
+
+ @JsonIgnore
+ public Optional getFileFromCache() {
+ if (md5Check()) {
+ return Optional.of(Paths.get(localPath).toFile());
+ }
+ return Optional.empty();
+
+ }
+
+ public synchronized void updateCacheFile(File fileToCache) throws Exception {
+ Path cacheFilePath = Paths.get(localPath);
+
+ // If the cache directory does not exist create it
+ if (!Files.exists(cacheFilePath.getParent())) {
+ Files.createDirectories(cacheFilePath.getParent());
+ }
+ FileUtils.copyFile(fileToCache, cacheFilePath.toFile());
+ final String md5 = this.calculateMD5();
+ final String eTag;
+ if (lastETag == null) {
+ eTag = this.eTag;
+ } else {
+ eTag = this.lastETag;
+ }
+ FileCached newFileCached = new FileCached(
+ this.remoteURL,
+ this.localPath,
+ eTag, // Initialize the right eTag with the last eTag because the file was updated
+ eTag,
+ md5,
+ this.cacheControl
+ );
+ cachedFiles.put(remoteURL, newFileCached);
+ updateCacheFilesInfo();
+ }
+
+ public synchronized void invalidateCache() throws IOException {
+ cachedFiles.remove(remoteURL);
+ Files.deleteIfExists(Paths.get(localPath));
+ }
+
+ private String calculateMD5() throws IOException, NoSuchAlgorithmException {
+ if (exists()) {
+ return FileHash.hash(Paths.get(localPath).toFile(), "MD5");
+ }
+ return null;
+ }
+
+ @JsonIgnore
+ public boolean md5Check() {
+ try {
+ return !Objects.isNull(getMD5()) && Objects.equals(calculateMD5(), getMD5());
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ @JsonIgnore
+ public LocalDateTime getExpiresTime() {
+ final int maxAge;
+ if (cacheControl != null) {
+ maxAge = cacheControl.getMaxAge();
+ } else {
+ maxAge = 0;
+ }
+ if (createdAt != null) {
+ return LocalDateTime.parse(createdAt, DateTimeFormatter.ISO_DATE_TIME)
+ .plusSeconds(maxAge);
+ }
+ return LocalDateTime.now();
+
+ }
+
+ public String getExpires() {
+ return getExpiresTime().toString();
+ }
+
+ public String getMD5() {
+ return md5;
+ }
+
+ public String geteTag() {
+ return eTag;
+ }
+
+ public String getRemoteURL() {
+ return remoteURL;
+ }
+
+ public String getLocalPath() {
+ return localPath;
+ }
+
+ public String getCreatedAt() {
+ return createdAt;
+ }
+
+ public CacheControl getCacheControl() {
+ return cacheControl;
+ }
+
+ @Override
+ public String toString() {
+ return "FileCached{" +
+ "eTag='" + eTag + '\'' +
+ ", lastETag='" + lastETag + '\'' +
+ ", remoteURL='" + remoteURL + '\'' +
+ ", localPath='" + localPath + '\'' +
+ ", md5='" + md5 + '\'' +
+ ", createdAt='" + createdAt + '\'' +
+ ", cacheControl=" + cacheControl +
+ '}';
+ }
+ }
+}
diff --git a/arduino-core/src/cc/arduino/utils/network/HttpConnectionManager.java b/arduino-core/src/cc/arduino/utils/network/HttpConnectionManager.java
new file mode 100644
index 00000000000..acb754d5055
--- /dev/null
+++ b/arduino-core/src/cc/arduino/utils/network/HttpConnectionManager.java
@@ -0,0 +1,150 @@
+/*
+ * This file is part of Arduino.
+ *
+ * Copyright 2019 Arduino LLC (http://www.arduino.cc/)
+ *
+ * Arduino is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * As a special exception, you may use this file as part of a free software
+ * library without restriction. Specifically, if other files instantiate
+ * templates or use macros or inline functions from this file, or you compile
+ * this file and link it with other files to produce an executable, this
+ * file does not by itself cause the resulting executable to be covered by
+ * the GNU General Public License. This exception does not however
+ * invalidate any other reasons why the executable file might be covered by
+ * the GNU General Public License.
+ */
+
+package cc.arduino.utils.network;
+
+import cc.arduino.net.CustomProxySelector;
+import org.apache.commons.codec.binary.Base64;
+import processing.app.BaseNoGui;
+import processing.app.PreferencesData;
+
+import javax.script.ScriptException;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.Proxy;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.UUID;
+import java.util.function.Consumer;
+
+public class HttpConnectionManager {
+ private static final String userAgent;
+ private static final int connectTimeout;
+ private static final int maxRedirectNumber;
+ private final URL requestURL;
+ private final String id;
+
+
+ static {
+ final String defaultUserAgent = String.format(
+ "ArduinoIDE/%s (%s; %s; %s; %s) Java/%s (%s)",
+ BaseNoGui.VERSION_NAME,
+ System.getProperty("os.name"),
+ System.getProperty("os.version"),
+ System.getProperty("os.arch"),
+ System.getProperty("user.language"),
+ System.getProperty("java.version"),
+ System.getProperty("java.vendor")
+ );
+ userAgent = PreferencesData.get("http.user_agent", defaultUserAgent);
+ int connectTimeoutFromConfig = 5000;
+ try {
+ connectTimeoutFromConfig = PreferencesData.getInteger("http.connection_timeout_ms", 5000);
+ } catch (NumberFormatException e) {
+ System.err.println("Error parsing http.connection_timeout_ms config: " + e.getMessage());
+ }
+ connectTimeout = connectTimeoutFromConfig;
+ // Set by default 20 max redirect to follow
+ int maxRedirectNumberConfig = 20;
+ try {
+ maxRedirectNumberConfig = PreferencesData.getInteger("http.max_redirect_number", 20);
+ } catch (NumberFormatException e) {
+ System.err.println("Error parsing http.max_redirect_number config: " + e.getMessage());
+ }
+ maxRedirectNumber = maxRedirectNumberConfig;
+ }
+
+ public HttpConnectionManager(URL requestURL) {
+ this.requestURL = requestURL;
+ if (requestURL.getHost().endsWith("arduino.cc")) {
+ final String idString = PreferencesData.get("update.id", "0");
+ id = Long.toString(Long.parseLong(idString));
+ } else {
+ id = null;
+ }
+
+ }
+
+ public HttpURLConnection makeConnection(Consumer beforeConnection)
+ throws IOException, NoSuchMethodException, ScriptException, URISyntaxException {
+ return makeConnection(this.requestURL, 0, beforeConnection);
+ }
+
+
+ public HttpURLConnection makeConnection()
+ throws IOException, NoSuchMethodException, ScriptException, URISyntaxException {
+ return makeConnection(this.requestURL, 0, (c) -> {
+ });
+ }
+
+ private HttpURLConnection makeConnection(URL requestURL, int movedTimes,
+ Consumer beforeConnection) throws IOException, URISyntaxException, ScriptException, NoSuchMethodException {
+ if (movedTimes > maxRedirectNumber) {
+ throw new IOException("Too many redirect " + requestURL);
+ }
+
+ Proxy proxy = new CustomProxySelector(PreferencesData.getMap()).getProxyFor(requestURL.toURI());
+
+ final String requestId = UUID.randomUUID().toString().toUpperCase().replace("-", "").substring(0, 16);
+ HttpURLConnection connection = (HttpURLConnection) requestURL.openConnection(proxy);
+
+ // see https://github.com/arduino/Arduino/issues/10264
+ // Workaround for https://bugs.openjdk.java.net/browse/JDK-8163921
+ connection.setRequestProperty("Accept", "*/*");
+
+ connection.setRequestProperty("User-agent", userAgent);
+ connection.setRequestProperty("X-Request-ID", requestId);
+ if (id != null) {
+ connection.setRequestProperty("X-ID", id);
+ }
+ if (requestURL.getUserInfo() != null) {
+ String auth = "Basic " + new String(
+ new Base64().encode(requestURL.getUserInfo().getBytes()));
+ connection.setRequestProperty("Authorization", auth);
+ }
+
+ int initialSize = 0;
+ connection.setRequestProperty("Range", "bytes=" + initialSize + "-");
+ connection.setConnectTimeout(connectTimeout);
+ beforeConnection.accept(connection);
+
+ // Connect
+ connection.connect();
+ int resp = connection.getResponseCode();
+
+ if (resp == HttpURLConnection.HTTP_MOVED_PERM || resp == HttpURLConnection.HTTP_MOVED_TEMP) {
+ URL newUrl = new URL(connection.getHeaderField("Location"));
+ return this.makeConnection(newUrl, movedTimes + 1, beforeConnection);
+ }
+
+ return connection;
+ }
+
+}
+
diff --git a/arduino-core/src/processing/app/BaseNoGui.java b/arduino-core/src/processing/app/BaseNoGui.java
index 0dfbd4ccc74..ea51e376e73 100644
--- a/arduino-core/src/processing/app/BaseNoGui.java
+++ b/arduino-core/src/processing/app/BaseNoGui.java
@@ -2,7 +2,6 @@
import cc.arduino.Constants;
import cc.arduino.contributions.GPGDetachedSignatureVerifier;
-import cc.arduino.contributions.SignatureVerificationFailedException;
import cc.arduino.contributions.VersionComparator;
import cc.arduino.contributions.libraries.LibrariesIndexer;
import cc.arduino.contributions.packages.ContributedPlatform;
@@ -42,9 +41,9 @@
public class BaseNoGui {
/** Version string to be used for build */
- public static final int REVISION = 10808;
+ public static final int REVISION = 10820;
/** Extended version string displayed on GUI */
- public static final String VERSION_NAME = "1.8.8";
+ public static final String VERSION_NAME = "1.8.20";
public static final String VERSION_NAME_LONG;
// Current directory to use for relative paths specified on the
@@ -223,7 +222,7 @@ static public File getDefaultSketchbookFolder() {
public static DiscoveryManager getDiscoveryManager() {
if (discoveryManager == null) {
- discoveryManager = new DiscoveryManager();
+ discoveryManager = new DiscoveryManager(packages);
}
return discoveryManager;
}
@@ -482,11 +481,11 @@ static public void initPackages() throws Exception {
try {
indexer.parseIndex();
- } catch (JsonProcessingException | SignatureVerificationFailedException e) {
+ } catch (JsonProcessingException e) {
File indexFile = indexer.getIndexFile(Constants.DEFAULT_INDEX_FILE_NAME);
File indexSignatureFile = indexer.getIndexFile(Constants.DEFAULT_INDEX_FILE_NAME + ".sig");
- FileUtils.deleteIfExists(indexFile);
- FileUtils.deleteIfExists(indexSignatureFile);
+ indexFile.delete();
+ indexSignatureFile.delete();
throw e;
}
indexer.syncWithFilesystem();
@@ -502,15 +501,15 @@ static public void initPackages() throws Exception {
librariesIndexer.parseIndex();
} catch (JsonProcessingException e) {
File librariesIndexFile = librariesIndexer.getIndexFile();
- FileUtils.deleteIfExists(librariesIndexFile);
+ librariesIndexFile.delete();
}
if (discoveryManager == null) {
- discoveryManager = new DiscoveryManager();
+ discoveryManager = new DiscoveryManager(packages);
}
}
- static protected void initPlatform() {
+ static public void initPlatform() {
try {
Class> platformClass = Class.forName("processing.app.Platform");
if (OSUtils.isMacOS()) {
@@ -676,7 +675,9 @@ static public void onBoardOrPortChange() {
// Libraries located in the latest folders on the list can override
// other libraries with the same name.
librariesIndexer.setLibrariesFolders(librariesFolders);
- librariesIndexer.setArchitecturePriority(getTargetPlatform().getId());
+ if (getTargetPlatform() != null) {
+ librariesIndexer.setArchitecturePriority(getTargetPlatform().getId());
+ }
librariesIndexer.rescanLibraries();
populateImportToLibraryTable();
@@ -893,7 +894,7 @@ static public void saveFile(String str, File file) throws IOException {
PApplet.saveStrings(temp, strArray);
try {
- file = file.getCanonicalFile();
+ file = file.toPath().toRealPath().toFile().getCanonicalFile();
} catch (IOException e) {
}
diff --git a/arduino-core/src/processing/app/I18n.java b/arduino-core/src/processing/app/I18n.java
index 0ab961aa9ed..1f1a9f93703 100644
--- a/arduino-core/src/processing/app/I18n.java
+++ b/arduino-core/src/processing/app/I18n.java
@@ -106,10 +106,6 @@ public static String format(String fmt, Object... args) {
* This method is an hack to extract words with gettext tool.
*/
protected static void unusedStrings() {
- // These phrases are defined in the "platform.txt".
- tr("Arduino AVR Boards");
- tr("Arduino ARM (32-bits) Boards");
-
// This word is defined in the "boards.txt".
tr("Processor");
}
diff --git a/arduino-core/src/processing/app/Platform.java b/arduino-core/src/processing/app/Platform.java
index 28a7ba0f550..c76148df18f 100644
--- a/arduino-core/src/processing/app/Platform.java
+++ b/arduino-core/src/processing/app/Platform.java
@@ -31,19 +31,7 @@
import javax.swing.*;
import java.io.File;
import java.io.IOException;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.ArrayList;
-import java.util.Arrays;
-
-import java.net.URL;
-import java.net.URLConnection;
-import java.net.HttpURLConnection;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.DeserializationFeature;
-import java.io.InputStream;
+import java.util.*;
import static processing.app.I18n.tr;
@@ -64,8 +52,6 @@
* know if name is proper Java package syntax.)
*/
public class Platform {
-
-
/**
* Set the default L & F. While I enjoy the bounty of the sixteen possible
* exception types that this UIManager method might throw, I feel that in
@@ -170,6 +156,7 @@ private static void loadLib(File lib) {
}
private native String resolveDeviceAttachedToNative(String serial);
+
private native String[] listSerialsNative();
public String preListAllCandidateDevices() {
@@ -180,7 +167,7 @@ public List listSerials() {
return new ArrayList<>(Arrays.asList(listSerialsNative()));
}
- public List listSerialsNames(){
+ public List listSerialsNames() {
List list = new LinkedList<>();
for (String port : listSerialsNative()) {
list.add(port.split("_")[0]);
@@ -188,46 +175,6 @@ public List listSerialsNames(){
return list;
}
- public static class BoardCloudAPIid {
- public BoardCloudAPIid() { }
- private String name;
- private String architecture;
- private String id;
- public String getName() { return name; }
- public String getArchitecture() { return architecture; }
- public String getId() { return id; }
- public void setName(String tmp) { name = tmp; }
- public void setArchitecture(String tmp) { architecture = tmp; }
- public void setId(String tmp) { id = tmp; }
- }
-
- public synchronized void getBoardWithMatchingVidPidFromCloud(String vid, String pid) {
- // this method is less useful in Windows < WIN10 since you need drivers to be already installed
- ObjectMapper mapper = new ObjectMapper();
- mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
- try {
- URL jsonUrl = new URL("http", "api-builder.arduino.cc", 80, "/builder/v1/boards/0x"+vid+"/0x"+pid);
- URLConnection connection = jsonUrl.openConnection();
- connection.connect();
- HttpURLConnection httpConnection = (HttpURLConnection) connection;
- int code = httpConnection.getResponseCode();
- if (code == 404) {
- return;
- }
- InputStream is = httpConnection.getInputStream();
- BoardCloudAPIid board = mapper.readValue(is, BoardCloudAPIid.class);
- // Launch a popup with a link to boardmanager#board.getName()
- // replace spaces with &
- String realBoardName = board.getName().replaceAll("\\(.*?\\)", "").trim();
- String boardNameReplaced = realBoardName.replaceAll(" ", "&");
- String message = I18n.format(tr("{0}Install this package{1} to use your {2} board"), "", "", realBoardName);
- BaseNoGui.setBoardManagerLink(message);
- } catch (Exception e) {
- // No connection no problem, fail silently
- //e.printStackTrace();
- }
- }
-
public synchronized Map resolveDeviceByVendorIdProductId(String serial, Map packages) {
String vid_pid_iSerial = resolveDeviceAttachedToNative(serial);
for (TargetPackage targetPackage : packages.values()) {
@@ -254,9 +201,10 @@ public synchronized Map resolveDeviceByVendorIdProductId(String
}
Map boardData = new HashMap<>();
boardData.put("board", board);
- boardData.put("vid", vids.get(i));
- boardData.put("pid", pids.get(i));
- String extrafields = vid_pid_iSerial.substring(vidPid.length()+1);
+ // remove 0x from VID / PID to keep them as reported by liblistserial
+ boardData.put("vid", vids.get(i).replaceAll("0x", ""));
+ boardData.put("pid", pids.get(i).replaceAll("0x", ""));
+ String extrafields = vid_pid_iSerial.substring(vidPid.length() + 1);
String[] parts = extrafields.split("_");
boardData.put("iserial", parts[0]);
return boardData;
diff --git a/arduino-core/src/processing/app/PreferencesData.java b/arduino-core/src/processing/app/PreferencesData.java
index 8ab4852b0b9..11a250d689c 100644
--- a/arduino-core/src/processing/app/PreferencesData.java
+++ b/arduino-core/src/processing/app/PreferencesData.java
@@ -1,9 +1,14 @@
package processing.app;
-import static processing.app.I18n.format;
-import static processing.app.I18n.tr;
+import cc.arduino.Constants;
+import cc.arduino.i18n.Languages;
+import org.apache.commons.compress.utils.IOUtils;
+import processing.app.helpers.PreferencesHelper;
+import processing.app.helpers.PreferencesMap;
+import processing.app.legacy.PApplet;
+import processing.app.legacy.PConstants;
-import java.awt.Font;
+import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
@@ -13,13 +18,8 @@
import java.util.MissingResourceException;
import java.util.stream.Collectors;
-import org.apache.commons.compress.utils.IOUtils;
-
-import cc.arduino.i18n.Languages;
-import processing.app.helpers.PreferencesHelper;
-import processing.app.helpers.PreferencesMap;
-import processing.app.legacy.PApplet;
-import processing.app.legacy.PConstants;
+import static processing.app.I18n.format;
+import static processing.app.I18n.tr;
public class PreferencesData {
@@ -50,6 +50,9 @@ static public void init(File file) throws Exception {
//ignore
}
+ // Start with a clean slate
+ prefs = new PreferencesMap();
+
// start by loading the defaults, in case something
// important was deleted from the user prefs
try {
@@ -265,11 +268,21 @@ static public Font getFont(String attr) {
}
public static Collection getCollection(String key) {
- return Arrays.asList(get(key, "").split(","));
+ return Arrays.stream(get(key, "").split(","))
+ // Remove empty strings from the collection
+ .filter((v) -> !v.trim().isEmpty())
+ .collect(Collectors.toList());
}
public static void setCollection(String key, Collection values) {
String value = values.stream().collect(Collectors.joining(","));
set(key, value);
}
+
+ public static boolean areInsecurePackagesAllowed() {
+ if (getBoolean(Constants.ALLOW_INSECURE_PACKAGES, false)) {
+ return true;
+ }
+ return getBoolean(Constants.PREF_CONTRIBUTIONS_TRUST_ALL, false);
+ }
}
diff --git a/arduino-core/src/processing/app/Serial.java b/arduino-core/src/processing/app/Serial.java
index 484ac11909b..edc5e8f0c0f 100644
--- a/arduino-core/src/processing/app/Serial.java
+++ b/arduino-core/src/processing/app/Serial.java
@@ -116,7 +116,7 @@ public static boolean touchForCDCReset(String iname) throws SerialException {
}
}
- private Serial(String iname, int irate, char iparity, int idatabits, float istopbits, boolean setRTS, boolean setDTR) throws SerialException {
+ protected Serial(String iname, int irate, char iparity, int idatabits, float istopbits, boolean setRTS, boolean setDTR) throws SerialException {
//if (port != null) port.close();
//this.parent = parent;
//parent.attach(this);
@@ -131,6 +131,11 @@ private Serial(String iname, int irate, char iparity, int idatabits, float istop
if (istopbits == 1.5f) stopbits = SerialPort.STOPBITS_1_5;
if (istopbits == 2) stopbits = SerialPort.STOPBITS_2;
+ // This is required for unit-testing
+ if (iname.equals("none")) {
+ return;
+ }
+
try {
port = new SerialPort(iname);
port.openPort();
@@ -175,31 +180,54 @@ public synchronized void serialEvent(SerialPortEvent serialEvent) {
if (serialEvent.isRXCHAR()) {
try {
byte[] buf = port.readBytes(serialEvent.getEventValue());
- int next = 0;
- while(next < buf.length) {
- while(next < buf.length && outToMessage.hasRemaining()) {
- int spaceInIn = inFromSerial.remaining();
- int copyNow = buf.length - next < spaceInIn ? buf.length - next : spaceInIn;
- inFromSerial.put(buf, next, copyNow);
- next += copyNow;
- inFromSerial.flip();
- bytesToStrings.decode(inFromSerial, outToMessage, false);
- inFromSerial.compact();
- }
- outToMessage.flip();
- if(outToMessage.hasRemaining()) {
- char[] chars = new char[outToMessage.remaining()];
- outToMessage.get(chars);
- message(chars, chars.length);
- }
- outToMessage.clear();
- }
+ processSerialEvent(buf);
} catch (SerialPortException e) {
errorMessage("serialEvent", e);
}
}
}
+ public void processSerialEvent(byte[] buf) {
+ int next = 0;
+ // This uses a CharsetDecoder to convert from bytes to UTF-8 in
+ // a streaming fashion (i.e. where characters might be split
+ // over multiple reads). This needs the data to be in a
+ // ByteBuffer (inFromSerial, which we also use to store leftover
+ // incomplete characters for the nexst run) and produces a
+ // CharBuffer (outToMessage), which we then convert to char[] to
+ // pass onwards.
+ // Note that these buffers switch from input to output mode
+ // using flip/compact/clear
+ while (next < buf.length || inFromSerial.position() > 0) {
+ do {
+ // This might be 0 when all data was already read from buf
+ // (but then there will be data in inFromSerial left to
+ // decode).
+ int copyNow = Math.min(buf.length - next, inFromSerial.remaining());
+ inFromSerial.put(buf, next, copyNow);
+ next += copyNow;
+
+ inFromSerial.flip();
+ bytesToStrings.decode(inFromSerial, outToMessage, false);
+ inFromSerial.compact();
+
+ // When there are multi-byte characters, outToMessage might
+ // still have room, so add more bytes if we have any.
+ } while (next < buf.length && outToMessage.hasRemaining());
+
+ // If no output was produced, the input only contained
+ // incomplete characters, so we're done processing
+ if (outToMessage.position() == 0)
+ break;
+
+ outToMessage.flip();
+ char[] chars = new char[outToMessage.remaining()];
+ outToMessage.get(chars);
+ message(chars, chars.length);
+ outToMessage.clear();
+ }
+ }
+
/**
* This method is intented to be extended to receive messages
* coming from serial port.
diff --git a/arduino-core/src/processing/app/SerialPortList.java b/arduino-core/src/processing/app/SerialPortList.java
index 04e8c46b5ab..f231ad6fb9c 100644
--- a/arduino-core/src/processing/app/SerialPortList.java
+++ b/arduino-core/src/processing/app/SerialPortList.java
@@ -74,7 +74,7 @@ public class SerialPortList {
}
}
- //since 2.1.0 -> Fully rewrited port name comparator
+ //since 2.1.0 -> Fully rewritten port name comparator
private static final Comparator PORTNAMES_COMPARATOR = new Comparator() {
@Override
diff --git a/arduino-core/src/processing/app/debug/LegacyTargetBoard.java b/arduino-core/src/processing/app/debug/LegacyTargetBoard.java
index 16770a63525..09e7ac5085c 100644
--- a/arduino-core/src/processing/app/debug/LegacyTargetBoard.java
+++ b/arduino-core/src/processing/app/debug/LegacyTargetBoard.java
@@ -100,4 +100,8 @@ public TargetPlatform getContainerPlatform() {
return containerPlatform;
}
+ @Override
+ public String getFQBN() {
+ return getContainerPlatform().getContainerPackage().getId() + ":" + getContainerPlatform().getId() + ":" + getId();
+ }
}
diff --git a/arduino-core/src/processing/app/debug/LegacyTargetPlatform.java b/arduino-core/src/processing/app/debug/LegacyTargetPlatform.java
index c00378c48b4..f3c1dd45b30 100644
--- a/arduino-core/src/processing/app/debug/LegacyTargetPlatform.java
+++ b/arduino-core/src/processing/app/debug/LegacyTargetPlatform.java
@@ -245,4 +245,9 @@ public String toString() {
res += " " + boardId + " = " + boards.get(boardId) + "\n";
return res + "}";
}
+
+ @Override
+ public boolean isInSketchbook() {
+ return getFolder().getAbsolutePath().startsWith(BaseNoGui.getSketchbookHardwareFolder().getAbsolutePath());
+ }
}
diff --git a/arduino-core/src/processing/app/debug/Sizer.java b/arduino-core/src/processing/app/debug/Sizer.java
index 4d54d8d52c8..6e748a94162 100644
--- a/arduino-core/src/processing/app/debug/Sizer.java
+++ b/arduino-core/src/processing/app/debug/Sizer.java
@@ -60,7 +60,7 @@ public long[] computeSize() throws RunnerException {
int r = 0;
try {
String pattern = prefs.get("recipe.size.pattern");
- String cmd[] = StringReplacer.formatAndSplit(pattern, prefs, true);
+ String cmd[] = StringReplacer.formatAndSplit(pattern, prefs);
exception = null;
textSize = -1;
diff --git a/arduino-core/src/processing/app/debug/TargetBoard.java b/arduino-core/src/processing/app/debug/TargetBoard.java
index 5dae869060e..d635bbf1d0f 100644
--- a/arduino-core/src/processing/app/debug/TargetBoard.java
+++ b/arduino-core/src/processing/app/debug/TargetBoard.java
@@ -92,4 +92,6 @@ public interface TargetBoard {
public TargetPlatform getContainerPlatform();
+ public String getFQBN();
+
}
diff --git a/arduino-core/src/processing/app/debug/TargetPlatform.java b/arduino-core/src/processing/app/debug/TargetPlatform.java
index 4b13cf87ac7..330b260bb16 100644
--- a/arduino-core/src/processing/app/debug/TargetPlatform.java
+++ b/arduino-core/src/processing/app/debug/TargetPlatform.java
@@ -94,4 +94,10 @@ public interface TargetPlatform {
*/
public TargetPackage getContainerPackage();
+ /**
+ * Returns true if the platform is installed in a subfolder of the sketchbook
+ *
+ * @return
+ */
+ public boolean isInSketchbook();
}
diff --git a/arduino-core/src/processing/app/helpers/BoardCloudResolver.java b/arduino-core/src/processing/app/helpers/BoardCloudResolver.java
new file mode 100644
index 00000000000..f1d4894caaa
--- /dev/null
+++ b/arduino-core/src/processing/app/helpers/BoardCloudResolver.java
@@ -0,0 +1,142 @@
+/*
+ * This file is part of Arduino.
+ *
+ * Copyright 2015 Arduino LLC (http://www.arduino.cc/)
+ *
+ * Arduino is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * As a special exception, you may use this file as part of a free software
+ * library without restriction. Specifically, if other files instantiate
+ * templates or use macros or inline functions from this file, or you compile
+ * this file and link it with other files to produce an executable, this
+ * file does not by itself cause the resulting executable to be covered by
+ * the GNU General Public License. This exception does not however
+ * invalidate any other reasons why the executable file might be covered by
+ * the GNU General Public License.
+ */
+
+package processing.app.helpers;
+
+import cc.arduino.utils.network.HttpConnectionManager;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import processing.app.BaseNoGui;
+import processing.app.I18n;
+import processing.app.debug.TargetBoard;
+import processing.app.debug.TargetPackage;
+import processing.app.debug.TargetPlatform;
+
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Map;
+
+import static processing.app.I18n.tr;
+
+public class BoardCloudResolver {
+
+ public synchronized void getBoardBy(String vid, String pid) {
+ // this method is less useful in Windows < WIN10 since you need drivers to be already installed
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ try {
+ URL jsonUrl = new URL(String.format("https://builder.arduino.cc/builder/v1/boards/0x%s/0x%s", vid, pid));
+
+ final HttpURLConnection httpConnection = new HttpConnectionManager(jsonUrl)
+ .makeConnection();
+ int code = httpConnection.getResponseCode();
+ if (code == 404) {
+ return;
+ }
+ InputStream is = httpConnection.getInputStream();
+ BoardCloudAPIid board = mapper.readValue(is, BoardCloudAPIid.class);
+ // Launch a popup with a link to boardmanager#board.getName()
+ // replace spaces with &
+ String realBoardName = board.getName().replaceAll("\\(.*?\\)", "").trim();
+ String boardNameReplaced = realBoardName.replaceAll(" ", "&");
+ String message = I18n.format(tr("{0}Install this package{1} to use your {2} board"), "", "", realBoardName);
+ BaseNoGui.setBoardManagerLink(message);
+ } catch (Exception e) {
+ // No connection no problem, fail silently
+ //e.printStackTrace();
+ }
+ }
+
+ public String resolveDeviceByBoardID(Map packages, String boardId) {
+ assert packages != null;
+ assert boardId != null;
+ for (TargetPackage targetPackage : packages.values()) {
+ for (TargetPlatform targetPlatform : targetPackage.getPlatforms().values()) {
+ for (TargetBoard board : targetPlatform.getBoards().values()) {
+ if (boardId.equals(board.getId())) {
+ return board.getName();
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ private static class BoardCloudAPIid {
+
+ private String fqbn;
+ private String name;
+ private String architecture;
+ private String id;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String tmp) {
+ name = tmp;
+ }
+
+ public String getFqbn() {
+ return fqbn;
+ }
+
+ public void setFqbn(String fqbn) {
+ this.fqbn = fqbn;
+ }
+
+ public String getArchitecture() {
+ return architecture;
+ }
+
+ public void setArchitecture(String tmp) {
+ architecture = tmp;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String tmp) {
+ id = tmp;
+ }
+
+ @Override
+ public String toString() {
+ return "BoardCloudAPIid{" +
+ "name='" + name + '\'' +
+ ", fqbn='" + fqbn + '\'' +
+ ", architecture='" + architecture + '\'' +
+ ", id='" + id + '\'' +
+ '}';
+ }
+ }
+
+}
diff --git a/arduino-core/src/processing/app/helpers/CommandlineParser.java b/arduino-core/src/processing/app/helpers/CommandlineParser.java
index 83d34fed7bf..4c8b3a241b4 100644
--- a/arduino-core/src/processing/app/helpers/CommandlineParser.java
+++ b/arduino-core/src/processing/app/helpers/CommandlineParser.java
@@ -41,6 +41,7 @@ private enum ACTION {
private String getPref;
private String boardToInstall;
private String libraryToInstall;
+ private Optional uploadPort = Optional.empty();
private final List filenames = new LinkedList<>();
public CommandlineParser(String[] args) {
@@ -141,7 +142,7 @@ public void parseArgumentsPhase1() {
i++;
if (i >= args.length)
BaseNoGui.showError(null, tr("Argument required for --port"), 3);
- BaseNoGui.selectSerialPort(args[i]);
+ uploadPort = Optional.of(args[i]);
if (action == ACTION.GUI)
action = ACTION.NOOP;
continue;
@@ -356,4 +357,8 @@ public String getLibraryToInstall() {
public boolean isPreserveTempFiles() {
return preserveTempFiles;
}
+
+ public Optional getUploadPort() {
+ return uploadPort;
+ }
}
diff --git a/arduino-core/src/processing/app/helpers/FileUtils.java b/arduino-core/src/processing/app/helpers/FileUtils.java
index 5e30319dc6a..f2a1603b698 100644
--- a/arduino-core/src/processing/app/helpers/FileUtils.java
+++ b/arduino-core/src/processing/app/helpers/FileUtils.java
@@ -2,16 +2,25 @@
import org.apache.commons.compress.utils.IOUtils;
-import java.io.*;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
-import java.util.*;
-import java.util.regex.Pattern;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
public class FileUtils {
private static final List SOURCE_CONTROL_FOLDERS = Arrays.asList("CVS", "RCS", ".git", ".svn", ".hg", ".bzr");
- private static final Pattern BACKSLASH = Pattern.compile("\\\\");
/**
* Checks, whether the child directory is a subdirectory of the base directory.
@@ -109,75 +118,6 @@ public static File createTempFolder(File parent, String prefix, String suffix) t
return Files.createDirectories(Paths.get(parent.getAbsolutePath(), prefix + suffix)).toFile();
}
- //
- // Compute relative path to "target" from a directory "origin".
- //
- // If "origin" is not absolute, it is relative from the current directory.
- // If "target" is not absolute, it is relative from "origin".
- //
- // by Shigeru KANEMOTO at SWITCHSCIENCE.
- //
- public static String relativePath(String origin, String target) {
- try {
- origin = (new File(origin)).getCanonicalPath();
- File targetFile = new File(target);
- if (targetFile.isAbsolute())
- target = targetFile.getCanonicalPath();
- else
- target = (new File(origin, target)).getCanonicalPath();
- } catch (IOException e) {
- return null;
- }
-
- if (origin.equals(target)) {
- // origin and target is identical.
- return ".";
- }
-
- if (origin.equals(File.separator)) {
- // origin is root.
- return "." + target;
- }
-
- String prefix = "";
- String root = File.separator;
-
- if (System.getProperty("os.name").indexOf("Windows") != -1) {
- if (origin.startsWith("\\\\") || target.startsWith("\\\\")) {
- // Windows UNC path not supported.
- return null;
- }
-
- char originLetter = origin.charAt(0);
- char targetLetter = target.charAt(0);
- if (Character.isLetter(originLetter) && Character.isLetter(targetLetter)) {
- // Windows only
- if (originLetter != targetLetter) {
- // Drive letters differ
- return null;
- }
- }
-
- prefix = "" + originLetter + ':';
- root = prefix + File.separator;
- }
-
- String relative = "";
- while (!target.startsWith(origin + File.separator)) {
- origin = (new File(origin)).getParent();
- if (origin.equals(root))
- origin = prefix;
- relative += "..";
- relative += File.separator;
- }
-
- return relative + target.substring(origin.length() + 1);
- }
-
- public static String getLinuxPathFrom(File file) {
- return BACKSLASH.matcher(file.getAbsolutePath()).replaceAll("/");
- }
-
public static boolean isSCCSOrHiddenFile(File file) {
return isSCCSFolder(file) || isHiddenFile(file);
}
@@ -209,25 +149,34 @@ public static String readFileToString(File file, String encoding) throws IOExcep
}
}
- public static List readFileToListOfStrings(File file) throws IOException {
- List strings = new LinkedList<>();
- BufferedReader reader = null;
+ /**
+ * Writes the given data to the given file, creating the file if it does not exist.
+ * This method is equivalent to calling {@code writeStringToFile(file, data, StandardCharsets.UTF_8)}.
+ * @param file - The file to write to.
+ * @param data - The string to write.
+ * @throws IOException If an I/O error occurs.
+ */
+ public static void writeStringToFile(File file, String data) throws IOException {
+ writeStringToFile(file, data, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Writes the given data to the given file, creating the file if it does not exist.
+ * @param file - The file to write to.
+ * @param data - The string to write.
+ * @param charset - The charset used to convert the string to bytes.
+ * @throws IOException If an I/O error occurs.
+ */
+ public static void writeStringToFile(File file, String data, Charset charset) throws IOException {
+ OutputStream out = null;
try {
- reader = new BufferedReader(new FileReader(file));
- String line;
- while ((line = reader.readLine()) != null) {
- line = line.replaceAll("\r", "").replaceAll("\n", "").replaceAll(" ", "");
- strings.add(line);
- }
- return strings;
+ out = new FileOutputStream(file);
+ out.write(data.getBytes(charset));
} finally {
- if (reader != null) {
- reader.close();
- }
+ IOUtils.closeQuietly(out);
}
}
-
/**
* Returns true if the given file has any of the given extensions.
*
@@ -236,10 +185,6 @@ public static List