From 0291d26162099319c433d63276615fc7c2afd711 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2014 14:48:01 -0500 Subject: [PATCH 01/63] Initial project skeleton --- .gitignore | 4 ++ README.md | 4 ++ pom.xml | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 pom.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d42ea6e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.classpath +/.project +/.settings +/target/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..ee56045 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +SciJava Plugins: Platforms +-------------------------- + +A collection of core platform plugins for SciJava applications. diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..2fca6a6 --- /dev/null +++ b/pom.xml @@ -0,0 +1,110 @@ + + + 4.0.0 + + + org.scijava + pom-scijava + 1.167 + + + + scijava-plugins-platforms + 0.1.0-SNAPSHOT + + SciJava Plugins: Platforms + Core platform plugins for SciJava applications. + 2010 + + + + Simplified BSD License + repo + + + + + + ctrueden + Curtis Rueden + ctrueden@wisc.edu + http://loci.wisc.edu/people/curtis-rueden + UW-Madison LOCI + http://loci.wisc.edu/ + + architect + developer + + -6 + + + + + + SciJava + https://groups.google.com/group/scijava + https://groups.google.com/group/scijava + scijava@googlegroups.com + https://groups.google.com/group/scijava + + + + + scm:git:git://github.com/scijava/scijava-plugins-platforms + scm:git:git@github.com:scijava/scijava-plugins-platforms + HEAD + https://github.com/scijava/scijava-plugins-platforms + + + + GitHub Issues + http://github.com/scijava/scijava-plugins-platforms/issues + + + + Jenkins + http://jenkins.imagej.net/job/SciJava-plugins-platforms/ + + + + + + org.scijava + scijava-common + + + + + com.apple + AppleJavaExtensions + 1.5 + provided + + + + + + + + maven-jar-plugin + + + + org.scijava.plugins.platforms + + + + + + org.codehaus.mojo + license-maven-plugin + + bsd_2 + Board of Regents of the University of +Wisconsin-Madison. + + + + + + From 440dec547d7cde25b3ae5315163c3982a06d147c Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2014 15:05:07 -0500 Subject: [PATCH 02/63] Migrate platforms from ImageJ Migrated from: https://github.com/imagej/imagej/tree/imagej-2.0.0-beta-7.9/plugins/platforms --- LICENSE.txt | 25 +++ .../macosx/MacOSXAppEventDispatcher.java | 203 ++++++++++++++++++ .../platforms/macosx/MacOSXPlatform.java | 171 +++++++++++++++ .../platforms/windows/WindowsPlatform.java | 67 ++++++ 4 files changed, 466 insertions(+) create mode 100644 LICENSE.txt create mode 100644 src/main/java/org/scijava/plugins/platforms/macosx/MacOSXAppEventDispatcher.java create mode 100644 src/main/java/org/scijava/plugins/platforms/macosx/MacOSXPlatform.java create mode 100644 src/main/java/org/scijava/plugins/platforms/windows/WindowsPlatform.java diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..0c97575 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,25 @@ +Copyright (c) 2010 - 2014, Board of Regents of the University of +Wisconsin-Madison. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXAppEventDispatcher.java b/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXAppEventDispatcher.java new file mode 100644 index 0000000..43ad824 --- /dev/null +++ b/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXAppEventDispatcher.java @@ -0,0 +1,203 @@ +/* + * #%L + * Core platform plugins for SciJava applications. + * %% + * Copyright (C) 2010 - 2014 Board of Regents of the University of + * Wisconsin-Madison. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.plugins.platforms.macosx; + +import com.apple.eawt.AboutHandler; +import com.apple.eawt.AppEvent.AboutEvent; +import com.apple.eawt.AppEvent.AppForegroundEvent; +import com.apple.eawt.AppEvent.AppHiddenEvent; +import com.apple.eawt.AppEvent.AppReOpenedEvent; +import com.apple.eawt.AppEvent.OpenFilesEvent; +import com.apple.eawt.AppEvent.PreferencesEvent; +import com.apple.eawt.AppEvent.PrintFilesEvent; +import com.apple.eawt.AppEvent.QuitEvent; +import com.apple.eawt.AppEvent.ScreenSleepEvent; +import com.apple.eawt.AppEvent.SystemSleepEvent; +import com.apple.eawt.AppEvent.UserSessionEvent; +import com.apple.eawt.AppForegroundListener; +import com.apple.eawt.AppHiddenListener; +import com.apple.eawt.AppReOpenedListener; +import com.apple.eawt.Application; +import com.apple.eawt.OpenFilesHandler; +import com.apple.eawt.PreferencesHandler; +import com.apple.eawt.PrintFilesHandler; +import com.apple.eawt.QuitHandler; +import com.apple.eawt.QuitResponse; +import com.apple.eawt.ScreenSleepListener; +import com.apple.eawt.SystemSleepListener; +import com.apple.eawt.UserSessionListener; + +import org.scijava.event.EventService; +import org.scijava.platform.event.AppAboutEvent; +import org.scijava.platform.event.AppFocusEvent; +import org.scijava.platform.event.AppOpenFilesEvent; +import org.scijava.platform.event.AppPreferencesEvent; +import org.scijava.platform.event.AppPrintEvent; +import org.scijava.platform.event.AppQuitEvent; +import org.scijava.platform.event.AppReOpenEvent; +import org.scijava.platform.event.AppScreenSleepEvent; +import org.scijava.platform.event.AppSystemSleepEvent; +import org.scijava.platform.event.AppUserSessionEvent; +import org.scijava.platform.event.AppVisibleEvent; + +/** + * Rebroadcasts Mac OS X application events as ImageJ events. + * + * @author Curtis Rueden + */ +public class MacOSXAppEventDispatcher implements AboutHandler, + AppForegroundListener, AppHiddenListener, AppReOpenedListener, + PreferencesHandler, PrintFilesHandler, QuitHandler, ScreenSleepListener, + SystemSleepListener, UserSessionListener, OpenFilesHandler +{ + + private final EventService eventService; + + public MacOSXAppEventDispatcher(final EventService eventService) { + this(Application.getApplication(), eventService); + } + + public MacOSXAppEventDispatcher(final Application app, + final EventService eventService) + { + this.eventService = eventService; + app.setAboutHandler(this); + app.setPreferencesHandler(this); + app.setPrintFileHandler(this); + app.setQuitHandler(this); + app.addAppEventListener(this); + app.setOpenFileHandler(this); + } + + // -- AboutHandler methods -- + + @Override + public void handleAbout(final AboutEvent e) { + eventService.publish(new AppAboutEvent()); + } + + // -- PreferencesHandler methods -- + + @Override + public void handlePreferences(final PreferencesEvent e) { + eventService.publish(new AppPreferencesEvent()); + } + + // -- PrintFilesHandler -- + + @Override + public void printFiles(final PrintFilesEvent e) { + eventService.publish(new AppPrintEvent()); + } + + // -- QuitHandler methods -- + + @Override + public void handleQuitRequestWith(final QuitEvent e, final QuitResponse r) { + eventService.publish(new AppQuitEvent()); + r.cancelQuit(); + } + + // -- UserSessionListener methods -- + + @Override + public void userSessionActivated(final UserSessionEvent e) { + eventService.publish(new AppUserSessionEvent(true)); + } + + @Override + public void userSessionDeactivated(final UserSessionEvent e) { + eventService.publish(new AppUserSessionEvent(false)); + } + + // -- SystemSleepListener methods -- + + @Override + public void systemAboutToSleep(final SystemSleepEvent e) { + eventService.publish(new AppSystemSleepEvent(true)); + } + + @Override + public void systemAwoke(final SystemSleepEvent e) { + eventService.publish(new AppSystemSleepEvent(false)); + } + + // -- ScreenSleepListener methods -- + + @Override + public void screenAboutToSleep(final ScreenSleepEvent e) { + eventService.publish(new AppScreenSleepEvent(true)); + } + + @Override + public void screenAwoke(final ScreenSleepEvent e) { + eventService.publish(new AppScreenSleepEvent(false)); + } + + // -- AppHiddenListener methods -- + + @Override + public void appHidden(final AppHiddenEvent e) { + eventService.publish(new AppVisibleEvent(false)); + } + + @Override + public void appUnhidden(final AppHiddenEvent e) { + eventService.publish(new AppVisibleEvent(true)); + } + + // -- AppForegroundListener methods -- + + @Override + public void appMovedToBackground(final AppForegroundEvent e) { + eventService.publish(new AppFocusEvent(false)); + } + + @Override + public void appRaisedToForeground(final AppForegroundEvent e) { + eventService.publish(new AppFocusEvent(true)); + } + + // -- AppReOpenedListener methods -- + + @Override + public void appReOpened(final AppReOpenedEvent e) { + eventService.publish(new AppReOpenEvent()); + } + + // -- OpenFilesHandler methods -- + + @Override + public void openFiles(final OpenFilesEvent event) { + eventService.publish(new AppOpenFilesEvent(event.getFiles())); + } + +} diff --git a/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXPlatform.java b/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXPlatform.java new file mode 100644 index 0000000..547e910 --- /dev/null +++ b/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXPlatform.java @@ -0,0 +1,171 @@ +/* + * #%L + * Core platform plugins for SciJava applications. + * %% + * Copyright (C) 2010 - 2014 Board of Regents of the University of + * Wisconsin-Madison. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.plugins.platforms.macosx; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.JFrame; +import javax.swing.JMenuBar; + +import org.scijava.command.Command; +import org.scijava.command.CommandInfo; +import org.scijava.command.CommandService; +import org.scijava.display.event.window.WinActivatedEvent; +import org.scijava.event.EventHandler; +import org.scijava.event.EventService; +import org.scijava.event.EventSubscriber; +import org.scijava.module.ModuleInfo; +import org.scijava.module.event.ModulesUpdatedEvent; +import org.scijava.platform.AbstractPlatform; +import org.scijava.platform.AppEventService; +import org.scijava.platform.Platform; +import org.scijava.platform.PlatformService; +import org.scijava.plugin.Plugin; + +/** + * A platform implementation for handling Mac OS X platform issues: + * + * + * @author Curtis Rueden + */ +@Plugin(type = Platform.class, name = "Mac OS X") +public class MacOSXPlatform extends AbstractPlatform { + + /** Debugging flag to allow easy toggling of Mac screen menu bar behavior. */ + private static final boolean SCREEN_MENU = true; + + @SuppressWarnings("unused") + private Object appEventDispatcher; + + private JMenuBar menuBar; + + private List> subscribers; + + // -- Platform methods -- + + @Override + public String osName() { + return "Mac OS X"; + } + + @Override + public void configure(final PlatformService service) { + super.configure(service); + + // use Mac OS X screen menu bar + if (SCREEN_MENU) System.setProperty("apple.laf.useScreenMenuBar", "true"); + + // remove app commands from menu structure + if (SCREEN_MENU) removeAppCommandsFromMenu(); + + // translate Mac OS X application events into ImageJ events + final EventService eventService = getPlatformService().getEventService(); + try { + appEventDispatcher = new MacOSXAppEventDispatcher(eventService); + } + catch (final NoClassDefFoundError e) { + // the interfaces implemented by MacOSXAppEventDispatcher might not be + // available: + // - on MacOSX Tiger without recent Java Updates + // - on earlier MacOSX versions + } + + // subscribe to relevant window-related events + subscribers = eventService.subscribe(this); + } + + @Override + public void open(final URL url) throws IOException { + if (getPlatformService().exec("open", url.toString()) != 0) { + throw new IOException("Could not open " + url); + } + } + + @Override + public boolean registerAppMenus(final Object menus) { + if (SCREEN_MENU && menus instanceof JMenuBar) { + menuBar = (JMenuBar) menus; + } + return false; + } + + // -- Disposable methods -- + + @Override + public void dispose() { + getPlatformService().getEventService().unsubscribe(subscribers); + } + + // -- Event handlers -- + + @EventHandler + protected void onEvent(final WinActivatedEvent evt) { + if (!SCREEN_MENU || !isTarget()) return; + + final Object window = evt.getWindow(); + if (!(window instanceof JFrame)) return; + + // assign the singleton menu bar to newly activated window + ((JFrame) window).setJMenuBar(menuBar); + } + + // -- Helper methods -- + + private void removeAppCommandsFromMenu() { + final PlatformService platformService = getPlatformService(); + final EventService eventService = platformService.getEventService(); + final CommandService commandService = platformService.getCommandService(); + final AppEventService appEventService = + platformService.getAppEventService(); + + // get the list of commands being handled at the application level + final List> commands = + appEventService.getCommands(); + + // remove said commands from the main menu bar + // (the Mac application menu will trigger them instead) + final ArrayList infos = new ArrayList(); + for (final Class command : commands) { + final CommandInfo info = commandService.getCommand(command); + info.setMenuPath(null); + infos.add(info); + } + eventService.publish(new ModulesUpdatedEvent(infos)); + } + +} diff --git a/src/main/java/org/scijava/plugins/platforms/windows/WindowsPlatform.java b/src/main/java/org/scijava/plugins/platforms/windows/WindowsPlatform.java new file mode 100644 index 0000000..00ea371 --- /dev/null +++ b/src/main/java/org/scijava/plugins/platforms/windows/WindowsPlatform.java @@ -0,0 +1,67 @@ +/* + * #%L + * Core platform plugins for SciJava applications. + * %% + * Copyright (C) 2010 - 2014 Board of Regents of the University of + * Wisconsin-Madison. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.plugins.platforms.windows; + +import java.io.IOException; +import java.net.URL; + +import org.scijava.platform.AbstractPlatform; +import org.scijava.platform.Platform; +import org.scijava.plugin.Plugin; + +/** + * A platform implementation for handling Windows platform issues. + * + * @author Johannes Schindelin + */ +@Plugin(type = Platform.class, name = "Windows") +public class WindowsPlatform extends AbstractPlatform { + + // -- Platform methods -- + + @Override + public String osName() { + return "Windows"; + } + + @Override + public void open(final URL url) throws IOException { + final String cmd; + if (System.getProperty("os.name").startsWith("Windows 2000")) { + cmd = "rundll32 shell32.dll,ShellExec_RunDLL"; + } + else cmd = "rundll32 url.dll,FileProtocolHandler"; + if (getPlatformService().exec(cmd, url.toString()) != 0) { + throw new IOException("Could not open " + url); + } + } + +} From 1168305ce501e2b57ddcfb1a54ff935e071f29da Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2014 15:07:41 -0500 Subject: [PATCH 03/63] Bump to next development cycle Signed-off-by: Curtis Rueden --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2fca6a6..0f8d49f 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ scijava-plugins-platforms - 0.1.0-SNAPSHOT + 0.1.1-SNAPSHOT SciJava Plugins: Platforms Core platform plugins for SciJava applications. From cf516948d43847399ff6cd832ef66afc952ac4e5 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 25 Apr 2014 16:18:56 -0500 Subject: [PATCH 04/63] Standardize gitignore syntax --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d42ea6e..10d81e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /.classpath /.project -/.settings +/.settings/ /target/ From f8a8053695fdc941e6efad897036ba21131033d6 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 14 May 2014 14:21:17 -0500 Subject: [PATCH 05/63] POM: add ImageJ Maven repository We need it for the AppleJavaExtensions 1.5, which are unfortunately not available from Maven Central. --- pom.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pom.xml b/pom.xml index 0f8d49f..bfa08c3 100644 --- a/pom.xml +++ b/pom.xml @@ -66,6 +66,13 @@ http://jenkins.imagej.net/job/SciJava-plugins-platforms/ + + + imagej.public + http://maven.imagej.net/content/groups/public + + + From 46d5c83143c27445a2936b0cd33f4504ff0d4e15 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 14 May 2014 14:21:47 -0500 Subject: [PATCH 06/63] POM: tweak whitespace --- pom.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/pom.xml b/pom.xml index bfa08c3..bbc1df5 100644 --- a/pom.xml +++ b/pom.xml @@ -87,7 +87,6 @@ 1.5 provided - From aa710a2adccf5dfe4d4d3d71b014243491c2f0b5 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 14 May 2014 14:23:10 -0500 Subject: [PATCH 07/63] Bump to next development cycle Signed-off-by: Curtis Rueden --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index bbc1df5..a110914 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ scijava-plugins-platforms - 0.1.1-SNAPSHOT + 0.1.2-SNAPSHOT SciJava Plugins: Platforms Core platform plugins for SciJava applications. From 24438aa5cf35db9984ba52976f900853537f8641 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 20 Jun 2014 14:21:31 -0500 Subject: [PATCH 08/63] Bump parent to 2.22 Signed-off-by: Johannes Schindelin --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a110914..6b08c55 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.scijava pom-scijava - 1.167 + 2.22 From aa487de14c264087ce87f00337ee00b51c1ce22c Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 6 Aug 2014 14:51:06 -0500 Subject: [PATCH 09/63] MacOSXPlatform: remove all "app-command" commands Any command tagged with that attribute should be removed from the menus. This is a more extensible approach than relying on the AppEventService to inform of us every single such command. See also: https://github.com/scijava/scijava-common/commit/ca91f4e161558c8a90ce5ca7022f78453963d5fe https://github.com/imagej/imagej-plugins-commands/commit/403d3e2afe771a9c13ca5edf95c8dba02843392f --- .../platforms/macosx/MacOSXPlatform.java | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXPlatform.java b/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXPlatform.java index 547e910..dbda2c9 100644 --- a/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXPlatform.java +++ b/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXPlatform.java @@ -38,7 +38,6 @@ import javax.swing.JFrame; import javax.swing.JMenuBar; -import org.scijava.command.Command; import org.scijava.command.CommandInfo; import org.scijava.command.CommandService; import org.scijava.display.event.window.WinActivatedEvent; @@ -48,7 +47,6 @@ import org.scijava.module.ModuleInfo; import org.scijava.module.event.ModulesUpdatedEvent; import org.scijava.platform.AbstractPlatform; -import org.scijava.platform.AppEventService; import org.scijava.platform.Platform; import org.scijava.platform.PlatformService; import org.scijava.plugin.Plugin; @@ -150,20 +148,16 @@ private void removeAppCommandsFromMenu() { final PlatformService platformService = getPlatformService(); final EventService eventService = platformService.getEventService(); final CommandService commandService = platformService.getCommandService(); - final AppEventService appEventService = - platformService.getAppEventService(); - // get the list of commands being handled at the application level - final List> commands = - appEventService.getCommands(); - - // remove said commands from the main menu bar - // (the Mac application menu will trigger them instead) + // NB: Search for commands being handled at the application level. + // We remove such commands from the main menu bar; + // the Mac application menu will trigger them instead. final ArrayList infos = new ArrayList(); - for (final Class command : commands) { - final CommandInfo info = commandService.getCommand(command); - info.setMenuPath(null); - infos.add(info); + for (final CommandInfo info : commandService.getCommands()) { + if (info.is("app-command")) { + info.setMenuPath(null); + infos.add(info); + } } eventService.publish(new ModulesUpdatedEvent(infos)); } From b429afc0f8459112cc14d25f628dbb259dac5f02 Mon Sep 17 00:00:00 2001 From: Mark Hiner Date: Thu, 7 Aug 2014 12:28:47 -0500 Subject: [PATCH 10/63] Bump to latest parent pom Updated pom-scijava to version 3.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6b08c55..c7854b4 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.scijava pom-scijava - 2.22 + 3.0 From 7eb823a3038208f21d9e3976a4c1283b1c0c570f Mon Sep 17 00:00:00 2001 From: Mark Hiner Date: Thu, 7 Aug 2014 12:29:25 -0500 Subject: [PATCH 11/63] Bump to next development cycle Signed-off-by: Mark Hiner --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c7854b4..a11120f 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ scijava-plugins-platforms - 0.1.2-SNAPSHOT + 0.1.3-SNAPSHOT SciJava Plugins: Platforms Core platform plugins for SciJava applications. From 8d2d7f5295bf948d168781d669b754727fe87dc7 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 5 Nov 2014 09:56:18 -0600 Subject: [PATCH 12/63] Use OrangeExtensions instead of Apple ones The OrangeExtensions are a pluggable jar containing stubs for the Apple Java Extensions, updated for Java 5 & 6. They are kept more up to date than the official Apple ones, and also are available on Central. For details, see: https://ymasory.github.io/OrangeExtensions/ Thanks to @gab1one for the idea: https://github.com/knime-ip/knip-scijava-bundles/issues/4 --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index a11120f..900c83f 100644 --- a/pom.xml +++ b/pom.xml @@ -82,9 +82,9 @@ - com.apple - AppleJavaExtensions - 1.5 + com.yuvimasory + orange-extensions + 1.3.0 provided From de5c913072a77b7e2ea26dcaffac67b5a81f8147 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 5 Nov 2014 10:53:13 -0600 Subject: [PATCH 13/63] Work around a typo in the orange-extensions See: https://github.com/ymasory/OrangeExtensions/pull/10 --- .../plugins/platforms/macosx/MacOSXAppEventDispatcher.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXAppEventDispatcher.java b/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXAppEventDispatcher.java index 43ad824..976f3f0 100644 --- a/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXAppEventDispatcher.java +++ b/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXAppEventDispatcher.java @@ -145,11 +145,16 @@ public void systemAboutToSleep(final SystemSleepEvent e) { eventService.publish(new AppSystemSleepEvent(true)); } - @Override + //@Override public void systemAwoke(final SystemSleepEvent e) { eventService.publish(new AppSystemSleepEvent(false)); } + public void systemAweoke(final SystemSleepEvent e) { + // HACK: To make com.yuvimasory:orange-extensions:1.3 happy. + // See: https://github.com/ymasory/OrangeExtensions/pull/10 + } + // -- ScreenSleepListener methods -- @Override From caf6d4ad3eba94f961aef95e0c1ca8421ee4b39a Mon Sep 17 00:00:00 2001 From: Mark Hiner Date: Fri, 19 Dec 2014 14:42:46 -0600 Subject: [PATCH 14/63] Bump to latest parent pom Updated to pom-scijava 5.3.3 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 900c83f..751dd5e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.scijava pom-scijava - 3.0 + 5.3.3 From 5e24680fa6df67bba51f9d09268b321ead8d413f Mon Sep 17 00:00:00 2001 From: Mark Hiner Date: Fri, 19 Dec 2014 14:49:24 -0600 Subject: [PATCH 15/63] Bump to next development cycle Signed-off-by: Jenkins --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 751dd5e..addee1a 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ scijava-plugins-platforms - 0.1.3-SNAPSHOT + 0.1.4-SNAPSHOT SciJava Plugins: Platforms Core platform plugins for SciJava applications. From 975a01d481894027a91a7b986cc54c0fa1f3ae79 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 11 Feb 2015 07:08:03 -0600 Subject: [PATCH 16/63] Happy belated New Year 2015 --- LICENSE.txt | 2 +- .../plugins/platforms/macosx/MacOSXAppEventDispatcher.java | 2 +- .../org/scijava/plugins/platforms/macosx/MacOSXPlatform.java | 2 +- .../org/scijava/plugins/platforms/windows/WindowsPlatform.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index 0c97575..f52d840 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2010 - 2014, Board of Regents of the University of +Copyright (c) 2010 - 2015, Board of Regents of the University of Wisconsin-Madison. All rights reserved. diff --git a/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXAppEventDispatcher.java b/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXAppEventDispatcher.java index 976f3f0..8444d4e 100644 --- a/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXAppEventDispatcher.java +++ b/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXAppEventDispatcher.java @@ -2,7 +2,7 @@ * #%L * Core platform plugins for SciJava applications. * %% - * Copyright (C) 2010 - 2014 Board of Regents of the University of + * Copyright (C) 2010 - 2015 Board of Regents of the University of * Wisconsin-Madison. * %% * Redistribution and use in source and binary forms, with or without diff --git a/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXPlatform.java b/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXPlatform.java index dbda2c9..bec989b 100644 --- a/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXPlatform.java +++ b/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXPlatform.java @@ -2,7 +2,7 @@ * #%L * Core platform plugins for SciJava applications. * %% - * Copyright (C) 2010 - 2014 Board of Regents of the University of + * Copyright (C) 2010 - 2015 Board of Regents of the University of * Wisconsin-Madison. * %% * Redistribution and use in source and binary forms, with or without diff --git a/src/main/java/org/scijava/plugins/platforms/windows/WindowsPlatform.java b/src/main/java/org/scijava/plugins/platforms/windows/WindowsPlatform.java index 00ea371..86a3148 100644 --- a/src/main/java/org/scijava/plugins/platforms/windows/WindowsPlatform.java +++ b/src/main/java/org/scijava/plugins/platforms/windows/WindowsPlatform.java @@ -2,7 +2,7 @@ * #%L * Core platform plugins for SciJava applications. * %% - * Copyright (C) 2010 - 2014 Board of Regents of the University of + * Copyright (C) 2010 - 2015 Board of Regents of the University of * Wisconsin-Madison. * %% * Redistribution and use in source and binary forms, with or without From 1f110974c4422d8da47f7929163f16a7f4fea09a Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 28 Apr 2015 23:03:35 -0500 Subject: [PATCH 17/63] README.md: add Jenkins build status badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ee56045..e43c824 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +![](http://jenkins.imagej.net/job/SciJava-plugins-platforms/lastBuild/badge/icon) + SciJava Plugins: Platforms -------------------------- From 1daa0322a2bd388e8b177962de15def1cf03663d Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 29 Apr 2015 14:45:20 -0500 Subject: [PATCH 18/63] README.md: link Jenkins badge to the job --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e43c824..9a762b9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![](http://jenkins.imagej.net/job/SciJava-plugins-platforms/lastBuild/badge/icon) +[![](http://jenkins.imagej.net/job/SciJava-plugins-platforms/lastBuild/badge/icon)](http://jenkins.imagej.net/job/SciJava-plugins-platforms/) SciJava Plugins: Platforms -------------------------- From 18c92bf457e0f7f4a146a2a29cd757c2e32d13fd Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 6 Oct 2015 14:33:14 -0500 Subject: [PATCH 19/63] POM: override contributors, to avoid inheriting it --- pom.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pom.xml b/pom.xml index addee1a..d8d66bb 100644 --- a/pom.xml +++ b/pom.xml @@ -38,6 +38,13 @@ -6 + + + None + From d6c41bd6d303b583c35a2ae35684432b0024fca2 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 6 Oct 2015 14:33:32 -0500 Subject: [PATCH 20/63] POM: bump parent to pom-scijava 8.3.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d8d66bb..c0cd365 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.scijava pom-scijava - 5.3.3 + 8.3.0 From 8b8eda649044efff14ded16198b5bacb6efe3591 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 6 Oct 2015 14:34:09 -0500 Subject: [PATCH 21/63] POM: tidy up the whitespace This was done with: mvn tidy:pom Best not to fight the formatting. --- pom.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pom.xml b/pom.xml index c0cd365..1fa1385 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,6 @@ SciJava Plugins: Platforms Core platform plugins for SciJava applications. 2010 - Simplified BSD License @@ -62,12 +61,10 @@ HEAD https://github.com/scijava/scijava-plugins-platforms - GitHub Issues http://github.com/scijava/scijava-plugins-platforms/issues - Jenkins http://jenkins.imagej.net/job/SciJava-plugins-platforms/ @@ -119,5 +116,4 @@ Wisconsin-Madison. - From c59dec842a5c7b041c58ac77cc82eb5e4f190da2 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 6 Oct 2015 14:36:09 -0500 Subject: [PATCH 22/63] Rename uses of "Mac OS X" to just "OS X" As of OS X 10.8 "Mountain Lion", Apple has completely dropped the "Mac" prefix from their OS X line of operating systems: http://www.macrumors.com/2012/02/16/apple-officially-drops-mac-name-from-os-x-mountain-lion/ 3.6 years later, we follow suit! --- pom.xml | 2 +- .../OSXAppEventDispatcher.java} | 10 +++++----- .../OSXPlatform.java} | 18 +++++++++--------- 3 files changed, 15 insertions(+), 15 deletions(-) rename src/main/java/org/scijava/plugins/platforms/{macosx/MacOSXAppEventDispatcher.java => osx/OSXAppEventDispatcher.java} (95%) rename src/main/java/org/scijava/plugins/platforms/{macosx/MacOSXPlatform.java => osx/OSXPlatform.java} (91%) diff --git a/pom.xml b/pom.xml index 1fa1385..2e6aef1 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ scijava-plugins-platforms - 0.1.4-SNAPSHOT + 0.2.0-SNAPSHOT SciJava Plugins: Platforms Core platform plugins for SciJava applications. diff --git a/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXAppEventDispatcher.java b/src/main/java/org/scijava/plugins/platforms/osx/OSXAppEventDispatcher.java similarity index 95% rename from src/main/java/org/scijava/plugins/platforms/macosx/MacOSXAppEventDispatcher.java rename to src/main/java/org/scijava/plugins/platforms/osx/OSXAppEventDispatcher.java index 8444d4e..2befbb5 100644 --- a/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXAppEventDispatcher.java +++ b/src/main/java/org/scijava/plugins/platforms/osx/OSXAppEventDispatcher.java @@ -28,7 +28,7 @@ * #L% */ -package org.scijava.plugins.platforms.macosx; +package org.scijava.plugins.platforms.osx; import com.apple.eawt.AboutHandler; import com.apple.eawt.AppEvent.AboutEvent; @@ -69,11 +69,11 @@ import org.scijava.platform.event.AppVisibleEvent; /** - * Rebroadcasts Mac OS X application events as ImageJ events. + * Rebroadcasts OS X application events as ImageJ events. * * @author Curtis Rueden */ -public class MacOSXAppEventDispatcher implements AboutHandler, +public class OSXAppEventDispatcher implements AboutHandler, AppForegroundListener, AppHiddenListener, AppReOpenedListener, PreferencesHandler, PrintFilesHandler, QuitHandler, ScreenSleepListener, SystemSleepListener, UserSessionListener, OpenFilesHandler @@ -81,11 +81,11 @@ public class MacOSXAppEventDispatcher implements AboutHandler, private final EventService eventService; - public MacOSXAppEventDispatcher(final EventService eventService) { + public OSXAppEventDispatcher(final EventService eventService) { this(Application.getApplication(), eventService); } - public MacOSXAppEventDispatcher(final Application app, + public OSXAppEventDispatcher(final Application app, final EventService eventService) { this.eventService = eventService; diff --git a/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXPlatform.java b/src/main/java/org/scijava/plugins/platforms/osx/OSXPlatform.java similarity index 91% rename from src/main/java/org/scijava/plugins/platforms/macosx/MacOSXPlatform.java rename to src/main/java/org/scijava/plugins/platforms/osx/OSXPlatform.java index bec989b..2cde1a6 100644 --- a/src/main/java/org/scijava/plugins/platforms/macosx/MacOSXPlatform.java +++ b/src/main/java/org/scijava/plugins/platforms/osx/OSXPlatform.java @@ -28,7 +28,7 @@ * #L% */ -package org.scijava.plugins.platforms.macosx; +package org.scijava.plugins.platforms.osx; import java.io.IOException; import java.net.URL; @@ -52,17 +52,17 @@ import org.scijava.plugin.Plugin; /** - * A platform implementation for handling Mac OS X platform issues: + * A platform implementation for handling Apple OS X platform issues: *
    *
  • Application events are rebroadcast as ImageJ events.
  • - *
  • Mac OS X screen menu bar is enabled.
  • + *
  • OS X screen menu bar is enabled.
  • *
  • Special screen menu bar menu items are handled.
  • *
