Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 22 additions & 30 deletions src/main/java/qupath/ext/template/DemoExtension.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import qupath.lib.gui.prefs.PathPrefs;

import java.io.IOException;
import java.util.ResourceBundle;


/**
Expand All @@ -33,20 +34,25 @@
* </pre>
*/
public class DemoExtension implements QuPathExtension, GitHubProject {

// TODO: add and modify strings to this resource bundle as needed
/**
* A resource bundle containing all the text used by the extension. This may be useful for translation to other languages.
* Note that this is optional and you can define the text within the code and FXML files that you use.
*/
private static final ResourceBundle resources = ResourceBundle.getBundle("qupath.ext.template.ui.strings");
private static final Logger logger = LoggerFactory.getLogger(DemoExtension.class);

/**
* Display name for your extension
* TODO: define this
*/
private static final String EXTENSION_NAME = "My Java extension";
private static final String EXTENSION_NAME = resources.getString("name");

/**
* Short description, used under 'Extensions > Installed extensions'
* TODO: define this
*/
private static final String EXTENSION_DESCRIPTION = "This is just a demo to show how extensions work";
private static final String EXTENSION_DESCRIPTION = resources.getString("description");

/**
* QuPath version that the extension is designed to work with.
Expand Down Expand Up @@ -74,25 +80,24 @@ public class DemoExtension implements QuPathExtension, GitHubProject {
* 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 static BooleanProperty enableExtensionProperty = PathPrefs.createPersistentPreference(
private static final 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<Integer>} rather than {@link IntegerProperty}
* because of the type of GUI element we use to manage it.
*/
private static Property<Integer> numThreadsProperty = PathPrefs.createPersistentPreference(
"demo.num.threads", 1).asObject();
private static final Property<Integer> integerOption = PathPrefs.createPersistentPreference(
"demo.num.option", 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<Integer> numThreadsProperty() {
return numThreadsProperty;
public static Property<Integer> integerOptionProperty() {
return integerOption;
}

/**
Expand All @@ -107,7 +112,6 @@ public void installExtension(QuPathGUI qupath) {
return;
}
isInstalled = true;
addPreference(qupath);
addPreferenceToPane(qupath);
addMenuItem(qupath);
}
Expand All @@ -119,8 +123,8 @@ public void installExtension(QuPathGUI qupath) {
* @param qupath The currently running QuPathGUI instance.
*/
private void addPreferenceToPane(QuPathGUI qupath) {
var propertyItem = new PropertyItemBuilder<>(enableExtensionProperty, Boolean.class)
.name("Enable extension")
var propertyItem = new PropertyItemBuilder<>(enableExtensionProperty, Boolean.class)
.name(resources.getString("menu.enable"))
.category("Demo extension")
.description("Enable the demo extension")
.build();
Expand All @@ -130,26 +134,10 @@ private void addPreferenceToPane(QuPathGUI qupath) {
.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(
enableExtensionProperty,
Boolean.class,
"Enable my extension",
EXTENSION_NAME,
"Enable my extension");
}

/**
* Demo showing how a new command can be added to a QuPath menu.
* @param qupath
* @param qupath The QuPath GUI
*/
private void addMenuItem(QuPathGUI qupath) {
var menu = qupath.getMenu("Extensions>" + EXTENSION_NAME, true);
Expand All @@ -167,15 +155,19 @@ private void createStage() {
try {
stage = new Stage();
Scene scene = new Scene(InterfaceController.createInstance());
stage.initOwner(QuPathGUI.getInstance().getStage());
stage.setTitle(resources.getString("stage.title"));
stage.setScene(scene);
stage.setResizable(false);
} catch (IOException e) {
Dialogs.showErrorMessage("Extension Error", "GUI loading failed");
Dialogs.showErrorMessage(resources.getString("error"), resources.getString("error.gui-loading-failed"));
logger.error("Unable to load extension interface FXML", e);
}
}
stage.show();
}


@Override
public String getName() {
return EXTENSION_NAME;
Expand Down
24 changes: 17 additions & 7 deletions src/main/java/qupath/ext/template/ui/InterfaceController.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,29 @@
/**
* 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<Integer> threadSpinner;
private Spinner<Integer> integerOptionSpinner;

/**
* Create a new instance of the interface controller.
* @return a new instance of the interface controller
* @throws IOException
* @throws IOException If reading the extension FXML files fails.
*/
public static InterfaceController createInstance() throws IOException {
return new InterfaceController();
}

/**
* This method reads an FXML file. These are markup files containing the structure of a UI element.
* <p>
* Fields in this class tagged with <code>@FXML</code> correspond to UI elements, and methods tagged with <code>@FXML</code> are methods triggered by actions on the UI (e.g., mouse clicks).
* <p>
* We consider the use of FXML to be "best practice" for UI creation, as it separates logic from layout and enables easier use of CSS. However, it is not mandatory, and you could instead define the layout of the UI using code.
* @throws IOException If the FXML can't be read successfully.
*/
private InterfaceController() throws IOException {
var url = InterfaceController.class.getResource("interface.fxml");
FXMLLoader loader = new FXMLLoader(url, resources);
Expand All @@ -41,17 +48,20 @@ private InterfaceController() throws IOException {
// 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) -> {
integerOptionSpinner.getValueFactory().valueProperty().bindBidirectional(DemoExtension.integerOptionProperty());
integerOptionSpinner.getValueFactory().valueProperty().addListener((observableValue, oldValue, newValue) -> {
Dialogs.showInfoNotification(
resources.getString("title"),
String.format(resources.getString("threads"), newValue));
String.format(resources.getString("option.integer.option-set-to"), newValue));
});
}

@FXML
private void runDemoExtension() {
System.out.println("Demo extension run");
Dialogs.showInfoNotification(
resources.getString("run.title"),
resources.getString("run.message")
);
}


Expand Down
25 changes: 19 additions & 6 deletions src/main/resources/qupath/ext/template/ui/interface.fxml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,24 @@
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<?import javafx.geometry.Insets?>
<fx:root type="VBox" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/20" xmlns:fx="http://javafx.com/fxml/1">
<Button onAction="#runDemoExtension" text="Run"/>
<Spinner fx:id="threadSpinner" prefWidth="75.0">
<valueFactory>
<SpinnerValueFactory.IntegerSpinnerValueFactory max="96" min="1" />
</valueFactory>
</Spinner>
<VBox spacing="5" alignment="CENTER_RIGHT">
<padding>
<Insets topRightBottomLeft="3"/>
</padding>
<HBox spacing="2" alignment="CENTER_LEFT">
<Label text="%option.integer.text">
<Tooltip text="%option.integer.tooltip"/>
</Label>
<Spinner fx:id="integerOptionSpinner" prefWidth="75.0">
<valueFactory>
<SpinnerValueFactory.IntegerSpinnerValueFactory max="100" min="1" />
</valueFactory>
</Spinner>
</HBox>
<Button onAction="#runDemoExtension" text="%run.button.text">
<Tooltip text="%run.button.tooltip"/>
</Button>
</VBox>
</fx:root>
18 changes: 17 additions & 1 deletion src/main/resources/qupath/ext/template/ui/strings.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
title = Demo extension

threads = Threads set to %d
name = My Java extension
description = This is just a demo to show how extensions work
menu.enable = Enable my extension
menu.description = Click this buton to enable the demo extension

stage.title = Demo dialog
option.integer.text = An option:
option.integer.tooltip = A numeric option that you can set (it has no effect here).
option.integer.option-set-to = Option set to %d
run.button.text = Run
run.button.tooltip = An example of a button that causes the extension to do something.\
Here it does nothing.
run.title = Run completed!
run.message = You can inform users of the outcome here

error = Demo extension error
error.gui-loading-failed = GUI loading failed
Loading