├── .gitattributes ├── .github └── workflows │ ├── maven-build-installer-macos.yml │ ├── maven-build-installer-unix.yml │ ├── maven-build-installer-windows.yml │ └── maven-package.yml ├── .gitignore ├── LICENSE ├── README.md ├── app-icon.icns ├── app-icon.ico ├── app-icon.png ├── docs ├── java-15-jpackage.md ├── qna.md └── sample-run.md ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── changenode │ │ ├── BaseApplication.java │ │ ├── BaseForm.form │ │ ├── BaseForm.java │ │ ├── Log.java │ │ ├── Plugin.java │ │ └── plugin │ │ ├── DarkMode.java │ │ ├── DesktopHandlers.java │ │ ├── DesktopIntegration.java │ │ ├── FileDrop.java │ │ ├── HelloWorld.java │ │ ├── LogFile.java │ │ └── StandardMenus.java └── resources │ └── hello.txt └── packaging ├── add-launch-to-msi.js ├── linux-jpackage.txt ├── mac-jpackage.txt └── win-jpackage.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/maven-build-installer-macos.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Build macOS Installer 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: macos-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 16 20 | uses: actions/setup-java@v1 21 | with: 22 | java-version: 16 23 | - name: Cache Maven packages 24 | uses: actions/cache@v2 25 | with: 26 | path: ~/.m2 27 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 28 | restore-keys: ${{ runner.os }}-m2 29 | - name: Build with Maven 30 | run: mvn -B clean install --file pom.xml 31 | - name: Update Automatic Release 32 | uses: marvinpinto/action-automatic-releases@latest 33 | with: 34 | repo_token: "${{ secrets.GITHUB_TOKEN}}" 35 | automatic_release_tag: "macOS-latest" 36 | prerelease: true 37 | title: "macOS Development Build" 38 | files: ./target/*.dmg 39 | -------------------------------------------------------------------------------- /.github/workflows/maven-build-installer-unix.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Build Unix Installer 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up JDK 16 19 | uses: actions/setup-java@v1 20 | with: 21 | java-version: 16 22 | - name: Cache Maven packages 23 | uses: actions/cache@v2 24 | with: 25 | path: ~/.m2 26 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 27 | restore-keys: ${{ runner.os }}-m2 28 | - name: Build with Maven 29 | run: mvn -B clean install --file pom.xml 30 | - name: Update Automatic Release 31 | uses: marvinpinto/action-automatic-releases@latest 32 | with: 33 | repo_token: "${{ secrets.GITHUB_TOKEN}}" 34 | automatic_release_tag: "Ubuntu-latest" 35 | prerelease: true 36 | title: "Ubuntu Development Build" 37 | files: ./target/*.deb 38 | -------------------------------------------------------------------------------- /.github/workflows/maven-build-installer-windows.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Build Windows Installer 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | runs-on: windows-latest 15 | steps: 16 | - name: Download Wix 17 | uses: i3h/download-release-asset@v1 18 | with: 19 | owner: wixtoolset 20 | repo: wix3 21 | tag: wix3112rtm 22 | file: wix311-binaries.zip 23 | - name: Decompress Wix 24 | uses: DuckSoft/extract-7z-action@v1.0 25 | with: 26 | pathSource: wix311-binaries.zip 27 | pathTarget: ./target/wix 28 | - name: Add Wix to Path 29 | run: echo "$HOME/target/wix" >> $GITHUB_PATH 30 | - uses: actions/checkout@v2 31 | - name: Set up JDK 16 32 | uses: actions/setup-java@v1 33 | with: 34 | java-version: 16 35 | - name: Cache Maven packages 36 | uses: actions/cache@v2 37 | with: 38 | path: ~/.m2 39 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 40 | restore-keys: ${{ runner.os }}-m2 41 | - name: Build with Maven 42 | run: mvn -B clean install --file pom.xml 43 | - name: Update Automatic Release 44 | uses: marvinpinto/action-automatic-releases@latest 45 | with: 46 | repo_token: "${{ secrets.GITHUB_TOKEN}}" 47 | automatic_release_tag: "Windows-latest" 48 | prerelease: true 49 | title: "Windows Development Build" 50 | files: ./target/*.msi 51 | -------------------------------------------------------------------------------- /.github/workflows/maven-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Maven Basic Package 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: macos-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 16 20 | uses: actions/setup-java@v1 21 | with: 22 | java-version: 16 23 | - name: Cache Maven packages 24 | uses: actions/cache@v2 25 | with: 26 | path: ~/.m2 27 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 28 | restore-keys: ${{ runner.os }}-m2 29 | - name: Build with Maven 30 | run: mvn -B clean package --file pom.xml 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.war 15 | *.nar 16 | *.ear 17 | *.zip 18 | *.tar.gz 19 | *.rar 20 | 21 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 22 | hs_err_pid* 23 | 24 | # Maven 25 | target/ 26 | 27 | # IntelliJ project files 28 | *.iml 29 | *.iws 30 | *.ipr 31 | .idea/ 32 | 33 | # OS 34 | .DS_Store 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaFX + Maven = Native Desktop Apps 2 | 3 | [Spring Boot](https://spring.io/projects/spring-boot) + 4 | [jpackage](https://docs.oracle.com/en/java/javase/15/docs/specs/man/jpackage.html) + 5 | [Maven](http://maven.apache.org) template project for generating native desktop applications. 6 | 7 | # Goal 8 | 9 | 1. Build nice, small cross-platform [Spring Boot](https://spring.io/projects/spring-boot) based desktop apps with native 10 | installers 11 | - Apx 30-40mb .dmg, .msi and .deb installers - check out the example builds in 12 | [releases](https://github.com/wiverson/desktop-spring-boot/releases). 13 | 2. Just use Maven - no shell scripts required. 14 | - Use standard Maven dependency system to manage dependencies 15 | 3. 16 | 17 | Generate [macOS (.dmg), Windows (.msi) and Unix (e.g. deb/rpm)](https://github.com/wiverson/maven-jpackage-template/releases) 18 | installers/packages automatically 19 | with [GitHub Actions](https://github.com/wiverson/maven-jpackage-template/tree/main/.github/workflows) 20 | 21 | In many ways this project provides a Java developer with tooling similar to Electron 22 | or [Neutralino.js](https://neutralino.js.org) 23 | 24 | ## Overview 25 | 26 | This template uses a [Maven plugin](https://github.com/wiverson/jtoolprovider-plugin) to generate a custom JVM and 27 | installer package for a Spring Boot application run as a desktop app. Conceptually this is similar to Electron 28 | 29 | The basic requirements are just Java 16 and Maven. [Java 15 will work](docs/java-15-jpackage.md), although it requires a 30 | bit of setup. 31 | 32 | - On macOS XCode is required. 33 | - On Windows the free [WiX Toolset](https://wixtoolset.org/) is required. 34 | 35 | The project includes [GitHub Actions](https://github.com/wiverson/maven-jpackage-template/tree/main/.github/workflows) 36 | which automatically generate macOS, Windows, and Linux installers. 37 | 38 | The generated installers come in at around 30-40mb. The example source in the project includes demonstrations of several 39 | native desktop features - for example, drag-and-drop from the Finder/Explorer, as well as a few macOS Dock integration 40 | examples. Removing the code and the demonstration dependendencies gets a "Hello World" build size closer to 30mb than 41 | 40mb. 42 | 43 | ## Key Features 44 | 45 | Here are few cool things in this template: 46 | 47 | - Only uses Java and Maven. No shell scripts required. 48 | - Includes sample [GitHub Actions](https://github.com/wiverson/maven-jpackage-template/tree/main/.github/workflows) to 49 | build macOS, Windows and Linux installers 50 | - Demonstrates setting the application icon 51 | - Builds a .dmg on macOS, .msi on Windows, and .deb on Linux 52 | - Bundles the JavaFX SDK & modules to simplify getting started. 53 | - Template includes several examples of JavaFX / native desktop integration 54 | - Drag & drop with Finder / Explorer 55 | - Change the Dock icon dynamically on macOS 56 | - Menu on the top for macOS, in the window itself on Windows 57 | - Request user attention (bouncing dock icon) on macOS 58 | 59 | Once you get started, you might find these lists of tutorials, tools, libraries for 60 | [JavaFX](https://gist.github.com/wiverson/6c7f49819016cece906f0e8cea195ea2) 61 | and general [Java desktop integration](https://gist.github.com/wiverson/e9dfd73ca9a9a222b2d0a3d68ae3f129) helpful. 62 | 63 | # Usage 64 | 65 | Once everything is installed (see below) it's really easy to use: 66 | 67 | To generate an installer, just run... 68 | 69 | `mvn clean install` 70 | 71 | To do everything up until the actual installer generation (including generating the custom JVM)... 72 | 73 | `mvn clean package` 74 | 75 | # Installation 76 | 77 | 1. Install [OpenJDK Java 16](https://adoptopenjdk.net/) or 78 | [Oracle Java 16](https://www.oracle.com/java/technologies/javase-downloads.html). 79 | - Verify by opening a fresh Terminal/Command Prompt and typing `java --version`. 80 | 2. Install [Apache Maven 3.6.3](http://maven.apache.org/install.html) or later and make sure it's on your path. 81 | - Verify this by opening a fresh Terminal/Command Prompt and typing `mvn --version`. 82 | 3. macOS: verify XCode is installed and needed agreements accepted. 83 | - Launch XCode and accept the license, or verify in Terminal with the command `sudo xcodebuild -license`. 84 | 5. Windows: install [Wix 3 binaries](https://github.com/wixtoolset/wix3/releases/). 85 | - Installing Wix via the installer should be sufficient for jpackage to find it. 86 | 3. Clone/download this project. 87 | 6. Final step: run `mvn clean install` from the root of the project to generate the `target\TestApp.dmg` 88 | or `target\TestApp.msi` (installer). 89 | - Note that the actual generated installer will include a version number in the file name 90 | - For reference, here is a complete run log for [a successful run](docs/sample-run.md). 91 | 92 | Because these builds use stripped down JVM images, the 93 | [generated installers are in the 30-40mb range](https://github.com/wiverson/maven-jpackage-template/releases). 94 | 95 | # Sponsor 96 | 97 | This project is sponsored by [ChangeNode.com](https://changenode.com/) - if you would like to add easy automatic 98 | updates, crash reporting, analytics, etc. to your Java/JavaFX desktop application, go check it out... and be sure to 99 | subscribe for more information about desktop Java development. 100 | 101 | # Help 102 | 103 | Problems? Make sure everything is installed and working right! 104 | 105 | - Compiler not recognizing the --release option? Probably on an old JDK. 106 | - Can't find jdeps? Probably on an old JDK. 107 | - Can't find jpackage? Probably haven't set up your system 108 | to [allow Java 15 to enable preview packages]((docs/java-15-jpackage.md)). 109 | - Unrecognized option: --add-modules jdk.incubator.jpackage 110 | - Could be a left-over MAVEN_OPTS setting when you switched from Java 15 to Java 16 111 | - If you are still on Java 15, you may not have 112 | [MAVEN_OPTS set correctly](https://github.com/wiverson/maven-jpackage-template/issues/2). 113 | 114 | If you need consulting support, feel free to reach out to [ChangeNode.com](https://changenode.com/). 115 | 116 | # Q&A 117 | 118 | If you are using the template, browsing the [Q&A](docs/qna.md) is highly recommended. 119 | -------------------------------------------------------------------------------- /app-icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiverson/desktop-spring-boot/32ef03da527457aacd64f4dce6649a0330e43ba0/app-icon.icns -------------------------------------------------------------------------------- /app-icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiverson/desktop-spring-boot/32ef03da527457aacd64f4dce6649a0330e43ba0/app-icon.ico -------------------------------------------------------------------------------- /app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiverson/desktop-spring-boot/32ef03da527457aacd64f4dce6649a0330e43ba0/app-icon.png -------------------------------------------------------------------------------- /docs/java-15-jpackage.md: -------------------------------------------------------------------------------- 1 | # Java 15 jpackage Configuration 2 | 3 | The [Java 15 branch](https://github.com/wiverson/maven-jpackage-template/tree/java-15) of this template is available, 4 | but no future updates are expected. You are strongly encouraged to move to Java 16, where jpackage is fully integrated 5 | into the JDK. 6 | 7 | In Java 15, the jpackage tool is only available as an incubator project. This means that you have to pass a special flag 8 | to the JVM to enable it. This project relies on [jtoolprovider-plugin](https://github.com/wiverson/jtoolprovider-plugin) 9 | to perform key build steps. To generate the actual installers, the jpackage tool must be available to the ToolProvider 10 | API. Adding a `MAVEN_OPTS` environment variable is the solution. 11 | 12 | ## macOS & Linux 13 | 14 | On macOS and Linux this can be done by adding to the following line to the `~/.zshrc` file (or a similar file on Linux, 15 | depending on your preferred shell). 16 | 17 | `export MAVEN_OPTS="--add-modules jdk.incubator.jpackage"` 18 | 19 | ## Windows 20 | 21 | Current versions of Windows 10 have a nice UI for adding an environment variable. You can find it in the modern control 22 | panel via search - just start a search for "env" and that should bring up the appropriate control panel. Note that on 23 | Windows, you don't need the quote marks - here's a 24 | [screenshot illustrating the proper configuration for a Windows 10 environment](https://github.com/wiverson/maven-jpackage-template/issues/2) 25 | . 26 | 27 | ## IntelliJ Maven Options Bug 28 | 29 | Unfortunately, as of this writing adding this entry to the IntelliJ options for Maven (either in the IntelliJ Maven JVM 30 | importer UI or via `project-directory/.mvn/jvm.config`) will break the Maven sync. This bug is tracked by JetBrains as 31 | [IDEA-246963](https://youtrack.jetbrains.com/issue/IDEA-246963). There is a `.mvn/Xjvm.config file` in this project - 32 | once the bug is fixed, or if you use a different editor, just try renaming that file to `jvm.config`. 33 | 34 | ## Summary 35 | 36 | So, to summarize: if you are on Java 15 you'll have to go through extra hoops to get Maven to work, and you'll likely 37 | run into compatibility issues due to a bug in IntelliJ. Or, just update to Java 16 and call it a day. 38 | -------------------------------------------------------------------------------- /docs/qna.md: -------------------------------------------------------------------------------- 1 | # Q&A 2 | 3 | ### Q: Can you give me a few more details about how this works? 4 | 5 | A: Maven plugins are used to copy all the project dependencies into a folder, generate a slimmed-down JVM, and then 6 | generate a platform-specific installer. 7 | The [pom.xml](https://github.com/wiverson/maven-jpackage-template/blob/main/pom.xml) 8 | is heavily commented! 9 | 10 | ### Q: What are all the folders in the target directory? 11 | 12 | A: Here are the folders unique to this project: 13 | 14 | - `dependency` - all of the jar files declared by your Maven project, including the jar containing your code 15 | - `installer-work` - the platform specific working files generated by jpackage. 16 | - [Wix Toolset](https://wixtoolset.org) files on Windows 17 | - [Script Editor](https://support.apple.com/guide/script-editor/welcome/mac) on macOS. 18 | - `jvm-image` - Platform-specific, trimmed down JVM. 19 | - `packaging` - Platform-specific commands, filtered by Maven and fed to jpackage. 20 | 21 | Standard Maven folders: 22 | 23 | - `classes` - generated by the javac compiler, contains your compiled code. 24 | - `generated-sources` - side-effect of the javac compiler 25 | - `maven-archiver` - side-effect of Maven execution. 26 | - `maven-status` - side-effect of Maven compiler 27 | 28 | ### Q: Any Tips for Windows? 29 | 30 | A: First, make sure you **set a custom Windows installer UUID for your project**, as described in 31 | the [pom.xml](https://github.com/wiverson/maven-jpackage-template/blob/main/pom.xml)! 32 | 33 | This UUID is used to uniquely identify the app as YOUR app by the Windows installer system, and is critical for allowing 34 | users to seamlessly upgrade. You can quickly [grab a UUID of your own](https://www.uuidgenerator.net/) and pop that 35 | value in instead. By default jpackage will generate a UUID automatically, but this automatic UUID is easily regenerated 36 | with minor changes to your application, breaking the Windows installer upgrade chain. 37 | 38 | The [Windows GitHub workflow](https://github.com/wiverson/maven-jpackage-template/blob/main/.github/workflows/maven-build-installer-windows.yml) 39 | for this project downloads the Wix Installer toolkit and adds it to the path to automatically build the Windows 40 | installer. On your local dev machine just install [WiX Toolset](https://wixtoolset.org/) 41 | locally instead - it'll be a lot faster. 42 | 43 | ### Q: I'm getting errors when I'm trying to use javafx.web? 44 | 45 | A: GitHub won't allow cloning a template if the source has files over 10mb in size. The javafx.web components basically 46 | bundle a full native web browser under the covers. As of JavaFX 15 the javafx.web.jmod is roughly 25mb in size. If you 47 | need it, you can [download it](https://gluonhq.com/products/javafx/) and install it in the JavaFX projects in your local 48 | project. 49 | 50 | If you are delivering a project that essentially amounts to a Java web application bundled as a web application, instead 51 | of bundling JavaFX and a WebKit browser, I would suggest creating a small preferences UI using Swing and 52 | the [System Tray API](https://docs.oracle.com/javase/tutorial/uiswing/misc/systemtray.html). With a nice modern look and 53 | feel such as [FlatLaf](https://www.formdev.com/flatlaf/), you can create a very nice preferences panel and then use the 54 | standard Java [Desktop API](https://docs.oracle.com/javase/9/docs/api/java/awt/Desktop.html) to just launch the user's 55 | browser to the local URL. 56 | 57 | If you drop me a note that this is something you are interested in, I may go ahead and create a template for this... :) 58 | 59 | ### Q: Tell me a bit about the Linux version? 60 | 61 | A: The current GitHub Workflow for the Linux build runs on a GitHub Ubuntu instance, and by default it generates a 62 | amd64.deb file. jpackage supports other distribution formats, including rpm, so if you want a different packaging format 63 | you can tweak the 64 | [GitHub Action for the Ubuntu build](https://github.com/wiverson/maven-jpackage-template/blob/main/.github/workflows/maven-build-installer-unix.yml) 65 | and 66 | the [jpackage command for Unix](https://github.com/wiverson/maven-jpackage-template/blob/main/src/packaging/unix-jpackage.txt) 67 | to generate whatever you need. As long as you can find the right combination of configuration flags for 68 | [jpackage](https://docs.oracle.com/en/java/javase/15/docs/specs/man/jpackage.html) and can set up the GitHub Actions 69 | runner to match, you can generate whatever packaging you might need. If you need, you could set up several combinations 70 | of Maven profile and GitHub Action to generate as many different builds as you wish to support. For example, you could 71 | support generating macOS .dmg and .pkg files, Windows .msi and .exe, Linux .deb and .rpm in several different binary 72 | formats. 73 | 74 | ### Q: What about macOS Signing? 75 | 76 | A: You will likely need to add additional options to ship properly on macOS - most notably, you will want to sign and 77 | notarize your app for macOS to make everything work without end user warnings. Check out tools such 78 | as [Gon](https://github.com/nordcloud/gon) 79 | or [this command-line signing tutorial](https://blog.dgunia.de/2020/02/12/signed-macos-programs-with-java-14/). 80 | 81 | ### Q: Can I generate macOS installers on Windows, or Windows installers on macOS? Or macOS/Windows on Linux? 82 | 83 | A: [No](https://openjdk.java.net/jeps/392), but this project uses GitHub workflows to generate 84 | [macOS](https://github.com/wiverson/maven-jpackage-template/blob/main/.github/workflows/maven-build-installer.yml) 85 | and 86 | [Windows](https://github.com/wiverson/maven-jpackage-template/blob/main/.github/workflows/maven-build-installer-windows.yml) ( 87 | and Linux) 88 | installers automatically, regardless of your development platform. This means that (for example) 89 | you could do your dev work on Linux and rely on the GitHub Actions to generate macOS and Windows builds. If you need 90 | help, reach out to [ChangeNode.com](https://changenode.com/). 91 | 92 | You still should (of course) do platform specific testing on your apps, but that's a different topic. 93 | 94 | ### Q: Does this support auto-updating, crash reporting, or analytics? 95 | 96 | A: No... for that, you should check out [ChangeNode.com](https://changenode.com/)! 97 | 98 | ### Q: I'd rather use shell scripts. 99 | 100 | A: Ok. Check out [JPackageScriptFX](https://github.com/dlemmermann/JPackageScriptFX) - the original shell scripts used 101 | as a reference when I initially started work on this project. 102 | 103 | ### Q: I didn't realize JavaFX was so cool - any pointers? 104 | 105 | Sure - here's 106 | my [personal list of cool JavaFX resources](https://gist.github.com/wiverson/6c7f49819016cece906f0e8cea195ea2), which 107 | includes links to a few other big lists of resources. 108 | 109 | ### Q: Any Maven tips? 110 | 111 | If you are not familiar with the standard Maven build lifecycle, you are highly encouraged to review the documentation 112 | ["Introduction to the Build Lifecycle"](https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html) 113 | to understand how Maven builds work. 114 | 115 | The project also uses os-activated, platform-specific 116 | [profiles](https://maven.apache.org/guides/introduction/introduction-to-profiles.html) for configuration - really cool 117 | for setting up platform-specific stuff. 118 | 119 | ### Q: Wait, didn't this project use to try to completely modularize the application first? 120 | 121 | A: Correct. 122 | 123 | There were two previous versions of this template, and both strategies were discarded as impractical in the real world. 124 | 125 | The first approach was to **try to create a shaded jar** (a single jar containing all of the project dependencies) and 126 | then use jdeps to create a module-info.java and add it to the shaded jar. This failed to account for things like 127 | multi-release jars and jars with service declarations. While it worked for trivial applications, it fell apart with more 128 | complex real world usage. In particular, attempting to integrate Spring Boot into the application caused many issues to 129 | appear. 130 | 131 | The second attempt took a more sophisticated approach - **using jdeps to automatically modularize all of the project 132 | dependencies.** 133 | A new [`collect-modules` goal](https://github.com/wiverson/jtoolprovider-plugin/blob/main/collect-modules-doc.md) 134 | was added. In brief, the plugin would walk through the entire Maven dependency tree and sort all of the declared 135 | dependencies into folders. The dependencies that already were modularized (both basic and multi-release jars) went into 136 | one folder, and ordinary non-modularized jars went into another. The plugin would then attempt to use jdeps to 137 | automatically generate module-info.java and add the compiled module-info.classes into each jar. 138 | 139 | Unfortunately, this also failed. There were numerous errors, such a circular references between libraries (e.g. slf4j 140 | and logback). Some jars would only work when jdeps generated open module-info.java files, and others would only work 141 | when jdeps generated module-info.java files with package level exports. Many, many jars would have large numbers of 142 | packages exposed, which mean that the resulting module-info.java files contained many, many entries. The error messages 143 | and the resolution for these messages were very, very confusing. The terminology around modules is unfortunately very 144 | inaccessible to a typical Java developer - for example, a "static" declaration in a Java module-info.java is used to 145 | denote a concept similar to the Maven notion of "provided" - but unfortunately jdeps fails to run if a needed module is 146 | declared as a transitive reference. Even if it's optional. 147 | 148 | In the end, even with the plugin, the error messages and the resolution of those messages is just simply not something a 149 | typical Java developer can be expected to understand or fix. 150 | 151 | Which brings us back to this project. The end result is effective the same - just change 152 | the `javafx.media,javafx.controls,javafx.fxml,java.logging` declaration in the pom.xml to 153 | specify the JVM modules you need and just skip all trying to modularize your app. 154 | 155 | For now, I would consider the creation and use of module-info.java to effective be a system programming interface for 156 | the JDK itself and system-level modules such as JavaFX itself, where the entire dependency tree is very carefully 157 | enforced - and likely includes native code. For ordinary developers, just enjoy the benefits of a trimmed down custom 158 | JVM and don't worry about it. After all the challenges working with 159 | [(incorrectly written!) module-info.java files](https://github.com/sormuras/modules/tree/main/doc/suspicious) 160 | I found just in the Spring Boot dependency graph, I would highly suggest that maintainers for ordinary, non-native Java 161 | libraries remove their existing module-info.java files. 162 | 163 | It's a pity, as I think that if jdeps, jlink, and jpackage were set up to me more user-friendly, I think it would be a 164 | very interesting system that might lead to slimmed down, cloud-friendly applications. For me, the acid test for Java 165 | module adoption is probably Spring Boot. It's very, very popular. From a technical standpoint, it uses technologies such 166 | as reflection that are often notoriously tricky when working with static compilation heavily. An end user should be able 167 | to simply start working with Spring Boot and at at most a few commands to their build process. Clearly projects such 168 | as [Spring Native](https://github.com/spring-projects-experimental/spring-native) 169 | shows there is an interest in this space. 170 | -------------------------------------------------------------------------------- /docs/sample-run.md: -------------------------------------------------------------------------------- 1 | 2 | For reference, here is the complete output from a successful run of the project. 3 | 4 | Note that on my (reasonably modern) Windows 10 laptop the line... 5 | 6 | `[INFO] --- jtoolprovider-plugin:1.0.23:java-tool (jpackage-win-installer) @ maven-jpackage-template ---` 7 | 8 | ...can take a little while to run. 9 | 10 | Here is a sample execution of `mvn clean install` 11 | 12 | ``` 13 | [INFO] Scanning for projects... 14 | [INFO] 15 | [INFO] ---------------< com.changenode:maven-jpackage-template >--------------- 16 | [INFO] Building maven-jpackage-template 1.0-SNAPSHOT 17 | [INFO] --------------------------------[ jar ]--------------------------------- 18 | [INFO] 19 | [INFO] --- maven-clean-plugin:3.1.0:clean (default-clean) @ maven-jpackage-template --- 20 | [INFO] Deleting /Users/wiverson/src/sample-project/target 21 | [INFO] 22 | [INFO] --- maven-resources-plugin:3.2.0:resources (default-resources) @ maven-jpackage-template --- 23 | [INFO] Using 'UTF-8' encoding to copy filtered resources. 24 | [INFO] Using 'UTF-8' encoding to copy filtered properties files. 25 | [INFO] Copying 3 resources to /Users/wiverson/src/sample-project/target/packaging 26 | [INFO] 27 | [INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ maven-jpackage-template --- 28 | [INFO] Changes detected - recompiling the module! 29 | [INFO] Compiling 9 source files to /Users/wiverson/src/sample-project/target/classes 30 | [INFO] 31 | [INFO] --- maven-resources-plugin:3.2.0:testResources (default-testResources) @ maven-jpackage-template --- 32 | [INFO] Using 'UTF-8' encoding to copy filtered resources. 33 | [INFO] Using 'UTF-8' encoding to copy filtered properties files. 34 | [INFO] skip non existing resourceDirectory /Users/wiverson/src/sample-project/src/test/resources 35 | [INFO] 36 | [INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ maven-jpackage-template --- 37 | [INFO] No sources to compile 38 | [INFO] 39 | [INFO] --- maven-surefire-plugin:3.0.0-M5:test (default-test) @ maven-jpackage-template --- 40 | [INFO] No tests to run. 41 | [INFO] 42 | [INFO] --- maven-jar-plugin:3.2.0:jar (default-jar) @ maven-jpackage-template --- 43 | [INFO] Building jar: /Users/wiverson/src/sample-project/target/dependency/maven-jpackage-template-1.0-SNAPSHOT.jar 44 | [INFO] 45 | [INFO] --- maven-dependency-plugin:3.1.2:copy-dependencies (copy-dependencies) @ maven-jpackage-template --- 46 | [INFO] Copying javafaker-1.0.2.jar to /Users/wiverson/src/sample-project/target/dependency/javafaker-1.0.2.jar 47 | [INFO] Copying commons-lang3-3.5.jar to /Users/wiverson/src/sample-project/target/dependency/commons-lang3-3.5.jar 48 | [INFO] Copying snakeyaml-1.23-android.jar to /Users/wiverson/src/sample-project/target/dependency/snakeyaml-1.23-android.jar 49 | [INFO] Copying generex-1.0.2.jar to /Users/wiverson/src/sample-project/target/dependency/generex-1.0.2.jar 50 | [INFO] Copying automaton-1.11-8.jar to /Users/wiverson/src/sample-project/target/dependency/automaton-1.11-8.jar 51 | [INFO] 52 | [INFO] --- jtoolprovider-plugin:1.0.34:java-tool (jlink) @ maven-jpackage-template --- 53 | [INFO] 54 | [INFO] --- maven-install-plugin:3.0.0-M1:install (default-install) @ maven-jpackage-template --- 55 | [INFO] Skipping artifact installation 56 | [INFO] 57 | [INFO] --- jtoolprovider-plugin:1.0.34:java-tool (jpackage) @ maven-jpackage-template --- 58 | [INFO] ------------------------------------------------------------------------ 59 | [INFO] BUILD SUCCESS 60 | [INFO] ------------------------------------------------------------------------ 61 | [INFO] Total time: 20.392 s 62 | [INFO] Finished at: 2021-03-16T14:34:34-07:00 63 | [INFO] ------------------------------------------------------------------------ 64 | ``` 65 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.devhow 8 | desktop-spring-boot 9 | 1.0-SNAPSHOT 10 | 11 | 12 | Spring Boot as a desktop app 13 | 14 | 15 | 16 | com.changenode.BaseApplication 17 | 19 | TestApp 20 | 25 | yy.w.ukkmm 26 | ${maven.build.timestamp} 27 | 28 | 29 | 31 | java.desktop,java.logging 32 | 33 | 34 | UTF-8 35 | 36 | 16 37 | 16 38 | 39 | 42 | aaaaaaaa-0000-aaaa-aaaa-aaaaaaaaaaaa 43 | 44 | Sample Vendor 45 | 46 | 47 | 48 | 50 | 51 | com.github.javafaker 52 | javafaker 53 | 1.0.2 54 | 55 | 56 | 57 | com.formdev 58 | flatlaf 59 | 1.1.2 60 | 61 | 62 | com.formdev 63 | flatlaf-intellij-themes 64 | 1.1.2 65 | 66 | 67 | 68 | de.jangassen 69 | nsmenufx 70 | 3.1.0 71 | 72 | 73 | 74 | 75 | 76 | 77 | 79 | ${project.basedir}/src/packaging 80 | true 81 | ${project.build.directory}/packaging 82 | 83 | 84 | 85 | ${project.basedir}/src/main/resources 86 | 87 | 88 | 89 | 90 | 91 | org.apache.maven.plugins 92 | maven-install-plugin 93 | 94 | 95 | true 96 | 97 | 98 | 99 | org.apache.maven.plugins 100 | maven-dependency-plugin 101 | 102 | 103 | 105 | copy-dependencies 106 | package 107 | 108 | copy-dependencies 109 | 110 | 111 | 112 | 113 | 114 | 116 | org.apache.maven.plugins 117 | maven-jar-plugin 118 | 119 | ${project.build.directory}/dependency 120 | 121 | 122 | 123 | 124 | io.github.wiverson 125 | jtoolprovider-plugin 126 | 1.0.34 127 | 128 | 130 | 131 | jlink 132 | package 133 | 134 | java-tool 135 | 136 | 137 | jlink 138 | ${project.build.directory}/jvm-image/ 139 | ${jvm.modules} 140 | ${project.build.directory}/jvm-image 141 | 142 | --strip-native-commands 143 | --no-header-files 144 | --strip-debug 145 | --no-man-pages 146 | --compress=2 147 | 148 | 149 | 150 | 157 | 158 | jpackage 159 | install 160 | 161 | java-tool 162 | 163 | 164 | jpackage 165 | ${project.build.directory}/installer-work 166 | @${project.build.directory}/packaging/${platform}-jpackage.txt 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | org.apache.maven.plugins 176 | maven-clean-plugin 177 | 3.1.0 178 | 179 | 180 | org.apache.maven.plugins 181 | maven-compiler-plugin 182 | 3.8.1 183 | 184 | 185 | org.apache.maven.plugins 186 | maven-dependency-plugin 187 | 3.1.2 188 | 189 | 190 | org.apache.maven.plugins 191 | maven-install-plugin 192 | 3.0.0-M1 193 | 194 | 195 | org.apache.maven.plugins 196 | maven-jar-plugin 197 | 3.2.0 198 | 199 | 200 | org.apache.maven.plugins 201 | maven-project-info-reports-plugin 202 | 3.1.1 203 | 204 | 205 | org.apache.maven.plugins 206 | maven-resources-plugin 207 | 3.2.0 208 | 209 | 210 | 211 | org.apache.maven.plugins 212 | maven-site-plugin 213 | 3.9.1 214 | 215 | 216 | org.apache.maven.plugins 217 | maven-surefire-plugin 218 | 3.0.0-M5 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 229 | 230 | org.codehaus.mojo 231 | versions-maven-plugin 232 | 2.8.1 233 | 234 | 235 | 236 | dependency-updates-report 237 | plugin-updates-report 238 | property-updates-report 239 | 240 | 241 | 242 | 243 | false 244 | 245 | 246 | 247 | 248 | 249 | 253 | 254 | unix-active 255 | 256 | 257 | unix 258 | 259 | 260 | 261 | linux 262 | 263 | 264 | 265 | windows-active 266 | 267 | 268 | windows 269 | 270 | 271 | 272 | win 273 | 274 | 275 | 276 | 277 | 280 | org.codehaus.mojo 281 | exec-maven-plugin 282 | 3.0.0 283 | 284 | 285 | install 286 | add-launch-to-msi 287 | 288 | exec 289 | 290 | 291 | 292 | 293 | cscript 294 | ${project.build.directory}/msi-result.log 295 | ${project.build.directory} 296 | 297 | ${project.build.directory}/packaging/add-launch-to-msi.js 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | mac-active 306 | 307 | 308 | mac 309 | 310 | 311 | 312 | mac 313 | 314 | 315 | 316 | 317 | -------------------------------------------------------------------------------- /src/main/java/com/changenode/BaseApplication.java: -------------------------------------------------------------------------------- 1 | package com.changenode; 2 | 3 | import com.changenode.plugin.*; 4 | import com.formdev.flatlaf.intellijthemes.FlatDraculaIJTheme; 5 | import de.jangassen.MenuToolkit; 6 | import de.jangassen.model.AppearanceMode; 7 | 8 | import javax.swing.*; 9 | import java.io.*; 10 | 11 | import static com.changenode.plugin.StandardMenus.isMac; 12 | import static javax.swing.filechooser.FileSystemView.getFileSystemView; 13 | 14 | public class BaseApplication implements Log { 15 | 16 | public static File outputFile; 17 | /** 18 | * This is the very simple "registry" for the various demonstration features of this application. 19 | */ 20 | private final Plugin[] plugins = new Plugin[]{new StandardMenus(), new HelloWorld(), new FileDrop(), 21 | new DesktopIntegration(), new LogFile(), new DarkMode(), new DesktopHandlers()}; 22 | 23 | BaseForm baseForm; 24 | 25 | public static void main(String[] args) { 26 | /* 27 | * This little bit of code causes this application to route the debugging output for this application to a 28 | * log file in your "default" directory. 29 | * */ 30 | try { 31 | outputFile = File.createTempFile("debug", ".log", getFileSystemView().getDefaultDirectory()); 32 | PrintStream output = new PrintStream(new BufferedOutputStream(new FileOutputStream(outputFile)), true); 33 | System.setOut(output); 34 | System.setErr(output); 35 | } catch (IOException e) { 36 | e.printStackTrace(); 37 | } 38 | 39 | if (isMac()) { 40 | System.setProperty("apple.laf.useScreenMenuBar", "true"); 41 | } 42 | 43 | System.setProperty("flatlaf.useWindowDecorations", "true"); 44 | 45 | MenuToolkit.toolkit().setAppearanceMode(AppearanceMode.DARK); 46 | 47 | FlatDraculaIJTheme.install(); 48 | 49 | JFrame.setDefaultLookAndFeelDecorated(true); 50 | 51 | BaseApplication baseApplication = new BaseApplication(); 52 | baseApplication.openGUI(); 53 | } 54 | 55 | private void openGUI() { 56 | 57 | JFrame jFrame = new JFrame("Base Application"); 58 | 59 | baseForm = new BaseForm(); 60 | jFrame.add(baseForm.panel); 61 | 62 | jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 63 | 64 | jFrame.setSize(1024, 768); 65 | 66 | JMenuBar jMenuBar = new JMenuBar(); 67 | jFrame.setJMenuBar(jMenuBar); 68 | jFrame.setVisible(true); 69 | 70 | for (Plugin plugin : plugins) { 71 | log("Setting up... " + plugin.getClass().getName()); 72 | plugin.setup(jFrame, baseForm.textArea, baseForm.toolBar, this, jMenuBar); 73 | } 74 | 75 | } 76 | 77 | @Override 78 | public void log(String s) { 79 | System.out.println(s); 80 | baseForm.textArea.append(System.lineSeparator()); 81 | baseForm.textArea.append(s); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/changenode/BaseForm.form: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | -------------------------------------------------------------------------------- /src/main/java/com/changenode/BaseForm.java: -------------------------------------------------------------------------------- 1 | package com.changenode; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | 6 | public class BaseForm { 7 | public JTextArea textArea; 8 | public JToolBar toolBar; 9 | public JLabel statusLabel; 10 | public JPanel panel; 11 | 12 | { 13 | // GUI initializer generated by IntelliJ IDEA GUI Designer 14 | // >>> IMPORTANT!! <<< 15 | // DO NOT EDIT OR ADD ANY CODE HERE! 16 | $$$setupUI$$$(); 17 | } 18 | 19 | /** 20 | * Method generated by IntelliJ IDEA GUI Designer 21 | * >>> IMPORTANT!! <<< 22 | * DO NOT edit this method OR call it in your code! 23 | * 24 | * @noinspection ALL 25 | */ 26 | private void $$$setupUI$$$() { 27 | panel = new JPanel(); 28 | panel.setLayout(new BorderLayout(0, 0)); 29 | toolBar = new JToolBar(); 30 | panel.add(toolBar, BorderLayout.NORTH); 31 | textArea = new JTextArea(); 32 | textArea.setLineWrap(true); 33 | panel.add(textArea, BorderLayout.CENTER); 34 | statusLabel = new JLabel(); 35 | statusLabel.setText("Status"); 36 | panel.add(statusLabel, BorderLayout.SOUTH); 37 | } 38 | 39 | /** 40 | * @noinspection ALL 41 | */ 42 | public JComponent $$$getRootComponent$$$() { 43 | return panel; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/changenode/Log.java: -------------------------------------------------------------------------------- 1 | package com.changenode; 2 | 3 | public interface Log { 4 | void log(String s); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/changenode/Plugin.java: -------------------------------------------------------------------------------- 1 | package com.changenode; 2 | 3 | import javax.swing.*; 4 | 5 | /** 6 | * This is a very basic, leaky example of a plugin interface 7 | */ 8 | public interface Plugin { 9 | 10 | void setup(JFrame stage, JTextArea textArea, JToolBar toolBar, Log log, JMenuBar menuBar); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/changenode/plugin/DarkMode.java: -------------------------------------------------------------------------------- 1 | package com.changenode.plugin; 2 | 3 | import com.changenode.Log; 4 | import com.changenode.Plugin; 5 | import com.formdev.flatlaf.intellijthemes.FlatDraculaIJTheme; 6 | import com.formdev.flatlaf.intellijthemes.FlatLightFlatIJTheme; 7 | import de.jangassen.MenuToolkit; 8 | import de.jangassen.model.AppearanceMode; 9 | 10 | import javax.swing.*; 11 | 12 | public class DarkMode implements Plugin { 13 | 14 | private boolean isDark = true; 15 | 16 | private JFrame scene; 17 | private JButton toggleDark; 18 | 19 | @Override 20 | public void setup(JFrame stage, JTextArea textArea, JToolBar toolBar, Log log, JMenuBar menuBar) { 21 | scene = stage; 22 | toggleDark = new JButton(); 23 | toggleDark.setText("Light"); 24 | toggleDark.addActionListener(e -> toggleDark()); 25 | toolBar.add(toggleDark); 26 | } 27 | 28 | private void toggleDark() { 29 | if (isDark) { 30 | //TODO Set light 31 | toggleDark.setText("Light"); 32 | MenuToolkit.toolkit().setAppearanceMode(AppearanceMode.LIGHT); 33 | FlatLightFlatIJTheme.install(); 34 | } else { 35 | //TODO set dark 36 | toggleDark.setText("Dark"); 37 | MenuToolkit.toolkit().setAppearanceMode(AppearanceMode.DARK); 38 | FlatDraculaIJTheme.install(); 39 | } 40 | isDark = !isDark; 41 | 42 | JFrame.setDefaultLookAndFeelDecorated(true); 43 | SwingUtilities.updateComponentTreeUI(scene); 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/changenode/plugin/DesktopHandlers.java: -------------------------------------------------------------------------------- 1 | package com.changenode.plugin; 2 | 3 | import com.changenode.Log; 4 | import com.changenode.Plugin; 5 | 6 | import javax.swing.*; 7 | import java.awt.*; 8 | import java.awt.desktop.*; 9 | import java.io.File; 10 | 11 | public class DesktopHandlers implements Plugin, SystemEventListener, AboutHandler, OpenFilesHandler, OpenURIHandler, PreferencesHandler { 12 | private Log log; 13 | 14 | @Override 15 | public void setup(JFrame stage, JTextArea textArea, JToolBar toolBar, Log log, JMenuBar menuBar) { 16 | this.log = log; 17 | 18 | Desktop.getDesktop().addAppEventListener(this); 19 | Desktop.getDesktop().setAboutHandler(this); 20 | Desktop.getDesktop().setOpenFileHandler(this); 21 | Desktop.getDesktop().setOpenURIHandler(this); 22 | Desktop.getDesktop().setPreferencesHandler(this); 23 | } 24 | 25 | @Override 26 | public void handleAbout(AboutEvent e) { 27 | log.log("AboutEvent received."); 28 | } 29 | 30 | @Override 31 | public void openFiles(OpenFilesEvent e) { 32 | log.log("Open Files search term: " + e.getSearchTerm()); 33 | for (File f : e.getFiles()) { 34 | log.log("File open: " + f.getAbsolutePath()); 35 | } 36 | } 37 | 38 | @Override 39 | public void openURI(OpenURIEvent e) { 40 | log.log("OpenURIEvent:" + e.getURI().toString()); 41 | } 42 | 43 | @Override 44 | public void handlePreferences(PreferencesEvent e) { 45 | log.log("PreferencesEvent received."); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/changenode/plugin/DesktopIntegration.java: -------------------------------------------------------------------------------- 1 | package com.changenode.plugin; 2 | 3 | import com.changenode.Log; 4 | import com.changenode.Plugin; 5 | 6 | import javax.swing.*; 7 | import java.awt.*; 8 | import java.awt.Taskbar.Feature; 9 | import java.awt.image.BufferedImage; 10 | 11 | import static com.changenode.plugin.StandardMenus.Configure; 12 | import static java.awt.Taskbar.getTaskbar; 13 | import static java.awt.Taskbar.isTaskbarSupported; 14 | 15 | public class DesktopIntegration implements Plugin { 16 | 17 | int currentIconProgress = 1; 18 | 19 | Image defaultIcon; 20 | Image redCircleIcon; 21 | 22 | public JMenu extraDesktopIntegration(Log log) { 23 | if (!isTaskbarSupported()) 24 | return null; 25 | 26 | log.log(""); 27 | log.log("Desktop integration flags for this platform include:"); 28 | 29 | for (Feature feature : Feature.values()) { 30 | log.log(" " + feature.name() + " " + getTaskbar().isSupported(feature)); 31 | } 32 | 33 | if (getTaskbar().isSupported(Feature.ICON_IMAGE)) { 34 | defaultIcon = getTaskbar().getIconImage(); 35 | 36 | BufferedImage bufferedImage = new BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB); 37 | Graphics2D graphics2D = bufferedImage.createGraphics(); 38 | graphics2D.setColor(Color.red); 39 | graphics2D.fillOval(0, 0, 256, 256); 40 | graphics2D.dispose(); 41 | 42 | redCircleIcon = bufferedImage; 43 | 44 | } 45 | JMenuItem useCustomIcon = Configure("Use Custom App Icon", x -> getTaskbar().setIconImage(redCircleIcon)); 46 | JMenuItem useDefaultAppIcon = Configure("Use Default App Icon", x -> getTaskbar().setIconImage(defaultIcon)); 47 | useCustomIcon.setEnabled(getTaskbar().isSupported(Feature.ICON_IMAGE)); 48 | useDefaultAppIcon.setEnabled(getTaskbar().isSupported(Feature.ICON_IMAGE)); 49 | 50 | JMenu desktopIntegration = new JMenu("Desktop"); 51 | 52 | JMenuItem setIconBadge = Configure("Set Badge", x -> getTaskbar().setIconBadge("1")); 53 | JMenuItem removeIconBadge = Configure("Remove Badge", x -> getTaskbar().setIconBadge("1")); 54 | 55 | setIconBadge.setEnabled(getTaskbar().isSupported(Feature.ICON_BADGE_TEXT)); 56 | removeIconBadge.setEnabled(getTaskbar().isSupported(Feature.ICON_BADGE_TEXT)); 57 | 58 | JMenuItem addProgress = Configure("Add Icon Progress", x -> getTaskbar().setProgressValue(currentIconProgress++)); 59 | JMenuItem clearProgress = Configure("Clear Icon Progress", x -> { 60 | currentIconProgress = -1; 61 | getTaskbar().setProgressValue(currentIconProgress++); 62 | }); 63 | addProgress.setEnabled(getTaskbar().isSupported(Feature.PROGRESS_VALUE)); 64 | clearProgress.setEnabled(getTaskbar().isSupported(Feature.PROGRESS_VALUE)); 65 | 66 | JMenuItem requestUserAttention = Configure("Request User Attention (5s)", x -> requestUserAttention()); 67 | 68 | requestUserAttention.setEnabled(getTaskbar().isSupported(Feature.USER_ATTENTION)); 69 | 70 | desktopIntegration.add(setIconBadge); 71 | desktopIntegration.add(removeIconBadge); 72 | desktopIntegration.add(addProgress); 73 | desktopIntegration.add(clearProgress); 74 | desktopIntegration.add(useCustomIcon); 75 | desktopIntegration.add(useDefaultAppIcon); 76 | desktopIntegration.add(requestUserAttention); 77 | 78 | return desktopIntegration; 79 | } 80 | 81 | private void requestUserAttention() { 82 | 83 | SwingWorker task = new SwingWorker() { 84 | 85 | @Override 86 | protected Void doInBackground() { 87 | try { 88 | Thread.sleep(5000); 89 | } catch (InterruptedException e) { 90 | e.printStackTrace(); 91 | } 92 | getTaskbar().requestUserAttention(true, true); 93 | return null; 94 | } 95 | 96 | }; 97 | 98 | new Thread(task).start(); 99 | } 100 | 101 | @Override 102 | public void setup(JFrame stage, JTextArea textArea, JToolBar toolBar, Log log, JMenuBar menuBar) { 103 | menuBar.add(extraDesktopIntegration(log)); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/com/changenode/plugin/FileDrop.java: -------------------------------------------------------------------------------- 1 | package com.changenode.plugin; 2 | 3 | import com.changenode.Log; 4 | import com.changenode.Plugin; 5 | 6 | import javax.swing.*; 7 | import java.io.File; 8 | 9 | public class FileDrop implements Plugin { 10 | 11 | public void setupFileDropTarget(JTextArea textArea, Log log) { 12 | 13 | //TODO 14 | 15 | // textArea.set 16 | // 17 | // textArea.setOnDragOver(event -> { 18 | // if (event.getGestureSource() != textArea && event.getDragboard().hasFiles()) { 19 | // event.acceptTransferModes(TransferMode.COPY_OR_MOVE); 20 | // } 21 | // event.consume(); 22 | // 23 | // }); 24 | // 25 | // textArea.setOnDragEntered(event -> textArea.setBackground( 26 | // new Background(new BackgroundFill(Color.CORNFLOWERBLUE, CornerRadii.EMPTY, Insets.EMPTY)))); 27 | // 28 | // textArea.setOnDragExited(event -> textArea.setBackground( 29 | // new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY)))); 30 | // 31 | // textArea.setOnDragDropped(event -> { 32 | // Dragboard db = event.getDragboard(); 33 | // boolean success = false; 34 | // if (db.hasFiles()) { 35 | // 36 | // for (File file : db.getFiles()) { 37 | // log.log(file.getAbsolutePath()); 38 | // } 39 | // 40 | // success = true; 41 | // } 42 | // /* let the source know whether the information was successfully transferred and used */ 43 | // event.setDropCompleted(success); 44 | // 45 | // event.consume(); 46 | // }); 47 | } 48 | 49 | 50 | 51 | @Override 52 | public void setup(JFrame stage, JTextArea textArea, JToolBar toolBar, Log log, JMenuBar menuBar) { 53 | log.log("Try dragging one or more files and/or directories here from another application."); 54 | setupFileDropTarget(textArea, log); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/changenode/plugin/HelloWorld.java: -------------------------------------------------------------------------------- 1 | package com.changenode.plugin; 2 | 3 | import com.changenode.Log; 4 | import com.changenode.Plugin; 5 | 6 | import javax.swing.*; 7 | 8 | public class HelloWorld implements Plugin { 9 | 10 | JButton button; 11 | 12 | @Override 13 | public void setup(JFrame stage, JTextArea textArea, JToolBar toolBar, Log log, JMenuBar menuBar) { 14 | button = new JButton(); 15 | button.setText("Hello World"); 16 | button.addActionListener(event -> log.log("Hello World! " + java.util.Calendar.getInstance().getTime())); 17 | toolBar.add(button); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/changenode/plugin/LogFile.java: -------------------------------------------------------------------------------- 1 | package com.changenode.plugin; 2 | 3 | import com.changenode.BaseApplication; 4 | import com.changenode.Log; 5 | import com.changenode.Plugin; 6 | 7 | import javax.swing.*; 8 | 9 | import static java.awt.Desktop.getDesktop; 10 | import static java.lang.System.out; 11 | import static java.util.Calendar.getInstance; 12 | 13 | public class LogFile implements Plugin { 14 | @Override 15 | public void setup(JFrame stage, JTextArea textArea, JToolBar toolBar, Log log, JMenuBar menuBar) { 16 | 17 | JMenu menu = new JMenu("Debug"); 18 | JMenuItem findDebugLog = new JMenuItem("Find Debug Log"); 19 | findDebugLog.addActionListener(e -> showDebugLog()); 20 | 21 | JMenuItem writeHelloWorldToLog = new JMenuItem("Write Hello World to Log"); 22 | writeHelloWorldToLog.addActionListener(e -> out.println("Hello World! " + getInstance().getTime())); 23 | 24 | menu.add(findDebugLog); 25 | menu.add(writeHelloWorldToLog); 26 | menuBar.add(menu); 27 | } 28 | 29 | private void showDebugLog() { 30 | getDesktop().browseFileDirectory(BaseApplication.outputFile); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/changenode/plugin/StandardMenus.java: -------------------------------------------------------------------------------- 1 | package com.changenode.plugin; 2 | 3 | import com.changenode.Log; 4 | import com.changenode.Plugin; 5 | 6 | import javax.swing.*; 7 | import java.awt.event.ActionListener; 8 | import java.awt.event.KeyEvent; 9 | import java.io.File; 10 | 11 | import static java.lang.System.getProperty; 12 | 13 | public class StandardMenus implements Plugin { 14 | 15 | private JFrame stage; 16 | private JMenuBar menuBar; 17 | private Log output; 18 | 19 | public static boolean isMac() { 20 | return getProperty("os.name").contains("Mac"); 21 | } 22 | 23 | public static JMenuItem Configure(String name, ActionListener action) { 24 | return Configure(name, action, 0); 25 | } 26 | 27 | public static JMenuItem Configure(String name, ActionListener action, int keyCode) { 28 | JMenuItem item = new JMenuItem(name); 29 | item.addActionListener(action); 30 | if (keyCode != 0) 31 | item.setMnemonic(keyCode); 32 | return item; 33 | } 34 | 35 | private void openFileDialog() { 36 | JFileChooser fileChooser = new JFileChooser(); 37 | fileChooser.setDialogTitle("Open File"); 38 | int result = fileChooser.showOpenDialog(stage); 39 | 40 | if (result == JFileChooser.APPROVE_OPTION) { 41 | File file = fileChooser.getSelectedFile(); 42 | output.log(file.getAbsolutePath()); 43 | return; 44 | } else { 45 | output.log("Open File cancelled."); 46 | } 47 | } 48 | 49 | public void standardMenus() { 50 | 51 | JMenu file = new JMenu("File"); 52 | JMenuItem newFile = Configure("New", x -> output.log("File -> New"), KeyEvent.VK_N); 53 | JMenuItem open = Configure("Open...", x -> openFileDialog(), KeyEvent.VK_O); 54 | 55 | file.add(newFile); 56 | file.add(open); 57 | 58 | if (!isMac()) { 59 | JMenuItem quit = Configure("Quit", x -> System.exit(0), KeyEvent.VK_Q); 60 | file.add(quit); 61 | } 62 | 63 | JMenu edit = new JMenu("Edit"); 64 | JMenuItem undo = Configure("Undo", x -> output.log("Undo"), KeyEvent.VK_Z); 65 | edit.add(undo); 66 | JMenuItem redo = Configure("Redo", x -> output.log("Redo"), KeyEvent.VK_R); 67 | edit.add(redo); 68 | edit.addSeparator(); 69 | JMenuItem cut = Configure("Cut", x -> output.log("Cut"), KeyEvent.VK_CUT); 70 | edit.add(cut); 71 | JMenuItem copy = Configure("Copy", x -> output.log("Copy"), KeyEvent.VK_COPY); 72 | edit.add(copy); 73 | JMenuItem paste = Configure("Paste", x -> output.log("Paste"), KeyEvent.VK_PASTE); 74 | edit.add(paste); 75 | 76 | menuBar.add(file); 77 | menuBar.add(edit); 78 | } 79 | 80 | @Override 81 | public void setup(JFrame stage, JTextArea textArea, JToolBar toolBar, Log log, JMenuBar menuBar) { 82 | this.menuBar = menuBar; 83 | this.output = log; 84 | this.stage = stage; 85 | 86 | standardMenus(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/resources/hello.txt: -------------------------------------------------------------------------------- 1 | A simple hello file that is copied into the application. 2 | -------------------------------------------------------------------------------- /src/packaging/add-launch-to-msi.js: -------------------------------------------------------------------------------- 1 | // run with command 2 | // cscript add-change.js 3 | var installer = WScript.CreateObject("WindowsInstaller.Installer"); 4 | var database = installer.OpenDatabase("${app.name}-${app.version}.msi", 1); 5 | var sql 6 | var view 7 | 8 | sql = "SELECT File from File where FileName='${app.name}.exe'"; 9 | view = database.OpenView(sql); 10 | view.Execute(); 11 | var file = view.Fetch().StringData(1) 12 | WScript.StdErr.WriteLine(file); 13 | view.Close(); 14 | 15 | try { 16 | sql = "INSERT INTO `CustomAction` (`Action`,`Type`,`Source`) VALUES ('ExecuteAfterFinalize','2258','" + file + "')" 17 | WScript.StdErr.WriteLine(sql); 18 | view = database.OpenView(sql); 19 | view.Execute(); 20 | view.Close(); 21 | 22 | sql = "INSERT INTO `InstallExecuteSequence` (`Action`,`Condition`,`Sequence`) VALUES ('ExecuteAfterFinalize','NOT Installed','6700')" 23 | WScript.StdErr.WriteLine(sql); 24 | view = database.OpenView(sql); 25 | view.Execute(); 26 | view.Close(); 27 | WScript.StdErr.WriteLine("Committing changes"); 28 | database.Commit(); 29 | } catch (e) { 30 | WScript.StdErr.WriteLine(e); 31 | WScript.Quit(1); 32 | } 33 | -------------------------------------------------------------------------------- /src/packaging/linux-jpackage.txt: -------------------------------------------------------------------------------- 1 | --name ${app.name} 2 | --icon ${project.basedir}/app-icon.png 3 | --dest ${project.build.directory} 4 | --main-jar ${project.build.finalName}.jar 5 | --main-class ${main-class} 6 | --input ${project.build.directory}/dependency 7 | --app-version ${app.version} 8 | --runtime-image ${project.build.directory}/jvm-image 9 | --temp ${project.build.directory}/installer-work -------------------------------------------------------------------------------- /src/packaging/mac-jpackage.txt: -------------------------------------------------------------------------------- 1 | --name ${app.name} 2 | --icon ${project.basedir}/app-icon.icns 3 | --dest ${project.build.directory} 4 | --main-jar ${project.build.finalName}.jar 5 | --main-class ${main-class} 6 | --input ${project.build.directory}/dependency 7 | --app-version ${app.version} 8 | --runtime-image ${project.build.directory}/jvm-image 9 | --temp ${project.build.directory}/installer-work -------------------------------------------------------------------------------- /src/packaging/win-jpackage.txt: -------------------------------------------------------------------------------- 1 | --type msi 2 | --name ${app.name} 3 | --win-menu 4 | --win-menu-group ${vendor} 5 | --vendor ${windows.vendor} 6 | --icon ${project.basedir}\app-icon.ico 7 | --dest ${project.build.directory} 8 | --main-jar ${project.build.finalName}.jar 9 | --main-class ${main-class} 10 | --input ${project.build.directory}\dependency 11 | --app-version ${app.version} 12 | --runtime-image ${project.build.directory}\jvm-image 13 | --temp ${project.build.directory}\installer-work 14 | --win-upgrade-uuid ${windows.upgrade.uuid} 15 | --description "${project.description}" 16 | --copyright "(C) ${windows.vendor}" --------------------------------------------------------------------------------