* * @author Curtis Rueden */ -@Plugin(type = Platform.class, name = "Mac OS X") -public class MacOSXPlatform extends AbstractPlatform { +@Plugin(type = Platform.class, name = "OS X") +public class OSXPlatform extends AbstractPlatform { /** Debugging flag to allow easy toggling of Mac screen menu bar behavior. */ private static final boolean SCREEN_MENU = true; @@ -78,23 +78,23 @@ public class MacOSXPlatform extends AbstractPlatform { @Override public String osName() { - return "Mac OS X"; + return "OS X"; } @Override public void configure(final PlatformService service) { super.configure(service); - // use Mac OS X screen menu bar + // use OS X screen menu bar if (SCREEN_MENU) System.setProperty("apple.laf.useScreenMenuBar", "true"); // remove app commands from menu structure if (SCREEN_MENU) removeAppCommandsFromMenu(); - // translate Mac OS X application events into ImageJ events + // translate OS X application events into ImageJ events final EventService eventService = getPlatformService().getEventService(); try { - appEventDispatcher = new MacOSXAppEventDispatcher(eventService); + appEventDispatcher = new OSXAppEventDispatcher(eventService); } catch (final NoClassDefFoundError e) { // the interfaces implemented by MacOSXAppEventDispatcher might not be From 2d62a294bb88e731813dae2b9599e8d5a23813d5 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 19 Nov 2015 08:48:24 -0600 Subject: [PATCH 23/63] POM: update developers and contributors See: http://imagej.net/Team --- pom.xml | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 2e6aef1..a54f622 100644 --- a/pom.xml +++ b/pom.xml @@ -31,18 +31,40 @@ UW-Madison LOCI http://loci.wisc.edu/ - architect + founder + lead developer + debugger + reviewer + support + maintainer + + -6 + + + hinerm + Mark Hiner + hiner@wisc.edu + http://loci.wisc.edu/people/mark-hiner + UW-Madison LOCI + http://loci.wisc.edu/ + + lead + developer + debugger + reviewer + support + maintainer -6 - - None + + Johannes Schindelin + http://imagej.net/User:Schindelin + dscho + From ed73ab1b3f2bb2f474430fe6041aa0201fdbdebb Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 19 Nov 2015 08:48:50 -0600 Subject: [PATCH 24/63] POM: bump parent to pom-scijava 9.0.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a54f622..c887a53 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.scijava pom-scijava - 8.3.0 + 9.0.0 From c239c853747f5c2bf1e6af2ea27bb4c71fa7ec44 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 19 Nov 2015 09:24:09 -0600 Subject: [PATCH 25/63] POM: remove redundant mailingLists section It is inherited from the pom-scijava parent. --- pom.xml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pom.xml b/pom.xml index c887a53..174c7ed 100644 --- a/pom.xml +++ b/pom.xml @@ -67,16 +67,6 @@ - - - SciJava - https://groups.google.com/group/scijava - https://groups.google.com/group/scijava - scijava@googlegroups.com - https://groups.google.com/group/scijava - - - scm:git:git://github.com/scijava/scijava-plugins-platforms scm:git:git@github.com:scijava/scijava-plugins-platforms From 9d8cfae32c2c6f03f0e6b2f9c70628d3224da572 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 19 Nov 2015 12:47:33 -0600 Subject: [PATCH 26/63] Bump to next development cycle Signed-off-by: Jenkins --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 174c7ed..58b3257 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ scijava-plugins-platforms - 0.2.0-SNAPSHOT + 0.2.1-SNAPSHOT SciJava Plugins: Platforms Core platform plugins for SciJava applications. From 4981276848fdae7fb32a26e08776afbc400a4e58 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 4 May 2016 13:46:47 -0500 Subject: [PATCH 27/63] Use imagej.net URL for all developers And remove the superfluous (and prone to obsolescence) other info. --- pom.xml | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/pom.xml b/pom.xml index 58b3257..fe6e376 100644 --- a/pom.xml +++ b/pom.xml @@ -26,10 +26,7 @@ ctrueden Curtis Rueden - ctrueden@wisc.edu - http://loci.wisc.edu/people/curtis-rueden - UW-Madison LOCI - http://loci.wisc.edu/ + http://imagej.net/User:Rueden founder lead @@ -39,15 +36,11 @@ support maintainer - -6 hinerm Mark Hiner - hiner@wisc.edu - http://loci.wisc.edu/people/mark-hiner - UW-Madison LOCI - http://loci.wisc.edu/ + http://imagej.net/User:Hinerm lead developer @@ -56,7 +49,6 @@ support maintainer - -6 From 4ab06c9b4840fdd5d4fcfc39c7c19e0c5e96cd93 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 9 Jun 2016 16:56:57 -0500 Subject: [PATCH 28/63] POM: goodbye Mark! --- pom.xml | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/pom.xml b/pom.xml index fe6e376..9647aec 100644 --- a/pom.xml +++ b/pom.xml @@ -37,21 +37,13 @@ maintainer - - hinerm - Mark Hiner - http://imagej.net/User:Hinerm - - lead - developer - debugger - reviewer - support - maintainer - - + + Mark Hiner + http://imagej.net/User:Hinerm + hinerm + Johannes Schindelin http://imagej.net/User:Schindelin From fe3b5809308ec0d6a4f9e452c66357823edb6046 Mon Sep 17 00:00:00 2001 From: Yili Zhao Date: Fri, 16 Dec 2016 14:58:54 -0600 Subject: [PATCH 29/63] Fix Windows default browser open issue --- .../plugins/platforms/windows/WindowsPlatform.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/scijava/plugins/platforms/windows/WindowsPlatform.java b/src/main/java/org/scijava/plugins/platforms/windows/WindowsPlatform.java index 86a3148..e0025b7 100644 --- a/src/main/java/org/scijava/plugins/platforms/windows/WindowsPlatform.java +++ b/src/main/java/org/scijava/plugins/platforms/windows/WindowsPlatform.java @@ -55,11 +55,18 @@ public String osName() { @Override public void open(final URL url) throws IOException { final String cmd; + final String arg; + // NB: the cmd and arg separate is necessary for Windows OS + // to open the default browser correctly. if (System.getProperty("os.name").startsWith("Windows 2000")) { - cmd = "rundll32 shell32.dll,ShellExec_RunDLL"; + cmd = "rundll32"; + arg = "shell32.dll,ShellExec_RunDLL"; } - else cmd = "rundll32 url.dll,FileProtocolHandler"; - if (getPlatformService().exec(cmd, url.toString()) != 0) { + else { + cmd = "rundll32"; + arg = "url.dll,FileProtocolHandler"; + } + if (getPlatformService().exec(cmd, arg, url.toString()) != 0) { throw new IOException("Could not open " + url); } } From ea3294dc98e258edd8e45c1741305e3d1524eeac Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 30 Jan 2017 10:30:27 -0600 Subject: [PATCH 30/63] Update parent to org.scijava:pom-scijava:12.0.0 See: http://forum.imagej.net/t/split-boms-from-parent-configuration/2563 --- pom.xml | 48 +++++++++++++++++++++++------------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/pom.xml b/pom.xml index 9647aec..6b55000 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.scijava pom-scijava - 9.0.0 + 12.0.0 @@ -14,7 +14,12 @@ SciJava Plugins: Platforms Core platform plugins for SciJava applications. + https://github.com/scijava/scijava-plugins-platforms 2010 + + SciJava + http://www.scijava.org/ + Simplified BSD License @@ -51,6 +56,16 @@ + + + SciJava + https://groups.google.com/group/scijava + https://groups.google.com/group/scijava + scijava@googlegroups.com + https://groups.google.com/group/scijava + + + scm:git:git://github.com/scijava/scijava-plugins-platforms scm:git:git@github.com:scijava/scijava-plugins-platforms @@ -66,6 +81,13 @@ http://jenkins.imagej.net/job/SciJava-plugins-platforms/ + + org.scijava.plugins.platforms + bsd_2 + Board of Regents of the University of +Wisconsin-Madison. + + imagej.public @@ -88,28 +110,4 @@ provided - - - - - maven-jar-plugin - - - - org.scijava.plugins.platforms - - - - - - org.codehaus.mojo - license-maven-plugin - - bsd_2 - Board of Regents of the University of -Wisconsin-Madison. - - - - From 23c88582065016f29be9c224009dd1bf8af9ddc7 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 30 Jan 2017 10:37:44 -0600 Subject: [PATCH 31/63] Bump to next development cycle Signed-off-by: Jenkins --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6b55000..67a43ed 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ scijava-plugins-platforms - 0.2.1-SNAPSHOT + 0.2.2-SNAPSHOT SciJava Plugins: Platforms Core platform plugins for SciJava applications. From 8e31cf4e79539f24d13b79cba6ba9e0579dad65d Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 6 Feb 2017 10:36:15 -0600 Subject: [PATCH 32/63] POM: update pom-scijava parent to 13.1.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 67a43ed..ce515d0 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.scijava pom-scijava - 12.0.0 + 13.1.0 From 7654e1b9030d522f4f4ffba20745d7b2a1fa711c Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 6 Feb 2017 10:37:24 -0600 Subject: [PATCH 33/63] Rename OS X -> macOS Thanks Apple, for renaming your OS... again. To be fair, IMHO, the name "macOS" is much better than "OS X". :-) --- pom.xml | 2 +- .../MacOSAppEventDispatcher.java} | 10 ++++----- .../MacOSPlatform.java} | 22 +++++++++---------- 3 files changed, 17 insertions(+), 17 deletions(-) rename src/main/java/org/scijava/plugins/platforms/{osx/OSXAppEventDispatcher.java => macos/MacOSAppEventDispatcher.java} (95%) rename src/main/java/org/scijava/plugins/platforms/{osx/OSXPlatform.java => macos/MacOSPlatform.java} (89%) diff --git a/pom.xml b/pom.xml index ce515d0..0341337 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ scijava-plugins-platforms - 0.2.2-SNAPSHOT + 0.3.0-SNAPSHOT SciJava Plugins: Platforms Core platform plugins for SciJava applications. diff --git a/src/main/java/org/scijava/plugins/platforms/osx/OSXAppEventDispatcher.java b/src/main/java/org/scijava/plugins/platforms/macos/MacOSAppEventDispatcher.java similarity index 95% rename from src/main/java/org/scijava/plugins/platforms/osx/OSXAppEventDispatcher.java rename to src/main/java/org/scijava/plugins/platforms/macos/MacOSAppEventDispatcher.java index 2befbb5..706e2f4 100644 --- a/src/main/java/org/scijava/plugins/platforms/osx/OSXAppEventDispatcher.java +++ b/src/main/java/org/scijava/plugins/platforms/macos/MacOSAppEventDispatcher.java @@ -28,7 +28,7 @@ * #L% */ -package org.scijava.plugins.platforms.osx; +package org.scijava.plugins.platforms.macos; import com.apple.eawt.AboutHandler; import com.apple.eawt.AppEvent.AboutEvent; @@ -69,11 +69,11 @@ import org.scijava.platform.event.AppVisibleEvent; /** - * Rebroadcasts OS X application events as ImageJ events. + * Rebroadcasts macOS application events as ImageJ events. * * @author Curtis Rueden */ -public class OSXAppEventDispatcher implements AboutHandler, +public class MacOSAppEventDispatcher implements AboutHandler, AppForegroundListener, AppHiddenListener, AppReOpenedListener, PreferencesHandler, PrintFilesHandler, QuitHandler, ScreenSleepListener, SystemSleepListener, UserSessionListener, OpenFilesHandler @@ -81,11 +81,11 @@ public class OSXAppEventDispatcher implements AboutHandler, private final EventService eventService; - public OSXAppEventDispatcher(final EventService eventService) { + public MacOSAppEventDispatcher(final EventService eventService) { this(Application.getApplication(), eventService); } - public OSXAppEventDispatcher(final Application app, + public MacOSAppEventDispatcher(final Application app, final EventService eventService) { this.eventService = eventService; diff --git a/src/main/java/org/scijava/plugins/platforms/osx/OSXPlatform.java b/src/main/java/org/scijava/plugins/platforms/macos/MacOSPlatform.java similarity index 89% rename from src/main/java/org/scijava/plugins/platforms/osx/OSXPlatform.java rename to src/main/java/org/scijava/plugins/platforms/macos/MacOSPlatform.java index 2cde1a6..7baa410 100644 --- a/src/main/java/org/scijava/plugins/platforms/osx/OSXPlatform.java +++ b/src/main/java/org/scijava/plugins/platforms/macos/MacOSPlatform.java @@ -28,7 +28,7 @@ * #L% */ -package org.scijava.plugins.platforms.osx; +package org.scijava.plugins.platforms.macos; import java.io.IOException; import java.net.URL; @@ -52,17 +52,17 @@ import org.scijava.plugin.Plugin; /** - * A platform implementation for handling Apple OS X platform issues: + * A platform implementation for handling Apple macOS platform issues: *
    *
  • Application events are rebroadcast as ImageJ events.
  • - *
  • OS X screen menu bar is enabled.
  • + *
  • macOS screen menu bar is enabled.
  • *
  • Special screen menu bar menu items are handled.
  • *
* * @author Curtis Rueden */ -@Plugin(type = Platform.class, name = "OS X") -public class OSXPlatform extends AbstractPlatform { +@Plugin(type = Platform.class, name = "macOS") +public class MacOSPlatform extends AbstractPlatform { /** Debugging flag to allow easy toggling of Mac screen menu bar behavior. */ private static final boolean SCREEN_MENU = true; @@ -78,29 +78,29 @@ public class OSXPlatform extends AbstractPlatform { @Override public String osName() { - return "OS X"; + return "macOS"; } @Override public void configure(final PlatformService service) { super.configure(service); - // use OS X screen menu bar + // use macOS screen menu bar if (SCREEN_MENU) System.setProperty("apple.laf.useScreenMenuBar", "true"); // remove app commands from menu structure if (SCREEN_MENU) removeAppCommandsFromMenu(); - // translate OS X application events into ImageJ events + // translate macOS application events into ImageJ events final EventService eventService = getPlatformService().getEventService(); try { - appEventDispatcher = new OSXAppEventDispatcher(eventService); + appEventDispatcher = new MacOSAppEventDispatcher(eventService); } catch (final NoClassDefFoundError e) { - // the interfaces implemented by MacOSXAppEventDispatcher might not be + // the interfaces implemented by MacOSAppEventDispatcher might not be // available: // - on MacOSX Tiger without recent Java Updates - // - on earlier MacOSX versions + // - on earlier OS versions } // subscribe to relevant window-related events From d8af7f461cc402fdeef06ff0154161f751388eb2 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 6 Feb 2017 10:38:31 -0600 Subject: [PATCH 34/63] MacOSPlatform: avoid deprecated method call --- .../java/org/scijava/plugins/platforms/macos/MacOSPlatform.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/scijava/plugins/platforms/macos/MacOSPlatform.java b/src/main/java/org/scijava/plugins/platforms/macos/MacOSPlatform.java index 7baa410..a57e8f0 100644 --- a/src/main/java/org/scijava/plugins/platforms/macos/MacOSPlatform.java +++ b/src/main/java/org/scijava/plugins/platforms/macos/MacOSPlatform.java @@ -92,7 +92,7 @@ public void configure(final PlatformService service) { if (SCREEN_MENU) removeAppCommandsFromMenu(); // translate macOS application events into ImageJ events - final EventService eventService = getPlatformService().getEventService(); + final EventService eventService = getPlatformService().eventService(); try { appEventDispatcher = new MacOSAppEventDispatcher(eventService); } From 24021994a96bd5ac9df4357cb2b23fb17fda836a Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 28 Feb 2017 08:16:41 -0600 Subject: [PATCH 35/63] Switch from Jenkins to Travis CI --- .travis.yml | 12 ++++++++++++ .travis/build.sh | 7 +++++++ .travis/notify.sh | 2 ++ .travis/settings.xml | 14 ++++++++++++++ README.md | 2 +- pom.xml | 4 ++-- 6 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 .travis.yml create mode 100755 .travis/build.sh create mode 100755 .travis/notify.sh create mode 100644 .travis/settings.xml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..694dde4 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: java +jdk: oraclejdk8 +branches: + only: master +install: true +script: ".travis/build.sh" +after_success: ".travis/notify.sh Travis-Success" +after_failure: ".travis/notify.sh Travis-Failure" +env: + global: + - secure: Fm1sN5h3QDONaPy32xr/ClyvCwvdZZaEvi6u36vcgNCaJ3JMja3eMdj5qBbOQxt4oCHqPQwfyBttvzIMZcJ+mo0F++++k5DCTGbzRy8ZMXzUIAEm7ofA3Mw1hlm9DSYlcEnim+kUBzUuTxVJT5jOfWImbmqk6nEg5Rgu3/N6XuI= + - secure: VbKkdewIYYzM3ntapdUgjPd1ISkIqzXSCXquqkUhhUBRdBu1xIVTSYFtm4swGX0wY6h3Vq+qspIZVOP+NhiaiypUaEYPP9p3g8/aVroF86Dmcgb/cCpUIsM2SP0NzP9ReWUsUcETHNObk2oD69le3zZh9ImIhQre0CARI7TlgMI= diff --git a/.travis/build.sh b/.travis/build.sh new file mode 100755 index 0000000..4c2f8d2 --- /dev/null +++ b/.travis/build.sh @@ -0,0 +1,7 @@ +#!/bin/sh +dir="$(dirname "$0")" +test "$TRAVIS_SECURE_ENV_VARS" = true \ + -a "$TRAVIS_PULL_REQUEST" = false \ + -a "$TRAVIS_BRANCH" = master && + mvn -Pdeploy-to-imagej deploy --settings "$dir/settings.xml" || + mvn install diff --git a/.travis/notify.sh b/.travis/notify.sh new file mode 100755 index 0000000..b3b239e --- /dev/null +++ b/.travis/notify.sh @@ -0,0 +1,2 @@ +#!/bin/sh +curl -fs "https://jenkins.imagej.net/job/$1/buildWithParameters?token=$TOKEN_NAME&repo=$TRAVIS_REPO_SLUG&commit=$TRAVIS_COMMIT&pr=$TRAVIS_PULL_REQUEST" diff --git a/.travis/settings.xml b/.travis/settings.xml new file mode 100644 index 0000000..71a5630 --- /dev/null +++ b/.travis/settings.xml @@ -0,0 +1,14 @@ + + + + imagej.releases + travis + ${env.MAVEN_PASS} + + + imagej.snapshots + travis + ${env.MAVEN_PASS} + + + diff --git a/README.md b/README.md index 9a762b9..91d11fc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![](http://jenkins.imagej.net/job/SciJava-plugins-platforms/lastBuild/badge/icon)](http://jenkins.imagej.net/job/SciJava-plugins-platforms/) +[![](https://travis-ci.org/scijava/scijava-plugins-platforms.svg?branch=master)](https://travis-ci.org/scijava/scijava-plugins-platforms) SciJava Plugins: Platforms -------------------------- diff --git a/pom.xml b/pom.xml index 0341337..7fbb976 100644 --- a/pom.xml +++ b/pom.xml @@ -77,8 +77,8 @@ http://github.com/scijava/scijava-plugins-platforms/issues - Jenkins - http://jenkins.imagej.net/job/SciJava-plugins-platforms/ + Travis CI + https://travis-ci.org/scijava/scijava-plugins-platforms From 019f3d7254daaee3e8897b923d4df951f0e692f7 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 26 Apr 2017 15:39:14 -0500 Subject: [PATCH 36/63] Fix the Travis configuration Without this, failed builds of master trigger another "mvn install". See: https://gist.github.com/ctrueden/ae0f024a0cdf2cb53c915d75b0759553 --- .travis/build.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.travis/build.sh b/.travis/build.sh index 4c2f8d2..8cddb5f 100755 --- a/.travis/build.sh +++ b/.travis/build.sh @@ -1,7 +1,10 @@ #!/bin/sh dir="$(dirname "$0")" -test "$TRAVIS_SECURE_ENV_VARS" = true \ +if [ "$TRAVIS_SECURE_ENV_VARS" = true \ -a "$TRAVIS_PULL_REQUEST" = false \ - -a "$TRAVIS_BRANCH" = master && - mvn -Pdeploy-to-imagej deploy --settings "$dir/settings.xml" || + -a "$TRAVIS_BRANCH" = master ] +then + mvn -Pdeploy-to-imagej deploy --settings "$dir/settings.xml" +else mvn install +fi From e65e4e9be433a07b7ac507fbea48ad035bb08b16 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 30 Apr 2017 14:06:03 -0500 Subject: [PATCH 37/63] Bump to next development cycle Signed-off-by: Curtis Rueden --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7fbb976..e63d34f 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ scijava-plugins-platforms - 0.3.0-SNAPSHOT + 0.3.1-SNAPSHOT SciJava Plugins: Platforms Core platform plugins for SciJava applications. From fdce7d8f0706390f032508bcf863674ff4239289 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 15 Sep 2017 13:53:07 -0500 Subject: [PATCH 38/63] MacOSPlatform: fix osName() It needs to match the os.name system property. Noticed by Jan Eglinger. --- .../org/scijava/plugins/platforms/macos/MacOSPlatform.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/scijava/plugins/platforms/macos/MacOSPlatform.java b/src/main/java/org/scijava/plugins/platforms/macos/MacOSPlatform.java index a57e8f0..0a3cc07 100644 --- a/src/main/java/org/scijava/plugins/platforms/macos/MacOSPlatform.java +++ b/src/main/java/org/scijava/plugins/platforms/macos/MacOSPlatform.java @@ -78,7 +78,9 @@ public class MacOSPlatform extends AbstractPlatform { @Override public String osName() { - return "macOS"; + // NB: The value of the os.name system property for activation purposes; + // see org.scijava.platform.Platform#isTarget(). + return "Mac OS X"; } @Override From a72c2315616d75fde958bd2aed190ad2ab781b53 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 15 Sep 2017 13:53:37 -0500 Subject: [PATCH 39/63] MacOSPlatform: fix warnings --- .../scijava/plugins/platforms/macos/MacOSPlatform.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/scijava/plugins/platforms/macos/MacOSPlatform.java b/src/main/java/org/scijava/plugins/platforms/macos/MacOSPlatform.java index 0a3cc07..3a7dcd1 100644 --- a/src/main/java/org/scijava/plugins/platforms/macos/MacOSPlatform.java +++ b/src/main/java/org/scijava/plugins/platforms/macos/MacOSPlatform.java @@ -128,7 +128,7 @@ public boolean registerAppMenus(final Object menus) { @Override public void dispose() { - getPlatformService().getEventService().unsubscribe(subscribers); + getPlatformService().eventService().unsubscribe(subscribers); } // -- Event handlers -- @@ -148,13 +148,13 @@ protected void onEvent(final WinActivatedEvent evt) { private void removeAppCommandsFromMenu() { final PlatformService platformService = getPlatformService(); - final EventService eventService = platformService.getEventService(); - final CommandService commandService = platformService.getCommandService(); + final EventService eventService = platformService.eventService(); + final CommandService commandService = platformService.commandService(); // NB: Search for commands being handled at the application level. // We remove such commands from the main menu bar; // the Mac application menu will trigger them instead. - final ArrayList infos = new ArrayList(); + final ArrayList infos = new ArrayList<>(); for (final CommandInfo info : commandService.getCommands()) { if (info.is("app-command")) { info.setMenuPath(null); From 90cd0712e513daf705672445d19d019b4597c57b Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 5 Oct 2017 16:40:00 -0500 Subject: [PATCH 40/63] Update Travis configuration This will hopefully reduce the need for future en masse updates. --- .travis.yml | 6 +++--- .travis/build.sh | 11 ++--------- .travis/notify.sh | 2 -- 3 files changed, 5 insertions(+), 14 deletions(-) delete mode 100755 .travis/notify.sh diff --git a/.travis.yml b/.travis.yml index 694dde4..61bdf85 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,11 @@ language: java jdk: oraclejdk8 branches: - only: master + only: + - master + - "/.*-[0-9]+\\..*/" install: true script: ".travis/build.sh" -after_success: ".travis/notify.sh Travis-Success" -after_failure: ".travis/notify.sh Travis-Failure" env: global: - secure: Fm1sN5h3QDONaPy32xr/ClyvCwvdZZaEvi6u36vcgNCaJ3JMja3eMdj5qBbOQxt4oCHqPQwfyBttvzIMZcJ+mo0F++++k5DCTGbzRy8ZMXzUIAEm7ofA3Mw1hlm9DSYlcEnim+kUBzUuTxVJT5jOfWImbmqk6nEg5Rgu3/N6XuI= diff --git a/.travis/build.sh b/.travis/build.sh index 8cddb5f..e939b6c 100755 --- a/.travis/build.sh +++ b/.travis/build.sh @@ -1,10 +1,3 @@ #!/bin/sh -dir="$(dirname "$0")" -if [ "$TRAVIS_SECURE_ENV_VARS" = true \ - -a "$TRAVIS_PULL_REQUEST" = false \ - -a "$TRAVIS_BRANCH" = master ] -then - mvn -Pdeploy-to-imagej deploy --settings "$dir/settings.xml" -else - mvn install -fi +curl -fsLO https://raw.githubusercontent.com/scijava/scijava-scripts/master/travis-build.sh +sh travis-build.sh diff --git a/.travis/notify.sh b/.travis/notify.sh deleted file mode 100755 index b3b239e..0000000 --- a/.travis/notify.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -curl -fs "https://jenkins.imagej.net/job/$1/buildWithParameters?token=$TOKEN_NAME&repo=$TRAVIS_REPO_SLUG&commit=$TRAVIS_COMMIT&pr=$TRAVIS_PULL_REQUEST" From 676a7a334966b57d001dce9d23aea8af332aa65e Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 5 Oct 2017 16:40:00 -0500 Subject: [PATCH 41/63] POM: update pom-scijava parent to 17.1.1 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e63d34f..96c1c7a 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.scijava pom-scijava - 13.1.0 + 17.1.1 From 972d3aa1bc4da175d2825db40fbc9442177546f2 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 5 Oct 2017 16:40:00 -0500 Subject: [PATCH 42/63] POM: deploy releases to the ImageJ repository --- pom.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pom.xml b/pom.xml index 96c1c7a..8c3de93 100644 --- a/pom.xml +++ b/pom.xml @@ -86,6 +86,9 @@ bsd_2 Board of Regents of the University of Wisconsin-Madison. + + + deploy-to-imagej From d7849d7b494c6b676b57ee55babb77371c313137 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 22 Dec 2017 16:47:53 -0600 Subject: [PATCH 43/63] Bump to next development cycle Signed-off-by: Curtis Rueden --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8c3de93..80e5582 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ scijava-plugins-platforms - 0.3.1-SNAPSHOT + 0.3.2-SNAPSHOT SciJava Plugins: Platforms Core platform plugins for SciJava applications. From 2dce2ca574a11df77725a84b7e53ec3bd271ef88 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 4 Apr 2019 13:35:02 -0500 Subject: [PATCH 44/63] POM: use HTTPS where feasible --- pom.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 80e5582..31b4bbf 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ 2010 SciJava - http://www.scijava.org/ + https://scijava.org/ @@ -31,7 +31,7 @@ ctrueden Curtis Rueden - http://imagej.net/User:Rueden + https://imagej.net/User:Rueden founder lead @@ -46,12 +46,12 @@ Mark Hiner - http://imagej.net/User:Hinerm + https://imagej.net/User:Hinerm hinerm Johannes Schindelin - http://imagej.net/User:Schindelin + https://imagej.net/User:Schindelin dscho @@ -94,7 +94,7 @@ Wisconsin-Madison. imagej.public - http://maven.imagej.net/content/groups/public + https://maven.imagej.net/content/groups/public From 3e34187b7fecc88fab04d8c37317748e17de8e11 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 30 Apr 2019 17:04:47 -0500 Subject: [PATCH 45/63] POM: maven.imagej.net -> maven.scijava.org --- pom.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 31b4bbf..9cd530f 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.scijava pom-scijava - 17.1.1 + 26.0.0 @@ -87,14 +87,14 @@ Board of Regents of the University of Wisconsin-Madison. - - deploy-to-imagej + + deploy-to-scijava - imagej.public - https://maven.imagej.net/content/groups/public + scijava.public + https://maven.scijava.org/content/groups/public From d86d78bcb237eae63aeefcf97544657897367921 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 30 Apr 2019 17:04:47 -0500 Subject: [PATCH 46/63] Travis: build using openjdk8 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 61bdf85..b74ecca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: java -jdk: oraclejdk8 +jdk: openjdk8 branches: only: - master From 5da28181b7c88880ca2f8eb75f16158db88ae4d0 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 30 Apr 2019 17:04:47 -0500 Subject: [PATCH 47/63] Travis: remove obsolete Maven settings --- .travis/settings.xml | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 .travis/settings.xml diff --git a/.travis/settings.xml b/.travis/settings.xml deleted file mode 100644 index 71a5630..0000000 --- a/.travis/settings.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - imagej.releases - travis - ${env.MAVEN_PASS} - - - imagej.snapshots - travis - ${env.MAVEN_PASS} - - - From 4e804906af52a2f79829fcd37431f7c25ffd0973 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 1 Jul 2021 15:37:18 -0500 Subject: [PATCH 48/63] Switch from Travis CI to GitHub Actions --- {.travis => .github}/build.sh | 4 +-- .github/setup.sh | 3 +++ .github/workflows/build-main.yml | 42 ++++++++++++++++++++++++++++++++ .github/workflows/build-pr.yml | 35 ++++++++++++++++++++++++++ .travis.yml | 12 --------- README.md | 2 +- pom.xml | 6 ++--- 7 files changed, 86 insertions(+), 18 deletions(-) rename {.travis => .github}/build.sh (61%) create mode 100755 .github/setup.sh create mode 100644 .github/workflows/build-main.yml create mode 100644 .github/workflows/build-pr.yml delete mode 100644 .travis.yml diff --git a/.travis/build.sh b/.github/build.sh similarity index 61% rename from .travis/build.sh rename to .github/build.sh index e939b6c..7da4262 100755 --- a/.travis/build.sh +++ b/.github/build.sh @@ -1,3 +1,3 @@ #!/bin/sh -curl -fsLO https://raw.githubusercontent.com/scijava/scijava-scripts/master/travis-build.sh -sh travis-build.sh +curl -fsLO https://raw.githubusercontent.com/scijava/scijava-scripts/master/ci-build.sh +sh ci-build.sh diff --git a/.github/setup.sh b/.github/setup.sh new file mode 100755 index 0000000..f359bbe --- /dev/null +++ b/.github/setup.sh @@ -0,0 +1,3 @@ +#!/bin/sh +curl -fsLO https://raw.githubusercontent.com/scijava/scijava-scripts/master/ci-setup-github-actions.sh +sh ci-setup-github-actions.sh diff --git a/.github/workflows/build-main.yml b/.github/workflows/build-main.yml new file mode 100644 index 0000000..45b6b5e --- /dev/null +++ b/.github/workflows/build-main.yml @@ -0,0 +1,42 @@ +name: build + +on: + push: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Cache m2 folder + uses: actions/cache@v2 + env: + cache-name: cache-m2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-build-${{ env.cache-name }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - name: Set up JDK 8 + uses: actions/setup-java@v2 + with: + java-version: '8' + distribution: 'zulu' + - name: Set up CI environment + run: .github/setup.sh + - name: Execute the build + run: .github/build.sh + env: + GPG_KEY_NAME: ${{ secrets.GPG_KEY_NAME }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + MAVEN_USER: ${{ secrets.MAVEN_USER }} + MAVEN_PASS: ${{ secrets.MAVEN_PASS }} + OSSRH_PASS: ${{ secrets.OSSRH_PASS }} + SIGNING_ASC: ${{ secrets.SIGNING_ASC }} diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml new file mode 100644 index 0000000..92a0192 --- /dev/null +++ b/.github/workflows/build-pr.yml @@ -0,0 +1,35 @@ +name: build PR + +on: + pull_request: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Cache m2 folder + uses: actions/cache@v2 + env: + cache-name: cache-m2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-build-${{ env.cache-name }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - name: Set up JDK 8 + uses: actions/setup-java@v2 + with: + java-version: '8' + distribution: 'zulu' + - name: Set up CI environment + run: .github/setup.sh + - name: Execute the build + run: .github/build.sh diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b74ecca..0000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -language: java -jdk: openjdk8 -branches: - only: - - master - - "/.*-[0-9]+\\..*/" -install: true -script: ".travis/build.sh" -env: - global: - - secure: Fm1sN5h3QDONaPy32xr/ClyvCwvdZZaEvi6u36vcgNCaJ3JMja3eMdj5qBbOQxt4oCHqPQwfyBttvzIMZcJ+mo0F++++k5DCTGbzRy8ZMXzUIAEm7ofA3Mw1hlm9DSYlcEnim+kUBzUuTxVJT5jOfWImbmqk6nEg5Rgu3/N6XuI= - - secure: VbKkdewIYYzM3ntapdUgjPd1ISkIqzXSCXquqkUhhUBRdBu1xIVTSYFtm4swGX0wY6h3Vq+qspIZVOP+NhiaiypUaEYPP9p3g8/aVroF86Dmcgb/cCpUIsM2SP0NzP9ReWUsUcETHNObk2oD69le3zZh9ImIhQre0CARI7TlgMI= diff --git a/README.md b/README.md index 91d11fc..8f285b8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![](https://travis-ci.org/scijava/scijava-plugins-platforms.svg?branch=master)](https://travis-ci.org/scijava/scijava-plugins-platforms) +[![](https://github.com/scijava/scijava-plugins-platforms/actions/workflows/build-main.yml/badge.svg)](https://github.com/scijava/scijava-plugins-platforms/actions/workflows/build-main.yml) SciJava Plugins: Platforms -------------------------- diff --git a/pom.xml b/pom.xml index 9cd530f..c9e6029 100644 --- a/pom.xml +++ b/pom.xml @@ -77,8 +77,8 @@ http://github.com/scijava/scijava-plugins-platforms/issues - Travis CI - https://travis-ci.org/scijava/scijava-plugins-platforms + GitHub Actions + https://github.com/scijava/scijava-plugins-platforms/actions @@ -88,7 +88,7 @@ Wisconsin-Madison. - deploy-to-scijava + sign,deploy-to-scijava From 8d183a9a4940054d62780ea239b175414cbf3a71 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 2 Jul 2021 09:49:03 -0500 Subject: [PATCH 49/63] CI: build release tags --- .github/workflows/build-main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-main.yml b/.github/workflows/build-main.yml index 45b6b5e..3fb5622 100644 --- a/.github/workflows/build-main.yml +++ b/.github/workflows/build-main.yml @@ -4,6 +4,8 @@ on: push: branches: - master + tags: + - "*-[0-9]+.*" jobs: build: From 33e1642dcb2b7df97b2e260f88efa3293d8f67c7 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 8 Nov 2021 15:42:22 -0600 Subject: [PATCH 50/63] POM: Stop using git:// protocol with github.com The GitHub platform is discontinuing support for it. See: https://github.blog/2021-09-01-improving-git-protocol-security-github/#whats-changing --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c9e6029..143aa3d 100644 --- a/pom.xml +++ b/pom.xml @@ -67,7 +67,7 @@
- scm:git:git://github.com/scijava/scijava-plugins-platforms + scm:git:https://github.com/scijava/scijava-plugins-platforms scm:git:git@github.com:scijava/scijava-plugins-platforms HEAD https://github.com/scijava/scijava-plugins-platforms From fb6d4394fee2bb79320b6e1ec5657c86c9548aa3 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 14 Jun 2022 15:54:11 -0500 Subject: [PATCH 51/63] CI: cache ~/.m2/repository correctly --- .github/workflows/build-main.yml | 18 +++--------------- .github/workflows/build-pr.yml | 18 +++--------------- 2 files changed, 6 insertions(+), 30 deletions(-) diff --git a/.github/workflows/build-main.yml b/.github/workflows/build-main.yml index 3fb5622..5ef5692 100644 --- a/.github/workflows/build-main.yml +++ b/.github/workflows/build-main.yml @@ -13,24 +13,12 @@ jobs: steps: - uses: actions/checkout@v2 - - - name: Cache m2 folder - uses: actions/cache@v2 - env: - cache-name: cache-m2 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-build-${{ env.cache-name }} - restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- - - - name: Set up JDK 8 - uses: actions/setup-java@v2 + - name: Set up Java + uses: actions/setup-java@v3 with: java-version: '8' distribution: 'zulu' + cache: 'maven' - name: Set up CI environment run: .github/setup.sh - name: Execute the build diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 92a0192..925b576 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -11,24 +11,12 @@ jobs: steps: - uses: actions/checkout@v2 - - - name: Cache m2 folder - uses: actions/cache@v2 - env: - cache-name: cache-m2 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-build-${{ env.cache-name }} - restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- - - - name: Set up JDK 8 - uses: actions/setup-java@v2 + - name: Set up Java + uses: actions/setup-java@v3 with: java-version: '8' distribution: 'zulu' + cache: 'maven' - name: Set up CI environment run: .github/setup.sh - name: Execute the build From bc2b0b770c64ebbe1774783a64704fec1c7e7249 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 4 Feb 2026 11:34:36 -0600 Subject: [PATCH 52/63] Rename to scijava-desktop * Update pom.xml accordingly, and to current best practices. * Unify package hierarchy under org.scijava.desktop prefix. * Update the README. --- README.md | 14 ++++++---- pom.xml | 28 +++++++++---------- .../links/AbstractLinkHandler.java | 2 +- .../links/DefaultLinkService.java | 2 +- .../{ => desktop}/links/LinkHandler.java | 2 +- .../{ => desktop}/links/LinkService.java | 2 +- .../scijava/{ => desktop}/links/Links.java | 2 +- .../links/console/LinkArgument.java | 4 +-- .../macos/MacOSAppEventDispatcher.java | 2 +- .../platform}/macos/MacOSPlatform.java | 2 +- .../platform}/windows/WindowsPlatform.java | 2 +- .../{ => desktop}/links/LinksTest.java | 2 +- 12 files changed, 33 insertions(+), 31 deletions(-) rename src/main/java/org/scijava/{ => desktop}/links/AbstractLinkHandler.java (97%) rename src/main/java/org/scijava/{ => desktop}/links/DefaultLinkService.java (98%) rename src/main/java/org/scijava/{ => desktop}/links/LinkHandler.java (98%) rename src/main/java/org/scijava/{ => desktop}/links/LinkService.java (98%) rename src/main/java/org/scijava/{ => desktop}/links/Links.java (98%) rename src/main/java/org/scijava/{ => desktop}/links/console/LinkArgument.java (97%) rename src/main/java/org/scijava/{plugins/platforms => desktop/platform}/macos/MacOSAppEventDispatcher.java (99%) rename src/main/java/org/scijava/{plugins/platforms => desktop/platform}/macos/MacOSPlatform.java (99%) rename src/main/java/org/scijava/{plugins/platforms => desktop/platform}/windows/WindowsPlatform.java (98%) rename src/test/java/org/scijava/{ => desktop}/links/LinksTest.java (98%) diff --git a/README.md b/README.md index e03d6cc..21975b1 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ -[![Build Status](https://github.com/scijava/scijava-links/actions/workflows/build.yml/badge.svg)](https://github.com/scijava/scijava-links/actions/workflows/build.yml) +[![Build Status](https://github.com/scijava/scijava-desktop/actions/workflows/build.yml/badge.svg)](https://github.com/scijava/scijava-desktop/actions/workflows/build.yml) -This package provides a subsystem for SciJava applications -that enables handling of URI-based links via plugins. +This component provides supporting code for SciJava applications +to manage their integration with the system native desktop: -It is kept separate from the SciJava Common core classes -because it requires Java 11 as a minimum, due to its use -of java.awt.Desktop features not present in Java 8. +* SciJava Platform plugins for macOS and Windows +* A links subsystem to handle URI-based links via plugins + +The scijava-desktop component requires Java 11 as a minimum, due +to its use of java.awt.Desktop features not present in Java 8. diff --git a/pom.xml b/pom.xml index e64c534..9e45ff4 100644 --- a/pom.xml +++ b/pom.xml @@ -5,17 +5,17 @@ org.scijava pom-scijava - 40.0.0 + 43.0.0 org.scijava - scijava-links - 1.0.1-SNAPSHOT + scijava-desktop + 1.0.0-SNAPSHOT - SciJava Links - URL scheme handlers for SciJava. - https://github.com/scijava/scijava-links + SciJava Desktop + Desktop integration for SciJava. + https://github.com/scijava/scijava-desktop 2010 SciJava @@ -47,12 +47,12 @@ Mark Hiner - https://imagej.net/User:Hinerm + https://imagej.net/people/hinerm hinerm Johannes Schindelin - https://imagej.net/User:Schindelin + https://imagej.net/people/dscho dscho @@ -78,24 +78,24 @@ - scm:git:https://github.com/scijava/scijava-links - scm:git:git@github.com:scijava/scijava-links + scm:git:https://github.com/scijava/scijava-desktop + scm:git:git@github.com:scijava/scijava-desktop HEAD - https://github.com/scijava/scijava-links + https://github.com/scijava/scijava-desktop GitHub Issues - https://github.com/scijava/scijava-links/issues + https://github.com/scijava/scijava-desktop/issues GitHub Actions - https://github.com/scijava/scijava-links/actions + https://github.com/scijava/scijava-desktop/actions - org.scijava.links + org.scijava.desktop 11 bsd_2 SciJava developers. diff --git a/src/main/java/org/scijava/links/AbstractLinkHandler.java b/src/main/java/org/scijava/desktop/links/AbstractLinkHandler.java similarity index 97% rename from src/main/java/org/scijava/links/AbstractLinkHandler.java rename to src/main/java/org/scijava/desktop/links/AbstractLinkHandler.java index 567f4d1..9728e93 100644 --- a/src/main/java/org/scijava/links/AbstractLinkHandler.java +++ b/src/main/java/org/scijava/desktop/links/AbstractLinkHandler.java @@ -26,7 +26,7 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ -package org.scijava.links; +package org.scijava.desktop.links; import org.scijava.plugin.AbstractHandlerPlugin; diff --git a/src/main/java/org/scijava/links/DefaultLinkService.java b/src/main/java/org/scijava/desktop/links/DefaultLinkService.java similarity index 98% rename from src/main/java/org/scijava/links/DefaultLinkService.java rename to src/main/java/org/scijava/desktop/links/DefaultLinkService.java index b5c8750..bfdd2a5 100644 --- a/src/main/java/org/scijava/links/DefaultLinkService.java +++ b/src/main/java/org/scijava/desktop/links/DefaultLinkService.java @@ -26,7 +26,7 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ -package org.scijava.links; +package org.scijava.desktop.links; import org.scijava.event.ContextCreatedEvent; import org.scijava.event.EventHandler; diff --git a/src/main/java/org/scijava/links/LinkHandler.java b/src/main/java/org/scijava/desktop/links/LinkHandler.java similarity index 98% rename from src/main/java/org/scijava/links/LinkHandler.java rename to src/main/java/org/scijava/desktop/links/LinkHandler.java index 5dfded8..eb77e47 100644 --- a/src/main/java/org/scijava/links/LinkHandler.java +++ b/src/main/java/org/scijava/desktop/links/LinkHandler.java @@ -26,7 +26,7 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ -package org.scijava.links; +package org.scijava.desktop.links; import org.scijava.plugin.HandlerPlugin; diff --git a/src/main/java/org/scijava/links/LinkService.java b/src/main/java/org/scijava/desktop/links/LinkService.java similarity index 98% rename from src/main/java/org/scijava/links/LinkService.java rename to src/main/java/org/scijava/desktop/links/LinkService.java index f9dec83..236a039 100644 --- a/src/main/java/org/scijava/links/LinkService.java +++ b/src/main/java/org/scijava/desktop/links/LinkService.java @@ -26,7 +26,7 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ -package org.scijava.links; +package org.scijava.desktop.links; import org.scijava.log.Logger; import org.scijava.plugin.HandlerService; diff --git a/src/main/java/org/scijava/links/Links.java b/src/main/java/org/scijava/desktop/links/Links.java similarity index 98% rename from src/main/java/org/scijava/links/Links.java rename to src/main/java/org/scijava/desktop/links/Links.java index 76d2a50..404265a 100644 --- a/src/main/java/org/scijava/links/Links.java +++ b/src/main/java/org/scijava/desktop/links/Links.java @@ -26,7 +26,7 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ -package org.scijava.links; +package org.scijava.desktop.links; import java.net.URI; import java.util.LinkedHashMap; diff --git a/src/main/java/org/scijava/links/console/LinkArgument.java b/src/main/java/org/scijava/desktop/links/console/LinkArgument.java similarity index 97% rename from src/main/java/org/scijava/links/console/LinkArgument.java rename to src/main/java/org/scijava/desktop/links/console/LinkArgument.java index 213a951..3142cf7 100644 --- a/src/main/java/org/scijava/links/console/LinkArgument.java +++ b/src/main/java/org/scijava/desktop/links/console/LinkArgument.java @@ -26,12 +26,12 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ -package org.scijava.links.console; +package org.scijava.desktop.links.console; import org.scijava.Priority; import org.scijava.console.AbstractConsoleArgument; import org.scijava.console.ConsoleArgument; -import org.scijava.links.LinkService; +import org.scijava.desktop.links.LinkService; import org.scijava.log.Logger; import org.scijava.plugin.Parameter; import org.scijava.plugin.Plugin; diff --git a/src/main/java/org/scijava/plugins/platforms/macos/MacOSAppEventDispatcher.java b/src/main/java/org/scijava/desktop/platform/macos/MacOSAppEventDispatcher.java similarity index 99% rename from src/main/java/org/scijava/plugins/platforms/macos/MacOSAppEventDispatcher.java rename to src/main/java/org/scijava/desktop/platform/macos/MacOSAppEventDispatcher.java index 706e2f4..0141a9c 100644 --- a/src/main/java/org/scijava/plugins/platforms/macos/MacOSAppEventDispatcher.java +++ b/src/main/java/org/scijava/desktop/platform/macos/MacOSAppEventDispatcher.java @@ -28,7 +28,7 @@ * #L% */ -package org.scijava.plugins.platforms.macos; +package org.scijava.desktop.platform.macos; import com.apple.eawt.AboutHandler; import com.apple.eawt.AppEvent.AboutEvent; diff --git a/src/main/java/org/scijava/plugins/platforms/macos/MacOSPlatform.java b/src/main/java/org/scijava/desktop/platform/macos/MacOSPlatform.java similarity index 99% rename from src/main/java/org/scijava/plugins/platforms/macos/MacOSPlatform.java rename to src/main/java/org/scijava/desktop/platform/macos/MacOSPlatform.java index 3a7dcd1..370d804 100644 --- a/src/main/java/org/scijava/plugins/platforms/macos/MacOSPlatform.java +++ b/src/main/java/org/scijava/desktop/platform/macos/MacOSPlatform.java @@ -28,7 +28,7 @@ * #L% */ -package org.scijava.plugins.platforms.macos; +package org.scijava.desktop.platform.macos; import java.io.IOException; import java.net.URL; diff --git a/src/main/java/org/scijava/plugins/platforms/windows/WindowsPlatform.java b/src/main/java/org/scijava/desktop/platform/windows/WindowsPlatform.java similarity index 98% rename from src/main/java/org/scijava/plugins/platforms/windows/WindowsPlatform.java rename to src/main/java/org/scijava/desktop/platform/windows/WindowsPlatform.java index e0025b7..bd6eaab 100644 --- a/src/main/java/org/scijava/plugins/platforms/windows/WindowsPlatform.java +++ b/src/main/java/org/scijava/desktop/platform/windows/WindowsPlatform.java @@ -28,7 +28,7 @@ * #L% */ -package org.scijava.plugins.platforms.windows; +package org.scijava.desktop.platform.windows; import java.io.IOException; import java.net.URL; diff --git a/src/test/java/org/scijava/links/LinksTest.java b/src/test/java/org/scijava/desktop/links/LinksTest.java similarity index 98% rename from src/test/java/org/scijava/links/LinksTest.java rename to src/test/java/org/scijava/desktop/links/LinksTest.java index 5ffc2f3..f812899 100644 --- a/src/test/java/org/scijava/links/LinksTest.java +++ b/src/test/java/org/scijava/desktop/links/LinksTest.java @@ -27,7 +27,7 @@ * #L% */ -package org.scijava.links; +package org.scijava.desktop.links; import org.junit.Test; From eb456a333990ddd2ffae6792410c2865f1591b12 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 4 Feb 2026 11:35:26 -0600 Subject: [PATCH 53/63] Update license headers --- LICENSE.txt | 2 +- .../java/org/scijava/desktop/links/AbstractLinkHandler.java | 4 ++-- .../java/org/scijava/desktop/links/DefaultLinkService.java | 4 ++-- src/main/java/org/scijava/desktop/links/LinkHandler.java | 4 ++-- src/main/java/org/scijava/desktop/links/LinkService.java | 4 ++-- src/main/java/org/scijava/desktop/links/Links.java | 4 ++-- .../java/org/scijava/desktop/links/console/LinkArgument.java | 4 ++-- .../desktop/platform/macos/MacOSAppEventDispatcher.java | 5 ++--- .../org/scijava/desktop/platform/macos/MacOSPlatform.java | 5 ++--- .../scijava/desktop/platform/windows/WindowsPlatform.java | 5 ++--- src/test/java/org/scijava/desktop/links/LinksTest.java | 4 ++-- 11 files changed, 21 insertions(+), 24 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index 5954dca..a9c6e00 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2010 - 2025, SciJava developers. +Copyright (c) 2010 - 2026, SciJava developers. All rights reserved. Redistribution and use in source and binary forms, with or without modification, diff --git a/src/main/java/org/scijava/desktop/links/AbstractLinkHandler.java b/src/main/java/org/scijava/desktop/links/AbstractLinkHandler.java index 9728e93..bfbb680 100644 --- a/src/main/java/org/scijava/desktop/links/AbstractLinkHandler.java +++ b/src/main/java/org/scijava/desktop/links/AbstractLinkHandler.java @@ -1,8 +1,8 @@ /*- * #%L - * URL scheme handlers for SciJava. + * Desktop integration for SciJava. * %% - * Copyright (C) 2023 - 2025 SciJava developers. + * Copyright (C) 2010 - 2026 SciJava developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/org/scijava/desktop/links/DefaultLinkService.java b/src/main/java/org/scijava/desktop/links/DefaultLinkService.java index bfdd2a5..dbbbbae 100644 --- a/src/main/java/org/scijava/desktop/links/DefaultLinkService.java +++ b/src/main/java/org/scijava/desktop/links/DefaultLinkService.java @@ -1,8 +1,8 @@ /*- * #%L - * URL scheme handlers for SciJava. + * Desktop integration for SciJava. * %% - * Copyright (C) 2023 - 2025 SciJava developers. + * Copyright (C) 2010 - 2026 SciJava developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/org/scijava/desktop/links/LinkHandler.java b/src/main/java/org/scijava/desktop/links/LinkHandler.java index eb77e47..e31d257 100644 --- a/src/main/java/org/scijava/desktop/links/LinkHandler.java +++ b/src/main/java/org/scijava/desktop/links/LinkHandler.java @@ -1,8 +1,8 @@ /*- * #%L - * URL scheme handlers for SciJava. + * Desktop integration for SciJava. * %% - * Copyright (C) 2023 - 2025 SciJava developers. + * Copyright (C) 2010 - 2026 SciJava developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/org/scijava/desktop/links/LinkService.java b/src/main/java/org/scijava/desktop/links/LinkService.java index 236a039..ed8cd36 100644 --- a/src/main/java/org/scijava/desktop/links/LinkService.java +++ b/src/main/java/org/scijava/desktop/links/LinkService.java @@ -1,8 +1,8 @@ /*- * #%L - * URL scheme handlers for SciJava. + * Desktop integration for SciJava. * %% - * Copyright (C) 2023 - 2025 SciJava developers. + * Copyright (C) 2010 - 2026 SciJava developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/org/scijava/desktop/links/Links.java b/src/main/java/org/scijava/desktop/links/Links.java index 404265a..3f1fd06 100644 --- a/src/main/java/org/scijava/desktop/links/Links.java +++ b/src/main/java/org/scijava/desktop/links/Links.java @@ -1,8 +1,8 @@ /*- * #%L - * URL scheme handlers for SciJava. + * Desktop integration for SciJava. * %% - * Copyright (C) 2023 - 2025 SciJava developers. + * Copyright (C) 2010 - 2026 SciJava developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/org/scijava/desktop/links/console/LinkArgument.java b/src/main/java/org/scijava/desktop/links/console/LinkArgument.java index 3142cf7..206a840 100644 --- a/src/main/java/org/scijava/desktop/links/console/LinkArgument.java +++ b/src/main/java/org/scijava/desktop/links/console/LinkArgument.java @@ -1,8 +1,8 @@ /*- * #%L - * URL scheme handlers for SciJava. + * Desktop integration for SciJava. * %% - * Copyright (C) 2023 - 2025 SciJava developers. + * Copyright (C) 2010 - 2026 SciJava developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/org/scijava/desktop/platform/macos/MacOSAppEventDispatcher.java b/src/main/java/org/scijava/desktop/platform/macos/MacOSAppEventDispatcher.java index 0141a9c..f938faa 100644 --- a/src/main/java/org/scijava/desktop/platform/macos/MacOSAppEventDispatcher.java +++ b/src/main/java/org/scijava/desktop/platform/macos/MacOSAppEventDispatcher.java @@ -1,9 +1,8 @@ /* * #%L - * Core platform plugins for SciJava applications. + * Desktop integration for SciJava. * %% - * Copyright (C) 2010 - 2015 Board of Regents of the University of - * Wisconsin-Madison. + * Copyright (C) 2010 - 2026 SciJava developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/org/scijava/desktop/platform/macos/MacOSPlatform.java b/src/main/java/org/scijava/desktop/platform/macos/MacOSPlatform.java index 370d804..9d26c54 100644 --- a/src/main/java/org/scijava/desktop/platform/macos/MacOSPlatform.java +++ b/src/main/java/org/scijava/desktop/platform/macos/MacOSPlatform.java @@ -1,9 +1,8 @@ /* * #%L - * Core platform plugins for SciJava applications. + * Desktop integration for SciJava. * %% - * Copyright (C) 2010 - 2015 Board of Regents of the University of - * Wisconsin-Madison. + * Copyright (C) 2010 - 2026 SciJava developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/org/scijava/desktop/platform/windows/WindowsPlatform.java b/src/main/java/org/scijava/desktop/platform/windows/WindowsPlatform.java index bd6eaab..0b0f3c2 100644 --- a/src/main/java/org/scijava/desktop/platform/windows/WindowsPlatform.java +++ b/src/main/java/org/scijava/desktop/platform/windows/WindowsPlatform.java @@ -1,9 +1,8 @@ /* * #%L - * Core platform plugins for SciJava applications. + * Desktop integration for SciJava. * %% - * Copyright (C) 2010 - 2015 Board of Regents of the University of - * Wisconsin-Madison. + * Copyright (C) 2010 - 2026 SciJava developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/test/java/org/scijava/desktop/links/LinksTest.java b/src/test/java/org/scijava/desktop/links/LinksTest.java index f812899..027ad00 100644 --- a/src/test/java/org/scijava/desktop/links/LinksTest.java +++ b/src/test/java/org/scijava/desktop/links/LinksTest.java @@ -1,8 +1,8 @@ /*- * #%L - * URL scheme handlers for SciJava. + * Desktop integration for SciJava. * %% - * Copyright (C) 2023 - 2025 SciJava developers. + * Copyright (C) 2010 - 2026 SciJava developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: From 13ed115945046b82a7ee1990e4d26b3beaf22558 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 25 Nov 2025 09:50:51 -0600 Subject: [PATCH 54/63] Fix terminology: ImageJ -> SciJava --- .../desktop/platform/macos/MacOSAppEventDispatcher.java | 2 +- .../org/scijava/desktop/platform/macos/MacOSPlatform.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/scijava/desktop/platform/macos/MacOSAppEventDispatcher.java b/src/main/java/org/scijava/desktop/platform/macos/MacOSAppEventDispatcher.java index f938faa..e752c82 100644 --- a/src/main/java/org/scijava/desktop/platform/macos/MacOSAppEventDispatcher.java +++ b/src/main/java/org/scijava/desktop/platform/macos/MacOSAppEventDispatcher.java @@ -68,7 +68,7 @@ import org.scijava.platform.event.AppVisibleEvent; /** - * Rebroadcasts macOS application events as ImageJ events. + * Rebroadcasts macOS application events as SciJava events. * * @author Curtis Rueden */ diff --git a/src/main/java/org/scijava/desktop/platform/macos/MacOSPlatform.java b/src/main/java/org/scijava/desktop/platform/macos/MacOSPlatform.java index 9d26c54..88c230c 100644 --- a/src/main/java/org/scijava/desktop/platform/macos/MacOSPlatform.java +++ b/src/main/java/org/scijava/desktop/platform/macos/MacOSPlatform.java @@ -53,7 +53,7 @@ /** * A platform implementation for handling Apple macOS platform issues: *
    - *
  • Application events are rebroadcast as ImageJ events.
  • + *
  • Application events are rebroadcast as SciJava events.
  • *
  • macOS screen menu bar is enabled.
  • *
  • Special screen menu bar menu items are handled.
  • *
@@ -92,7 +92,7 @@ public void configure(final PlatformService service) { // remove app commands from menu structure if (SCREEN_MENU) removeAppCommandsFromMenu(); - // translate macOS application events into ImageJ events + // translate macOS application events into SciJava events final EventService eventService = getPlatformService().eventService(); try { appEventDispatcher = new MacOSAppEventDispatcher(eventService); From 4517654c918eabf4f6453eed353a2568b50bb293 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 25 Nov 2025 08:45:01 -0600 Subject: [PATCH 55/63] Add Windows URI scheme registration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements automatic registration of URI schemes with the Windows Registry, enabling users to click custom links (e.g., fiji://open) in browsers or other applications to launch the Java application. Key components: - SchemeInstaller interface for platform-independent scheme registration - WindowsSchemeInstaller using Windows reg commands (no JNA dependency) - LinkHandler.getSchemes() for handlers to declare supported schemes - DefaultLinkService auto-registers schemes on context initialization The launcher sets the scijava.app.executable system property to specify the executable path for registration. Registration uses HKEY_CURRENT_USER and requires no admin privileges. Includes comprehensive tests and documentation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 56 ++++ doc/WINDOWS.md | 136 +++++++++ .../desktop/links/DefaultLinkService.java | 85 +++++- .../scijava/desktop/links/LinkHandler.java | 17 ++ .../desktop/links/SchemeInstaller.java | 84 ++++++ .../windows/WindowsSchemeInstaller.java | 266 ++++++++++++++++++ .../windows/WindowsSchemeInstallerTest.java | 217 ++++++++++++++ 7 files changed, 857 insertions(+), 4 deletions(-) create mode 100644 doc/WINDOWS.md create mode 100644 src/main/java/org/scijava/desktop/links/SchemeInstaller.java create mode 100644 src/main/java/org/scijava/desktop/platform/windows/WindowsSchemeInstaller.java create mode 100644 src/test/java/org/scijava/desktop/platform/windows/WindowsSchemeInstallerTest.java diff --git a/README.md b/README.md index 21975b1..0793bd0 100644 --- a/README.md +++ b/README.md @@ -8,3 +8,59 @@ to manage their integration with the system native desktop: The scijava-desktop component requires Java 11 as a minimum, due to its use of java.awt.Desktop features not present in Java 8. + +## Features + +- **Link handling**: Register custom handlers for URI schemes through the `LinkHandler` plugin interface +- **CLI integration**: Automatic handling of URI arguments passed on the command line via `ConsoleArgument` +- **OS integration**: Automatic registration of URI schemes with the operating system (Windows supported, macOS/Linux planned) + +## Usage + +### Creating a Link Handler + +Implement the `LinkHandler` interface to handle custom URI schemes: + +```java +@Plugin(type = LinkHandler.class) +public class MyLinkHandler extends AbstractLinkHandler { + + @Override + public boolean supports(URI uri) { + return "myapp".equals(uri.getScheme()); + } + + @Override + public void handle(URI uri) { + // Handle the URI + System.out.println("Handling: " + uri); + } + + @Override + public List getSchemes() { + // Return schemes to register with the OS + return Arrays.asList("myapp"); + } +} +``` + +### OS Registration + +On Windows, URI schemes returned by `LinkHandler.getSchemes()` are automatically registered +in the Windows Registry when the `LinkService` initializes. This allows users to click +links like `myapp://action` in web browsers or other applications, which will launch your +Java application with the URI as a command-line argument. + +The registration uses `HKEY_CURRENT_USER` and requires no administrator privileges. + +See [doc/WINDOWS.md](doc/WINDOWS.md) for details. + +## Architecture + +- `LinkService` - Service for routing URIs to appropriate handlers +- `LinkHandler` - Plugin interface for implementing custom URI handlers +- `LinkArgument` - Console argument plugin that recognizes URIs on the command line +- `SchemeInstaller` - Interface for OS-specific URI scheme registration +- `WindowsSchemeInstaller` - Windows implementation using registry commands + +The launcher should set the `scijava.app.executable` system property to enable URI scheme registration. diff --git a/doc/WINDOWS.md b/doc/WINDOWS.md new file mode 100644 index 0000000..ed8a7b1 --- /dev/null +++ b/doc/WINDOWS.md @@ -0,0 +1,136 @@ +# Windows URI Scheme Registration + +This document describes how URI scheme registration works on Windows in scijava-links. + +## Overview + +When a SciJava application starts on Windows, the `DefaultLinkService` automatically: + +1. Collects all URI schemes from registered `LinkHandler` plugins via `getSchemes()` +2. Reads the executable path from the `scijava.app.executable` system property +3. Registers each scheme in the Windows Registry under `HKEY_CURRENT_USER\Software\Classes` + +## Registry Structure + +For a scheme named `myapp`, the following registry structure is created: + +``` +HKEY_CURRENT_USER\Software\Classes\myapp + (Default) = "URL:myapp" + URL Protocol = "" + shell\ + open\ + command\ + (Default) = "C:\Path\To\App.exe" "%1" +``` + +## Implementation Details + +### SchemeInstaller Interface + +The `SchemeInstaller` interface provides a platform-independent API for URI scheme registration: + +- `isSupported()` - Checks if the installer works on the current platform +- `install(scheme, executablePath)` - Registers a URI scheme +- `isInstalled(scheme)` - Checks if a scheme is already registered +- `getInstalledPath(scheme)` - Gets the executable path for a registered scheme +- `uninstall(scheme)` - Removes a URI scheme registration + +### WindowsSchemeInstaller + +The Windows implementation uses the `reg` command-line tool to manipulate the registry: + +- **No JNA dependency**: Uses native Windows `reg` commands via `ProcessBuilder` +- **No admin rights**: Registers under `HKEY_CURRENT_USER` (not `HKEY_LOCAL_MACHINE`) +- **Idempotent**: Safely handles re-registration with the same or different paths +- **Robust error handling**: Proper timeouts, error logging, and validation + +### Executable Path Configuration + +The launcher must set the `scijava.app.executable` system property to the absolute path of the application's executable. This property is used by `DefaultLinkService` during URI scheme registration. + +Example launcher configuration: +```bash +java -Dscijava.app.executable="C:\Program Files\MyApp\MyApp.exe" -jar myapp.jar +``` + +On Windows, the launcher typically sets this to the `.exe` file path. On macOS, it would be the path inside the `.app` bundle. On Linux, it would be the shell script or executable. + +## Example Handler + +Here's a complete example of a `LinkHandler` that registers a custom scheme: + +```java +package com.example; + +import org.scijava.links.AbstractLinkHandler; +import org.scijava.links.LinkHandler; +import org.scijava.log.LogService; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; + +@Plugin(type = LinkHandler.class) +public class ExampleLinkHandler extends AbstractLinkHandler { + + @Parameter(required = false) + private LogService log; + + @Override + public boolean supports(final URI uri) { + return "example".equals(uri.getScheme()); + } + + @Override + public void handle(final URI uri) { + if (log != null) { + log.info("Handling example URI: " + uri); + } + + // Parse the URI and perform actions + String operation = Links.operation(uri); + Map params = Links.query(uri); + + // Your business logic here + // ... + } + + @Override + public List getSchemes() { + // This tells the system to register "example://" links on Windows + return Arrays.asList("example"); + } +} +``` + +## Testing + +The Windows scheme installation can be tested on Windows systems: + +```bash +mvn test -Dtest=WindowsSchemeInstallerTest +``` + +Tests are automatically skipped on non-Windows platforms using JUnit's `Assume.assumeTrue()`. + +To test with a specific executable path, set the system property: +```bash +mvn test -Dscijava.app.executable="C:\Path\To\App.exe" +``` + +## Platform Notes + +**macOS**: URI schemes are declared in the application's `Info.plist` within the `.app` bundle. This is configured at build/packaging time, not at runtime, since the bundle is typically code-signed and immutable. + +**Linux**: URI schemes are declared in `.desktop` files, which is part of broader desktop integration (icons, MIME types, etc.). This functionality belongs in `scijava-plugins-platforms` rather than this component. + +**Windows**: Runtime registration is appropriate because the Windows Registry is designed for runtime modifications, and registration under `HKEY_CURRENT_USER` requires no elevated privileges. + +## Future Enhancements + +- **Scheme validation**: Validate scheme names against RFC 3986 +- **User prompts**: Optional confirmation before registering schemes +- **Uninstallation**: Automatic cleanup on application uninstall diff --git a/src/main/java/org/scijava/desktop/links/DefaultLinkService.java b/src/main/java/org/scijava/desktop/links/DefaultLinkService.java index dbbbbae..cf72c17 100644 --- a/src/main/java/org/scijava/desktop/links/DefaultLinkService.java +++ b/src/main/java/org/scijava/desktop/links/DefaultLinkService.java @@ -30,12 +30,18 @@ import org.scijava.event.ContextCreatedEvent; import org.scijava.event.EventHandler; +import org.scijava.desktop.links.SchemeInstaller; +import org.scijava.desktop.platform.windows.WindowsSchemeInstaller; +import org.scijava.log.LogService; import org.scijava.plugin.AbstractHandlerService; +import org.scijava.plugin.Parameter; import org.scijava.plugin.Plugin; import org.scijava.service.Service; import java.awt.Desktop; import java.net.URI; +import java.util.HashSet; +import java.util.Set; /** * Default implementation of {@link LinkService}. @@ -45,13 +51,84 @@ @Plugin(type = Service.class) public class DefaultLinkService extends AbstractHandlerService implements LinkService { + @Parameter(required = false) + private LogService log; + @EventHandler private void onEvent(final ContextCreatedEvent evt) { // Register URI handler with the desktop system, if possible. - if (!Desktop.isDesktopSupported()) return; - final Desktop desktop = Desktop.getDesktop(); - if (!desktop.isSupported(Desktop.Action.APP_OPEN_URI)) return; - desktop.setOpenURIHandler(event -> handle(event.getURI())); + if (Desktop.isDesktopSupported()) { + final Desktop desktop = Desktop.getDesktop(); + if (desktop.isSupported(Desktop.Action.APP_OPEN_URI)) { + desktop.setOpenURIHandler(event -> handle(event.getURI())); + } + } + + // Register URI schemes with the operating system (Windows only). + installSchemes(); + } + + /** + * Installs URI schemes with the operating system. + *

+ * This method collects all schemes supported by registered {@link LinkHandler} + * plugins and registers them with the OS (currently Windows only). + *

+ */ + private void installSchemes() { + // Create the appropriate installer for this platform + final SchemeInstaller installer = createInstaller(); + if (installer == null || !installer.isSupported()) { + if (log != null) log.debug("Scheme installation not supported on this platform"); + return; + } + + // Get executable path from system property + final String executablePath = System.getProperty("scijava.app.executable"); + if (executablePath == null) { + if (log != null) log.debug("No executable path set (scijava.app.executable property)"); + return; + } + + // Collect all schemes from registered handlers + final Set schemes = collectSchemes(); + if (schemes.isEmpty()) { + if (log != null) log.debug("No URI schemes to register"); + return; + } + + // Install each scheme + for (final String scheme : schemes) { + try { + installer.install(scheme, executablePath); + } + catch (final Exception e) { + if (log != null) log.error("Failed to install URI scheme: " + scheme, e); + } + } + } + + /** + * Creates the appropriate {@link SchemeInstaller} for the current platform. + *

+ * Currently only Windows is supported. macOS uses Info.plist in the .app bundle + * (configured at build time), and Linux .desktop file management belongs in + * scijava-plugins-platforms. + *

+ */ + private SchemeInstaller createInstaller() { + return new WindowsSchemeInstaller(log); + } + + /** + * Collects all URI schemes from registered {@link LinkHandler} plugins. + */ + private Set collectSchemes() { + final Set schemes = new HashSet<>(); + for (final LinkHandler handler : getInstances()) { + schemes.addAll(handler.getSchemes()); + } + return schemes; } } diff --git a/src/main/java/org/scijava/desktop/links/LinkHandler.java b/src/main/java/org/scijava/desktop/links/LinkHandler.java index e31d257..41808ac 100644 --- a/src/main/java/org/scijava/desktop/links/LinkHandler.java +++ b/src/main/java/org/scijava/desktop/links/LinkHandler.java @@ -31,6 +31,8 @@ import org.scijava.plugin.HandlerPlugin; import java.net.URI; +import java.util.Collections; +import java.util.List; /** * A plugin for handling URI links. @@ -46,6 +48,21 @@ public interface LinkHandler extends HandlerPlugin { */ void handle(URI uri); + /** + * Gets the URI schemes that this handler supports. + *

+ * This method is used for registering URI schemes with the operating system. + * Handlers should return a list of scheme names (e.g., "fiji", "imagej") + * that they can handle. Return an empty list if the handler does not + * require OS-level scheme registration. + *

+ * + * @return List of URI schemes supported by this handler + */ + default List getSchemes() { + return Collections.emptyList(); + } + @Override default Class getType() { return URI.class; diff --git a/src/main/java/org/scijava/desktop/links/SchemeInstaller.java b/src/main/java/org/scijava/desktop/links/SchemeInstaller.java new file mode 100644 index 0000000..ebd5576 --- /dev/null +++ b/src/main/java/org/scijava/desktop/links/SchemeInstaller.java @@ -0,0 +1,84 @@ +/*- + * #%L + * Desktop integration for SciJava. + * %% + * Copyright (C) 2010 - 2026 SciJava developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.desktop.links; + +import java.io.IOException; + +/** + * Interface for installing URI scheme handlers in the operating system. + *

+ * Implementations provide OS-specific logic for registering custom URI schemes + * so that clicking links with those schemes will launch the Java application. + *

+ * + * @author Curtis Rueden + */ +public interface SchemeInstaller { + + /** + * Checks if this installer is supported on the current platform. + * + * @return true if the installer can run on this OS + */ + boolean isSupported(); + + /** + * Installs a URI scheme handler in the operating system. + * + * @param scheme The URI scheme to register (e.g., "fiji", "imagej") + * @param executablePath The absolute path to the executable to launch + * @throws IOException if installation fails + */ + void install(String scheme, String executablePath) throws IOException; + + /** + * Checks if a URI scheme is already registered. + * + * @param scheme The URI scheme to check + * @return true if the scheme is already registered + */ + boolean isInstalled(String scheme); + + /** + * Gets the executable path registered for a given scheme. + * + * @param scheme The URI scheme to query + * @return The registered executable path, or null if not registered + */ + String getInstalledPath(String scheme); + + /** + * Uninstalls a URI scheme handler from the operating system. + * + * @param scheme The URI scheme to unregister + * @throws IOException if uninstallation fails + */ + void uninstall(String scheme) throws IOException; +} diff --git a/src/main/java/org/scijava/desktop/platform/windows/WindowsSchemeInstaller.java b/src/main/java/org/scijava/desktop/platform/windows/WindowsSchemeInstaller.java new file mode 100644 index 0000000..f88c11c --- /dev/null +++ b/src/main/java/org/scijava/desktop/platform/windows/WindowsSchemeInstaller.java @@ -0,0 +1,266 @@ +/*- + * #%L + * Desktop integration for SciJava. + * %% + * Copyright (C) 2010 - 2026 SciJava developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.scijava.desktop.platform.windows; + +import org.scijava.desktop.links.SchemeInstaller; +import org.scijava.log.LogService; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; + +/** + * Windows implementation of {@link SchemeInstaller} using the Windows Registry. + *

+ * This implementation uses the {@code reg} command-line tool to manipulate + * the Windows Registry under {@code HKEY_CURRENT_USER\Software\Classes}. + * No administrator privileges are required. + *

+ * + * @author Curtis Rueden + * @author Marwan Zouinkhi + */ +public class WindowsSchemeInstaller implements SchemeInstaller { + + private static final long COMMAND_TIMEOUT_SECONDS = 10; + + private final LogService log; + + public WindowsSchemeInstaller(final LogService log) { + this.log = log; + } + + @Override + public boolean isSupported() { + final String os = System.getProperty("os.name"); + return os != null && os.toLowerCase().contains("win"); + } + + @Override + public void install(final String scheme, final String executablePath) throws IOException { + if (!isSupported()) { + throw new UnsupportedOperationException("Windows registry installation not supported on: " + System.getProperty("os.name")); + } + + // Validate inputs + if (scheme == null || scheme.isEmpty()) { + throw new IllegalArgumentException("Scheme cannot be null or empty"); + } + if (executablePath == null || executablePath.isEmpty()) { + throw new IllegalArgumentException("Executable path cannot be null or empty"); + } + + // Check if already installed with same path + if (isInstalled(scheme)) { + final String existingPath = getInstalledPath(scheme); + if (executablePath.equals(existingPath)) { + if (log != null) log.debug("Scheme '" + scheme + "' already registered to: " + existingPath); + return; + } + } + + // Registry key paths (HKCU = HKEY_CURRENT_USER, no admin rights needed) + final String keyPath = "HKCU\\Software\\Classes\\" + scheme; + final String shellPath = keyPath + "\\shell"; + final String openPath = shellPath + "\\open"; + final String commandPath = openPath + "\\command"; + + // Commands to register the URI scheme + final String[][] commands = { + {"reg", "add", keyPath, "/f"}, + {"reg", "add", keyPath, "/ve", "/d", "URL:" + scheme, "/f"}, + {"reg", "add", keyPath, "/v", "URL Protocol", "/f"}, + {"reg", "add", shellPath, "/f"}, + {"reg", "add", openPath, "/f"}, + {"reg", "add", commandPath, "/ve", "/d", "\"" + executablePath + "\" \"%1\"", "/f"} + }; + + // Execute commands + for (final String[] command : commands) { + if (!executeCommand(command)) { + throw new IOException("Failed to execute registry command: " + String.join(" ", command)); + } + } + + if (log != null) log.info("Registered URI scheme '" + scheme + "' to: " + executablePath); + } + + @Override + public boolean isInstalled(final String scheme) { + if (!isSupported()) return false; + + final String keyPath = "HKCU\\Software\\Classes\\" + scheme; + return executeCommand(new String[]{"reg", "query", keyPath}); + } + + @Override + public String getInstalledPath(final String scheme) { + if (!isInstalled(scheme)) return null; + + final String commandPath = "HKCU\\Software\\Classes\\" + scheme + "\\shell\\open\\command"; + try { + final ProcessBuilder pb = new ProcessBuilder("reg", "query", commandPath, "/ve"); + final Process process = pb.start(); + + // Read output + final StringBuilder output = new StringBuilder(); + try (final BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + } + + // Wait for completion + final boolean finished = process.waitFor(COMMAND_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (!finished) { + process.destroyForcibly(); + return null; + } + + if (process.exitValue() != 0) { + return null; + } + + // Parse output to extract path + // Format: " (Default) REG_SZ \"C:\path\to\app.exe\" \"%1\"" + return parsePathFromRegQueryOutput(output.toString()); + } + catch (final IOException | InterruptedException e) { + if (log != null) log.debug("Failed to query registry for scheme: " + scheme, e); + return null; + } + } + + @Override + public void uninstall(final String scheme) throws IOException { + if (!isSupported()) { + throw new UnsupportedOperationException("Windows registry uninstallation not supported on: " + System.getProperty("os.name")); + } + + if (!isInstalled(scheme)) { + if (log != null) log.debug("Scheme '" + scheme + "' is not installed"); + return; + } + + final String keyPath = "HKCU\\Software\\Classes\\" + scheme; + final String[] command = {"reg", "delete", keyPath, "/f"}; + + if (!executeCommand(command)) { + throw new IOException("Failed to uninstall scheme: " + scheme); + } + + if (log != null) log.info("Uninstalled URI scheme: " + scheme); + } + + // -- Helper methods -- + + /** + * Executes a Windows command and returns whether it succeeded. + */ + private boolean executeCommand(final String[] command) { + try { + final ProcessBuilder pb = new ProcessBuilder(command); + pb.redirectErrorStream(true); + final Process process = pb.start(); + + // Consume output to prevent blocking + try (final BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + final StringBuilder output = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + if (log != null && !output.toString().trim().isEmpty()) { + log.debug("Command output: " + output); + } + } + + // Wait for completion + final boolean finished = process.waitFor(COMMAND_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (!finished) { + if (log != null) log.warn("Command timed out: " + String.join(" ", command)); + process.destroyForcibly(); + return false; + } + + final int exitCode = process.exitValue(); + if (exitCode != 0 && log != null) { + log.debug("Command failed with exit code " + exitCode + ": " + String.join(" ", command)); + } + + return exitCode == 0; + } + catch (final IOException e) { + if (log != null) log.error("Failed to execute command: " + String.join(" ", command), e); + return false; + } + catch (final InterruptedException e) { + if (log != null) log.error("Command interrupted: " + String.join(" ", command), e); + Thread.currentThread().interrupt(); + return false; + } + } + + /** + * Parses the executable path from {@code reg query} output. + * Expected format: " (Default) REG_SZ \"C:\path\to\app.exe\" \"%1\"" + */ + private String parsePathFromRegQueryOutput(final String output) { + // Find the line containing REG_SZ or REG_EXPAND_SZ + for (final String line : output.split("\n")) { + if (line.contains("REG_SZ") || line.contains("REG_EXPAND_SZ")) { + // Extract the value after REG_SZ + final int regSzIndex = line.indexOf("REG_SZ"); + final int regExpandSzIndex = line.indexOf("REG_EXPAND_SZ"); + final int startIndex = Math.max(regSzIndex, regExpandSzIndex) + (regSzIndex > 0 ? 6 : 13); + + if (startIndex < line.length()) { + String value = line.substring(startIndex).trim(); + // Remove "%1" parameter if present + if (value.endsWith(" \"%1\"")) { + value = value.substring(0, value.length() - 5).trim(); + } else if (value.endsWith(" %1")) { + value = value.substring(0, value.length() - 3).trim(); + } + // Remove surrounding quotes if present + if (value.startsWith("\"") && value.endsWith("\"") && value.length() > 1) { + value = value.substring(1, value.length() - 1); + } + return value; + } + } + } + return null; + } +} diff --git a/src/test/java/org/scijava/desktop/platform/windows/WindowsSchemeInstallerTest.java b/src/test/java/org/scijava/desktop/platform/windows/WindowsSchemeInstallerTest.java new file mode 100644 index 0000000..028227c --- /dev/null +++ b/src/test/java/org/scijava/desktop/platform/windows/WindowsSchemeInstallerTest.java @@ -0,0 +1,217 @@ +/*- + * #%L + * Desktop integration for SciJava. + * %% + * Copyright (C) 2010 - 2026 SciJava developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.desktop.platform.windows; + +import org.junit.After; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; + +import static org.junit.Assert.*; + +/** + * Tests {@link WindowsSchemeInstaller}. + * + * @author Curtis Rueden + */ +public class WindowsSchemeInstallerTest { + + private static final String TEST_SCHEME = "scijava-test"; + private WindowsSchemeInstaller installer; + + @Before + public void setUp() { + installer = new WindowsSchemeInstaller(null); + // Only run tests on Windows + Assume.assumeTrue("Tests only run on Windows", installer.isSupported()); + } + + @After + public void tearDown() { + // Clean up test scheme if it was installed + if (installer != null && installer.isInstalled(TEST_SCHEME)) { + try { + installer.uninstall(TEST_SCHEME); + } + catch (final IOException e) { + // Ignore cleanup errors + } + } + } + + @Test + public void testIsSupported() { + final String os = System.getProperty("os.name"); + final boolean expectedSupport = os != null && os.toLowerCase().contains("win"); + assertEquals(expectedSupport, installer.isSupported()); + } + + @Test + public void testInstallAndUninstall() throws IOException { + // Arrange + final String execPath = "C:\\Program Files\\Test\\test.exe"; + + // Ensure scheme is not already installed + if (installer.isInstalled(TEST_SCHEME)) { + installer.uninstall(TEST_SCHEME); + } + assertFalse("Test scheme should not be installed initially", installer.isInstalled(TEST_SCHEME)); + + // Act - Install + installer.install(TEST_SCHEME, execPath); + + // Assert - Installed + assertTrue("Scheme should be installed", installer.isInstalled(TEST_SCHEME)); + final String installedPath = installer.getInstalledPath(TEST_SCHEME); + assertEquals("Installed path should match", execPath, installedPath); + + // Act - Uninstall + installer.uninstall(TEST_SCHEME); + + // Assert - Uninstalled + assertFalse("Scheme should be uninstalled", installer.isInstalled(TEST_SCHEME)); + assertNull("Installed path should be null after uninstall", installer.getInstalledPath(TEST_SCHEME)); + } + + @Test + public void testInstallTwiceWithSamePath() throws IOException { + // Arrange + final String execPath = "C:\\Program Files\\Test\\test.exe"; + + // Ensure scheme is not already installed + if (installer.isInstalled(TEST_SCHEME)) { + installer.uninstall(TEST_SCHEME); + } + + // Act - Install twice + installer.install(TEST_SCHEME, execPath); + installer.install(TEST_SCHEME, execPath); // Should not fail + + // Assert + assertTrue("Scheme should still be installed", installer.isInstalled(TEST_SCHEME)); + assertEquals("Path should match", execPath, installer.getInstalledPath(TEST_SCHEME)); + } + + @Test + public void testInstallWithDifferentPath() throws IOException { + // Arrange + final String execPath1 = "C:\\Program Files\\Test1\\test.exe"; + final String execPath2 = "C:\\Program Files\\Test2\\test.exe"; + + // Ensure scheme is not already installed + if (installer.isInstalled(TEST_SCHEME)) { + installer.uninstall(TEST_SCHEME); + } + + // Act - Install with first path + installer.install(TEST_SCHEME, execPath1); + assertEquals("First path should be installed", execPath1, installer.getInstalledPath(TEST_SCHEME)); + + // Act - Install with second path (should update) + installer.install(TEST_SCHEME, execPath2); + + // Assert + assertTrue("Scheme should still be installed", installer.isInstalled(TEST_SCHEME)); + assertEquals("Path should be updated", execPath2, installer.getInstalledPath(TEST_SCHEME)); + } + + @Test + public void testGetInstalledPathForNonExistentScheme() { + // Arrange - ensure scheme doesn't exist + if (installer.isInstalled(TEST_SCHEME)) { + try { + installer.uninstall(TEST_SCHEME); + } + catch (final IOException e) { + // Ignore + } + } + + // Act + final String path = installer.getInstalledPath(TEST_SCHEME); + + // Assert + assertNull("Path should be null for non-existent scheme", path); + } + + @Test + public void testUninstallNonExistentScheme() throws IOException { + // Arrange - ensure scheme doesn't exist + if (installer.isInstalled(TEST_SCHEME)) { + installer.uninstall(TEST_SCHEME); + } + + // Act - uninstall non-existent scheme (should not fail) + installer.uninstall(TEST_SCHEME); + + // Assert + assertFalse("Scheme should not be installed", installer.isInstalled(TEST_SCHEME)); + } + + @Test(expected = IllegalArgumentException.class) + public void testInstallWithNullScheme() throws IOException { + installer.install(null, "C:\\test.exe"); + } + + @Test(expected = IllegalArgumentException.class) + public void testInstallWithEmptyScheme() throws IOException { + installer.install("", "C:\\test.exe"); + } + + @Test(expected = IllegalArgumentException.class) + public void testInstallWithNullPath() throws IOException { + installer.install(TEST_SCHEME, null); + } + + @Test(expected = IllegalArgumentException.class) + public void testInstallWithEmptyPath() throws IOException { + installer.install(TEST_SCHEME, ""); + } + + @Test + public void testInstallWithPathContainingSpaces() throws IOException { + // Arrange + final String execPath = "C:\\Program Files\\My App\\app.exe"; + + // Ensure scheme is not already installed + if (installer.isInstalled(TEST_SCHEME)) { + installer.uninstall(TEST_SCHEME); + } + + // Act + installer.install(TEST_SCHEME, execPath); + + // Assert + assertTrue("Scheme should be installed", installer.isInstalled(TEST_SCHEME)); + assertEquals("Path with spaces should be handled correctly", execPath, installer.getInstalledPath(TEST_SCHEME)); + } +} From 0d6f36b59eb637896be4bec971499ed243c40d6a Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 25 Nov 2025 17:14:08 -0600 Subject: [PATCH 56/63] Add Linux URI scheme registration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends URI scheme registration to Linux via .desktop file manipulation. The LinuxSchemeInstaller modifies the MimeType field in the .desktop file to add x-scheme-handler entries, then registers them using xdg-mime. Key components: - DesktopFile: Simple parser/writer for .desktop files - LinuxSchemeInstaller: Adds URI schemes to existing .desktop files - Updates DefaultLinkService to support both Windows and Linux The .desktop file path is specified via the scijava.app.desktop-file system property. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../desktop/links/DefaultLinkService.java | 23 +- .../desktop/platform/linux/DesktopFile.java | 232 ++++++++++++++++ .../platform/linux/LinuxSchemeInstaller.java | 248 ++++++++++++++++++ .../linux/LinuxSchemeInstallerTest.java | 232 ++++++++++++++++ 4 files changed, 729 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/scijava/desktop/platform/linux/DesktopFile.java create mode 100644 src/main/java/org/scijava/desktop/platform/linux/LinuxSchemeInstaller.java create mode 100644 src/test/java/org/scijava/desktop/platform/linux/LinuxSchemeInstallerTest.java diff --git a/src/main/java/org/scijava/desktop/links/DefaultLinkService.java b/src/main/java/org/scijava/desktop/links/DefaultLinkService.java index cf72c17..46f700b 100644 --- a/src/main/java/org/scijava/desktop/links/DefaultLinkService.java +++ b/src/main/java/org/scijava/desktop/links/DefaultLinkService.java @@ -31,6 +31,7 @@ import org.scijava.event.ContextCreatedEvent; import org.scijava.event.EventHandler; import org.scijava.desktop.links.SchemeInstaller; +import org.scijava.desktop.platform.linux.LinuxSchemeInstaller; import org.scijava.desktop.platform.windows.WindowsSchemeInstaller; import org.scijava.log.LogService; import org.scijava.plugin.AbstractHandlerService; @@ -64,7 +65,7 @@ private void onEvent(final ContextCreatedEvent evt) { } } - // Register URI schemes with the operating system (Windows only). + // Register URI schemes with the operating system. installSchemes(); } @@ -72,7 +73,7 @@ private void onEvent(final ContextCreatedEvent evt) { * Installs URI schemes with the operating system. *

* This method collects all schemes supported by registered {@link LinkHandler} - * plugins and registers them with the OS (currently Windows only). + * plugins and registers them with the OS (Windows and Linux supported). *

*/ private void installSchemes() { @@ -111,13 +112,23 @@ private void installSchemes() { /** * Creates the appropriate {@link SchemeInstaller} for the current platform. *

- * Currently only Windows is supported. macOS uses Info.plist in the .app bundle - * (configured at build time), and Linux .desktop file management belongs in - * scijava-plugins-platforms. + * Windows and Linux are supported via runtime registration. macOS uses Info.plist + * in the .app bundle (configured at build time, not at runtime). *

*/ private SchemeInstaller createInstaller() { - return new WindowsSchemeInstaller(log); + final String os = System.getProperty("os.name"); + if (os == null) return null; + + final String osLower = os.toLowerCase(); + if (osLower.contains("linux")) { + return new LinuxSchemeInstaller(log); + } + else if (osLower.contains("win")) { + return new WindowsSchemeInstaller(log); + } + + return null; // macOS or other unsupported platforms } /** diff --git a/src/main/java/org/scijava/desktop/platform/linux/DesktopFile.java b/src/main/java/org/scijava/desktop/platform/linux/DesktopFile.java new file mode 100644 index 0000000..17377b6 --- /dev/null +++ b/src/main/java/org/scijava/desktop/platform/linux/DesktopFile.java @@ -0,0 +1,232 @@ +/*- + * #%L + * Desktop integration for SciJava. + * %% + * Copyright (C) 2010 - 2026 SciJava developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.desktop.platform.linux; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Simple parser and writer for Linux .desktop files. + *

+ * Supports reading and writing key-value pairs within the [Desktop Entry] section. + * This implementation is minimal and focused on URI scheme registration needs. + *

+ * + * @author Curtis Rueden + */ +public class DesktopFile { + + private final Map entries; + private final List comments; + + public DesktopFile() { + this.entries = new LinkedHashMap<>(); + this.comments = new ArrayList<>(); + } + + /** + * Parses a .desktop file from disk. + * + * @param path Path to the .desktop file + * @return Parsed DesktopFile + * @throws IOException if reading fails + */ + public static DesktopFile parse(final Path path) throws IOException { + final DesktopFile df = new DesktopFile(); + boolean inDesktopEntry = false; + + try (final BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { + String line; + while ((line = reader.readLine()) != null) { + final String trimmed = line.trim(); + + // Track section + if (trimmed.equals("[Desktop Entry]")) { + inDesktopEntry = true; + continue; + } + if (trimmed.startsWith("[") && trimmed.endsWith("]")) { + inDesktopEntry = false; + continue; + } + + // Only process [Desktop Entry] section + if (!inDesktopEntry) continue; + + // Skip empty lines and comments + if (trimmed.isEmpty() || trimmed.startsWith("#")) { + df.comments.add(line); + continue; + } + + // Parse key=value + final int equals = line.indexOf('='); + if (equals > 0) { + final String key = line.substring(0, equals).trim(); + final String value = line.substring(equals + 1); + df.entries.put(key, value); + } + } + } + + return df; + } + + /** + * Writes the .desktop file to disk. + * + * @param path Path to write to + * @throws IOException if writing fails + */ + public void writeTo(final Path path) throws IOException { + // Ensure parent directory exists + final Path parent = path.getParent(); + if (parent != null && !Files.exists(parent)) { + Files.createDirectories(parent); + } + + try (final BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) { + writer.write("[Desktop Entry]"); + writer.newLine(); + + // Write key-value pairs + for (final Map.Entry entry : entries.entrySet()) { + writer.write(entry.getKey()); + writer.write('='); + writer.write(entry.getValue()); + writer.newLine(); + } + + // Write comments at the end + for (final String comment : comments) { + writer.write(comment); + writer.newLine(); + } + } + } + + /** + * Gets the value for a key. + * + * @param key The key + * @return The value, or null if not present + */ + public String get(final String key) { + return entries.get(key); + } + + /** + * Sets a key-value pair. + * + * @param key The key + * @param value The value + */ + public void set(final String key, final String value) { + entries.put(key, value); + } + + /** + * Checks if a MimeType entry contains a specific MIME type. + * + * @param mimeType The MIME type to check (e.g., "x-scheme-handler/fiji") + * @return true if the MimeType field contains this type + */ + public boolean hasMimeType(final String mimeType) { + final String mimeTypes = entries.get("MimeType"); + if (mimeTypes == null || mimeTypes.isEmpty()) return false; + + final String[] types = mimeTypes.split(";"); + for (final String type : types) { + if (type.trim().equals(mimeType)) { + return true; + } + } + return false; + } + + /** + * Adds a MIME type to the MimeType field. + *

+ * The MimeType field is a semicolon-separated list. This method appends + * the new type if it's not already present. + *

+ * + * @param mimeType The MIME type to add (e.g., "x-scheme-handler/fiji") + */ + public void addMimeType(final String mimeType) { + if (hasMimeType(mimeType)) return; // Already present + + String mimeTypes = entries.get("MimeType"); + if (mimeTypes == null || mimeTypes.isEmpty()) { + // Create new MimeType field + entries.put("MimeType", mimeType + ";"); + } + else { + // Append to existing + if (!mimeTypes.endsWith(";")) { + mimeTypes += ";"; + } + entries.put("MimeType", mimeTypes + mimeType + ";"); + } + } + + /** + * Removes a MIME type from the MimeType field. + * + * @param mimeType The MIME type to remove + */ + public void removeMimeType(final String mimeType) { + final String mimeTypes = entries.get("MimeType"); + if (mimeTypes == null || mimeTypes.isEmpty()) return; + + final List types = new ArrayList<>(); + for (final String type : mimeTypes.split(";")) { + final String trimmed = type.trim(); + if (!trimmed.isEmpty() && !trimmed.equals(mimeType)) { + types.add(trimmed); + } + } + + if (types.isEmpty()) { + entries.remove("MimeType"); + } + else { + entries.put("MimeType", String.join(";", types) + ";"); + } + } +} diff --git a/src/main/java/org/scijava/desktop/platform/linux/LinuxSchemeInstaller.java b/src/main/java/org/scijava/desktop/platform/linux/LinuxSchemeInstaller.java new file mode 100644 index 0000000..b1f30dc --- /dev/null +++ b/src/main/java/org/scijava/desktop/platform/linux/LinuxSchemeInstaller.java @@ -0,0 +1,248 @@ +/*- + * #%L + * Desktop integration for SciJava. + * %% + * Copyright (C) 2010 - 2026 SciJava developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.desktop.platform.linux; + +import org.scijava.desktop.links.SchemeInstaller; +import org.scijava.log.LogService; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.TimeUnit; + +/** + * Linux implementation of {@link SchemeInstaller} using .desktop files. + *

+ * This implementation modifies the .desktop file specified by the + * {@code scijava.app.desktop-file} system property to add URI scheme + * handlers via the MimeType field, then registers them using xdg-mime. + *

+ * + * @author Curtis Rueden + */ +public class LinuxSchemeInstaller implements SchemeInstaller { + + private static final long COMMAND_TIMEOUT_SECONDS = 10; + + private final LogService log; + + public LinuxSchemeInstaller(final LogService log) { + this.log = log; + } + + @Override + public boolean isSupported() { + final String os = System.getProperty("os.name"); + return os != null && os.toLowerCase().contains("linux"); + } + + @Override + public void install(final String scheme, final String executablePath) throws IOException { + if (!isSupported()) { + throw new UnsupportedOperationException("Linux .desktop file installation not supported on: " + System.getProperty("os.name")); + } + + // Validate inputs + if (scheme == null || scheme.isEmpty()) { + throw new IllegalArgumentException("Scheme cannot be null or empty"); + } + + // Get desktop file path from system property + final String desktopFileProp = System.getProperty("scijava.app.desktop-file"); + if (desktopFileProp == null || desktopFileProp.isEmpty()) { + throw new IOException("scijava.app.desktop-file property not set"); + } + + final Path desktopFile = Paths.get(desktopFileProp); + if (!Files.exists(desktopFile)) { + throw new IOException("Desktop file does not exist: " + desktopFile); + } + + // Parse desktop file + final DesktopFile df = DesktopFile.parse(desktopFile); + + // Check if scheme already registered + final String mimeType = "x-scheme-handler/" + scheme; + if (df.hasMimeType(mimeType)) { + if (log != null) log.debug("Scheme '" + scheme + "' already registered in: " + desktopFile); + return; + } + + // Add MIME type + df.addMimeType(mimeType); + + // Write back to file + df.writeTo(desktopFile); + + // Register with xdg-mime + final String desktopFileName = desktopFile.getFileName().toString(); + if (!executeCommand(new String[]{"xdg-mime", "default", desktopFileName, mimeType})) { + throw new IOException("Failed to register scheme with xdg-mime: " + scheme); + } + + // Update desktop database + final Path applicationsDir = desktopFile.getParent(); + if (applicationsDir != null) { + executeCommand(new String[]{"update-desktop-database", applicationsDir.toString()}); + // Note: update-desktop-database may fail if not installed, but this is non-critical + } + + if (log != null) log.info("Registered URI scheme '" + scheme + "' in: " + desktopFile); + } + + @Override + public boolean isInstalled(final String scheme) { + if (!isSupported()) return false; + + final String desktopFileProp = System.getProperty("scijava.app.desktop-file"); + if (desktopFileProp == null) return false; + + final Path desktopFile = Paths.get(desktopFileProp); + if (!Files.exists(desktopFile)) return false; + + try { + final DesktopFile df = DesktopFile.parse(desktopFile); + return df.hasMimeType("x-scheme-handler/" + scheme); + } + catch (final IOException e) { + if (log != null) log.debug("Failed to parse desktop file: " + desktopFile, e); + return false; + } + } + + @Override + public String getInstalledPath(final String scheme) { + if (!isInstalled(scheme)) return null; + + final String desktopFileProp = System.getProperty("scijava.app.desktop-file"); + if (desktopFileProp == null) return null; + + final Path desktopFile = Paths.get(desktopFileProp); + + try { + final DesktopFile df = DesktopFile.parse(desktopFile); + final String exec = df.get("Exec"); + if (exec == null) return null; + + // Parse executable path from Exec line (format: "/path/to/app %U") + final String[] parts = exec.split("\\s+"); + if (parts.length > 0) { + return parts[0]; + } + } + catch (final IOException e) { + if (log != null) log.debug("Failed to parse desktop file: " + desktopFile, e); + } + + return null; + } + + @Override + public void uninstall(final String scheme) throws IOException { + if (!isSupported()) { + throw new UnsupportedOperationException("Linux .desktop file uninstallation not supported on: " + System.getProperty("os.name")); + } + + if (!isInstalled(scheme)) { + if (log != null) log.debug("Scheme '" + scheme + "' is not installed"); + return; + } + + final String desktopFileProp = System.getProperty("scijava.app.desktop-file"); + final Path desktopFile = Paths.get(desktopFileProp); + + // Parse and remove MIME type + final DesktopFile df = DesktopFile.parse(desktopFile); + df.removeMimeType("x-scheme-handler/" + scheme); + df.writeTo(desktopFile); + + // Update desktop database + final Path applicationsDir = desktopFile.getParent(); + if (applicationsDir != null) { + executeCommand(new String[]{"update-desktop-database", applicationsDir.toString()}); + } + + if (log != null) log.info("Uninstalled URI scheme: " + scheme); + } + + // -- Helper methods -- + + /** + * Executes a command and returns whether it succeeded. + */ + private boolean executeCommand(final String[] command) { + try { + final ProcessBuilder pb = new ProcessBuilder(command); + pb.redirectErrorStream(true); + final Process process = pb.start(); + + // Consume output to prevent blocking + try (final BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + final StringBuilder output = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + if (log != null && !output.toString().trim().isEmpty()) { + log.debug("Command output: " + output); + } + } + + // Wait for completion + final boolean finished = process.waitFor(COMMAND_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (!finished) { + if (log != null) log.warn("Command timed out: " + String.join(" ", command)); + process.destroyForcibly(); + return false; + } + + final int exitCode = process.exitValue(); + if (exitCode != 0 && log != null) { + log.debug("Command failed with exit code " + exitCode + ": " + String.join(" ", command)); + } + + return exitCode == 0; + } + catch (final IOException e) { + if (log != null) log.error("Failed to execute command: " + String.join(" ", command), e); + return false; + } + catch (final InterruptedException e) { + if (log != null) log.error("Command interrupted: " + String.join(" ", command), e); + Thread.currentThread().interrupt(); + return false; + } + } +} diff --git a/src/test/java/org/scijava/desktop/platform/linux/LinuxSchemeInstallerTest.java b/src/test/java/org/scijava/desktop/platform/linux/LinuxSchemeInstallerTest.java new file mode 100644 index 0000000..0fb8c75 --- /dev/null +++ b/src/test/java/org/scijava/desktop/platform/linux/LinuxSchemeInstallerTest.java @@ -0,0 +1,232 @@ +/*- + * #%L + * Desktop integration for SciJava. + * %% + * Copyright (C) 2010 - 2026 SciJava developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.desktop.platform.linux; + +import org.junit.After; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.Assert.*; + +/** + * Tests {@link LinuxSchemeInstaller}. + * + * @author Curtis Rueden + */ +public class LinuxSchemeInstallerTest { + + private static final String TEST_SCHEME = "scijava-test"; + private LinuxSchemeInstaller installer; + private Path tempDesktopFile; + private String originalProperty; + + @Before + public void setUp() throws IOException { + installer = new LinuxSchemeInstaller(null); + // Only run tests on Linux + Assume.assumeTrue("Tests only run on Linux", installer.isSupported()); + + // Create temporary desktop file + tempDesktopFile = Files.createTempFile("test-app", ".desktop"); + + // Write basic desktop file content + final DesktopFile df = new DesktopFile(); + df.set("Type", "Application"); + df.set("Name", "Test App"); + df.set("Exec", "/usr/bin/test-app %U"); + df.writeTo(tempDesktopFile); + + // Set system property + originalProperty = System.getProperty("scijava.app.desktop-file"); + System.setProperty("scijava.app.desktop-file", tempDesktopFile.toString()); + } + + @After + public void tearDown() throws IOException { + // Restore original property + if (originalProperty != null) { + System.setProperty("scijava.app.desktop-file", originalProperty); + } + else { + System.clearProperty("scijava.app.desktop-file"); + } + + // Clean up temp file + if (tempDesktopFile != null && Files.exists(tempDesktopFile)) { + Files.delete(tempDesktopFile); + } + } + + @Test + public void testIsSupported() { + final String os = System.getProperty("os.name"); + final boolean expectedSupport = os != null && os.toLowerCase().contains("linux"); + assertEquals(expectedSupport, installer.isSupported()); + } + + @Test + public void testInstallAndUninstall() throws IOException { + // Arrange + final String execPath = "/usr/bin/test-app"; + + // Ensure scheme is not already installed + if (installer.isInstalled(TEST_SCHEME)) { + installer.uninstall(TEST_SCHEME); + } + assertFalse("Test scheme should not be installed initially", installer.isInstalled(TEST_SCHEME)); + + // Act - Install (note: xdg-mime may fail in test environment, but file should be modified) + try { + installer.install(TEST_SCHEME, execPath); + } + catch (final IOException e) { + // xdg-mime might not be available in test environment + // Check if file was at least modified + } + + // Assert - Check desktop file was modified + final DesktopFile df = DesktopFile.parse(tempDesktopFile); + assertTrue("Desktop file should contain MIME type", df.hasMimeType("x-scheme-handler/" + TEST_SCHEME)); + + // Act - Uninstall + installer.uninstall(TEST_SCHEME); + + // Assert - Uninstalled + final DesktopFile df2 = DesktopFile.parse(tempDesktopFile); + assertFalse("Desktop file should not contain MIME type", df2.hasMimeType("x-scheme-handler/" + TEST_SCHEME)); + } + + @Test + public void testInstallTwice() throws IOException { + // Arrange + final String execPath = "/usr/bin/test-app"; + + // Act - Install twice + try { + installer.install(TEST_SCHEME, execPath); + installer.install(TEST_SCHEME, execPath); // Should not fail + } + catch (final IOException e) { + // xdg-mime might not be available + } + + // Assert - Check only one entry + final DesktopFile df = DesktopFile.parse(tempDesktopFile); + final String mimeType = df.get("MimeType"); + assertNotNull("MimeType should be set", mimeType); + + // Count occurrences of the test scheme + int count = 0; + for (final String part : mimeType.split(";")) { + if (part.trim().equals("x-scheme-handler/" + TEST_SCHEME)) { + count++; + } + } + assertEquals("Scheme should appear exactly once", 1, count); + } + + @Test + public void testIsInstalledReturnsFalseWhenFileDoesNotExist() { + // Arrange + System.setProperty("scijava.app.desktop-file", "/nonexistent/path/app.desktop"); + + // Act & Assert + assertFalse("Should return false when desktop file doesn't exist", + installer.isInstalled(TEST_SCHEME)); + } + + @Test + public void testGetInstalledPath() throws IOException { + // Arrange + final String execPath = "/usr/bin/test-app"; + + try { + installer.install(TEST_SCHEME, execPath); + } + catch (final IOException e) { + // xdg-mime might not be available + } + + // Act + final String installedPath = installer.getInstalledPath(TEST_SCHEME); + + // Assert + assertEquals("Should return exec path from desktop file", execPath, installedPath); + } + + @Test(expected = IllegalArgumentException.class) + public void testInstallWithNullScheme() throws IOException { + installer.install(null, "/usr/bin/test-app"); + } + + @Test(expected = IllegalArgumentException.class) + public void testInstallWithEmptyScheme() throws IOException { + installer.install("", "/usr/bin/test-app"); + } + + @Test(expected = IOException.class) + public void testInstallWithoutDesktopFileProperty() throws IOException { + // Arrange + System.clearProperty("scijava.app.desktop-file"); + + // Act + installer.install(TEST_SCHEME, "/usr/bin/test-app"); + } + + @Test + public void testMultipleSchemes() throws IOException { + // Arrange + final String scheme1 = "scijava-test1"; + final String scheme2 = "scijava-test2"; + + // Act - Install two schemes + try { + installer.install(scheme1, "/usr/bin/test-app"); + installer.install(scheme2, "/usr/bin/test-app"); + } + catch (final IOException e) { + // xdg-mime might not be available + } + + // Assert - Both should be in desktop file + final DesktopFile df = DesktopFile.parse(tempDesktopFile); + assertTrue("Should have first scheme", df.hasMimeType("x-scheme-handler/" + scheme1)); + assertTrue("Should have second scheme", df.hasMimeType("x-scheme-handler/" + scheme2)); + + // Cleanup + installer.uninstall(scheme1); + installer.uninstall(scheme2); + } +} From a14a43d34425ddaf95c0da4d599ac19d134277ac Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 25 Nov 2025 17:14:22 -0600 Subject: [PATCH 57/63] Add Linux platform with .desktop file generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements LinuxPlatform to handle Linux-specific desktop integration. Creates .desktop files in ~/.local/share/applications/ for proper application integration including: - Application launcher in desktop menus - Application icon display - Executable path configuration - MimeType field for URI scheme registration (via scijava-links) Configuration via system properties: - scijava.app.name: Application name - scijava.app.executable: Path to executable - scijava.app.icon: Icon file path - scijava.app.directory: Working directory - scijava.app.desktop-file: .desktop file path (auto-set if not provided) The LinuxPlatform creates the basic .desktop file structure, and scijava-links later modifies it to add URI scheme handlers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../desktop/platform/linux/LinuxPlatform.java | 212 ++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 src/main/java/org/scijava/desktop/platform/linux/LinuxPlatform.java diff --git a/src/main/java/org/scijava/desktop/platform/linux/LinuxPlatform.java b/src/main/java/org/scijava/desktop/platform/linux/LinuxPlatform.java new file mode 100644 index 0000000..e818d8a --- /dev/null +++ b/src/main/java/org/scijava/desktop/platform/linux/LinuxPlatform.java @@ -0,0 +1,212 @@ +/* + * #%L + * Desktop integration for SciJava. + * %% + * Copyright (C) 2010 - 2026 SciJava developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.desktop.platform.linux; + +import org.scijava.log.LogService; +import org.scijava.platform.AbstractPlatform; +import org.scijava.platform.Platform; +import org.scijava.platform.PlatformService; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * A platform implementation for handling Linux platform issues. + *

+ * This implementation creates and maintains a .desktop file for the application, + * enabling proper desktop integration including: + *

+ *
    + *
  • Application launcher in menus
  • + *
  • Application icon
  • + *
  • File associations (via separate configuration)
  • + *
  • URI scheme handling (via scijava-links)
  • + *
+ * + * @author Curtis Rueden + */ +@Plugin(type = Platform.class, name = "Linux") +public class LinuxPlatform extends AbstractPlatform { + + @Parameter(required = false) + private LogService log; + + // -- Platform methods -- + + @Override + public String osName() { + return "Linux"; + } + + @Override + public void configure(final PlatformService service) { + super.configure(service); + + // Create or update .desktop file for desktop integration + try { + installDesktopFile(); + } + catch (final IOException e) { + if (log != null) { + log.error("Failed to install .desktop file", e); + } + } + } + + @Override + public void open(final URL url) throws IOException { + if (getPlatformService().exec("xdg-open", url.toString()) != 0) { + throw new IOException("Could not open " + url); + } + } + + // -- Helper methods -- + + /** + * Creates or updates the .desktop file for this application. + *

+ * The .desktop file path is determined by the {@code scijava.app.desktop-file} + * system property. If not set, defaults to {@code ~/.local/share/applications/.desktop}. + *

+ */ + private void installDesktopFile() throws IOException { + // Get configuration from system properties + String desktopFilePath = System.getProperty("scijava.app.desktop-file"); + + if (desktopFilePath == null) { + // Default location + final String appName = System.getProperty("scijava.app.name", "scijava-app"); + final String home = System.getProperty("user.home"); + desktopFilePath = home + "/.local/share/applications/" + sanitizeFileName(appName) + ".desktop"; + + // Set property for other components (e.g., scijava-links) + System.setProperty("scijava.app.desktop-file", desktopFilePath); + } + + final Path desktopFile = Paths.get(desktopFilePath); + + // Check if file already exists and is up-to-date + if (Files.exists(desktopFile) && isDesktopFileUpToDate(desktopFile)) { + if (log != null) { + log.debug("Desktop file is up-to-date: " + desktopFile); + } + return; + } + + // Get application properties + final String appName = System.getProperty("scijava.app.name", "SciJava Application"); + final String appExec = System.getProperty("scijava.app.executable"); + final String appIcon = System.getProperty("scijava.app.icon"); + final String appDir = System.getProperty("scijava.app.directory"); + + if (appExec == null) { + if (log != null) { + log.debug("No executable path set (scijava.app.executable property), skipping .desktop file creation"); + } + return; + } + + // Create parent directory if needed + final Path parent = desktopFile.getParent(); + if (parent != null && !Files.exists(parent)) { + Files.createDirectories(parent); + } + + // Write .desktop file + try (final BufferedWriter writer = Files.newBufferedWriter(desktopFile, StandardCharsets.UTF_8)) { + writer.write("[Desktop Entry]"); + writer.newLine(); + writer.write("Type=Application"); + writer.newLine(); + writer.write("Version=1.0"); + writer.newLine(); + writer.write("Name=" + appName); + writer.newLine(); + writer.write("GenericName=" + appName); + writer.newLine(); + writer.write("X-GNOME-FullName=" + appName); + writer.newLine(); + + if (appIcon != null) { + writer.write("Icon=" + appIcon); + writer.newLine(); + } + + writer.write("Exec=" + appExec + " %U"); + writer.newLine(); + + if (appDir != null) { + writer.write("Path=" + appDir); + writer.newLine(); + } + + writer.write("Terminal=false"); + writer.newLine(); + writer.write("Categories=Science;Education;"); + writer.newLine(); + + // MimeType field intentionally left empty + // scijava-links will add URI scheme handlers (x-scheme-handler/...) + writer.write("MimeType="); + writer.newLine(); + } + + // Make file readable (but not writable) by others + // This is standard practice for .desktop files + // Files.setPosixFilePermissions can be used here if needed + + if (log != null) { + log.info("Created desktop file: " + desktopFile); + } + } + + /** + * Checks if the desktop file is up-to-date with current system properties. + */ + private boolean isDesktopFileUpToDate(final Path desktopFile) { + // For now, simple existence check + // Future: could parse and compare with current properties + return Files.exists(desktopFile); + } + + /** + * Sanitizes a string for use as a file name. + */ + private String sanitizeFileName(final String name) { + return name.replaceAll("[^a-zA-Z0-9._-]", "-").toLowerCase(); + } +} From a2062174fad36c636880c266035dd2a329ec2f8a Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 2 Feb 2026 09:16:16 -0600 Subject: [PATCH 58/63] Improve and expand the DesktopFile API --- .../desktop/platform/linux/DesktopFile.java | 209 ++++++++++++++++-- .../linux/LinuxSchemeInstallerTest.java | 4 +- 2 files changed, 194 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/scijava/desktop/platform/linux/DesktopFile.java b/src/main/java/org/scijava/desktop/platform/linux/DesktopFile.java index 17377b6..9b8104b 100644 --- a/src/main/java/org/scijava/desktop/platform/linux/DesktopFile.java +++ b/src/main/java/org/scijava/desktop/platform/linux/DesktopFile.java @@ -41,33 +41,77 @@ import java.util.Map; /** - * Simple parser and writer for Linux .desktop files. + * Parser and writer for Linux .desktop files. *

- * Supports reading and writing key-value pairs within the [Desktop Entry] section. - * This implementation is minimal and focused on URI scheme registration needs. + * Instance-based API for reading and writing .desktop files. Supports all + * standard [Desktop Entry] section fields plus custom keys. This class + * provides a convenient way to manage desktop files for both application + * launching and URI scheme registration. + *

+ *

+ * Note: This class will eventually move to {@code org.scijava.util.DesktopFile} + * in scijava-common once the design is finalized. *

* * @author Curtis Rueden */ public class DesktopFile { + private final Path path; private final Map entries; private final List comments; - public DesktopFile() { + /** + * Creates a DesktopFile instance for the given path. + *

+ * If the file exists, it will be loaded when {@link #load()} is called. + * If it doesn't exist, the entries map will be empty until populated + * programmatically or {@link #load()} is called. + *

+ * + * @param path the path to the .desktop file + */ + public DesktopFile(final Path path) { + this.path = path; this.entries = new LinkedHashMap<>(); this.comments = new ArrayList<>(); } /** - * Parses a .desktop file from disk. + * Gets the file path for this desktop file. + * + * @return the path + */ + public Path path() { + return path; + } + + /** + * Checks if the file exists on disk. + * + * @return true if the file exists + */ + public boolean exists() { + return Files.exists(path); + } + + /** + * Loads the .desktop file from disk. + *

+ * Clears any existing entries and comments, then reads from the file. + * If the file doesn't exist, entries will be empty after this call. + *

* - * @param path Path to the .desktop file - * @return Parsed DesktopFile * @throws IOException if reading fails */ - public static DesktopFile parse(final Path path) throws IOException { - final DesktopFile df = new DesktopFile(); + public void load() throws IOException { + entries.clear(); + comments.clear(); + + if (!exists()) { + return; + } + boolean inDesktopEntry = false; try (final BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { @@ -90,7 +134,7 @@ public static DesktopFile parse(final Path path) throws IOException { // Skip empty lines and comments if (trimmed.isEmpty() || trimmed.startsWith("#")) { - df.comments.add(line); + comments.add(line); continue; } @@ -99,21 +143,21 @@ public static DesktopFile parse(final Path path) throws IOException { if (equals > 0) { final String key = line.substring(0, equals).trim(); final String value = line.substring(equals + 1); - df.entries.put(key, value); + entries.put(key, value); } } } - - return df; } /** - * Writes the .desktop file to disk. + * Saves the .desktop file to disk. + *

+ * Creates parent directories if needed. Overwrites any existing file. + *

* - * @param path Path to write to * @throws IOException if writing fails */ - public void writeTo(final Path path) throws IOException { + public void save() throws IOException { // Ensure parent directory exists final Path parent = path.getParent(); if (parent != null && !Files.exists(parent)) { @@ -140,6 +184,48 @@ public void writeTo(final Path path) throws IOException { } } + /** + * Deletes the file from disk. + * + * @throws IOException if deletion fails + */ + public void delete() throws IOException { + Files.deleteIfExists(path); + } + + /** + * Parses a .desktop file from disk (static convenience method). + * + * @param path Path to the .desktop file + * @return Parsed DesktopFile + * @throws IOException if reading fails + */ + public static DesktopFile parse(final Path path) throws IOException { + final DesktopFile df = new DesktopFile(path); + df.load(); + return df; + } + + /** + * Writes the .desktop file to disk (for backward compatibility). + * + * @param path Path to write to + * @throws IOException if writing fails + */ + public void writeTo(final Path path) throws IOException { + final Path oldPath = this.path; + // Temporarily change path, save, then restore + try { + // Create a temporary instance with the new path + final DesktopFile temp = new DesktopFile(path); + temp.entries.putAll(this.entries); + temp.comments.addAll(this.comments); + temp.save(); + } catch (final Exception e) { + throw new IOException("Failed to write to " + path, e); + } + } + /** * Gets the value for a key. * @@ -157,9 +243,98 @@ public String get(final String key) { * @param value The value */ public void set(final String key, final String value) { - entries.put(key, value); + if (value == null) { + entries.remove(key); + } else { + entries.put(key, value); + } } + // -- Standard field accessors -- + + public String getType() { + return get("Type"); + } + + public void setType(final String type) { + set("Type", type); + } + + public String getVersion() { + return get("Version"); + } + + public void setVersion(final String version) { + set("Version", version); + } + + public String getName() { + return get("Name"); + } + + public void setName(final String name) { + set("Name", name); + } + + public String getGenericName() { + return get("GenericName"); + } + + public void setGenericName(final String genericName) { + set("GenericName", genericName); + } + + public String getComment() { + return get("Comment"); + } + + public void setComment(final String comment) { + set("Comment", comment); + } + + public String getExec() { + return get("Exec"); + } + + public void setExec(final String exec) { + set("Exec", exec); + } + + public String getIcon() { + return get("Icon"); + } + + public void setIcon(final String icon) { + set("Icon", icon); + } + + public String getPath() { + return get("Path"); + } + + public void setPath(final String path) { + set("Path", path); + } + + public boolean getTerminal() { + final String value = get("Terminal"); + return "true".equalsIgnoreCase(value); + } + + public void setTerminal(final boolean terminal) { + set("Terminal", terminal ? "true" : "false"); + } + + public String getCategories() { + return get("Categories"); + } + + public void setCategories(final String categories) { + set("Categories", categories); + } + + // -- MimeType handling -- + /** * Checks if a MimeType entry contains a specific MIME type. * diff --git a/src/test/java/org/scijava/desktop/platform/linux/LinuxSchemeInstallerTest.java b/src/test/java/org/scijava/desktop/platform/linux/LinuxSchemeInstallerTest.java index 0fb8c75..04bae62 100644 --- a/src/test/java/org/scijava/desktop/platform/linux/LinuxSchemeInstallerTest.java +++ b/src/test/java/org/scijava/desktop/platform/linux/LinuxSchemeInstallerTest.java @@ -62,11 +62,11 @@ public void setUp() throws IOException { tempDesktopFile = Files.createTempFile("test-app", ".desktop"); // Write basic desktop file content - final DesktopFile df = new DesktopFile(); + final DesktopFile df = new DesktopFile(tempDesktopFile); df.set("Type", "Application"); df.set("Name", "Test App"); df.set("Exec", "/usr/bin/test-app %U"); - df.writeTo(tempDesktopFile); + df.save(); // Set system property originalProperty = System.getProperty("scijava.app.desktop-file"); From cc463f775264d1008d084b4dc7e54913a118d9f1 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 4 Feb 2026 12:25:38 -0600 Subject: [PATCH 59/63] Implement desktop integrations in Platform plugins A new DesktopIntegrationProvider interface adds on to Platform plugin functions, enabling them to query and modify the state of desktop-level integrations, including web links support and desktop icon presence. Co-authored-by: Claude Sonnet 4.5 --- README.md | 265 +++++++++++++++--- doc/WINDOWS.md | 259 +++++++++++++---- .../desktop/DesktopIntegrationProvider.java | 82 ++++++ .../desktop/options/OptionsDesktop.java | 151 ++++++++++ .../desktop/platform/linux/LinuxPlatform.java | 217 +++++++++----- .../desktop/platform/macos/MacOSPlatform.java | 38 ++- .../platform/windows/WindowsPlatform.java | 51 +++- 7 files changed, 907 insertions(+), 156 deletions(-) create mode 100644 src/main/java/org/scijava/desktop/DesktopIntegrationProvider.java create mode 100644 src/main/java/org/scijava/desktop/options/OptionsDesktop.java diff --git a/README.md b/README.md index 0793bd0..b328ad3 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,267 @@ +# scijava-desktop + [![Build Status](https://github.com/scijava/scijava-desktop/actions/workflows/build.yml/badge.svg)](https://github.com/scijava/scijava-desktop/actions/workflows/build.yml) -This component provides supporting code for SciJava applications -to manage their integration with the system native desktop: +Unified desktop integration for SciJava applications. -* SciJava Platform plugins for macOS and Windows -* A links subsystem to handle URI-based links via plugins +## Features -The scijava-desktop component requires Java 11 as a minimum, due -to its use of java.awt.Desktop features not present in Java 8. +The scijava-desktop component provides three kinds of desktop integration: -## Features +1. **URI Link Scheme Registration & Handling** + - Register custom URI schemes (e.g., `myapp://`) with the operating system + - Handle URI clicks from web browsers and other applications + - Automatic scheme registration on application startup + +2. **Desktop Icon Generation** + - Linux: `.desktop` file creation in application menus + - Windows: Start Menu shortcuts (planned) + - macOS: Application bundle support + +3. **File Extension Registration** (planned) + - Associate file types with your application + - Platform-specific MIME type handling + +## Platform Support + +- **Linux**: Full support for URI schemes and desktop icons via `.desktop` files +- **Windows**: URI scheme registration via Windows Registry (desktop icons planned) +- **macOS**: Read-only support (configuration via Info.plist at build time) + +## Requirements + +- Java 11 or later (due to use of `java.awt.Desktop` features) +- Platform-specific tools: + - Linux: `xdg-utils` (for `xdg-mime` and `update-desktop-database`) + - Windows: `reg` command (built-in) + - macOS: No runtime dependencies + +## Quick Start -- **Link handling**: Register custom handlers for URI schemes through the `LinkHandler` plugin interface -- **CLI integration**: Automatic handling of URI arguments passed on the command line via `ConsoleArgument` -- **OS integration**: Automatic registration of URI schemes with the operating system (Windows supported, macOS/Linux planned) +### 1. Add Dependency -## Usage +```xml + + org.scijava + scijava-desktop + + +``` + +### 2. Configure System Properties -### Creating a Link Handler +Set these properties when launching your application: -Implement the `LinkHandler` interface to handle custom URI schemes: +```bash +java -Dscijava.app.executable="/path/to/myapp" \ + -Dscijava.app.name="My Application" \ + -Dscijava.app.icon="/path/to/icon.png" \ + -jar myapp.jar +``` + +### 3. Create a LinkHandler Plugin ```java +package com.example; + +import org.scijava.desktop.links.AbstractLinkHandler; +import org.scijava.desktop.links.LinkHandler; +import org.scijava.plugin.Plugin; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; + @Plugin(type = LinkHandler.class) -public class MyLinkHandler extends AbstractLinkHandler { +public class MyAppLinkHandler extends AbstractLinkHandler { @Override - public boolean supports(URI uri) { + public boolean supports(final URI uri) { return "myapp".equals(uri.getScheme()); } @Override - public void handle(URI uri) { - // Handle the URI + public void handle(final URI uri) { + // Handle the URI (e.g., open a file, navigate to a view) System.out.println("Handling: " + uri); } @Override public List getSchemes() { - // Return schemes to register with the OS + // Schemes to register with the OS return Arrays.asList("myapp"); } } ``` -### OS Registration +### 4. Launch and Test -On Windows, URI schemes returned by `LinkHandler.getSchemes()` are automatically registered -in the Windows Registry when the `LinkService` initializes. This allows users to click -links like `myapp://action` in web browsers or other applications, which will launch your -Java application with the URI as a command-line argument. +1. Start your application +2. The `myapp://` scheme is automatically registered with your OS +3. Click a `myapp://action?param=value` link in your browser +4. Your application launches and handles the URI -The registration uses `HKEY_CURRENT_USER` and requires no administrator privileges. +## Architecture -See [doc/WINDOWS.md](doc/WINDOWS.md) for details. +### Link Handling System -## Architecture +- **LinkService**: Service for routing URIs to appropriate handlers +- **LinkHandler**: Plugin interface for implementing URI handlers +- **LinkArgument**: Console argument plugin for command-line URI handling +- **SchemeInstaller**: Platform-specific OS registration + +### Platform Integration + +- **Platform Plugins**: LinuxPlatform, WindowsPlatform, MacOSPlatform +- **DesktopIntegrationProvider**: Interface for querying/toggling desktop features +- **OptionsDesktop**: User-facing options plugin (Edit > Options > Desktop...) + +### State Management + +Desktop integration state is queried directly from the OS (not saved to preferences): +- On load: Query OS for current registration state +- On save: Apply changes directly to OS (e.g. registry, .desktop files) +- Keeps UI in sync with actual OS state + +## System Properties + +| Property | Description | Platforms | Required | +|----------------------------|--------------------------------|-----------|-----------------------------------------------------------| +| `scijava.app.executable` | Path to application executable | All | Yes (for URI schemes) | +| `scijava.app.name` | Application name | All | No (default: "SciJava") | +| `scijava.app.icon` | Icon path | All | No | +| `scijava.app.directory` | Application directory | All | No | +| `scijava.app.desktop-file` | Override .desktop file path | Linux | No (default: `~/.local/share/applications/.desktop`) | + +## User Interface + +Users can manage desktop integration via **Edit > Options > Desktop...** in your application: + +- **Enable web links**: Register/unregister URI schemes +- **Add desktop icon**: Install/remove application launcher + +The UI automatically grays out features that are not available on the current platform. + +## Documentation + +- [doc/WINDOWS.md](doc/WINDOWS.md) - Windows Registry-based URI scheme registration + +## Examples + +### Parse URI Components + +```java +import org.scijava.desktop.links.Links; + +URI uri = new URI("myapp://view/document?id=123&mode=edit"); + +String operation = Links.operation(uri); // "view" +List pathFragments = Links.pathFragments(uri); // ["view", "document"] +Map query = Links.query(uri); // {"id": "123", "mode": "edit"} +``` + +### Multiple Schemes + +```java +@Plugin(type = LinkHandler.class) +public class MultiSchemeLinkHandler extends AbstractLinkHandler { + + @Override + public boolean supports(final URI uri) { + String scheme = uri.getScheme(); + return "myapp".equals(scheme) || "myapp-dev".equals(scheme); + } + + @Override + public void handle(final URI uri) { + if ("myapp-dev".equals(uri.getScheme())) { + // Handle development scheme + } else { + // Handle production scheme + } + } + + @Override + public List getSchemes() { + return Arrays.asList("myapp", "myapp-dev"); + } +} +``` + +## Platform-Specific Details + +### Linux + +URI schemes are registered by: +1. Creating a `.desktop` file in `~/.local/share/applications/` +2. Adding `x-scheme-handler/` to the MimeType field +3. Registering with `xdg-mime default .desktop x-scheme-handler/` +4. Updating the desktop database with `update-desktop-database` + +Desktop icons are created by installing the `.desktop` file with appropriate fields (Name, Exec, Icon, Categories). + +### macOS + +URI schemes are declared in the application's `Info.plist` file within the `.app` bundle. This is configured at build time (bundle is code-signed and immutable), so runtime registration is not supported. -- `LinkService` - Service for routing URIs to appropriate handlers -- `LinkHandler` - Plugin interface for implementing custom URI handlers -- `LinkArgument` - Console argument plugin that recognizes URIs on the command line -- `SchemeInstaller` - Interface for OS-specific URI scheme registration -- `WindowsSchemeInstaller` - Windows implementation using registry commands +The MacOSPlatform correctly reports read-only status for all desktop integration features. -The launcher should set the `scijava.app.executable` system property to enable URI scheme registration. +### Windows + +URI schemes are registered in the Windows Registry under `HKEY_CURRENT_USER\Software\Classes\`. No administrator privileges are required. + +The registry structure: +``` +HKCU\Software\Classes\myapp + (Default) = "URL:myapp" + URL Protocol = "" + shell\open\command\ + (Default) = "C:\Path\To\App.exe" "%1" +``` + +## Known Issues + +### Hardcoded Elements (Needs Fixes) + +1. **Hardcoded "fiji" scheme**: WindowsPlatform:86,102 and LinuxPlatform:112,129 hardcode the "fiji" scheme instead of querying LinkHandler plugins. + - Impact: Only works for Fiji application + - Fix: See NEXT.md Work Item #1 + +2. **Hardcoded OS checks**: DefaultLinkService:119-132 directly checks OS name instead of using PlatformService. + - Impact: Violates plugin architecture + - Fix: See NEXT.md Work Items #2 and #3 + +### Missing Features + +- File extension registration +- Windows desktop icon (Start Menu shortcut) +- First launch dialog for opt-in + +See [NEXT.md](NEXT.md) for details on planned improvements. + +## Manual Testing + +**Windows**: +```bash +# Check registry after running your app +regedit +# Navigate to HKCU\Software\Classes\myapp +``` + +**Linux**: +```bash +# Check .desktop file +cat ~/.local/share/applications/myapp.desktop + +# Check MIME associations +xdg-mime query default x-scheme-handler/myapp +``` + +**Test URI handling**: +```bash +# Linux/macOS +xdg-open "myapp://test" + +# Windows +start "myapp://test" +``` diff --git a/doc/WINDOWS.md b/doc/WINDOWS.md index ed8a7b1..1ae38dd 100644 --- a/doc/WINDOWS.md +++ b/doc/WINDOWS.md @@ -1,18 +1,26 @@ -# Windows URI Scheme Registration +# Windows Desktop Integration -This document describes how URI scheme registration works on Windows in scijava-links. +This document describes how desktop integration works on Windows in scijava-desktop. -## Overview +## URI Scheme Registration -When a SciJava application starts on Windows, the `DefaultLinkService` automatically: +### Overview -1. Collects all URI schemes from registered `LinkHandler` plugins via `getSchemes()` -2. Reads the executable path from the `scijava.app.executable` system property -3. Registers each scheme in the Windows Registry under `HKEY_CURRENT_USER\Software\Classes` +The scijava-desktop component automatically registers URI schemes with Windows when a SciJava application starts. The registration is done via the Windows Registry and requires no administrator privileges. -## Registry Structure +### How It Works -For a scheme named `myapp`, the following registry structure is created: +When the application starts: + +1. `DefaultLinkService` initializes and listens for `ContextCreatedEvent` +2. Collects all URI schemes from registered `LinkHandler` plugins via `getSchemes()` +3. Reads the executable path from the `scijava.app.executable` system property +4. Creates a `WindowsSchemeInstaller` (currently hardcoded, should use platform plugin) +5. Registers each scheme in the Windows Registry + +### Registry Structure + +For a scheme named `myapp`, the following registry structure is created under `HKEY_CURRENT_USER`: ``` HKEY_CURRENT_USER\Software\Classes\myapp @@ -24,113 +32,254 @@ HKEY_CURRENT_USER\Software\Classes\myapp (Default) = "C:\Path\To\App.exe" "%1" ``` -## Implementation Details +### Registry Location + +**Key**: `HKEY_CURRENT_USER\Software\Classes\` -### SchemeInstaller Interface +**Reason**: Using `HKEY_CURRENT_USER` (HKCU) instead of `HKEY_LOCAL_MACHINE` (HKLM) means: +- No administrator rights required +- Registration is per-user (not system-wide) +- Safe for non-privileged users -The `SchemeInstaller` interface provides a platform-independent API for URI scheme registration: +### Implementation Details -- `isSupported()` - Checks if the installer works on the current platform +#### WindowsSchemeInstaller + +The `WindowsSchemeInstaller` class provides the platform-specific implementation: + +**Methods**: +- `isSupported()` - Returns true on Windows platforms - `install(scheme, executablePath)` - Registers a URI scheme - `isInstalled(scheme)` - Checks if a scheme is already registered - `getInstalledPath(scheme)` - Gets the executable path for a registered scheme - `uninstall(scheme)` - Removes a URI scheme registration -### WindowsSchemeInstaller +**Implementation Approach**: +- Uses the `reg` command-line tool (built into Windows) +- No JNA dependency required +- Executed via `ProcessBuilder` with proper timeout (10 seconds) +- Robust error handling and validation -The Windows implementation uses the `reg` command-line tool to manipulate the registry: +#### Example Commands -- **No JNA dependency**: Uses native Windows `reg` commands via `ProcessBuilder` -- **No admin rights**: Registers under `HKEY_CURRENT_USER` (not `HKEY_LOCAL_MACHINE`) -- **Idempotent**: Safely handles re-registration with the same or different paths -- **Robust error handling**: Proper timeouts, error logging, and validation +**Add a scheme**: +```cmd +reg add "HKCU\Software\Classes\myapp" /ve /d "URL:myapp" /f +reg add "HKCU\Software\Classes\myapp" /v "URL Protocol" /d "" /f +reg add "HKCU\Software\Classes\myapp\shell\open\command" /ve /d "\"C:\Path\To\App.exe\" \"%1\"" /f +``` -### Executable Path Configuration +**Check if a scheme exists**: +```cmd +reg query "HKCU\Software\Classes\myapp\shell\open\command" /ve +``` -The launcher must set the `scijava.app.executable` system property to the absolute path of the application's executable. This property is used by `DefaultLinkService` during URI scheme registration. +**Delete a scheme**: +```cmd +reg delete "HKCU\Software\Classes\myapp" /f +``` + +### Configuration + +#### System Properties + +Applications must set the `scijava.app.executable` system property for URI scheme registration to work: -Example launcher configuration: ```bash java -Dscijava.app.executable="C:\Program Files\MyApp\MyApp.exe" -jar myapp.jar ``` -On Windows, the launcher typically sets this to the `.exe` file path. On macOS, it would be the path inside the `.app` bundle. On Linux, it would be the shell script or executable. +**Important**: The path should point to the actual executable (`.exe` file) that Windows should launch when a URI is clicked. -## Example Handler +#### Launcher Configuration -Here's a complete example of a `LinkHandler` that registers a custom scheme: +For Windows applications with a native launcher: + +```batch +@echo off +set JAVA_HOME=C:\Program Files\Java\jdk-11 +set APP_HOME=%~dp0 + +"%JAVA_HOME%\bin\java.exe" ^ + -Dscijava.app.executable="%APP_HOME%\MyApp.exe" ^ + -jar "%APP_HOME%\lib\myapp.jar" +``` + +### Creating a LinkHandler + +To register custom URI schemes, create a `LinkHandler` plugin: ```java package com.example; -import org.scijava.links.AbstractLinkHandler; -import org.scijava.links.LinkHandler; -import org.scijava.log.LogService; -import org.scijava.plugin.Parameter; +import org.scijava.desktop.links.AbstractLinkHandler; +import org.scijava.desktop.links.LinkHandler; +import org.scijava.desktop.links.Links; import org.scijava.plugin.Plugin; import java.net.URI; import java.util.Arrays; import java.util.List; +import java.util.Map; @Plugin(type = LinkHandler.class) -public class ExampleLinkHandler extends AbstractLinkHandler { - - @Parameter(required = false) - private LogService log; +public class MyAppLinkHandler extends AbstractLinkHandler { @Override public boolean supports(final URI uri) { - return "example".equals(uri.getScheme()); + return "myapp".equals(uri.getScheme()); } @Override public void handle(final URI uri) { - if (log != null) { - log.info("Handling example URI: " + uri); - } - - // Parse the URI and perform actions + // Parse the URI String operation = Links.operation(uri); Map params = Links.query(uri); // Your business logic here - // ... + System.out.println("Operation: " + operation); + System.out.println("Parameters: " + params); } @Override public List getSchemes() { - // This tells the system to register "example://" links on Windows - return Arrays.asList("example"); + // This tells DefaultLinkService to register "myapp://" with Windows + return Arrays.asList("myapp"); } } ``` +### User Experience + +1. User installs and launches the application +2. Application automatically registers URI schemes with Windows +3. User clicks a `myapp://action?param=value` link in their browser +4. Windows launches the application with the URI as a command-line argument +5. `LinkArgument` plugin parses the URI and passes it to `LinkService` +6. `LinkService` routes the URI to the appropriate `LinkHandler` +7. Handler processes the request + +### Desktop Integration Options + +Users can manage URI scheme registration via **Edit > Options > Desktop...**: + +- **Enable web links**: Toggleable checkbox to register/unregister URI schemes +- State is queried from the actual Windows Registry (not saved to preferences) +- Changes apply immediately to the registry + +### Desktop Icon (Start Menu) + +**Status**: Not yet implemented + +**Planned Implementation**: +- Create Start Menu shortcut (.lnk file) +- Location: `%APPDATA%\Microsoft\Windows\Start Menu\Programs\.lnk` +- Use JNA or native executable for .lnk creation +- Toggle via OptionsDesktop UI + ## Testing -The Windows scheme installation can be tested on Windows systems: +### Manual Testing -```bash -mvn test -Dtest=WindowsSchemeInstallerTest +**Check Registry**: +1. Launch your application +2. Open `regedit` +3. Navigate to `HKEY_CURRENT_USER\Software\Classes\myapp` +4. Verify the structure matches the expected format + +**Test URI Handling**: +1. Create an HTML file with a link: `Test Link` +2. Open the HTML file in a browser +3. Click the link +4. Verify your application launches and handles the URI + +**Test from Command Line**: +```cmd +start myapp://test?param=value ``` -Tests are automatically skipped on non-Windows platforms using JUnit's `Assume.assumeTrue()`. +### Automated Tests -To test with a specific executable path, set the system property: +Run Windows-specific tests: ```bash -mvn test -Dscijava.app.executable="C:\Path\To\App.exe" +mvn test -Dtest=WindowsSchemeInstallerTest ``` -## Platform Notes +Tests automatically skip on non-Windows platforms using JUnit's `Assume.assumeTrue()`. + +## Known Issues + +### Hardcoded Scheme Name + +**Issue**: `WindowsPlatform` (lines 86, 102) currently hardcodes the "fiji" scheme instead of querying registered `LinkHandler` plugins. + +**Impact**: Only works for Fiji; breaks for other applications. + +**Workaround**: None; requires code fix. + +**Fix**: See NEXT.md Work Item #1 for the solution. + +### Hardcoded OS Checks + +**Issue**: `DefaultLinkService.createInstaller()` (lines 119-132) hardcodes OS name checks instead of using `PlatformService`. + +**Impact**: Violates plugin architecture. + +**Workaround**: None; requires code fix. + +**Fix**: See NEXT.md Work Items #2 and #3 for the solution. -**macOS**: URI schemes are declared in the application's `Info.plist` within the `.app` bundle. This is configured at build/packaging time, not at runtime, since the bundle is typically code-signed and immutable. +## Platform Comparison -**Linux**: URI schemes are declared in `.desktop` files, which is part of broader desktop integration (icons, MIME types, etc.). This functionality belongs in `scijava-plugins-platforms` rather than this component. +### Windows vs. Linux vs. macOS -**Windows**: Runtime registration is appropriate because the Windows Registry is designed for runtime modifications, and registration under `HKEY_CURRENT_USER` requires no elevated privileges. +| Feature | Windows | Linux | macOS | +|---------|---------|-------|-------| +| **URI Scheme Registration** | Runtime (Registry) | Runtime (.desktop file) | Build-time (Info.plist) | +| **Admin Rights Required** | No (HKCU) | No (user .desktop file) | N/A (bundle) | +| **Toggleable at Runtime** | Yes | Yes | No (read-only) | +| **Desktop Icon** | Planned (Start Menu) | Yes (.desktop file) | No (user pins to Dock) | +| **File Extensions** | Planned (Registry) | Planned (.desktop MIME types) | Build-time (Info.plist) | + +### Why Runtime Registration on Windows? + +Unlike macOS (where the `.app` bundle is code-signed and immutable), Windows allows runtime modification of the registry without special privileges. This makes it practical to register URI schemes when the application first runs, similar to how many Windows applications work. + +## Security Considerations + +### Registry Safety + +- Only writes to `HKEY_CURRENT_USER` (per-user settings) +- Does not modify system-wide settings in `HKEY_LOCAL_MACHINE` +- Only registers URI schemes declared by `LinkHandler` plugins +- Does not expose arbitrary command execution + +### Command-Line Injection + +The `%1` parameter in the registry command is properly quoted: +``` +"C:\Path\To\App.exe" "%1" +``` + +This prevents command-line injection attacks via malicious URIs. ## Future Enhancements -- **Scheme validation**: Validate scheme names against RFC 3986 -- **User prompts**: Optional confirmation before registering schemes -- **Uninstallation**: Automatic cleanup on application uninstall +1. **Start Menu Shortcut**: Implement desktop icon creation +2. **File Extension Registration**: Register file types in the registry +3. **Scheme Validation**: Validate scheme names against RFC 3986 +4. **User Prompts**: Optional confirmation before registering schemes +5. **Uninstallation**: Automatic cleanup on application uninstall +6. **Icon Support**: Associate icons with schemes and file types + +## Resources + +- [Microsoft URI Scheme Documentation](https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85)) +- [Windows Registry Reference](https://docs.microsoft.com/en-us/windows/win32/sysinfo/registry) +- [RFC 3986 - URI Generic Syntax](https://www.rfc-editor.org/rfc/rfc3986) + +## See Also + +- [NEXT.md](../NEXT.md) - Planned improvements +- [spec/DESKTOP_INTEGRATION_PLAN.md](../spec/DESKTOP_INTEGRATION_PLAN.md) - Architecture overview +- [spec/IMPLEMENTATION_SUMMARY.md](../spec/IMPLEMENTATION_SUMMARY.md) - Implementation details diff --git a/src/main/java/org/scijava/desktop/DesktopIntegrationProvider.java b/src/main/java/org/scijava/desktop/DesktopIntegrationProvider.java new file mode 100644 index 0000000..6127fe2 --- /dev/null +++ b/src/main/java/org/scijava/desktop/DesktopIntegrationProvider.java @@ -0,0 +1,82 @@ +/* + * #%L + * Desktop integration for SciJava. + * %% + * Copyright (C) 2010 - 2026 SciJava developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.desktop; + +import java.io.IOException; + +/** + * Marker interface for platform implementations that provide desktop + * integration features. + *

+ * Platforms implementing this interface can be queried for desktop integration + * capabilities via {@link #getDesktopIntegration()}. + *

+ * + * @author Curtis Rueden + */ +public interface DesktopIntegrationProvider { + + boolean isWebLinksEnabled(); + + boolean isWebLinksToggleable(); + + /** + * Enables or disables URI scheme registration (e.g., {@code fiji://} links). + *

+ * This operation only works if {@link #isWebLinksToggleable()} + * returns true. Otherwise, calling this method may throw + * {@link UnsupportedOperationException}. + *

+ * + * @param enable whether to enable or disable web links + * @throws IOException if the operation fails + * @throws UnsupportedOperationException if not supported on this platform + */ + void setWebLinksEnabled(final boolean enable) throws IOException; + + boolean isDesktopIconPresent(); + + boolean isDesktopIconToggleable(); + + /** + * Installs or removes the desktop icon (application launcher, menu entry). + *

+ * This operation only works if + * {@link #isDesktopIconToggleable()} returns true. + * Otherwise, calling this method may throw + * {@link UnsupportedOperationException}. + *

+ * + * @param install whether to install or remove the desktop icon + * @throws IOException if the operation fails + * @throws UnsupportedOperationException if not supported on this platform + */ + void setDesktopIconPresent(final boolean install) throws IOException; +} diff --git a/src/main/java/org/scijava/desktop/options/OptionsDesktop.java b/src/main/java/org/scijava/desktop/options/OptionsDesktop.java new file mode 100644 index 0000000..7423beb --- /dev/null +++ b/src/main/java/org/scijava/desktop/options/OptionsDesktop.java @@ -0,0 +1,151 @@ +/* + * #%L + * Desktop integration for SciJava. + * %% + * Copyright (C) 2010 - 2026 SciJava developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.desktop.options; + +import java.io.IOException; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.scijava.desktop.DesktopIntegrationProvider; +import org.scijava.log.LogService; +import org.scijava.options.OptionsPlugin; +import org.scijava.platform.Platform; +import org.scijava.platform.PlatformService; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; +import org.scijava.plugin.PluginInfo; +import org.scijava.plugin.SciJavaPlugin; + +/** + * Options plugin for managing desktop integration features. + *

+ * Provides a UI for enabling/disabling web links (URI schemes) and + * desktop icons. Settings are applied directly to the OS (not persisted + * to preferences), keeping the UI in sync with actual system state. + *

+ * + * @author Curtis Rueden + */ +@Plugin(type = OptionsPlugin.class, menuPath = "Edit > Options > Desktop...") +public class OptionsDesktop extends OptionsPlugin { + + @Parameter + private PlatformService platformService; + + @Parameter(required = false) + private LogService log; + + @Parameter(label = "Enable web links", persist = false, validater = "validateWebLinks", // + description = "Allow handling of URI link schemes from web browsers") + private boolean webLinksEnabled; + + @Parameter(label = "Add desktop icon", persist = false, validater = "validateDesktopIcon", // + description = "Install application icon in the system menu") + private boolean desktopIconPresent; + + @Override + public void load() { + webLinksEnabled = true; + desktopIconPresent = true; + for (final Platform platform : platformService.getTargetPlatforms()) { + if (!(platform instanceof DesktopIntegrationProvider)) continue; + final DesktopIntegrationProvider dip = (DesktopIntegrationProvider) platform; + // If any toggleable platform setting is off, uncheck that box. + if (dip.isDesktopIconToggleable() && !dip.isDesktopIconPresent()) desktopIconPresent = false; + if (dip.isWebLinksToggleable() && !dip.isWebLinksEnabled()) webLinksEnabled = false; + } + } + + @Override + public void run() { + for (final Platform platform : platformService.getTargetPlatforms()) { + if (!(platform instanceof DesktopIntegrationProvider)) continue; + final DesktopIntegrationProvider dip = (DesktopIntegrationProvider) platform; + try { + dip.setWebLinksEnabled(webLinksEnabled); + dip.setDesktopIconPresent(desktopIconPresent); + } + catch (final IOException e) { + if (log != null) { + log.error("Error applying desktop integration settings", e); + } + } + } + super.run(); + } + + // -- Validators -- + + public void validateWebLinks() { + validateSetting( + DesktopIntegrationProvider::isWebLinksToggleable, + DesktopIntegrationProvider::isWebLinksEnabled, + webLinksEnabled, + "Web links setting"); + } + + public void validateDesktopIcon() { + validateSetting( + DesktopIntegrationProvider::isDesktopIconToggleable, + DesktopIntegrationProvider::isDesktopIconPresent, + desktopIconPresent, + "Desktop icon presence"); + } + + // -- Helper methods -- + + private String name(Platform platform) { + final List> infos = + pluginService.getPluginsOfClass(platform.getClass()); + return infos.isEmpty() ? null : infos.get(0).getName(); + } + + private void validateSetting( + Function mutable, + Function getter, + boolean value, String settingDescription) + { + boolean toggleable = false; + boolean enabled = false; + Platform strictPlatform = null; + for (final Platform platform : platformService.getTargetPlatforms()) { + if (!(platform instanceof DesktopIntegrationProvider)) continue; + final DesktopIntegrationProvider dip = (DesktopIntegrationProvider) platform; + if (mutable.apply(dip)) toggleable = true; + else if (strictPlatform == null) strictPlatform = platform; + if (getter.apply(dip)) enabled = true; + } + if (!toggleable && enabled != value) { + final String platformName = strictPlatform == null ? "this platform" : name(strictPlatform); + throw new IllegalArgumentException(settingDescription + " cannot be changed on " + platformName + "."); + } + } +} diff --git a/src/main/java/org/scijava/desktop/platform/linux/LinuxPlatform.java b/src/main/java/org/scijava/desktop/platform/linux/LinuxPlatform.java index e818d8a..8fb8457 100644 --- a/src/main/java/org/scijava/desktop/platform/linux/LinuxPlatform.java +++ b/src/main/java/org/scijava/desktop/platform/linux/LinuxPlatform.java @@ -29,6 +29,9 @@ package org.scijava.desktop.platform.linux; +import org.scijava.app.AppService; +import org.scijava.desktop.DesktopIntegrationProvider; +import org.scijava.desktop.links.LinkService; import org.scijava.log.LogService; import org.scijava.platform.AbstractPlatform; import org.scijava.platform.Platform; @@ -36,10 +39,8 @@ import org.scijava.plugin.Parameter; import org.scijava.plugin.Plugin; -import java.io.BufferedWriter; import java.io.IOException; import java.net.URL; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -53,14 +54,22 @@ *
    *
  • Application launcher in menus
  • *
  • Application icon
  • - *
  • File associations (via separate configuration)
  • - *
  • URI scheme handling (via scijava-links)
  • + *
  • File associations
  • + *
  • URI scheme handling
  • *
* * @author Curtis Rueden */ @Plugin(type = Platform.class, name = "Linux") -public class LinuxPlatform extends AbstractPlatform { +public class LinuxPlatform extends AbstractPlatform + implements DesktopIntegrationProvider +{ + + @Parameter + private LinkService linkService; + + @Parameter + private AppService appService; @Parameter(required = false) private LogService log; @@ -94,8 +103,114 @@ public void open(final URL url) throws IOException { } } + // -- DesktopIntegrationProvider methods -- + + @Override + public boolean isWebLinksEnabled() { + try { + final DesktopFile df = getOrCreateDesktopFile(); + return df.hasMimeType("x-scheme-handler/fiji"); + } catch (final IOException e) { + if (log != null) { + log.debug("Failed to check web links status", e); + } + return false; + } + } + + @Override + public boolean isWebLinksToggleable() { return true; } + + @Override + public void setWebLinksEnabled(final boolean enable) throws IOException { + final DesktopFile df = getOrCreateDesktopFile(); + + if (enable) { + df.addMimeType("x-scheme-handler/fiji"); + } else { + df.removeMimeType("x-scheme-handler/fiji"); + } + + df.save(); + } + + @Override + public boolean isDesktopIconPresent() { + final Path desktopFilePath = getDesktopFilePath(); + return Files.exists(desktopFilePath); + } + + @Override + public boolean isDesktopIconToggleable() { return true; } + + @Override + public void setDesktopIconPresent(final boolean install) throws IOException { + final DesktopFile df = getOrCreateDesktopFile(); + + if (install) { + // Ensure .desktop file has all required fields + if (df.getName() == null) { + final String appName = System.getProperty("scijava.app.name", "SciJava Application"); + df.setName(appName); + } + if (df.getType() == null) { + df.setType("Application"); + } + if (df.getVersion() == null) { + df.setVersion("1.0"); + } + if (df.getExec() == null) { + final String appExec = System.getProperty("scijava.app.executable"); + if (appExec == null) { + throw new IOException("No executable path set (scijava.app.executable property)"); + } + df.setExec(appExec + " %U"); + } + if (df.getGenericName() == null) { + final String appName = System.getProperty("scijava.app.name", "SciJava Application"); + df.setGenericName(appName); + } + + // Set optional fields if provided + final String appIcon = System.getProperty("scijava.app.icon"); + if (appIcon != null && df.getIcon() == null) { + df.setIcon(appIcon); + } + + final String appDir = System.getProperty("scijava.app.directory"); + if (appDir != null && df.getPath() == null) { + df.setPath(appDir); + } + + if (df.getCategories() == null) { + df.setCategories("Science;Education;"); + } + + df.setTerminal(false); + + df.save(); + } + else { + df.delete(); + } + } + // -- Helper methods -- + /** + * Gets or creates a DesktopFile instance, loading it if it exists. + */ + private DesktopFile getOrCreateDesktopFile() throws IOException { + final Path path = getDesktopFilePath(); + final DesktopFile df = new DesktopFile(path); + + if (df.exists()) { + df.load(); + } + + return df; + } + /** * Creates or updates the .desktop file for this application. *

@@ -104,25 +219,12 @@ public void open(final URL url) throws IOException { *

*/ private void installDesktopFile() throws IOException { - // Get configuration from system properties - String desktopFilePath = System.getProperty("scijava.app.desktop-file"); - - if (desktopFilePath == null) { - // Default location - final String appName = System.getProperty("scijava.app.name", "scijava-app"); - final String home = System.getProperty("user.home"); - desktopFilePath = home + "/.local/share/applications/" + sanitizeFileName(appName) + ".desktop"; - - // Set property for other components (e.g., scijava-links) - System.setProperty("scijava.app.desktop-file", desktopFilePath); - } - - final Path desktopFile = Paths.get(desktopFilePath); + final Path desktopFilePath = getDesktopFilePath(); // Check if file already exists and is up-to-date - if (Files.exists(desktopFile) && isDesktopFileUpToDate(desktopFile)) { + if (Files.exists(desktopFilePath) && isDesktopFileUpToDate(desktopFilePath)) { if (log != null) { - log.debug("Desktop file is up-to-date: " + desktopFile); + log.debug("Desktop file is up-to-date: " + desktopFilePath); } return; } @@ -140,58 +242,43 @@ private void installDesktopFile() throws IOException { return; } - // Create parent directory if needed - final Path parent = desktopFile.getParent(); - if (parent != null && !Files.exists(parent)) { - Files.createDirectories(parent); - } + // Use DesktopFile to create and save + final DesktopFile df = new DesktopFile(desktopFilePath); + df.setType("Application"); + df.setVersion("1.0"); + df.setName(appName); + df.setGenericName(appName); + df.setExec(appExec + " %U"); + df.setTerminal(false); + df.setCategories("Science;Education;"); - // Write .desktop file - try (final BufferedWriter writer = Files.newBufferedWriter(desktopFile, StandardCharsets.UTF_8)) { - writer.write("[Desktop Entry]"); - writer.newLine(); - writer.write("Type=Application"); - writer.newLine(); - writer.write("Version=1.0"); - writer.newLine(); - writer.write("Name=" + appName); - writer.newLine(); - writer.write("GenericName=" + appName); - writer.newLine(); - writer.write("X-GNOME-FullName=" + appName); - writer.newLine(); - - if (appIcon != null) { - writer.write("Icon=" + appIcon); - writer.newLine(); - } + if (appIcon != null) { + df.setIcon(appIcon); + } - writer.write("Exec=" + appExec + " %U"); - writer.newLine(); + if (appDir != null) { + df.setPath(appDir); + } - if (appDir != null) { - writer.write("Path=" + appDir); - writer.newLine(); - } + // MimeType field intentionally left empty + // scijava-links will add URI scheme handlers (x-scheme-handler/...) - writer.write("Terminal=false"); - writer.newLine(); - writer.write("Categories=Science;Education;"); - writer.newLine(); + df.save(); - // MimeType field intentionally left empty - // scijava-links will add URI scheme handlers (x-scheme-handler/...) - writer.write("MimeType="); - writer.newLine(); + if (log != null) { + log.info("Created desktop file: " + desktopFilePath); } + } - // Make file readable (but not writable) by others - // This is standard practice for .desktop files - // Files.setPosixFilePermissions can be used here if needed - - if (log != null) { - log.info("Created desktop file: " + desktopFile); + private Path getDesktopFilePath() { + String desktopFilePath = System.getProperty("scijava.app.desktop-file"); + if (desktopFilePath == null) { + final String appName = System.getProperty("scijava.app.name", "scijava-app"); + final String home = System.getProperty("user.home"); + desktopFilePath = home + "/.local/share/applications/" + sanitizeFileName(appName) + ".desktop"; + System.setProperty("scijava.app.desktop-file", desktopFilePath); } + return Paths.get(desktopFilePath); } /** diff --git a/src/main/java/org/scijava/desktop/platform/macos/MacOSPlatform.java b/src/main/java/org/scijava/desktop/platform/macos/MacOSPlatform.java index 88c230c..870e2da 100644 --- a/src/main/java/org/scijava/desktop/platform/macos/MacOSPlatform.java +++ b/src/main/java/org/scijava/desktop/platform/macos/MacOSPlatform.java @@ -39,6 +39,7 @@ import org.scijava.command.CommandInfo; import org.scijava.command.CommandService; +import org.scijava.desktop.DesktopIntegrationProvider; import org.scijava.display.event.window.WinActivatedEvent; import org.scijava.event.EventHandler; import org.scijava.event.EventService; @@ -61,7 +62,9 @@ * @author Curtis Rueden */ @Plugin(type = Platform.class, name = "macOS") -public class MacOSPlatform extends AbstractPlatform { +public class MacOSPlatform extends AbstractPlatform + implements DesktopIntegrationProvider +{ /** Debugging flag to allow easy toggling of Mac screen menu bar behavior. */ private static final boolean SCREEN_MENU = true; @@ -123,6 +126,38 @@ public boolean registerAppMenus(final Object menus) { return false; } + // -- DesktopIntegrationProvider methods -- + + @Override + public boolean isWebLinksEnabled() { + // URI schemes are declared in Info.plist, which is immutable. + return true; + } + + @Override + public boolean isWebLinksToggleable() { + // URI schemes are declared in Info.plist, which is immutable. + return false; + } + + @Override + public void setWebLinksEnabled(final boolean enable) { + // Note: Operation has no effect here. + // URI scheme registration is immutable on macOS (configured in .app bundle). + } + + @Override + public boolean isDesktopIconPresent() { return false; } + + @Override + public boolean isDesktopIconToggleable() { return false; } + + @Override + public void setDesktopIconPresent(final boolean install) { + // Note: Operation has no effect here. + // Desktop icon installation is not supported on macOS (use Dock pinning instead). + } + // -- Disposable methods -- @Override @@ -162,5 +197,4 @@ private void removeAppCommandsFromMenu() { } eventService.publish(new ModulesUpdatedEvent(infos)); } - } diff --git a/src/main/java/org/scijava/desktop/platform/windows/WindowsPlatform.java b/src/main/java/org/scijava/desktop/platform/windows/WindowsPlatform.java index 0b0f3c2..79aafc0 100644 --- a/src/main/java/org/scijava/desktop/platform/windows/WindowsPlatform.java +++ b/src/main/java/org/scijava/desktop/platform/windows/WindowsPlatform.java @@ -32,17 +32,25 @@ import java.io.IOException; import java.net.URL; +import org.scijava.desktop.DesktopIntegrationProvider; +import org.scijava.log.LogService; import org.scijava.platform.AbstractPlatform; import org.scijava.platform.Platform; +import org.scijava.plugin.Parameter; import org.scijava.plugin.Plugin; /** * A platform implementation for handling Windows platform issues. - * + * * @author Johannes Schindelin */ @Plugin(type = Platform.class, name = "Windows") -public class WindowsPlatform extends AbstractPlatform { +public class WindowsPlatform extends AbstractPlatform + implements DesktopIntegrationProvider +{ + + @Parameter(required = false) + private LogService log; // -- Platform methods -- @@ -70,4 +78,43 @@ public void open(final URL url) throws IOException { } } + // -- DesktopIntegrationProvider methods -- + + @Override + public boolean isWebLinksEnabled() { + final WindowsSchemeInstaller installer = new WindowsSchemeInstaller(log); + return installer.isInstalled("fiji"); + } + + @Override + public boolean isWebLinksToggleable() { return true; } + + @Override + public void setWebLinksEnabled(final boolean enable) throws IOException { + final WindowsSchemeInstaller installer = new WindowsSchemeInstaller(log); + final String executablePath = System.getProperty("scijava.app.executable"); + + if (executablePath == null) { + throw new IOException("No executable path set (scijava.app.executable property)"); + } + + if (enable) { + installer.install("fiji", executablePath); + } + else { + installer.uninstall("fiji"); + } + } + + @Override + public boolean isDesktopIconPresent() { return false; } + + @Override + public boolean isDesktopIconToggleable() { return false; } + + @Override + public void setDesktopIconPresent(final boolean install) { + // Note: Operation has no effect here. + // Desktop icon installation is not supported on Windows (add to Start menu manually). + } } From 4fa842df7a1f7e62ab797f2907432a3073d7ff5e Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 4 Feb 2026 12:25:38 -0600 Subject: [PATCH 60/63] TEMP: Track implementation plan and remaining work --- NEXT.md | 249 +++++++++++++++++++++ spec/DESKTOP_INTEGRATION_PLAN.md | 321 ++++++++++++++++++++++++++ spec/IMPLEMENTATION_SUMMARY.md | 373 +++++++++++++++++++++++++++++++ 3 files changed, 943 insertions(+) create mode 100644 NEXT.md create mode 100644 spec/DESKTOP_INTEGRATION_PLAN.md create mode 100644 spec/IMPLEMENTATION_SUMMARY.md diff --git a/NEXT.md b/NEXT.md new file mode 100644 index 0000000..c1eb16d --- /dev/null +++ b/NEXT.md @@ -0,0 +1,249 @@ +# Next Steps: SciJava Desktop Integration + +This document outlines the remaining work for the scijava-desktop component. + +## Overview + +scijava-desktop provides unified desktop integration for SciJava applications, managing: +1. **URI link scheme registration & handling** (Linux, Windows, macOS) +2. **Desktop icon generation** (Linux .desktop file, Windows Start Menu - planned) +3. **File extension registration** (planned) + +The component uses a plugin system where Platform implementations (LinuxPlatform, WindowsPlatform, MacOSPlatform) handle platform-specific integration. + +## Current Status + +### ✅ Implemented +- URI link handling system (LinkService, LinkHandler interface) +- Platform-specific URI scheme registration (Windows Registry, Linux .desktop files) +- Desktop icon management for Linux +- DesktopIntegrationProvider interface for platform capabilities +- OptionsDesktop plugin for user preferences (Edit > Options > Desktop...) +- Platform plugins for Linux, Windows, and macOS + +### ⚠️ Partially Implemented (Needs Fixes) +- **Hardcoded scheme names**: WindowsPlatform:86,102 and LinuxPlatform:112,129 hardcode "fiji" scheme +- **Hardcoded OS checks**: DefaultLinkService:119-132 directly checks OS name strings and instantiates platform-specific installers + +### ❌ Not Yet Implemented +- File extension registration +- Windows Start Menu icon generation +- First launch dialog for desktop integration opt-in +- SchemeInstallerProvider interface for platforms + +## Priority Work Items + +### 1. Remove Hardcoded Scheme Names + +**Problem**: Both Windows and Linux platforms hardcode the "fiji" scheme instead of querying registered LinkHandlers. + +**Files to modify**: +- `src/main/java/org/scijava/desktop/platform/windows/WindowsPlatform.java` (lines 86, 102) +- `src/main/java/org/scijava/desktop/platform/linux/LinuxPlatform.java` (lines 112, 129) + +**Solution**: Query LinkService for all registered schemes from LinkHandler plugins. + +**Example** (WindowsPlatform.java): +```java +@Override +public boolean isWebLinksEnabled() { + final WindowsSchemeInstaller installer = new WindowsSchemeInstaller(log); + final Set schemes = collectSchemes(); + if (schemes.isEmpty()) return false; + + // Check if any scheme is installed + for (String scheme : schemes) { + if (installer.isInstalled(scheme)) return true; + } + return false; +} + +@Override +public void setWebLinksEnabled(final boolean enable) throws IOException { + final WindowsSchemeInstaller installer = new WindowsSchemeInstaller(log); + final String executablePath = System.getProperty("scijava.app.executable"); + if (executablePath == null) { + throw new IOException("No executable path set (scijava.app.executable property)"); + } + + final Set schemes = collectSchemes(); + for (String scheme : schemes) { + if (enable) { + installer.install(scheme, executablePath); + } else { + installer.uninstall(scheme); + } + } +} + +private Set collectSchemes() { + final Set schemes = new HashSet<>(); + if (linkService == null) return schemes; + + for (final LinkHandler handler : linkService.getInstances()) { + schemes.addAll(handler.getSchemes()); + } + return schemes; +} +``` + +Similar changes needed for LinuxPlatform. + +### 2. Add SchemeInstallerProvider Interface + +**Goal**: Allow platforms to provide SchemeInstaller instances without hardcoding in DefaultLinkService. + +**New file**: `src/main/java/org/scijava/desktop/links/SchemeInstallerProvider.java` + +```java +package org.scijava.desktop.links; + +/** + * Interface for platforms that provide {@link SchemeInstaller} functionality. + *

+ * Platform implementations can implement this interface to provide + * platform-specific URI scheme installation capabilities. + *

+ */ +public interface SchemeInstallerProvider { + /** + * Creates a SchemeInstaller for this platform. + * + * @return a SchemeInstaller, or null if not supported + */ + SchemeInstaller getSchemeInstaller(); +} +``` + +**Files to modify**: +- WindowsPlatform.java - implement SchemeInstallerProvider +- LinuxPlatform.java - implement SchemeInstallerProvider +- MacOSPlatform.java - implement SchemeInstallerProvider (return null) + +### 3. Refactor DefaultLinkService#createInstaller() + +**Problem**: Hardcoded OS checks violate the plugin architecture. + +**Current code** (lines 119-132): +```java +private SchemeInstaller createInstaller() { + final String os = System.getProperty("os.name"); + if (os == null) return null; + + final String osLower = os.toLowerCase(); + if (osLower.contains("linux")) { + return new LinuxSchemeInstaller(log); + } + else if (osLower.contains("win")) { + return new WindowsSchemeInstaller(log); + } + + return null; +} +``` + +**Refactored code**: +```java +@Parameter(required = false) +private PlatformService platformService; + +private SchemeInstaller createInstaller() { + if (platformService == null) return null; + + final Platform platform = platformService.platform(); + if (platform instanceof SchemeInstallerProvider) { + return ((SchemeInstallerProvider) platform).getSchemeInstaller(); + } + + return null; +} +``` + +### 4. First Launch Dialog (Optional) + +**Goal**: Prompt user on first launch to enable desktop integration. + +**Implementation approach**: +- Add `DesktopIntegrationService` to track first launch +- Publish `DesktopIntegrationPromptEvent` on first run +- Application (Fiji) can listen and show dialog + +**Dialog content**: +``` +Would you like to integrate [App Name] into your desktop? + +If you say Yes, I will: +- Add a [App Name] desktop icon +- Add [App Name] as a handler for supported file types +- Allow [App Name] to handle [scheme]:// web links + +Either way: "To change these settings in the future, use Edit > Options > Desktop..." +``` + +**Implementation notes**: +- Use `appService.getApp().getTitle()` for `[App Name]` to keep it generic +- Collect schemes from all `LinkHandler` plugins to populate `[scheme]://` +- Show this dialog only on first launch (track via preferences) +- Provide "Yes" and "No" options +- If user selects "Yes", enable all available desktop integration features +- If user selects "No", do nothing +- Store user preference by writing to a local configuration file -- avoids showing dialog again + +### 5. File Extension Registration (Future) + +**Scope**: Extend DesktopIntegrationProvider to support file type associations. + +**New methods**: +```java +boolean isFileExtensionsEnabled(); +boolean isFileExtensionsToggleable(); +void setFileExtensionsEnabled(boolean enable) throws IOException; +``` + +**Platform implementations**: +- **Linux**: Add MIME types to .desktop file (e.g., `image/tiff`, `application/x-imagej-macro`) +- **Windows**: Register file associations in Registry under `HKCU\Software\Classes\.` +- **macOS**: Declared in Info.plist (build-time, read-only) + +### 6. Windows Start Menu Icon + +**Goal**: Add desktop icon support for Windows. + +**Implementation**: +- Create Start Menu shortcut (.lnk file) in `%APPDATA%\Microsoft\Windows\Start Menu\Programs` +- Use JNA or native executable to create shortcuts +- Update WindowsPlatform.isDesktopIconToggleable() to return true + +## Testing Checklist + +- [ ] Test URI scheme registration on Windows (registry manipulation) +- [ ] Test URI scheme registration on Linux (.desktop file updates) +- [ ] Test desktop icon installation on Linux +- [ ] Verify macOS reports correct read-only status +- [ ] Test OptionsDesktop UI with multiple schemes +- [ ] Verify scheme collection from LinkHandler plugins +- [ ] Test toggling features on/off via UI +- [ ] Verify no hardcoded "fiji" references remain + +## System Properties Reference + +- `scijava.app.executable` - Path to application executable (required for all platforms) +- `scijava.app.name` - Application name (defaults to "SciJava Application") +- `scijava.app.icon` - Icon path (Linux only) +- `scijava.app.directory` - Working directory (Linux only) +- `scijava.app.desktop-file` - Override .desktop file path (Linux only) + +## Documentation Updates Needed + +- [ ] Update README.md with current feature status +- [ ] Update doc/WINDOWS.md to reflect generic scheme handling +- [ ] Create doc/LINUX.md documenting .desktop file integration +- [ ] Create doc/MACOS.md explaining Info.plist configuration +- [ ] Add examples of LinkHandler implementation + +## Questions to Resolve + +1. Should SchemeInstallerProvider be a separate interface or extend Platform? +2. How to handle partial scheme installation failures? +3. Should first launch dialog be mandatory or optional? +4. What file extensions should be supported by default? diff --git a/spec/DESKTOP_INTEGRATION_PLAN.md b/spec/DESKTOP_INTEGRATION_PLAN.md new file mode 100644 index 0000000..1841760 --- /dev/null +++ b/spec/DESKTOP_INTEGRATION_PLAN.md @@ -0,0 +1,321 @@ +# Desktop Integration Architecture + +## Overview + +The scijava-desktop component provides unified desktop integration for SciJava applications, managing three kinds of OS integration: + +1. **URI link scheme registration & handling** - Register custom URI schemes (e.g., `fiji://`) with the operating system +2. **Desktop icon generation** - Create application launchers in system menus +3. **File extension registration** - Associate file types with the application (planned) + +This component comes with support for Linux, macOS, and Windows out of the box, and uses a plugin system for platform-specific implementations. + +## Architecture + +### Core Components + +#### 1. LinkService & LinkHandler System + +**LinkService** (`org.scijava.desktop.links.LinkService`) +- Service for routing URIs to appropriate handlers +- Automatically registers URI schemes with the OS on application startup +- Delegates URI handling to LinkHandler plugins + +**LinkHandler** (`org.scijava.desktop.links.LinkHandler`) +- Plugin interface for handling specific URI schemes +- Implementations declare which schemes they support via `getSchemes()` +- Handle method processes the URI when clicked/opened + +**LinkArgument** (`org.scijava.desktop.links.LinkArgument`) +- Console argument plugin that recognizes URIs on the command line +- Passes URIs to LinkService for handling + +**SchemeInstaller** (`org.scijava.desktop.links.SchemeInstaller`) +- Platform-independent interface for OS registration +- Methods: `install()`, `uninstall()`, `isInstalled()`, `isSupported()` +- Implementations: WindowsSchemeInstaller, LinuxSchemeInstaller + +#### 2. Platform Integration System + +**DesktopIntegrationProvider** (`org.scijava.desktop.DesktopIntegrationProvider`) +- Interface implemented by Platform plugins +- Provides methods to query and toggle desktop integration features +- Methods: + - `boolean isWebLinksEnabled()` / `setWebLinksEnabled(boolean)` + - `boolean isWebLinksToggleable()` + - `boolean isDesktopIconPresent()` / `setDesktopIconPresent(boolean)` + - `boolean isDesktopIconToggleable()` + +**Platform Implementations**: +- `WindowsPlatform` - Registry-based URI scheme registration +- `LinuxPlatform` - .desktop file management +- `MacOSPlatform` - Read-only status (uses Info.plist at build time) + +**OptionsDesktop** (`org.scijava.desktop.options.OptionsDesktop`) +- User-facing options plugin (Edit > Options > Desktop...) +- Queries platform capabilities and displays toggleable features +- Applies changes directly to OS (no preference persistence) + +#### 3. Linux Desktop File Management + +**DesktopFile** (`org.scijava.desktop.platform.linux.DesktopFile`) +- Instance-based API for reading/writing Linux .desktop files +- Handles standard fields: Type, Name, Exec, Icon, Path, Categories, etc. +- MIME type management: `hasMimeType()`, `addMimeType()`, `removeMimeType()` +- Methods: `load()`, `save()`, `delete()`, `exists()` + +**LinuxSchemeInstaller** (`org.scijava.desktop.platform.linux.LinuxSchemeInstaller`) +- Uses DesktopFile to add/remove URI scheme handlers +- Registers schemes via `xdg-mime default` +- Updates desktop database with `update-desktop-database` + +## Platform-Specific Behavior + +### Linux + +**URI Schemes**: +- Adds `x-scheme-handler/` to .desktop file MimeType field +- Registers with `xdg-mime default .desktop x-scheme-handler/` +- Updates desktop database for system-wide recognition + +**Desktop Icon**: +- Creates .desktop file in `~/.local/share/applications/` +- Defaults to `~/.local/share/applications/.desktop` +- Override via `scijava.app.desktop-file` system property +- File includes: Name, Exec, Icon, Categories, MimeType + +**File Extensions** (planned): +- Add MIME types to .desktop file (e.g., `image/tiff`, `application/x-imagej-macro`) +- Register with `xdg-mime default` + +**Toggleability**: Both web links and desktop icon are toggleable + +### Windows + +**URI Schemes**: +- Registers schemes in `HKEY_CURRENT_USER\Software\Classes\` +- Uses `reg` command-line tool (no admin rights required) +- Creates registry structure: scheme → URL Protocol → shell → open → command + +**Desktop Icon**: +- Not yet implemented +- Plan: Create Start Menu shortcut (.lnk file) +- Location: `%APPDATA%\Microsoft\Windows\Start Menu\Programs` + +**File Extensions** (planned): +- Register in `HKCU\Software\Classes\.` + +**Toggleability**: Web links are toggleable; desktop icon not yet supported + +### macOS + +**URI Schemes**: +- Declared in Info.plist within the .app bundle +- Configured at build time (bundle is code-signed and immutable) +- No runtime registration needed + +**Desktop Icon**: +- Application bundle in /Applications (installed by user) +- User can pin to Dock manually + +**File Extensions**: +- Declared in Info.plist at build time + +**Toggleability**: All features are read-only (not toggleable at runtime) + +## State Management + +**No Preference Persistence**: Desktop integration state is NOT saved to preferences. Instead: +- On load: Query platform to get actual OS state +- On save: Write directly to OS (registry, files) +- Keeps settings UI in sync with reality +- Prevents sync issues if user manually modifies (e.g., deletes .desktop file) + +**Query Flow**: +``` +OptionsDesktop.load() + → PlatformService.platform() + → Platform instanceof DesktopIntegrationProvider + → isWebLinksEnabled() / isDesktopIconPresent() + → Query OS (registry, .desktop file, etc.) +``` + +**Apply Flow**: +``` +OptionsDesktop.save() + → setWebLinksEnabled(true) / setDesktopIconPresent(true) + → Platform modifies OS directly + → Windows: Update registry + → Linux: Create/modify .desktop file + → macOS: No-op (read-only) +``` + +## System Properties + +Applications configure desktop integration via system properties: + +| Property | Description | Platforms | Required | +|----------|-------------|-----------|----------| +| `scijava.app.executable` | Path to application executable | All | Yes (for URI schemes) | +| `scijava.app.name` | Application name | Linux | No (defaults to "SciJava Application") | +| `scijava.app.icon` | Icon path | Linux | No | +| `scijava.app.directory` | Working directory | Linux | No | +| `scijava.app.desktop-file` | Override .desktop file path | Linux | No (defaults to `~/.local/share/applications/.desktop`) | + +## Current Limitations & Known Issues + +### Hardcoded Elements + +1. **Scheme Names**: WindowsPlatform:86,102 and LinuxPlatform:112,129 hardcode "fiji" scheme + - Should query LinkService for registered schemes instead + - See NEXT.md Work Item #1 + +2. **OS Checks**: DefaultLinkService:119-132 hardcodes OS name checks + - Should use PlatformService to get active platform + - Should add SchemeInstallerProvider interface + - See NEXT.md Work Items #2 and #3 + +### Missing Features + +1. **File Extension Registration**: Mentioned but not implemented +2. **Windows Desktop Icon**: Not yet implemented (Start Menu shortcut) +3. **First Launch Dialog**: No opt-in prompt on first run +4. **Multiple Schemes**: Platforms assume single scheme per application + +## Usage Example + +### Creating a LinkHandler + +```java +package com.example; + +import org.scijava.desktop.links.AbstractLinkHandler; +import org.scijava.desktop.links.LinkHandler; +import org.scijava.plugin.Plugin; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; + +@Plugin(type = LinkHandler.class) +public class MyAppLinkHandler extends AbstractLinkHandler { + + @Override + public boolean supports(final URI uri) { + return "myapp".equals(uri.getScheme()); + } + + @Override + public void handle(final URI uri) { + // Handle the URI (e.g., open a file, navigate to a view) + String path = uri.getPath(); + // ... your logic here ... + } + + @Override + public List getSchemes() { + // Schemes to register with the OS + return Arrays.asList("myapp"); + } +} +``` + +### Configuring the Application + +Set system properties in your launcher script: + +```bash +java -Dscijava.app.executable="/path/to/myapp" \ + -Dscijava.app.name="My Application" \ + -Dscijava.app.icon="/path/to/icon.png" \ + -jar myapp.jar +``` + +### User Experience + +1. **Initial Setup**: Application can show first-launch dialog (planned) +2. **Options Dialog**: Users access Edit > Options > Desktop... +3. **Toggle Features**: Check/uncheck "Enable web links" and "Add desktop icon" +4. **OS Integration**: Changes apply immediately to registry/.desktop files + +## Implementation Status + +### ✅ Completed + +- LinkService & LinkHandler system +- SchemeInstaller interface & implementations +- Platform plugins (Linux, Windows, macOS) +- DesktopIntegrationProvider interface +- OptionsDesktop plugin +- DesktopFile utility (Linux) +- URI scheme registration (Windows Registry) +- URI scheme registration (Linux .desktop files) +- Desktop icon installation (Linux) + +### ⚠️ Needs Fixes + +- Remove hardcoded "fiji" scheme references +- Remove hardcoded OS checks in DefaultLinkService +- Add SchemeInstallerProvider interface + +### ❌ Not Implemented + +- File extension registration +- Windows desktop icon (Start Menu) +- First launch dialog +- Multiple scheme support in platforms +- Scheme validation (RFC 3986) + +## Testing Strategy + +### Unit Tests + +- SchemeInstaller implementations (mock registry/file I/O) +- Platform DesktopIntegrationProvider implementations +- DesktopFile parsing/writing + +### Integration Tests + +- OptionsDesktop lifecycle (load → modify → save) +- Platform-specific workflows (enable/disable on each OS) +- LinkHandler discovery and routing + +### Manual Testing + +- Windows: Check registry entries with `regedit` +- Linux: Verify .desktop file contents and xdg-mime associations +- macOS: Verify read-only status reported correctly +- All platforms: Click URI links in browsers and verify app launches + +## Design Decisions + +### Why No Preference Persistence? + +OS state is the source of truth. Users can manually modify registrations (delete .desktop files, edit registry), so we query the OS directly rather than trusting stale preferences. + +### Why Platform Plugins? + +The plugin system allows: +- No hardcoded OS checks +- Easy addition of new platforms +- Platform-specific behavior without coupling +- Runtime platform detection by SciJava's Platform system + +### Why Separate SchemeInstaller? + +LinkService needs to install schemes at startup, before UI is available. SchemeInstaller provides a non-UI API for OS registration, separate from the UI-facing DesktopIntegrationProvider. + +### Why Linux Creates .desktop on Platform.configure()? + +LinuxPlatform.configure() runs when the platform is activated, ensuring the .desktop file exists early for proper desktop integration. The file is then updated by DesktopIntegrationProvider methods when users toggle features. + +## Future Enhancements + +1. **Multi-scheme Support**: Allow platforms to handle multiple schemes independently +2. **Scheme Validation**: Validate scheme names against RFC 3986 +3. **Repair Functionality**: "Recheck Registration" button to verify/repair installations +4. **Better Error Reporting**: User-friendly messages for permission errors +5. **Event System**: Publish events when registration state changes +6. **File Extension Support**: Full implementation of file type associations +7. **Windows Desktop Icon**: Start Menu shortcut creation +8. **macOS App Store**: Handle sandboxed app limitations diff --git a/spec/IMPLEMENTATION_SUMMARY.md b/spec/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..c96b2d7 --- /dev/null +++ b/spec/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,373 @@ +# scijava-desktop Implementation Summary + +## Component Purpose + +The **scijava-desktop** component is a unified desktop integration library for SciJava applications, providing: + +1. **URI Link Scheme Registration & Handling** - Register and handle custom URI schemes (e.g., `myapp://action`) +2. **Desktop Icon Generation** - Create application launchers in system menus (Linux .desktop files, Windows Start Menu planned) +3. **File Extension Registration** - Associate file types with the application (planned) + +This component merges functionality from the former `scijava-links` and `scijava-plugins-platforms` repositories into a single, cohesive system. + +## Architecture Overview + +### Plugin-Based Platform System + +The component uses SciJava's plugin architecture to avoid hardcoded OS checks: + +- **Platform Plugins**: LinuxPlatform, WindowsPlatform, MacOSPlatform +- **Platform Detection**: Handled automatically by `PlatformService` from scijava-common +- **Platform Capabilities**: Each platform implements `DesktopIntegrationProvider` to expose capabilities + +### Link Handling System + +- **LinkService**: Routes URIs to registered handlers +- **LinkHandler**: Plugin interface for URI scheme implementations +- **SchemeInstaller**: Platform-specific OS registration (Windows Registry, Linux .desktop files) +- **LinkArgument**: Console argument plugin for CLI URI handling + +### Desktop Integration UI + +- **OptionsDesktop**: User-facing options plugin (Edit > Options > Desktop...) +- **DesktopIntegrationProvider**: Interface for querying/toggling integration features +- **No Preference Persistence**: State is always queried from OS, not saved to preferences + +## Implementation Details + +### Files Created + +| File | Purpose | +|------|---------| +| `DesktopIntegrationProvider.java` | Interface for platform integration capabilities | +| `OptionsDesktop.java` | User options plugin for managing desktop integration | +| `DesktopFile.java` | Linux .desktop file parser/writer | +| `LinkService.java` | Service interface for URI handling | +| `DefaultLinkService.java` | LinkService implementation with OS scheme registration | +| `LinkHandler.java` | Plugin interface for URI handlers | +| `AbstractLinkHandler.java` | Base class for LinkHandler implementations | +| `LinkArgument.java` | Console argument plugin for CLI URIs | +| `Links.java` | Utility class for URI parsing | +| `SchemeInstaller.java` | Interface for OS scheme registration | +| `WindowsSchemeInstaller.java` | Windows Registry-based registration | +| `LinuxSchemeInstaller.java` | Linux .desktop file-based registration | + +### Files Modified + +| File | Changes | +|------|---------| +| `WindowsPlatform.java` | Implements DesktopIntegrationProvider; URI scheme registration | +| `LinuxPlatform.java` | Implements DesktopIntegrationProvider; .desktop file management | +| `MacOSPlatform.java` | Implements DesktopIntegrationProvider (read-only status) | +| `pom.xml` | Updated dependencies and build configuration | + +## Feature Status + +### ✅ Fully Implemented + +#### URI Link Handling +- LinkService automatically registers schemes on application startup +- LinkHandler plugins declare schemes via `getSchemes()` +- LinkArgument processes URIs from command line +- Platform-specific registration (Windows Registry, Linux .desktop files) +- Runtime URI routing to appropriate handlers + +#### Desktop Icon (Linux) +- Automatic .desktop file creation on LinuxPlatform.configure() +- Toggleable via OptionsDesktop UI +- Configurable via system properties: + - `scijava.app.name` - Application name + - `scijava.app.executable` - Executable path + - `scijava.app.icon` - Icon path + - `scijava.app.directory` - Working directory + - `scijava.app.desktop-file` - Override .desktop file path + +#### Platform Capabilities +- Windows: URI schemes toggleable (registry) +- Linux: URI schemes and desktop icon toggleable (.desktop file) +- macOS: Read-only status (Info.plist at build time) + +#### Options UI +- Edit > Options > Desktop... menu entry +- Queries OS state on load +- Applies changes directly to OS on save +- Grays out non-toggleable features + +### ⚠️ Partially Implemented (Needs Fixes) + +#### Hardcoded Scheme Names +**Issue**: WindowsPlatform (lines 86, 102) and LinuxPlatform (lines 112, 129) hardcode "fiji" scheme instead of querying LinkService. + +**Impact**: Only works for applications named "fiji"; breaks for other SciJava applications. + +**Fix Required**: Query LinkService.getInstances() to collect schemes from all registered LinkHandler plugins. + +#### Hardcoded OS Checks +**Issue**: DefaultLinkService.createInstaller() (lines 119-132) directly checks `os.name` system property and instantiates platform-specific installers. + +**Impact**: Violates plugin architecture; makes adding new platforms difficult. + +**Fix Required**: +1. Add SchemeInstallerProvider interface +2. Platforms implement SchemeInstallerProvider +3. DefaultLinkService queries PlatformService.platform() instead of checking OS name + +### ❌ Not Yet Implemented + +#### File Extension Registration +**Status**: Architecture planned, not implemented + +**Planned Approach**: +- Extend DesktopIntegrationProvider with file extension methods +- Linux: Add MIME types to .desktop file +- Windows: Register in `HKCU\Software\Classes\.` +- macOS: Declared in Info.plist (build-time, read-only) + +#### Windows Desktop Icon +**Status**: Not implemented + +**Planned Approach**: +- Create Start Menu shortcut (.lnk file) +- Location: `%APPDATA%\Microsoft\Windows\Start Menu\Programs` +- Use JNA or native executable for .lnk creation +- Update WindowsPlatform.isDesktopIconToggleable() to return true + +#### First Launch Dialog +**Status**: Not implemented + +**Planned Approach**: +- Add DesktopIntegrationService to track first launch +- Publish DesktopIntegrationPromptEvent on first run +- Application (Fiji) listens and shows opt-in dialog +- Dialog text from TODO.md (generic, using appService.getApp().getTitle()) + +#### macOS URI Scheme Registration +**Status**: Documentation only; no runtime registration + +**Current State**: macOS platforms declare URI schemes in Info.plist at build time. The MacOSPlatform correctly reports read-only status, but there's no code to verify or assist with Info.plist configuration. + +## Platform-Specific Implementation Details + +### Linux Platform + +**URI Scheme Registration**: +``` +1. LinuxSchemeInstaller reads/writes .desktop file +2. Adds x-scheme-handler/ to MimeType field +3. Registers with: xdg-mime default .desktop x-scheme-handler/ +4. Updates desktop database: update-desktop-database ~/.local/share/applications +``` + +**Desktop Icon**: +``` +1. LinuxPlatform.configure() creates .desktop file if missing +2. Default location: ~/.local/share/applications/.desktop +3. File includes: Type, Name, Exec, Icon, Path, Terminal, Categories, MimeType +4. User can toggle via OptionsDesktop (creates/deletes file) +``` + +**Files**: +- `LinuxPlatform.java` (Platform plugin) +- `LinuxSchemeInstaller.java` (SchemeInstaller implementation) +- `DesktopFile.java` (Utility for .desktop file I/O) + +### Windows Platform + +**URI Scheme Registration**: +``` +1. WindowsSchemeInstaller uses 'reg' command-line tool +2. Registers in HKEY_CURRENT_USER\Software\Classes\ +3. Creates: (Default)="URL:", URL Protocol="", shell\open\command\(Default)=" %1" +4. No admin rights required (HKCU, not HKLM) +``` + +**Desktop Icon**: +- Not yet implemented +- WindowsPlatform.isDesktopIconToggleable() returns false + +**Files**: +- `WindowsPlatform.java` (Platform plugin) +- `WindowsSchemeInstaller.java` (SchemeInstaller implementation) + +### macOS Platform + +**URI Scheme Registration**: +- Declared in Info.plist at build time +- MacOSPlatform reports read-only status (not toggleable) +- No runtime registration code + +**Desktop Icon**: +- Application bundle installed by user (not programmatic) +- MacOSPlatform.isDesktopIconToggleable() returns false + +**Additional Features**: +- MacOSAppEventDispatcher handles desktop events (open file, open URI, quit, about, preferences) +- Screen menu bar support + +**Files**: +- `MacOSPlatform.java` (Platform plugin) +- `MacOSAppEventDispatcher.java` (Event handling) + +## System Properties + +| Property | Purpose | Platforms | Default | +|----------|---------|-----------|---------| +| `scijava.app.executable` | Executable path for URI scheme registration | All | None (required) | +| `scijava.app.name` | Application name for .desktop file | Linux | "SciJava Application" | +| `scijava.app.icon` | Icon path for .desktop file | Linux | None (optional) | +| `scijava.app.directory` | Working directory for .desktop file | Linux | None (optional) | +| `scijava.app.desktop-file` | Override .desktop file path | Linux | `~/.local/share/applications/.desktop` | + +## Usage Example + +### Application Configuration + +```bash +java -Dscijava.app.executable="/usr/local/bin/myapp" \ + -Dscijava.app.name="My Application" \ + -Dscijava.app.icon="/usr/share/icons/myapp.png" \ + -Dscijava.app.directory="/usr/local/share/myapp" \ + -jar myapp.jar +``` + +### LinkHandler Implementation + +```java +@Plugin(type = LinkHandler.class) +public class MyAppLinkHandler extends AbstractLinkHandler { + + @Override + public boolean supports(final URI uri) { + return "myapp".equals(uri.getScheme()); + } + + @Override + public void handle(final URI uri) { + String operation = Links.operation(uri); + Map params = Links.query(uri); + // ... handle URI ... + } + + @Override + public List getSchemes() { + return Arrays.asList("myapp"); + } +} +``` + +### User Workflow + +1. Launch application +2. LinkService automatically registers "myapp" scheme with OS +3. User clicks `myapp://action?param=value` in browser +4. OS launches application with URI as argument +5. LinkArgument parses URI and passes to LinkService +6. LinkService routes to MyAppLinkHandler +7. Handler processes the request + +## Design Rationale + +### Why No Preference Persistence? + +The OS is the source of truth for desktop integration state. Users can manually modify registrations (delete .desktop files, edit registry), so we always query the OS directly rather than trusting potentially stale preferences. + +### Why Platform Plugins? + +The plugin system provides: +- Automatic platform detection by PlatformService +- No hardcoded OS checks +- Easy addition of new platforms +- Platform-specific behavior without code coupling + +### Why Separate SchemeInstaller? + +LinkService needs to register schemes on application startup, before any UI is available. SchemeInstaller provides a non-UI API for OS registration, separate from the UI-facing DesktopIntegrationProvider used by OptionsDesktop. + +### Why DesktopFile Utility? + +Linux .desktop files are used by both LinuxPlatform (for desktop icon) and LinuxSchemeInstaller (for URI schemes). The DesktopFile utility avoids code duplication and provides a clean, instance-based API for .desktop file manipulation. + +## Testing + +### Current Test Coverage + +- Compilation with Java 11: ✅ +- Existing unit tests: ✅ (all pass) + +### Manual Testing Needed + +- [ ] Windows: Registry entries created correctly +- [ ] Windows: URI links launch application +- [ ] Linux: .desktop file created with correct content +- [ ] Linux: xdg-mime associations registered +- [ ] Linux: URI links launch application +- [ ] Linux: Desktop icon appears in application menu +- [ ] macOS: Read-only status reported correctly +- [ ] All platforms: OptionsDesktop UI displays correct state +- [ ] All platforms: Toggling features works as expected + +### Test Scenarios + +1. **Fresh Installation**: No existing .desktop file or registry entries +2. **Existing Registration**: Application already registered +3. **Manual Modification**: User deletes .desktop file or registry entry manually +4. **Multiple Schemes**: LinkHandler declares multiple schemes +5. **Permission Errors**: Insufficient permissions to write registry/files + +## Known Issues + +### Critical Issues + +1. **Hardcoded "fiji" scheme**: Prevents use by other applications + - Workaround: None; requires code fix + - Priority: High + +2. **Hardcoded OS checks**: Violates plugin architecture + - Workaround: None; requires code fix + - Priority: Medium + +### Minor Issues + +1. **No first launch dialog**: Users must discover OptionsDesktop manually + - Workaround: Application can show custom dialog + - Priority: Low + +2. **Single scheme assumption**: Platforms assume one scheme per app + - Workaround: Modify platform code to iterate schemes + - Priority: Medium + +3. **No file extension support**: Cannot register file types + - Workaround: None; feature not implemented + - Priority: Low + +## Next Steps + +See NEXT.md for detailed implementation plan, including: + +1. Remove hardcoded scheme names (Priority 1) +2. Add SchemeInstallerProvider interface (Priority 2) +3. Refactor DefaultLinkService#createInstaller() (Priority 3) +4. Implement first launch dialog (Optional) +5. Add file extension registration (Future) +6. Implement Windows desktop icon (Future) + +## Dependencies + +- **scijava-common**: Provides PlatformService, PluginService, AppService +- **Java 11+**: Required for java.awt.Desktop features +- **Platform-specific tools**: + - Windows: `reg` command (built-in) + - Linux: `xdg-mime`, `update-desktop-database` (part of xdg-utils) + - macOS: Info.plist (build-time configuration) + +## Backward Compatibility + +- **No API breaks**: All additions are new interfaces and implementations +- **Opt-in behavior**: Applications without LinkHandlers are unaffected +- **No scijava-common changes**: All platform extensions are in scijava-desktop + +## Summary + +The scijava-desktop component provides a solid foundation for desktop integration, with working implementations for URI scheme registration and desktop icon management on Linux and Windows. The main remaining work is to remove hardcoded elements (scheme names, OS checks) and implement planned features (file extensions, Windows icon, first launch dialog). + +The architecture is clean, plugin-based, and ready for production use once the hardcoded elements are addressed. From c3fb215d930d1d902e96238aefcc238010409553 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 4 Feb 2026 15:23:39 -0600 Subject: [PATCH 61/63] Generalize "fiji" references to "myapp" --- .../org/scijava/desktop/DesktopIntegrationProvider.java | 2 +- src/main/java/org/scijava/desktop/links/LinkHandler.java | 4 ++-- .../java/org/scijava/desktop/links/SchemeInstaller.java | 2 +- .../org/scijava/desktop/platform/linux/DesktopFile.java | 4 ++-- .../org/scijava/desktop/platform/linux/LinuxPlatform.java | 2 +- .../scijava/desktop/platform/windows/WindowsPlatform.java | 6 +++--- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/scijava/desktop/DesktopIntegrationProvider.java b/src/main/java/org/scijava/desktop/DesktopIntegrationProvider.java index 6127fe2..adb6d39 100644 --- a/src/main/java/org/scijava/desktop/DesktopIntegrationProvider.java +++ b/src/main/java/org/scijava/desktop/DesktopIntegrationProvider.java @@ -48,7 +48,7 @@ public interface DesktopIntegrationProvider { boolean isWebLinksToggleable(); /** - * Enables or disables URI scheme registration (e.g., {@code fiji://} links). + * Enables or disables URI scheme registration (e.g., {@code myapp://} links). *

* This operation only works if {@link #isWebLinksToggleable()} * returns true. Otherwise, calling this method may throw diff --git a/src/main/java/org/scijava/desktop/links/LinkHandler.java b/src/main/java/org/scijava/desktop/links/LinkHandler.java index 41808ac..b717771 100644 --- a/src/main/java/org/scijava/desktop/links/LinkHandler.java +++ b/src/main/java/org/scijava/desktop/links/LinkHandler.java @@ -51,8 +51,8 @@ public interface LinkHandler extends HandlerPlugin { /** * Gets the URI schemes that this handler supports. *

- * This method is used for registering URI schemes with the operating system. - * Handlers should return a list of scheme names (e.g., "fiji", "imagej") + * This method is used for registering URI schemes with the operating + * system. Handlers should return a list of scheme names (e.g., "myapp") * that they can handle. Return an empty list if the handler does not * require OS-level scheme registration. *

diff --git a/src/main/java/org/scijava/desktop/links/SchemeInstaller.java b/src/main/java/org/scijava/desktop/links/SchemeInstaller.java index ebd5576..00d20b1 100644 --- a/src/main/java/org/scijava/desktop/links/SchemeInstaller.java +++ b/src/main/java/org/scijava/desktop/links/SchemeInstaller.java @@ -52,7 +52,7 @@ public interface SchemeInstaller { /** * Installs a URI scheme handler in the operating system. * - * @param scheme The URI scheme to register (e.g., "fiji", "imagej") + * @param scheme The URI scheme to register (e.g., "myapp") * @param executablePath The absolute path to the executable to launch * @throws IOException if installation fails */ diff --git a/src/main/java/org/scijava/desktop/platform/linux/DesktopFile.java b/src/main/java/org/scijava/desktop/platform/linux/DesktopFile.java index 9b8104b..9c6006f 100644 --- a/src/main/java/org/scijava/desktop/platform/linux/DesktopFile.java +++ b/src/main/java/org/scijava/desktop/platform/linux/DesktopFile.java @@ -338,7 +338,7 @@ public void setCategories(final String categories) { /** * Checks if a MimeType entry contains a specific MIME type. * - * @param mimeType The MIME type to check (e.g., "x-scheme-handler/fiji") + * @param mimeType The MIME type to check (e.g., "x-scheme-handler/myapp") * @return true if the MimeType field contains this type */ public boolean hasMimeType(final String mimeType) { @@ -361,7 +361,7 @@ public boolean hasMimeType(final String mimeType) { * the new type if it's not already present. *

* - * @param mimeType The MIME type to add (e.g., "x-scheme-handler/fiji") + * @param mimeType The MIME type to add (e.g., "x-scheme-handler/myapp") */ public void addMimeType(final String mimeType) { if (hasMimeType(mimeType)) return; // Already present diff --git a/src/main/java/org/scijava/desktop/platform/linux/LinuxPlatform.java b/src/main/java/org/scijava/desktop/platform/linux/LinuxPlatform.java index 8fb8457..529320b 100644 --- a/src/main/java/org/scijava/desktop/platform/linux/LinuxPlatform.java +++ b/src/main/java/org/scijava/desktop/platform/linux/LinuxPlatform.java @@ -109,7 +109,7 @@ public void open(final URL url) throws IOException { public boolean isWebLinksEnabled() { try { final DesktopFile df = getOrCreateDesktopFile(); - return df.hasMimeType("x-scheme-handler/fiji"); + return df.hasMimeType("x-scheme-handler/myapp"); } catch (final IOException e) { if (log != null) { log.debug("Failed to check web links status", e); diff --git a/src/main/java/org/scijava/desktop/platform/windows/WindowsPlatform.java b/src/main/java/org/scijava/desktop/platform/windows/WindowsPlatform.java index 79aafc0..5a066b5 100644 --- a/src/main/java/org/scijava/desktop/platform/windows/WindowsPlatform.java +++ b/src/main/java/org/scijava/desktop/platform/windows/WindowsPlatform.java @@ -83,7 +83,7 @@ public void open(final URL url) throws IOException { @Override public boolean isWebLinksEnabled() { final WindowsSchemeInstaller installer = new WindowsSchemeInstaller(log); - return installer.isInstalled("fiji"); + return installer.isInstalled("myapp"); } @Override @@ -99,10 +99,10 @@ public void setWebLinksEnabled(final boolean enable) throws IOException { } if (enable) { - installer.install("fiji", executablePath); + installer.install("myapp", executablePath); } else { - installer.uninstall("fiji"); + installer.uninstall("myapp"); } } From 05ea44c4a47a98017c5392cbcbf77acfc088aa61 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 4 Feb 2026 15:59:44 -0600 Subject: [PATCH 62/63] Complete phase 1: fix hardcoded elements 1. Remove Hardcoded Scheme Names - WindowsPlatform: Now queries LinkService for schemes - LinuxPlatform: Now queries LinkService for schemes - Both platforms now: - Inject LinkService as a parameter - Implement collectSchemes() helper method - Check/install/uninstall ALL schemes dynamically 2. Add getSchemeInstaller method - New method: DesktopIntegrationProvider#getSchemeInstaller - Allows platforms to provide SchemeInstaller instances - All three platforms now implement this method: - WindowsPlatform: Returns WindowsSchemeInstaller - LinuxPlatform: Returns LinuxSchemeInstaller - MacOSPlatform: Returns null (Info.plist only) 3. Refactor DefaultLinkService#createInstaller() - Removed: Hardcoded OS name string checks - Added: Uses PlatformService.getTargetPlatforms() to find platform - Result: Plugin-based architecture, no OS-specific code in LinkService Co-authored-by: Claude Sonnet 4.5 --- NEXT.md | 117 ++++++++++++------ spec/DESKTOP_INTEGRATION_PLAN.md | 4 +- spec/IMPLEMENTATION_SUMMARY.md | 6 +- .../desktop/DesktopIntegrationProvider.java | 9 ++ .../desktop/links/DefaultLinkService.java | 30 +++-- .../desktop/platform/linux/LinuxPlatform.java | 53 ++++++-- .../desktop/platform/macos/MacOSPlatform.java | 7 ++ .../platform/windows/WindowsPlatform.java | 62 +++++++++- 8 files changed, 221 insertions(+), 67 deletions(-) diff --git a/NEXT.md b/NEXT.md index c1eb16d..ccf62ac 100644 --- a/NEXT.md +++ b/NEXT.md @@ -29,7 +29,19 @@ The component uses a plugin system where Platform implementations (LinuxPlatform - File extension registration - Windows Start Menu icon generation - First launch dialog for desktop integration opt-in -- SchemeInstallerProvider interface for platforms + +## Implementation Phases + +### Phase 1: Fix Hardcoded Elements (High Priority) +These fixes are required for scijava-desktop to work for applications other than Fiji. + +### Phase 2: File Extension Registration (High Priority) +Core functionality for Fiji and other scientific applications. + +### Phase 3: Polish (Medium Priority) +First launch dialog, Windows desktop icon, etc. + +--- ## Priority Work Items @@ -89,36 +101,16 @@ private Set collectSchemes() { Similar changes needed for LinuxPlatform. -### 2. Add SchemeInstallerProvider Interface +### 2. Add getSchemeInstaller method **Goal**: Allow platforms to provide SchemeInstaller instances without hardcoding in DefaultLinkService. -**New file**: `src/main/java/org/scijava/desktop/links/SchemeInstallerProvider.java` - -```java -package org.scijava.desktop.links; - -/** - * Interface for platforms that provide {@link SchemeInstaller} functionality. - *

- * Platform implementations can implement this interface to provide - * platform-specific URI scheme installation capabilities. - *

- */ -public interface SchemeInstallerProvider { - /** - * Creates a SchemeInstaller for this platform. - * - * @return a SchemeInstaller, or null if not supported - */ - SchemeInstaller getSchemeInstaller(); -} -``` +**New method**: `DesktopIntegrationProvider#getSchemeInstaller()` **Files to modify**: -- WindowsPlatform.java - implement SchemeInstallerProvider -- LinuxPlatform.java - implement SchemeInstallerProvider -- MacOSPlatform.java - implement SchemeInstallerProvider (return null) +- WindowsPlatform.java - implement getSchemeInstaller +- LinuxPlatform.java - implement getSchemeInstaller +- MacOSPlatform.java - implement getSchemeInstaller (return null) ### 3. Refactor DefaultLinkService#createInstaller() @@ -151,8 +143,8 @@ private SchemeInstaller createInstaller() { if (platformService == null) return null; final Platform platform = platformService.platform(); - if (platform instanceof SchemeInstallerProvider) { - return ((SchemeInstallerProvider) platform).getSchemeInstaller(); + if (platform instanceof DesktopIntegrationProvider) { + return ((DesktopIntegrationProvider) platform).getSchemeInstaller(); } return null; @@ -189,7 +181,7 @@ Either way: "To change these settings in the future, use Edit > Options > Deskto - If user selects "No", do nothing - Store user preference by writing to a local configuration file -- avoids showing dialog again -### 5. File Extension Registration (Future) +### 5. File Extension Registration (High Priority) **Scope**: Extend DesktopIntegrationProvider to support file type associations. @@ -201,9 +193,52 @@ void setFileExtensionsEnabled(boolean enable) throws IOException; ``` **Platform implementations**: -- **Linux**: Add MIME types to .desktop file (e.g., `image/tiff`, `application/x-imagej-macro`) -- **Windows**: Register file associations in Registry under `HKCU\Software\Classes\.` -- **macOS**: Declared in Info.plist (build-time, read-only) + +#### Linux (Complex - Custom MIME Types Required) + +**Problem**: Microscopy formats (.sdt, .czi, .nd2, .lif, etc.) lack standard MIME types. + +**Solution**: Register custom MIME types in `~/.local/share/mime/packages/fiji.xml` + +**Steps**: +1. Create extension → MIME type mapping + - Standard formats: use existing types (`image/tiff`, `image/png`) + - Microscopy formats: define custom types (`application/x-zeiss-czi`, `application/x-nikon-nd2`) +2. Generate `fiji.xml` with MIME type definitions +3. Install to `~/.local/share/mime/packages/` +4. Run `update-mime-database ~/.local/share/mime` +5. Add MIME types to .desktop file's `MimeType=` field + +**MIME type naming convention**: +- Use vendor-specific: `application/x-{vendor}-{format}` +- Examples: `application/x-zeiss-czi`, `application/x-becker-hickl-sdt` + +**Unregistration**: +- Remove MIME types from .desktop file (preserve URI schemes) +- Optionally delete `~/.local/share/mime/packages/fiji.xml` +- Run `update-mime-database` again + +#### Windows (Simple - SupportedTypes Only) + +**Solution**: Use `Applications\fiji.exe\SupportedTypes` registry approach + +**Steps**: +1. Create `HKCU\Software\Classes\Applications\fiji.exe\SupportedTypes` +2. Add each extension as a value: `.tif = ""` +3. All ~150-200 extensions in one registry location + +**Safety**: Deletion is safe - only removes our own `Applications\fiji.exe` tree + +**Unregistration**: +- Delete entire `HKCU\Software\Classes\Applications\fiji.exe` key + +#### macOS (Build-Time Only) + +**Solution**: Declare all extensions in Info.plist at build time + +**Format**: `CFBundleDocumentTypes` array with extension lists + +**No runtime action needed** - this is a packaging/build concern ### 6. Windows Start Menu Icon @@ -216,6 +251,7 @@ void setFileExtensionsEnabled(boolean enable) throws IOException; ## Testing Checklist +### Phase 1 (Hardcoded Elements) - [ ] Test URI scheme registration on Windows (registry manipulation) - [ ] Test URI scheme registration on Linux (.desktop file updates) - [ ] Test desktop icon installation on Linux @@ -225,6 +261,16 @@ void setFileExtensionsEnabled(boolean enable) throws IOException; - [ ] Test toggling features on/off via UI - [ ] Verify no hardcoded "fiji" references remain +### Phase 2 (File Extensions) +- [ ] Test Linux MIME type generation and installation +- [ ] Verify `update-mime-database` runs successfully +- [ ] Test file extension associations appear in file managers (Nautilus, Dolphin) +- [ ] Test Windows SupportedTypes registration +- [ ] Verify Fiji appears in "Open With" for all extensions +- [ ] Test unregistration (verify complete cleanup) +- [ ] Test with ~150-200 actual file extensions +- [ ] Verify no `application/octet-stream` claims + ## System Properties Reference - `scijava.app.executable` - Path to application executable (required for all platforms) @@ -243,7 +289,6 @@ void setFileExtensionsEnabled(boolean enable) throws IOException; ## Questions to Resolve -1. Should SchemeInstallerProvider be a separate interface or extend Platform? -2. How to handle partial scheme installation failures? -3. Should first launch dialog be mandatory or optional? -4. What file extensions should be supported by default? +1. How to handle partial scheme installation failures? +2. Should first launch dialog be mandatory or optional? +3. What file extensions should be supported by default? diff --git a/spec/DESKTOP_INTEGRATION_PLAN.md b/spec/DESKTOP_INTEGRATION_PLAN.md index 1841760..8095d70 100644 --- a/spec/DESKTOP_INTEGRATION_PLAN.md +++ b/spec/DESKTOP_INTEGRATION_PLAN.md @@ -172,7 +172,7 @@ Applications configure desktop integration via system properties: 2. **OS Checks**: DefaultLinkService:119-132 hardcodes OS name checks - Should use PlatformService to get active platform - - Should add SchemeInstallerProvider interface + - Should add getSchemeInstaller method - See NEXT.md Work Items #2 and #3 ### Missing Features @@ -256,7 +256,7 @@ java -Dscijava.app.executable="/path/to/myapp" \ - Remove hardcoded "fiji" scheme references - Remove hardcoded OS checks in DefaultLinkService -- Add SchemeInstallerProvider interface +- Add getSchemeInstaller method ### ❌ Not Implemented diff --git a/spec/IMPLEMENTATION_SUMMARY.md b/spec/IMPLEMENTATION_SUMMARY.md index c96b2d7..f070154 100644 --- a/spec/IMPLEMENTATION_SUMMARY.md +++ b/spec/IMPLEMENTATION_SUMMARY.md @@ -108,8 +108,8 @@ The component uses SciJava's plugin architecture to avoid hardcoded OS checks: **Impact**: Violates plugin architecture; makes adding new platforms difficult. **Fix Required**: -1. Add SchemeInstallerProvider interface -2. Platforms implement SchemeInstallerProvider +1. Add getSchemeInstaller method +2. Platforms implement getSchemeInstaller 3. DefaultLinkService queries PlatformService.platform() instead of checking OS name ### ❌ Not Yet Implemented @@ -345,7 +345,7 @@ Linux .desktop files are used by both LinuxPlatform (for desktop icon) and Linux See NEXT.md for detailed implementation plan, including: 1. Remove hardcoded scheme names (Priority 1) -2. Add SchemeInstallerProvider interface (Priority 2) +2. Add getSchemeInstaller method (Priority 2) 3. Refactor DefaultLinkService#createInstaller() (Priority 3) 4. Implement first launch dialog (Optional) 5. Add file extension registration (Future) diff --git a/src/main/java/org/scijava/desktop/DesktopIntegrationProvider.java b/src/main/java/org/scijava/desktop/DesktopIntegrationProvider.java index adb6d39..d47f414 100644 --- a/src/main/java/org/scijava/desktop/DesktopIntegrationProvider.java +++ b/src/main/java/org/scijava/desktop/DesktopIntegrationProvider.java @@ -31,6 +31,8 @@ import java.io.IOException; +import org.scijava.desktop.links.SchemeInstaller; + /** * Marker interface for platform implementations that provide desktop * integration features. @@ -79,4 +81,11 @@ public interface DesktopIntegrationProvider { * @throws UnsupportedOperationException if not supported on this platform */ void setDesktopIconPresent(final boolean install) throws IOException; + + /** + * Creates a SchemeInstaller for this platform. + * + * @return a SchemeInstaller, or null if not supported on this platform + */ + SchemeInstaller getSchemeInstaller(); } diff --git a/src/main/java/org/scijava/desktop/links/DefaultLinkService.java b/src/main/java/org/scijava/desktop/links/DefaultLinkService.java index 46f700b..51c6605 100644 --- a/src/main/java/org/scijava/desktop/links/DefaultLinkService.java +++ b/src/main/java/org/scijava/desktop/links/DefaultLinkService.java @@ -30,10 +30,11 @@ import org.scijava.event.ContextCreatedEvent; import org.scijava.event.EventHandler; +import org.scijava.desktop.DesktopIntegrationProvider; import org.scijava.desktop.links.SchemeInstaller; -import org.scijava.desktop.platform.linux.LinuxSchemeInstaller; -import org.scijava.desktop.platform.windows.WindowsSchemeInstaller; import org.scijava.log.LogService; +import org.scijava.platform.Platform; +import org.scijava.platform.PlatformService; import org.scijava.plugin.AbstractHandlerService; import org.scijava.plugin.Parameter; import org.scijava.plugin.Plugin; @@ -55,6 +56,9 @@ public class DefaultLinkService extends AbstractHandlerService @Parameter(required = false) private LogService log; + @Parameter(required = false) + private PlatformService platformService; + @EventHandler private void onEvent(final ContextCreatedEvent evt) { // Register URI handler with the desktop system, if possible. @@ -112,23 +116,23 @@ private void installSchemes() { /** * Creates the appropriate {@link SchemeInstaller} for the current platform. *

- * Windows and Linux are supported via runtime registration. macOS uses Info.plist - * in the .app bundle (configured at build time, not at runtime). + * Uses the platform plugin system to obtain the installer, avoiding + * hardcoded OS checks. Windows and Linux platforms provide runtime registration. + * macOS uses Info.plist in the .app bundle (configured at build time). *

*/ private SchemeInstaller createInstaller() { - final String os = System.getProperty("os.name"); - if (os == null) return null; + if (platformService == null) return null; - final String osLower = os.toLowerCase(); - if (osLower.contains("linux")) { - return new LinuxSchemeInstaller(log); - } - else if (osLower.contains("win")) { - return new WindowsSchemeInstaller(log); + // Find a platform that provides a SchemeInstaller + for (final Platform platform : platformService.getTargetPlatforms()) { + if (platform instanceof DesktopIntegrationProvider) { + final SchemeInstaller installer = ((DesktopIntegrationProvider) platform).getSchemeInstaller(); + if (installer != null) return installer; + } } - return null; // macOS or other unsupported platforms + return null; } /** diff --git a/src/main/java/org/scijava/desktop/platform/linux/LinuxPlatform.java b/src/main/java/org/scijava/desktop/platform/linux/LinuxPlatform.java index 529320b..3d92334 100644 --- a/src/main/java/org/scijava/desktop/platform/linux/LinuxPlatform.java +++ b/src/main/java/org/scijava/desktop/platform/linux/LinuxPlatform.java @@ -31,7 +31,9 @@ import org.scijava.app.AppService; import org.scijava.desktop.DesktopIntegrationProvider; +import org.scijava.desktop.links.LinkHandler; import org.scijava.desktop.links.LinkService; +import org.scijava.desktop.links.SchemeInstaller; import org.scijava.log.LogService; import org.scijava.platform.AbstractPlatform; import org.scijava.platform.Platform; @@ -44,6 +46,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.HashSet; +import java.util.List; +import java.util.Set; /** * A platform implementation for handling Linux platform issues. @@ -109,7 +114,14 @@ public void open(final URL url) throws IOException { public boolean isWebLinksEnabled() { try { final DesktopFile df = getOrCreateDesktopFile(); - return df.hasMimeType("x-scheme-handler/myapp"); + final Set schemes = collectSchemes(); + if (schemes.isEmpty()) return false; + + // Check if any scheme is registered + for (final String scheme : schemes) { + if (df.hasMimeType("x-scheme-handler/" + scheme)) return true; + } + return false; } catch (final IOException e) { if (log != null) { log.debug("Failed to check web links status", e); @@ -124,13 +136,17 @@ public boolean isWebLinksEnabled() { @Override public void setWebLinksEnabled(final boolean enable) throws IOException { final DesktopFile df = getOrCreateDesktopFile(); - - if (enable) { - df.addMimeType("x-scheme-handler/fiji"); - } else { - df.removeMimeType("x-scheme-handler/fiji"); + + final Set schemes = collectSchemes(); + for (final String scheme : schemes) { + final String mimeType = "x-scheme-handler/" + scheme; + if (enable) { + df.addMimeType(mimeType); + } else { + df.removeMimeType(mimeType); + } } - + df.save(); } @@ -195,6 +211,11 @@ public void setDesktopIconPresent(final boolean install) throws IOException { } } + @Override + public SchemeInstaller getSchemeInstaller() { + return new LinuxSchemeInstaller(log); + } + // -- Helper methods -- /** @@ -296,4 +317,22 @@ private boolean isDesktopFileUpToDate(final Path desktopFile) { private String sanitizeFileName(final String name) { return name.replaceAll("[^a-zA-Z0-9._-]", "-").toLowerCase(); } + + // -- Helper methods -- + + /** + * Collects all URI schemes from registered LinkHandler plugins. + */ + private Set collectSchemes() { + final Set schemes = new HashSet<>(); + if (linkService == null) return schemes; + + for (final LinkHandler handler : linkService.getInstances()) { + final List handlerSchemes = handler.getSchemes(); + if (handlerSchemes != null) { + schemes.addAll(handlerSchemes); + } + } + return schemes; + } } diff --git a/src/main/java/org/scijava/desktop/platform/macos/MacOSPlatform.java b/src/main/java/org/scijava/desktop/platform/macos/MacOSPlatform.java index 870e2da..e85e56e 100644 --- a/src/main/java/org/scijava/desktop/platform/macos/MacOSPlatform.java +++ b/src/main/java/org/scijava/desktop/platform/macos/MacOSPlatform.java @@ -40,6 +40,7 @@ import org.scijava.command.CommandInfo; import org.scijava.command.CommandService; import org.scijava.desktop.DesktopIntegrationProvider; +import org.scijava.desktop.links.SchemeInstaller; import org.scijava.display.event.window.WinActivatedEvent; import org.scijava.event.EventHandler; import org.scijava.event.EventService; @@ -158,6 +159,12 @@ public void setDesktopIconPresent(final boolean install) { // Desktop icon installation is not supported on macOS (use Dock pinning instead). } + @Override + public SchemeInstaller getSchemeInstaller() { + // macOS uses Info.plist for URI scheme registration (build-time only) + return null; + } + // -- Disposable methods -- @Override diff --git a/src/main/java/org/scijava/desktop/platform/windows/WindowsPlatform.java b/src/main/java/org/scijava/desktop/platform/windows/WindowsPlatform.java index 5a066b5..aed3997 100644 --- a/src/main/java/org/scijava/desktop/platform/windows/WindowsPlatform.java +++ b/src/main/java/org/scijava/desktop/platform/windows/WindowsPlatform.java @@ -31,8 +31,14 @@ import java.io.IOException; import java.net.URL; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import org.scijava.desktop.DesktopIntegrationProvider; +import org.scijava.desktop.links.LinkHandler; +import org.scijava.desktop.links.LinkService; +import org.scijava.desktop.links.SchemeInstaller; import org.scijava.log.LogService; import org.scijava.platform.AbstractPlatform; import org.scijava.platform.Platform; @@ -52,6 +58,9 @@ public class WindowsPlatform extends AbstractPlatform @Parameter(required = false) private LogService log; + @Parameter(required = false) + private LinkService linkService; + // -- Platform methods -- @Override @@ -83,7 +92,14 @@ public void open(final URL url) throws IOException { @Override public boolean isWebLinksEnabled() { final WindowsSchemeInstaller installer = new WindowsSchemeInstaller(log); - return installer.isInstalled("myapp"); + final Set schemes = collectSchemes(); + if (schemes.isEmpty()) return false; + + // Check if any scheme is installed + for (final String scheme : schemes) { + if (installer.isInstalled(scheme)) return true; + } + return false; } @Override @@ -98,11 +114,22 @@ public void setWebLinksEnabled(final boolean enable) throws IOException { throw new IOException("No executable path set (scijava.app.executable property)"); } - if (enable) { - installer.install("myapp", executablePath); - } - else { - installer.uninstall("myapp"); + final Set schemes = collectSchemes(); + for (final String scheme : schemes) { + try { + if (enable) { + installer.install(scheme, executablePath); + } + else { + installer.uninstall(scheme); + } + } + catch (final Exception e) { + if (log != null) { + log.error("Failed to " + (enable ? "install" : "uninstall") + + " URI scheme: " + scheme, e); + } + } } } @@ -117,4 +144,27 @@ public void setDesktopIconPresent(final boolean install) { // Note: Operation has no effect here. // Desktop icon installation is not supported on Windows (add to Start menu manually). } + + @Override + public SchemeInstaller getSchemeInstaller() { + return new WindowsSchemeInstaller(log); + } + + // -- Helper methods -- + + /** + * Collects all URI schemes from registered LinkHandler plugins. + */ + private Set collectSchemes() { + final Set schemes = new HashSet<>(); + if (linkService == null) return schemes; + + for (final LinkHandler handler : linkService.getInstances()) { + final List handlerSchemes = handler.getSchemes(); + if (handlerSchemes != null) { + schemes.addAll(handlerSchemes); + } + } + return schemes; + } } From 53d9833b33453dc25a07c5f5a85b21e9031e5da4 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 4 Feb 2026 19:33:21 -0600 Subject: [PATCH 63/63] Begin work on file extension registration 1. Extend DesktopIntegrationProvider Interface Added three new methods to DesktopIntegrationProvider: - boolean isFileExtensionsEnabled() - Check registered file associations - boolean isFileExtensionsToggleable() - Check if platform can toggle - void setFileExtensionsEnabled(boolean) - Toggle file associations 2. macOS Implementation (Read-Only) - Returns true for isFileExtensionsEnabled() (declared in Info.plist) - Returns false for isFileExtensionsToggleable() (immutable bundle) - setFileExtensionsEnabled() is a no-op with documentation 3. Windows Implementation (SupportedTypes Approach) - Register: HKCU\Software\Classes\Applications\.exe\SupportedTypes - Safety: Create/delete our own registry tree (no over-deletion risk) - Extension Collection: Add collectFileExtensions() method stub - Helper Methods: getExecutableName(), execRegistryCommand() - Makes app appear in "Open With" for all registered extensions 4. Linux Implementation (MIME Types + .desktop file) - Standard formats: Use existing MIME types (e.g. image/png) - Custom formats: Create ~/.local/share/mime/packages/.xml for PFFs - Run update-mime-database to register custom MIME types - Add MIME types to .desktop file's MimeType= field - Preserve URI scheme handlers when unregistering file extensions 5. Update OptionsDesktop UI - Added "Enable file type associations" checkbox - Integrated into load/run/validate flow - Automatically grays out if not toggleable on current platform Key Design Decisions: 1. Single checkbox enables/disables ALL file extensions 2. Windows safety: Uses Applications\SupportedTypes (separate tree) 3. Linux custom MIME types: Register formats that lack standard types 4. Extension source: Currently placeholder - will use IOPlugin later Co-authored-by: Claude Sonnet 4.5 --- .../desktop/DesktopIntegrationProvider.java | 23 ++ .../desktop/options/OptionsDesktop.java | 15 ++ .../desktop/platform/linux/LinuxPlatform.java | 203 ++++++++++++++++++ .../desktop/platform/macos/MacOSPlatform.java | 18 ++ .../platform/windows/WindowsPlatform.java | 142 ++++++++++++ 5 files changed, 401 insertions(+) diff --git a/src/main/java/org/scijava/desktop/DesktopIntegrationProvider.java b/src/main/java/org/scijava/desktop/DesktopIntegrationProvider.java index d47f414..19cf36a 100644 --- a/src/main/java/org/scijava/desktop/DesktopIntegrationProvider.java +++ b/src/main/java/org/scijava/desktop/DesktopIntegrationProvider.java @@ -82,6 +82,29 @@ public interface DesktopIntegrationProvider { */ void setDesktopIconPresent(final boolean install) throws IOException; + boolean isFileExtensionsEnabled(); + + boolean isFileExtensionsToggleable(); + + /** + * Enables or disables file extension associations (e.g., {@code .tif}, {@code .png}). + *

+ * This operation only works if {@link #isFileExtensionsToggleable()} + * returns true. Otherwise, calling this method may throw + * {@link UnsupportedOperationException}. + *

+ *

+ * When enabled, the application will be registered as a handler for all + * supported file extensions. The application appears in "Open With" menus, + * allowing users to choose it for specific file types. + *

+ * + * @param enable whether to enable or disable file extension associations + * @throws IOException if the operation fails + * @throws UnsupportedOperationException if not supported on this platform + */ + void setFileExtensionsEnabled(final boolean enable) throws IOException; + /** * Creates a SchemeInstaller for this platform. * diff --git a/src/main/java/org/scijava/desktop/options/OptionsDesktop.java b/src/main/java/org/scijava/desktop/options/OptionsDesktop.java index 7423beb..0a3fd84 100644 --- a/src/main/java/org/scijava/desktop/options/OptionsDesktop.java +++ b/src/main/java/org/scijava/desktop/options/OptionsDesktop.java @@ -71,16 +71,22 @@ public class OptionsDesktop extends OptionsPlugin { description = "Install application icon in the system menu") private boolean desktopIconPresent; + @Parameter(label = "Enable file type associations", persist = false, validater = "validateFileExtensions", // + description = "Register supported file extensions with the operating system") + private boolean fileExtensionsEnabled; + @Override public void load() { webLinksEnabled = true; desktopIconPresent = true; + fileExtensionsEnabled = true; for (final Platform platform : platformService.getTargetPlatforms()) { if (!(platform instanceof DesktopIntegrationProvider)) continue; final DesktopIntegrationProvider dip = (DesktopIntegrationProvider) platform; // If any toggleable platform setting is off, uncheck that box. if (dip.isDesktopIconToggleable() && !dip.isDesktopIconPresent()) desktopIconPresent = false; if (dip.isWebLinksToggleable() && !dip.isWebLinksEnabled()) webLinksEnabled = false; + if (dip.isFileExtensionsToggleable() && !dip.isFileExtensionsEnabled()) fileExtensionsEnabled = false; } } @@ -92,6 +98,7 @@ public void run() { try { dip.setWebLinksEnabled(webLinksEnabled); dip.setDesktopIconPresent(desktopIconPresent); + dip.setFileExtensionsEnabled(fileExtensionsEnabled); } catch (final IOException e) { if (log != null) { @@ -120,6 +127,14 @@ public void validateDesktopIcon() { "Desktop icon presence"); } + public void validateFileExtensions() { + validateSetting( + DesktopIntegrationProvider::isFileExtensionsToggleable, + DesktopIntegrationProvider::isFileExtensionsEnabled, + fileExtensionsEnabled, + "File extensions setting"); + } + // -- Helper methods -- private String name(Platform platform) { diff --git a/src/main/java/org/scijava/desktop/platform/linux/LinuxPlatform.java b/src/main/java/org/scijava/desktop/platform/linux/LinuxPlatform.java index 3d92334..77b7e0c 100644 --- a/src/main/java/org/scijava/desktop/platform/linux/LinuxPlatform.java +++ b/src/main/java/org/scijava/desktop/platform/linux/LinuxPlatform.java @@ -42,12 +42,18 @@ import org.scijava.plugin.Plugin; import java.io.IOException; +import java.io.InputStream; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Properties; import java.util.Set; /** @@ -79,6 +85,9 @@ public class LinuxPlatform extends AbstractPlatform @Parameter(required = false) private LogService log; + /** Cached MIME type mapping */ + private static Map extensionToMime = null; + // -- Platform methods -- @Override @@ -211,6 +220,77 @@ public void setDesktopIconPresent(final boolean install) throws IOException { } } + @Override + public boolean isFileExtensionsEnabled() { + try { + final DesktopFile df = getOrCreateDesktopFile(); + final Map mimeMapping = loadMimeTypeMapping(); + + // Check if any file extension MIME types are in the .desktop file + for (final String mimeType : mimeMapping.values()) { + if (df.hasMimeType(mimeType)) return true; + } + return false; + } catch (final IOException e) { + if (log != null) { + log.debug("Failed to check file extensions status", e); + } + return false; + } + } + + @Override + public boolean isFileExtensionsToggleable() { + return true; + } + + @Override + public void setFileExtensionsEnabled(final boolean enable) throws IOException { + final Map mimeMapping = loadMimeTypeMapping(); + if (mimeMapping.isEmpty()) { + if (log != null) { + log.warn("No file extensions to register"); + } + return; + } + + if (enable) { + // Register custom MIME types for formats without standard types + registerCustomMimeTypes(mimeMapping); + + // Add MIME types to .desktop file + final DesktopFile df = getOrCreateDesktopFile(); + for (final String mimeType : mimeMapping.values()) { + df.addMimeType(mimeType); + } + df.save(); + + if (log != null) { + log.info("Registered " + mimeMapping.size() + " file extension MIME types"); + } + } else { + // Remove file extension MIME types from .desktop file + // Keep URI scheme handlers (x-scheme-handler/...) + final DesktopFile df = getOrCreateDesktopFile(); + final Set uriSchemes = collectSchemes(); + + for (final String mimeType : mimeMapping.values()) { + df.removeMimeType(mimeType); + } + + // Re-add URI scheme handlers + for (final String scheme : uriSchemes) { + df.addMimeType("x-scheme-handler/" + scheme); + } + + df.save(); + + if (log != null) { + log.info("Unregistered file extension MIME types"); + } + } + } + @Override public SchemeInstaller getSchemeInstaller() { return new LinuxSchemeInstaller(log); @@ -335,4 +415,127 @@ private Set collectSchemes() { } return schemes; } + + /** + * Loads the file extension to MIME type mapping. + */ + private synchronized Map loadMimeTypeMapping() throws IOException { + if (extensionToMime != null) return extensionToMime; + + extensionToMime = new LinkedHashMap<>(); + + // TODO: Query IOService for formats + + return extensionToMime; + } + + /** + * Registers custom MIME types for formats that don't have standard types. + * Creates ~/.local/share/mime/packages/[appName].xml and runs update-mime-database. + */ + private void registerCustomMimeTypes(final Map mimeMapping) throws IOException { + // Separate standard from custom MIME types + final Map customTypes = new LinkedHashMap<>(); + for (final Map.Entry entry : mimeMapping.entrySet()) { + final String mimeType = entry.getValue(); + // Custom types use application/x- prefix + if (mimeType.startsWith("application/x-")) { + customTypes.put(entry.getKey(), mimeType); + } + } + + if (customTypes.isEmpty()) { + // No custom types to register + return; + } + + // Generate MIME types XML + final String appName = System.getProperty("scijava.app.name", "SciJava"); + final String mimeXml = generateMimeTypesXml(customTypes, appName); + + // Write to ~/.local/share/mime/packages/.xml + final Path mimeDir = Paths.get(System.getProperty("user.home"), + ".local/share/mime/packages"); + Files.createDirectories(mimeDir); + + final Path mimeFile = mimeDir.resolve(sanitizeFileName(appName) + ".xml"); + Files.writeString(mimeFile, mimeXml, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING); + + // Update MIME database + try { + final ProcessBuilder pb = new ProcessBuilder( + "update-mime-database", + Paths.get(System.getProperty("user.home"), ".local/share/mime").toString() + ); + final Process process = pb.start(); + final int exitCode = process.waitFor(); + + if (exitCode != 0) { + if (log != null) { + log.warn("update-mime-database exited with code " + exitCode); + } + } else if (log != null) { + log.info("Registered " + customTypes.size() + " custom MIME types"); + } + } catch (final Exception e) { + if (log != null) { + log.error("Failed to run update-mime-database", e); + } + } + } + + /** + * Generates MIME types XML for custom file formats. + */ + private String generateMimeTypesXml(final Map customTypes, + final String appName) + { + final StringBuilder xml = new StringBuilder(); + xml.append("\n"); + xml.append("\n"); + + for (final Map.Entry entry : customTypes.entrySet()) { + final String extension = entry.getKey(); + final String mimeType = entry.getValue(); + + // Generate human-readable comment from MIME type + final String comment = generateMimeTypeComment(mimeType); + + xml.append(" \n"); + xml.append(" ").append(comment).append("\n"); + xml.append(" \n"); + xml.append(" \n"); + } + + xml.append("\n"); + return xml.toString(); + } + + /** + * Generates a human-readable comment from a MIME type. + * For example, "application/x-zeiss-czi" becomes "Zeiss CZI File". + */ + private String generateMimeTypeComment(final String mimeType) { + // Extract the format part (e.g., "zeiss-czi" from "application/x-zeiss-czi") + final String format = mimeType.substring(mimeType.lastIndexOf('/') + 1); + + // Remove "x-" prefix if present + final String cleanFormat = format.startsWith("x-") ? + format.substring(2) : format; + + // Convert to title case + final String[] parts = cleanFormat.split("-"); + final StringBuilder comment = new StringBuilder(); + for (final String part : parts) { + if (comment.length() > 0) comment.append(' '); + comment.append(Character.toUpperCase(part.charAt(0))); + if (part.length() > 1) { + comment.append(part.substring(1)); + } + } + comment.append(" File"); + + return comment.toString(); + } } diff --git a/src/main/java/org/scijava/desktop/platform/macos/MacOSPlatform.java b/src/main/java/org/scijava/desktop/platform/macos/MacOSPlatform.java index e85e56e..935d4d0 100644 --- a/src/main/java/org/scijava/desktop/platform/macos/MacOSPlatform.java +++ b/src/main/java/org/scijava/desktop/platform/macos/MacOSPlatform.java @@ -159,6 +159,24 @@ public void setDesktopIconPresent(final boolean install) { // Desktop icon installation is not supported on macOS (use Dock pinning instead). } + @Override + public boolean isFileExtensionsEnabled() { + // File extensions are declared in Info.plist, which is immutable. + return true; + } + + @Override + public boolean isFileExtensionsToggleable() { + // File extensions are declared in Info.plist, which is immutable. + return false; + } + + @Override + public void setFileExtensionsEnabled(final boolean enable) { + // Note: Operation has no effect here. + // File extension registration is immutable on macOS (configured in .app bundle). + } + @Override public SchemeInstaller getSchemeInstaller() { // macOS uses Info.plist for URI scheme registration (build-time only) diff --git a/src/main/java/org/scijava/desktop/platform/windows/WindowsPlatform.java b/src/main/java/org/scijava/desktop/platform/windows/WindowsPlatform.java index aed3997..ca2416b 100644 --- a/src/main/java/org/scijava/desktop/platform/windows/WindowsPlatform.java +++ b/src/main/java/org/scijava/desktop/platform/windows/WindowsPlatform.java @@ -145,6 +145,106 @@ public void setDesktopIconPresent(final boolean install) { // Desktop icon installation is not supported on Windows (add to Start menu manually). } + @Override + public boolean isFileExtensionsEnabled() { + // Check if any extensions are registered + final Set extensions = collectFileExtensions(); + if (extensions.isEmpty()) return false; + + // For simplicity, check if the Applications key exists + // A more thorough check would verify each extension + try { + final String executableName = getExecutableName(); + if (executableName == null) return false; + + final ProcessBuilder pb = new ProcessBuilder( + "reg", "query", + "HKCU\\Software\\Classes\\Applications\\" + executableName, + "/v", "FriendlyAppName" + ); + final Process process = pb.start(); + final int exitCode = process.waitFor(); + return exitCode == 0; + } catch (final Exception e) { + if (log != null) { + log.debug("Failed to check file extensions status", e); + } + return false; + } + } + + @Override + public boolean isFileExtensionsToggleable() { + return true; + } + + @Override + public void setFileExtensionsEnabled(final boolean enable) throws IOException { + final String executablePath = System.getProperty("scijava.app.executable"); + if (executablePath == null) { + throw new IOException("No executable path set (scijava.app.executable property)"); + } + + final String executableName = getExecutableName(); + if (executableName == null) { + throw new IOException("Could not determine executable name"); + } + + final Set extensions = collectFileExtensions(); + if (extensions.isEmpty()) { + if (log != null) { + log.warn("No file extensions to register"); + } + return; + } + + if (enable) { + // Register using Applications\SupportedTypes approach + try { + // Create Applications key + execRegistryCommand("add", + "HKCU\\Software\\Classes\\Applications\\" + executableName, + "/f"); + + // Set friendly name + final String appName = System.getProperty("scijava.app.name", "SciJava Application"); + execRegistryCommand("add", + "HKCU\\Software\\Classes\\Applications\\" + executableName, + "/v", "FriendlyAppName", + "/d", appName, + "/f"); + + // Add each extension to SupportedTypes + for (final String ext : extensions) { + execRegistryCommand("add", + "HKCU\\Software\\Classes\\Applications\\" + executableName + "\\SupportedTypes", + "/v", "." + ext, + "/d", "", + "/f"); + } + + if (log != null) { + log.info("Registered " + extensions.size() + " file extensions for " + appName); + } + } catch (final Exception e) { + throw new IOException("Failed to register file extensions", e); + } + } else { + // Unregister by deleting the Applications key + try { + execRegistryCommand("delete", + "HKCU\\Software\\Classes\\Applications\\" + executableName, + "/f"); + + if (log != null) { + log.info("Unregistered file extensions"); + } + } catch (final Exception e) { + throw new IOException("Failed to unregister file extensions", e); + } + } + } + @Override public SchemeInstaller getSchemeInstaller() { return new WindowsSchemeInstaller(log); @@ -167,4 +267,46 @@ private Set collectSchemes() { } return schemes; } + + /** + * Collects all supported file extensions. + *

+ * TODO: This should query file format plugins (e.g., SCIFIO formats, ImageJ I/O plugins). + * For now, returns an empty set. + *

+ */ + private Set collectFileExtensions() { + final Set extensions = new HashSet<>(); + // TODO: Query IOService/SCIFIOService/FormatService for supported extensions + // For now, return empty set + return extensions; + } + + /** + * Extracts the executable file name from the full path. + * For example, "C:\Path\To\fiji.exe" returns "fiji.exe". + */ + private String getExecutableName() { + final String executablePath = System.getProperty("scijava.app.executable"); + if (executablePath == null) return null; + + final int lastSlash = Math.max( + executablePath.lastIndexOf('/'), + executablePath.lastIndexOf('\\') + ); + return lastSlash >= 0 ? executablePath.substring(lastSlash + 1) : executablePath; + } + + /** + * Executes a registry command. + */ + private void execRegistryCommand(final String... args) throws IOException, InterruptedException { + final ProcessBuilder pb = new ProcessBuilder(args); + pb.command().add(0, "reg"); + final Process process = pb.start(); + final int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new IOException("Registry command failed with exit code " + exitCode); + } + } }