diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index da301a18..b3c8dba6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,9 +12,9 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [11, 17] + java: [17] env: - DEFAULT_JAVA: 11 + DEFAULT_JAVA: 17 steps: @@ -25,7 +25,7 @@ jobs: - uses: actions/setup-java@v2 with: - distribution: 'zulu' + distribution: 'temurin' java-version: ${{ matrix.java }} - uses: gradle/wrapper-validation-action@v1 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 076ba741..f2129a3f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -26,8 +26,8 @@ jobs: - uses: actions/setup-java@v2 with: - distribution: 'zulu' - java-version: 11 + distribution: 'temurin' + java-version: 17 - uses: gradle/wrapper-validation-action@v1 diff --git a/.vscode/settings.json b/.vscode/settings.json index 9528aca8..58d9888b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,7 +4,7 @@ "**/.project": true, "**/.settings": true, "**/.factorypath": true, - "**/build/": true, + "**/build/": false, "**/bin/": true, ".gradle/": true, ".idea/": true, diff --git a/CHANGELOG.md b/CHANGELOG.md index 128b3003..b3e4c256 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.8.0] - unreleased +### Breaking change + +* Requires Java 17 + +### Feature + +* [#216](https://github.com/itsallcode/white-rabbit/pull/216): Add support for macOS. + ### Changed * [#215](https://github.com/itsallcode/white-rabbit/pull/215): Upgrade dependencies, fix sonar issues. diff --git a/README.md b/README.md index 9ddad481..846b4638 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ A time recording tool ### Requirements -* Java Runtime Environment (JRE) 11 or 17, e.g. [Eclipse Temurin](https://adoptium.net/). +* Java Runtime Environment (JRE) 17, e.g. [Eclipse Temurin](https://adoptium.net/). ### Install WhiteRabbit locally diff --git a/api/.settings/org.eclipse.jdt.core.prefs b/api/.settings/org.eclipse.jdt.core.prefs index 4e7b42e4..5b5a0b0f 100644 --- a/api/.settings/org.eclipse.jdt.core.prefs +++ b/api/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 -org.eclipse.jdt.core.compiler.compliance=11 -org.eclipse.jdt.core.compiler.source=11 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.source=17 org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false diff --git a/build.gradle b/build.gradle index 0bb430ab..1e75a53d 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,7 @@ plugins { id 'com.github.johnrengelman.shadow' version '7.0.0' apply false id "com.moowork.node" version "1.3.1" apply false id "com.github.ben-manes.versions" version "0.39.0" + id "org.panteleyev.jpackageplugin" version "1.3.1" apply false } ext { @@ -40,7 +41,7 @@ subprojects { java { toolchain { - languageVersion = JavaLanguageVersion.of(11) + languageVersion = JavaLanguageVersion.of(17) } modularity.inferModulePath = false } diff --git a/jfxui/.settings/org.eclipse.jdt.core.prefs b/jfxui/.settings/org.eclipse.jdt.core.prefs index 86b5fbff..6893f9ab 100644 --- a/jfxui/.settings/org.eclipse.jdt.core.prefs +++ b/jfxui/.settings/org.eclipse.jdt.core.prefs @@ -8,8 +8,8 @@ org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary= org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable org.eclipse.jdt.core.compiler.annotation.nullable.secondary= org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 -org.eclipse.jdt.core.compiler.compliance=11 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 org.eclipse.jdt.core.compiler.problem.APILeak=warning org.eclipse.jdt.core.compiler.problem.annotatedTypeArgumentToUnannotated=info org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning @@ -105,7 +105,7 @@ org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning -org.eclipse.jdt.core.compiler.source=11 +org.eclipse.jdt.core.compiler.source=17 org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false diff --git a/jfxui/build.gradle b/jfxui/build.gradle index 404e2e6d..0c4731c8 100644 --- a/jfxui/build.gradle +++ b/jfxui/build.gradle @@ -1,5 +1,6 @@ plugins { id 'com.github.johnrengelman.shadow' + id 'org.panteleyev.jpackageplugin' } ext { @@ -123,3 +124,43 @@ task uiTest(type: Test, group: 'verification') { check.dependsOn uiTest tasks["jacocoTestReport"].executionData fileTree(project.buildDir).include("jacoco/*.exec") + + +def jPackageDir = "$buildDir/jpackage-jars" + +task copyJPackageDependencies(type: Copy, dependsOn: ["jar"]) { + from(configurations.runtimeClasspath).into jPackageDir + from(tasks.jar.outputs.files).into jPackageDir +} + + +tasks.jpackage { + dependsOn("copyJPackageDependencies") + + input = jPackageDir + destination = "$buildDir/jpackage-dist" + + appName = "WhiteRabbit" + vendor = '"It\'s all code"' + copyright = '"Copyright (C) 2021 Christoph Pirkl "' + licenseFile = "${rootProject.rootDir}/LICENSE" + appDescription = '"A time recording tool"' + icon = "${projectDir}/src/main/resources/icon.png" + + mainJar = tasks.jar.archiveFileName.get() + mainClass = project.mainClass + + verbose = false + arguments = [] + javaOptions = ["-Dfile.encoding=UTF-8"] + additionalParameters = ["--about-url", "https://github.com/itsallcode/white-rabbit/blob/develop/README.md"] + windows { + winConsole = true + } + + mac { + macPackageIdentifier = "org.itsallcode.white-rabbit" + macPackageName = "WhiteRabbit" + icon = "${projectDir}/src/main/resources/icon.icns" + } +} diff --git a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/App.java b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/App.java index a473ee9f..0e7a1f2e 100644 --- a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/App.java +++ b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/App.java @@ -1,6 +1,7 @@ package org.itsallcode.whiterabbit.jfxui; import org.itsallcode.whiterabbit.jfxui.splashscreen.SplashScreenLoader; +import org.itsallcode.whiterabbit.jfxui.systemmenu.DesktopIntegration; import javafx.application.Application; @@ -11,6 +12,8 @@ public static void main(String[] args) System.setProperty("javafx.preloader", SplashScreenLoader.class.getName()); System.setProperty("log4j.skipJansi", "false"); System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); + + DesktopIntegration.getInstance().register(); Application.launch(JavaFxApp.class, args); } } diff --git a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/JavaFxApp.java b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/JavaFxApp.java index 4eb9a568..bc95b00d 100644 --- a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/JavaFxApp.java +++ b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/JavaFxApp.java @@ -17,6 +17,7 @@ import org.itsallcode.whiterabbit.jfxui.property.DelayedPropertyListener; import org.itsallcode.whiterabbit.jfxui.splashscreen.ProgressPreloaderNotification; import org.itsallcode.whiterabbit.jfxui.splashscreen.ProgressPreloaderNotification.Type; +import org.itsallcode.whiterabbit.jfxui.systemmenu.DesktopIntegration; import org.itsallcode.whiterabbit.jfxui.ui.AppUi; import org.itsallcode.whiterabbit.jfxui.ui.InterruptionDialog; import org.itsallcode.whiterabbit.jfxui.uistate.UiStateService; @@ -107,6 +108,8 @@ private void doInitialize() state = AppState.create(appService, UiStateService.loadState(config, new DelayedPropertyListener(appService.scheduler()))); actions = UiActions.create(config, state, appService, getHostServices()); + + DesktopIntegration.getInstance().setUiActions(actions); } private Config loadConfig() diff --git a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/OsCheck.java b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/OsCheck.java new file mode 100644 index 00000000..58c3ebf0 --- /dev/null +++ b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/OsCheck.java @@ -0,0 +1,60 @@ +package org.itsallcode.whiterabbit.jfxui; + + +import java.awt.Desktop; +import java.util.Locale; + +/** + * Helper class to check the operating system this Java VM runs in. + * + * please keep the notes below as a pseudo-license: + * + * http://stackoverflow.com/questions/228477/how-do-i-programmatically-determine-operating-system-in-java + * compare to + * http://svn.terracotta.org/svn/tc/dso/tags/2.6.4/code/base/common/src/com/tc/util/runtime/Os.java + * http://www.docjar.com/html/api/org/apache/commons/lang/SystemUtils.java.html + */ +public class OsCheck +{ + /** + * Types of Operating Systems + */ + public enum OSType + { + WINDOWS, MACOS, LINUX, OTHER + } + + /** + * Detect the operating system from the {@code os.name} System property and + * cache the result. + * + * @returns the operating system detected + */ + public OSType getOperatingSystemType() + { + return detectOperatingSystemType(); + } + + private static OSType detectOperatingSystemType() + { + final String os = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH); + if ((os.indexOf("mac") >= 0) || (os.indexOf("darwin") >= 0)) + { + return OSType.MACOS; + } + else if (os.indexOf("win") >= 0) + { + return OSType.WINDOWS; + } + else if (os.indexOf("linux") >= 0) + { + return OSType.LINUX; + } + return OSType.OTHER; + } + + public boolean isDesktopSupported() + { + return Desktop.isDesktopSupported(); + } +} diff --git a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/UiActions.java b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/UiActions.java index 92a68807..d1cd7624 100644 --- a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/UiActions.java +++ b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/UiActions.java @@ -4,6 +4,7 @@ import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Optional; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -15,16 +16,19 @@ import org.itsallcode.whiterabbit.logic.Config; import org.itsallcode.whiterabbit.logic.model.MonthIndex; import org.itsallcode.whiterabbit.logic.report.vacation.VacationReport; +import org.itsallcode.whiterabbit.logic.service.AppPropertiesService.AppProperties; import org.itsallcode.whiterabbit.logic.service.AppService; import javafx.application.HostServices; import javafx.application.Platform; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; +import javafx.scene.control.ButtonBar.ButtonData; import javafx.scene.control.ButtonType; +import javafx.stage.Modality; import javafx.stage.Stage; -public final class UiActions +public class UiActions { private static final Logger LOG = LogManager.getLogger(UiActions.class); @@ -146,4 +150,25 @@ public void exitApp() { Platform.exit(); } + + public void showAboutDialog() + { + JavaFxUtil.runOnFxApplicationThread(() -> { + final AppProperties appProperties = appService.getAppProperties(); + final Alert aboutDialog = new Alert(AlertType.INFORMATION); + aboutDialog.initModality(Modality.NONE); + if (state.getPrimaryStage().isPresent()) + { + aboutDialog.initOwner(state.getPrimaryStage().get()); + } + aboutDialog.setTitle("About White Rabbit"); + aboutDialog.setHeaderText("About White Rabbit:"); + aboutDialog.setContentText("Version: " + appProperties.getVersion()); + final ButtonType close = new ButtonType("Close", ButtonData.CANCEL_CLOSE); + final ButtonType homepage = new ButtonType("Open Homepage", ButtonData.HELP); + aboutDialog.getButtonTypes().setAll(close, homepage); + final Optional selectedButton = aboutDialog.showAndWait(); + selectedButton.filter(response -> response == homepage).ifPresent(buttonType -> this.openHomepage()); + }); + } } diff --git a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/systemmenu/DesktopIntegration.java b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/systemmenu/DesktopIntegration.java new file mode 100644 index 00000000..3b3d66d7 --- /dev/null +++ b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/systemmenu/DesktopIntegration.java @@ -0,0 +1,15 @@ +package org.itsallcode.whiterabbit.jfxui.systemmenu; + +import org.itsallcode.whiterabbit.jfxui.UiActions; + +public interface DesktopIntegration +{ + public static DesktopIntegration getInstance() + { + return StaticInstanceHolder.getInstance(); + } + + void register(); + + void setUiActions(UiActions actions); +} \ No newline at end of file diff --git a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/systemmenu/DesktopIntegrationImpl.java b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/systemmenu/DesktopIntegrationImpl.java new file mode 100644 index 00000000..b83b76e9 --- /dev/null +++ b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/systemmenu/DesktopIntegrationImpl.java @@ -0,0 +1,54 @@ +package org.itsallcode.whiterabbit.jfxui.systemmenu; + +import java.awt.Desktop; +import java.awt.Desktop.Action; +import java.awt.desktop.AboutEvent; +import java.util.Objects; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.itsallcode.whiterabbit.jfxui.UiActions; + +class DesktopIntegrationImpl implements DesktopIntegration +{ + private static final Logger LOG = LogManager.getLogger(DesktopIntegrationImpl.class); + + private final Desktop desktop; + + private UiActions actions; + + DesktopIntegrationImpl(Desktop desktop) + { + this.desktop = desktop; + } + + @Override + public void register() + { + LOG.debug("Registering desktop integration"); + if (desktop.isSupported(Action.APP_ABOUT)) + { + desktop.setAboutHandler(this::showAboutDialog); + } + } + + @Override + public void setUiActions(UiActions actions) + { + this.actions = Objects.requireNonNull(actions); + } + + private void showAboutDialog(AboutEvent e) + { + getActions().showAboutDialog(); + } + + private UiActions getActions() + { + if (actions == null) + { + throw new IllegalStateException("UI Actions not registered"); + } + return actions; + } +} diff --git a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/systemmenu/HeadlessDesktopIntegration.java b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/systemmenu/HeadlessDesktopIntegration.java new file mode 100644 index 00000000..6ebe044a --- /dev/null +++ b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/systemmenu/HeadlessDesktopIntegration.java @@ -0,0 +1,18 @@ +package org.itsallcode.whiterabbit.jfxui.systemmenu; + +import org.itsallcode.whiterabbit.jfxui.UiActions; + +class HeadlessDesktopIntegration implements DesktopIntegration +{ + @Override + public void register() + { + // Nothing to do + } + + @Override + public void setUiActions(UiActions actions) + { + // Nothing to do + } +} diff --git a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/systemmenu/StaticInstanceHolder.java b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/systemmenu/StaticInstanceHolder.java new file mode 100644 index 00000000..b8f7d3de --- /dev/null +++ b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/systemmenu/StaticInstanceHolder.java @@ -0,0 +1,46 @@ +package org.itsallcode.whiterabbit.jfxui.systemmenu; + +import java.awt.Desktop; + +import org.itsallcode.whiterabbit.jfxui.OsCheck; + +class StaticInstanceHolder +{ + private static DesktopIntegration instance; + + private StaticInstanceHolder() + { + // not instantiable + } + + static DesktopIntegration getInstance() + { + if (instance == null) + { + instance = new InstanceFactory(new OsCheck()).createInstance(); + } + return instance; + } + + static class InstanceFactory + { + private final OsCheck osCheck; + + InstanceFactory(OsCheck osCheck) + { + this.osCheck = osCheck; + } + + DesktopIntegration createInstance() + { + if (osCheck.isDesktopSupported()) + { + return new DesktopIntegrationImpl(Desktop.getDesktop()); + } + else + { + return new HeadlessDesktopIntegration(); + } + } + } +} diff --git a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/tray/Tray.java b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/tray/Tray.java index 38aabf01..4812a582 100644 --- a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/tray/Tray.java +++ b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/tray/Tray.java @@ -3,12 +3,15 @@ import java.awt.SystemTray; import java.awt.TrayIcon.MessageType; +import org.itsallcode.whiterabbit.jfxui.OsCheck; +import org.itsallcode.whiterabbit.jfxui.OsCheck.OSType; + public interface Tray { static Tray create(TrayCallback callback) { return SwingUtil.runOnSwingThread(() -> { - if (!SystemTray.isSupported()) + if (!isAwtSystemTraySupported()) { return new DummyTrayIcon(); } @@ -17,6 +20,11 @@ static Tray create(TrayCallback callback) }); } + static boolean isAwtSystemTraySupported() + { + return SystemTray.isSupported() && new OsCheck().getOperatingSystemType() != OSType.MACOS; + } + void displayMessage(String caption, String text, MessageType messageType); void removeTrayIcon(); diff --git a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/ui/AppUi.java b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/ui/AppUi.java index c6bb6215..eb77a41b 100644 --- a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/ui/AppUi.java +++ b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/ui/AppUi.java @@ -152,7 +152,7 @@ public AppUi build() private VBox createTopContainer() { - final MenuBar menuBar = new MenuBarBuilder(actions, primaryStage, appService, state.stoppedWorkingForToday) + final MenuBar menuBar = new MenuBarBuilder(actions, appService, state.stoppedWorkingForToday) .build(); final VBox topContainer = new VBox(); topContainer.getChildren().addAll(menuBar, createToolBar()); diff --git a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/ui/MenuBarBuilder.java b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/ui/MenuBarBuilder.java index a83d4c34..9d31272a 100644 --- a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/ui/MenuBarBuilder.java +++ b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/ui/MenuBarBuilder.java @@ -1,28 +1,20 @@ package org.itsallcode.whiterabbit.jfxui.ui; import java.util.Objects; -import java.util.Optional; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.itsallcode.whiterabbit.jfxui.UiActions; -import org.itsallcode.whiterabbit.logic.service.AppPropertiesService.AppProperties; import org.itsallcode.whiterabbit.logic.service.AppService; import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.event.ActionEvent; import javafx.event.EventHandler; -import javafx.scene.control.Alert; -import javafx.scene.control.Alert.AlertType; -import javafx.scene.control.ButtonBar.ButtonData; -import javafx.scene.control.ButtonType; import javafx.scene.control.Menu; import javafx.scene.control.MenuBar; import javafx.scene.control.MenuItem; import javafx.scene.control.SeparatorMenuItem; -import javafx.stage.Modality; -import javafx.stage.Stage; class MenuBarBuilder { @@ -30,13 +22,10 @@ class MenuBarBuilder private final UiActions actions; private final AppService appService; private final BooleanProperty stoppedWorkingForToday; - private final Stage primaryStage; - MenuBarBuilder(UiActions actions, Stage primaryStage, AppService appService, - BooleanProperty stoppedWorkingForToday) + MenuBarBuilder(UiActions actions, AppService appService, BooleanProperty stoppedWorkingForToday) { this.actions = actions; - this.primaryStage = primaryStage; this.appService = appService; this.stoppedWorkingForToday = Objects.requireNonNull(stoppedWorkingForToday); } @@ -70,8 +59,9 @@ public MenuBar build() menuItem("_Vacation report", "menuitem_vacation_report", actions::showVacationReport)); menuPlugins.getItems() .addAll(menuItem("_Plugin manager", "menuitem_pluginmanager", actions::showPluginManager)); - menuHelp.getItems().addAll(menuItem("_About", "menuitem_about", this::showAboutDialog)); + menuHelp.getItems().addAll(menuItem("_About", "menuitem_about", actions::showAboutDialog)); menuBar.getMenus().addAll(menuFile, menuCalculations, menuReports, menuPlugins, menuHelp); + menuBar.setUseSystemMenuBar(true); return menuBar; } @@ -110,20 +100,4 @@ private MenuItem menuItem(String label, String id, EventHandler act menuItem.setOnAction(action); return menuItem; } - - private void showAboutDialog() - { - final AppProperties appProperties = appService.getAppProperties(); - final Alert aboutDialog = new Alert(AlertType.INFORMATION); - aboutDialog.initModality(Modality.NONE); - aboutDialog.initOwner(primaryStage); - aboutDialog.setTitle("About White Rabbit"); - aboutDialog.setHeaderText("About White Rabbit:"); - aboutDialog.setContentText("Version: " + appProperties.getVersion()); - final ButtonType close = new ButtonType("Close", ButtonData.CANCEL_CLOSE); - final ButtonType homepage = new ButtonType("Open Homepage", ButtonData.HELP); - aboutDialog.getButtonTypes().setAll(close, homepage); - final Optional selectedButton = aboutDialog.showAndWait(); - selectedButton.filter(response -> response == homepage).ifPresent(buttonType -> actions.openHomepage()); - } } diff --git a/jfxui/src/main/resources/icon.icns b/jfxui/src/main/resources/icon.icns new file mode 100644 index 00000000..19273aa3 Binary files /dev/null and b/jfxui/src/main/resources/icon.icns differ diff --git a/jfxui/src/test/java/org/itsallcode/whiterabbit/jfxui/OsCheckTest.java b/jfxui/src/test/java/org/itsallcode/whiterabbit/jfxui/OsCheckTest.java new file mode 100644 index 00000000..d1108e02 --- /dev/null +++ b/jfxui/src/test/java/org/itsallcode/whiterabbit/jfxui/OsCheckTest.java @@ -0,0 +1,73 @@ +package org.itsallcode.whiterabbit.jfxui; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.itsallcode.whiterabbit.jfxui.OsCheck.OSType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class OsCheckTest +{ + private static final String OS_NAME_SYSTEM_PROPERTY = "os.name"; + private OsCheck osCheck; + + @BeforeEach + void setup() + { + osCheck = new OsCheck(); + } + + @Test + void getOperatingSystemType() + { + assertThat(osCheck.getOperatingSystemType()) + .isNotNull() + .isNotEqualTo(OSType.OTHER); + } + + @ParameterizedTest + @CsvSource(nullValues = + { "NULL" }, value = { + "Mac OS X, MACOS", + "MAC OS X, MACOS", + "Mac, MACOS", + "_Mac_, MACOS", + "Darwin, MACOS", + "Windows 10, WINDOWS", + "Windows 11, WINDOWS", + "Win, WINDOWS", + "_Win_, WINDOWS", + "linux, LINUX", + "LINUX DEBIAN, LINUX", + "unix, OTHER", + "NULL, OTHER" + }) + + void detectOperatingSystemType(String osNameSystemProperty, OSType expectedType) + { + final String orgValue = System.getProperty(OS_NAME_SYSTEM_PROPERTY); + try + { + setSystemProperty(osNameSystemProperty); + assertThat(osCheck.getOperatingSystemType()).isEqualTo(expectedType); + } + finally + { + setSystemProperty(orgValue); + } + } + + private void setSystemProperty(String value) + { + if (value == null) + { + System.clearProperty(OS_NAME_SYSTEM_PROPERTY); + } + else + { + System.setProperty(OS_NAME_SYSTEM_PROPERTY, value); + } + } +} diff --git a/jfxui/src/test/java/org/itsallcode/whiterabbit/jfxui/systemmenu/DesktopIntegrationImplTest.java b/jfxui/src/test/java/org/itsallcode/whiterabbit/jfxui/systemmenu/DesktopIntegrationImplTest.java new file mode 100644 index 00000000..a41127f4 --- /dev/null +++ b/jfxui/src/test/java/org/itsallcode/whiterabbit/jfxui/systemmenu/DesktopIntegrationImplTest.java @@ -0,0 +1,85 @@ +package org.itsallcode.whiterabbit.jfxui.systemmenu; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.awt.Desktop; +import java.awt.Desktop.Action; +import java.awt.desktop.AboutHandler; + +import org.itsallcode.whiterabbit.jfxui.UiActions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class DesktopIntegrationImplTest +{ + @Mock + private Desktop desktopMock; + @Mock + private UiActions uiActionsMock; + + private DesktopIntegrationImpl menuIntegration; + + @BeforeEach + void setUp() + { + menuIntegration = new DesktopIntegrationImpl(desktopMock); + } + + @Test + void settingNullUiActionsFails() + { + assertThatThrownBy(() -> menuIntegration.setUiActions(null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + void registersAboutHandler() + { + when(desktopMock.isSupported(Action.APP_ABOUT)).thenReturn(true); + menuIntegration.register(); + menuIntegration.setUiActions(uiActionsMock); + + getAboutHandler().handleAbout(null); + + verify(uiActionsMock).showAboutDialog(); + } + + @Test + void aboutHandlerFailsForMissingUiActions() + { + when(desktopMock.isSupported(Action.APP_ABOUT)).thenReturn(true); + menuIntegration.register(); + + final AboutHandler aboutHandler = getAboutHandler(); + + assertThatThrownBy(() -> aboutHandler.handleAbout(null)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("UI Actions not registered"); + } + + @Test + void aboutHandlerNotRegisteredWhenNotSupported() + { + when(desktopMock.isSupported(Action.APP_ABOUT)).thenReturn(false); + + menuIntegration.register(); + + verify(desktopMock, never()).setAboutHandler(any()); + } + + private AboutHandler getAboutHandler() + { + final ArgumentCaptor arg = ArgumentCaptor.forClass(AboutHandler.class); + verify(desktopMock).setAboutHandler(arg.capture()); + return arg.getValue(); + } +} diff --git a/jfxui/src/test/java/org/itsallcode/whiterabbit/jfxui/systemmenu/DesktopIntegrationTest.java b/jfxui/src/test/java/org/itsallcode/whiterabbit/jfxui/systemmenu/DesktopIntegrationTest.java new file mode 100644 index 00000000..cec57f37 --- /dev/null +++ b/jfxui/src/test/java/org/itsallcode/whiterabbit/jfxui/systemmenu/DesktopIntegrationTest.java @@ -0,0 +1,20 @@ +package org.itsallcode.whiterabbit.jfxui.systemmenu; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class DesktopIntegrationTest +{ + @Test + void getInstanceReturnsSameInstance() + { + assertThat(DesktopIntegration.getInstance()).isSameAs(DesktopIntegration.getInstance()); + } + + @Test + void getInstanceReturnsSameInstanceAsStaticInstanceHolder() + { + assertThat(DesktopIntegration.getInstance()).isSameAs(StaticInstanceHolder.getInstance()); + } +} diff --git a/jfxui/src/test/java/org/itsallcode/whiterabbit/jfxui/systemmenu/HeadlessDesktopIntegrationTest.java b/jfxui/src/test/java/org/itsallcode/whiterabbit/jfxui/systemmenu/HeadlessDesktopIntegrationTest.java new file mode 100644 index 00000000..3d7a8992 --- /dev/null +++ b/jfxui/src/test/java/org/itsallcode/whiterabbit/jfxui/systemmenu/HeadlessDesktopIntegrationTest.java @@ -0,0 +1,29 @@ +package org.itsallcode.whiterabbit.jfxui.systemmenu; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class HeadlessDesktopIntegrationTest +{ + private HeadlessDesktopIntegration menuIntegration; + + @BeforeEach + void setUp() + { + menuIntegration = new HeadlessDesktopIntegration(); + } + + @Test + void register() + { + assertDoesNotThrow(menuIntegration::register); + } + + @Test + void testSetUiActions() + { + assertDoesNotThrow(() -> menuIntegration.setUiActions(null)); + } +} diff --git a/jfxui/src/test/java/org/itsallcode/whiterabbit/jfxui/systemmenu/StaticInstanceHolderTest.java b/jfxui/src/test/java/org/itsallcode/whiterabbit/jfxui/systemmenu/StaticInstanceHolderTest.java new file mode 100644 index 00000000..1c53a143 --- /dev/null +++ b/jfxui/src/test/java/org/itsallcode/whiterabbit/jfxui/systemmenu/StaticInstanceHolderTest.java @@ -0,0 +1,59 @@ +package org.itsallcode.whiterabbit.jfxui.systemmenu; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.mockito.Mockito.when; + +import java.awt.Desktop; + +import org.itsallcode.whiterabbit.jfxui.OsCheck; +import org.itsallcode.whiterabbit.jfxui.systemmenu.StaticInstanceHolder.InstanceFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class StaticInstanceHolderTest +{ + @Mock + private OsCheck osCheckMock; + private InstanceFactory instanceFactory; + + @BeforeEach + void setup() + { + instanceFactory = new InstanceFactory(osCheckMock); + } + + @Test + void getInstanceReturnsSameInstance() + { + assertThat(StaticInstanceHolder.getInstance()).isSameAs(StaticInstanceHolder.getInstance()); + } + + @Test + void getInstanceReturnsSameTypeAsFactory() + { + assertThat(StaticInstanceHolder.getInstance()) + .hasSameClassAs(new InstanceFactory(new OsCheck()).createInstance()); + } + + @Test + void instanceFactoryCreatesHeadlessType() + { + when(osCheckMock.isDesktopSupported()).thenReturn(false); + + assertThat(instanceFactory.createInstance()).isInstanceOf(HeadlessDesktopIntegration.class); + } + + @Test + void instanceFactoryCreatesSupportedType() + { + assumeTrue(Desktop.isDesktopSupported(), "No headless support"); + when(osCheckMock.isDesktopSupported()).thenReturn(true); + + assertThat(instanceFactory.createInstance()).isInstanceOf(DesktopIntegrationImpl.class); + } +} diff --git a/logic/.settings/org.eclipse.jdt.core.prefs b/logic/.settings/org.eclipse.jdt.core.prefs index 7b6659fb..82af2752 100644 --- a/logic/.settings/org.eclipse.jdt.core.prefs +++ b/logic/.settings/org.eclipse.jdt.core.prefs @@ -10,9 +10,9 @@ org.eclipse.jdt.core.compiler.annotation.nullable.secondary= org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate -org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=11 +org.eclipse.jdt.core.compiler.compliance=17 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate @@ -112,7 +112,7 @@ org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.compiler.release=disabled -org.eclipse.jdt.core.compiler.source=11 +org.eclipse.jdt.core.compiler.source=17 org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false diff --git a/plugins/.settings/org.eclipse.jdt.core.prefs b/plugins/.settings/org.eclipse.jdt.core.prefs index 18ad8952..626e0e1d 100644 --- a/plugins/.settings/org.eclipse.jdt.core.prefs +++ b/plugins/.settings/org.eclipse.jdt.core.prefs @@ -1,4 +1,4 @@ eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 -org.eclipse.jdt.core.compiler.compliance=11 -org.eclipse.jdt.core.compiler.source=11 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.source=17 diff --git a/plugins/csv/.settings/org.eclipse.jdt.core.prefs b/plugins/csv/.settings/org.eclipse.jdt.core.prefs index 4e7b42e4..5b5a0b0f 100644 --- a/plugins/csv/.settings/org.eclipse.jdt.core.prefs +++ b/plugins/csv/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 -org.eclipse.jdt.core.compiler.compliance=11 -org.eclipse.jdt.core.compiler.source=11 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.source=17 org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false diff --git a/plugins/demo/.settings/org.eclipse.jdt.core.prefs b/plugins/demo/.settings/org.eclipse.jdt.core.prefs index 4e7b42e4..5b5a0b0f 100644 --- a/plugins/demo/.settings/org.eclipse.jdt.core.prefs +++ b/plugins/demo/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 -org.eclipse.jdt.core.compiler.compliance=11 -org.eclipse.jdt.core.compiler.source=11 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.source=17 org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false diff --git a/plugins/holiday-calculator/.settings/org.eclipse.jdt.core.prefs b/plugins/holiday-calculator/.settings/org.eclipse.jdt.core.prefs index 4e7b42e4..5b5a0b0f 100644 --- a/plugins/holiday-calculator/.settings/org.eclipse.jdt.core.prefs +++ b/plugins/holiday-calculator/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 -org.eclipse.jdt.core.compiler.compliance=11 -org.eclipse.jdt.core.compiler.source=11 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.source=17 org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false diff --git a/plugins/pmsmart/.settings/org.eclipse.jdt.core.prefs b/plugins/pmsmart/.settings/org.eclipse.jdt.core.prefs index 4e7b42e4..5b5a0b0f 100644 --- a/plugins/pmsmart/.settings/org.eclipse.jdt.core.prefs +++ b/plugins/pmsmart/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 -org.eclipse.jdt.core.compiler.compliance=11 -org.eclipse.jdt.core.compiler.source=11 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.source=17 org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false