diff --git a/build.gradle b/build.gradle index cf9a9b8..23807b0 100644 --- a/build.gradle +++ b/build.gradle @@ -50,6 +50,9 @@ dependencies { // See https://docs.gradle.org/current/userguide/platforms.html shadow libs.slf4j + // For JavaFX + shadow libs.qupath.fxtras + // If you aren't using Groovy, this can be removed shadow libs.bundles.groovy diff --git a/src/main/java/qupath/ext/template/DemoExtension.java b/src/main/java/qupath/ext/template/DemoExtension.java index 07de097..42d5d58 100644 --- a/src/main/java/qupath/ext/template/DemoExtension.java +++ b/src/main/java/qupath/ext/template/DemoExtension.java @@ -1,16 +1,24 @@ package qupath.ext.template; import javafx.beans.property.BooleanProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.Property; +import javafx.scene.Scene; import javafx.scene.control.MenuItem; +import javafx.stage.Stage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import qupath.ext.template.ui.InterfaceController; +import qupath.fx.dialogs.Dialogs; +import qupath.fx.prefs.controlsfx.PropertyItemBuilder; import qupath.lib.common.Version; import qupath.lib.gui.QuPathGUI; -import qupath.lib.gui.dialogs.Dialogs; import qupath.lib.gui.extensions.GitHubProject; import qupath.lib.gui.extensions.QuPathExtension; import qupath.lib.gui.prefs.PathPrefs; +import java.io.IOException; + /** * This is a demo to provide a template for creating a new QuPath extension. @@ -63,11 +71,35 @@ public class DemoExtension implements QuPathExtension, GitHubProject { private boolean isInstalled = false; /** - * A 'persistent preference' - showing how to create a property that is stored whenever QuPath is closed + * A 'persistent preference' - showing how to create a property that is stored whenever QuPath is closed. + * This preference will be managed in the main QuPath GUI preferences window. */ - private BooleanProperty enableExtensionProperty = PathPrefs.createPersistentPreference( + private static BooleanProperty enableExtensionProperty = PathPrefs.createPersistentPreference( "enableExtension", true); + + /** + * Another 'persistent preference'. + * This one will be managed using a GUI element created by the extension. + * We use {@link Property} rather than {@link IntegerProperty} + * because of the type of GUI element we use to manage it. + */ + private static Property numThreadsProperty = PathPrefs.createPersistentPreference( + "demo.num.threads", 1).asObject(); + + /** + * An example of how to expose persistent preferences to other classes in your extension. + * @return The persistent preference, so that it can be read or set somewhere else. + */ + public static Property numThreadsProperty() { + return numThreadsProperty; + } + + /** + * Create a stage for the extension to display + */ + private Stage stage; + @Override public void installExtension(QuPathGUI qupath) { if (isInstalled) { @@ -76,12 +108,35 @@ public void installExtension(QuPathGUI qupath) { } isInstalled = true; addPreference(qupath); + addPreferenceToPane(qupath); addMenuItem(qupath); } /** * Demo showing how to add a persistent preference to the QuPath preferences pane. - * @param qupath + * The preference will be in a section of the preference pane based on the + * category you set. The description is used as a tooltip. + * @param qupath The currently running QuPathGUI instance. + */ + private void addPreferenceToPane(QuPathGUI qupath) { + var propertyItem = new PropertyItemBuilder<>(enableExtensionProperty, Boolean.class) + .name("Enable extension") + .category("Demo extension") + .description("Enable the demo extension") + .build(); + qupath.getPreferencePane() + .getPropertySheet() + .getItems() + .add(propertyItem); + } + + /** + * Demo showing how to add a persistent preference. + * This will be loaded whenever QuPath launches, with the value retained unless + * the preferences are reset. + * However, users will not be able to edit it unless you create a GUI + * element that corresponds with it + * @param qupath The currently running QuPathGUI instance. */ private void addPreference(QuPathGUI qupath) { qupath.getPreferencePane().addPropertyPreference( @@ -99,15 +154,28 @@ private void addPreference(QuPathGUI qupath) { private void addMenuItem(QuPathGUI qupath) { var menu = qupath.getMenu("Extensions>" + EXTENSION_NAME, true); MenuItem menuItem = new MenuItem("My menu item"); - menuItem.setOnAction(e -> { - Dialogs.showMessageDialog(EXTENSION_NAME, - "Hello! This is my Java extension."); - }); + menuItem.setOnAction(e -> createStage()); menuItem.disableProperty().bind(enableExtensionProperty.not()); menu.getItems().add(menuItem); } - - + + /** + * Demo showing how to create a new stage with a JavaFX FXML interface. + */ + private void createStage() { + if (stage == null) { + try { + stage = new Stage(); + Scene scene = new Scene(InterfaceController.createInstance()); + stage.setScene(scene); + } catch (IOException e) { + Dialogs.showErrorMessage("Extension Error", "GUI loading failed"); + logger.error("Unable to load extension interface FXML", e); + } + } + stage.show(); + } + @Override public String getName() { return EXTENSION_NAME; diff --git a/src/main/java/qupath/ext/template/ui/InterfaceController.java b/src/main/java/qupath/ext/template/ui/InterfaceController.java new file mode 100644 index 0000000..b0ab335 --- /dev/null +++ b/src/main/java/qupath/ext/template/ui/InterfaceController.java @@ -0,0 +1,55 @@ +package qupath.ext.template.ui; + +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.control.ChoiceBox; +import javafx.scene.control.Spinner; +import javafx.scene.control.TextField; +import javafx.scene.layout.VBox; +import qupath.ext.template.DemoExtension; +import qupath.fx.dialogs.Dialogs; + +import java.io.IOException; +import java.util.ResourceBundle; + +/** + * Controller for UI pane contained in interface.fxml + */ + +public class InterfaceController extends VBox { + private static final ResourceBundle resources = ResourceBundle.getBundle("qupath.ext.template.ui.strings"); + + @FXML + private Spinner threadSpinner; + + public static InterfaceController createInstance() throws IOException { + return new InterfaceController(); + } + + private InterfaceController() throws IOException { + var url = InterfaceController.class.getResource("interface.fxml"); + FXMLLoader loader = new FXMLLoader(url, resources); + loader.setRoot(this); + loader.setController(this); + loader.load(); + + // For extensions with a small number of options, + // or with options that are very important for how the extension works, + // it may be better to present them all to the user in the main extension GUI, + // binding them to GUI elements, so they are updated when the user interacts with + // the GUI, and so that the GUI elements are updated if the preference changes + threadSpinner.getValueFactory().valueProperty().bindBidirectional(DemoExtension.numThreadsProperty()); + threadSpinner.getValueFactory().valueProperty().addListener((observableValue, oldValue, newValue) -> { + Dialogs.showInfoNotification( + resources.getString("title"), + String.format(resources.getString("threads"), newValue)); + }); + } + + @FXML + private void runDemoExtension() { + System.out.println("Demo extension run"); + } + + +} diff --git a/src/main/resources/qupath/ext/template/ui/interface.fxml b/src/main/resources/qupath/ext/template/ui/interface.fxml new file mode 100644 index 0000000..69f6322 --- /dev/null +++ b/src/main/resources/qupath/ext/template/ui/interface.fxml @@ -0,0 +1,16 @@ + + + + + + + + + +