diff --git a/.github/workflows/jpackage.yml b/.github/workflows/jpackage.yml index ae389bc..173edfd 100644 --- a/.github/workflows/jpackage.yml +++ b/.github/workflows/jpackage.yml @@ -19,10 +19,10 @@ jobs: - uses: actions/checkout@v4 - - name: Set up JDK 17 # TODO: check Java version + - name: Set up JDK 21 # TODO: check Java version uses: actions/setup-java@v4 with: - java-version: '17' + java-version: '21' distribution: 'temurin' - name: Validate Gradle wrapper diff --git a/README.md b/README.md index 9ccdf0b..038f18f 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,15 @@ This repo contains a template and instructions to help create a new extension for [QuPath](https://qupath.github.io). -It already contains two minimal extensions, so the first task is to make sure that they work. +It already contains two minimal extensions - one using Java, one using Groovy - so the first task is to make sure that they work. Then, it's a matter of customizing the code to make it more useful. -> There are two extensions to show that you can use either Java or Groovy. +> **Update!** +> For QuPath v0.6.0 this repo switched to use Kotlin DSL for Gradle build files - +> and also to use the [QuPath Gradle Plugin](https://github.com/qupath/qupath-gradle-plugin). +> +> The outcome is that the build files are _much_ simpler. + ## Build the extension @@ -33,39 +38,88 @@ QuPath. > If you don't do that, you'll need to drag *all* the extra dependences onto QuPath to install them as well. -## Set up in an IDE (optional) +## Configure the extension -During development, things are likely to be much easier if you work within an IDE. +Edit `settings.gradle.kts` to specify which version of QuPath your extension should be compatible with, e.g. -QuPath itself is developed using IntelliJ, and you can import the extension template there. +```kotlin +qupath { + version = "0.6.0-SNAPSHOT" +} +``` + +Edit `build.gradle.kts` to specify the details of your extension -However, for development and testing, it can help to import QuPath *and* the extension and have them in your IDE side-by-side. +```kotlin +qupathExtension { + name = "qupath-extension-template" + group = "io.github.qupath" + version = "0.1.0-SNAPSHOT" + description = "A simple QuPath extension" + automaticModule = "io.github.qupath.extension.template" +} +``` -In IntelliJ, you can do this in a few steps: -* Get QuPath's source code, as described at https://qupath.readthedocs.io/en/stable/docs/reference/building.html -* Store your extension code in a directory *beside* QuPath's code. So it should be located next to the `qupath` code directory. -* Import QuPath into IntelliJ as a Gradle project (you don't need to import the extension yet!) - * See https://www.jetbrains.com/help/idea/work-with-gradle-projects.html -* Within `qupath/settings.gradle` add the line `includeFlat 'your-extension-code-directory'` (updating the code directory as needed) -* Refresh the Gradle project in IntelliJ, and your extension code should appear -* Create a [Run configuration](https://www.jetbrains.com/help/idea/run-debug-configuration.html) in IntelliJ to launch QuPath. An example of how that looks is shown below: -QuPath run configuration in IntelliJ +## Run QuPath + the extension -Now when you run QuPath from IntelliJ, your extension should (hopefully) be found - there's no need to add it by drag & drop. +During development, your probably want to run QuPath easily with your extension installed for debugging. -## Customize the extension +### 0. Make sure you have Java installed +You'll need to install Java first. -There are a few fixed steps to customizing the extension, and then the main creative part where you add your own code. +At the time of writing, we use a Java 21 JDK downloaded from https://adoptium.net/ -### Update `settings.gradle` +> Java 21 is a 'Long Term Support' release - which is why we use it instead of the very latest version. + +### 1. Get QuPath's source code +You can find instructions at https://qupath.readthedocs.io/en/stable/docs/reference/building.html + +### 2. Create an `include-extra` file +Create a file called `include-extra` in the root directory of the QuPath source code (*not* the extension code!). + +Set the contents of this file to: +``` +[includeBuild] +/path/to/your/extension + +[dependencies] +extension-group:extension-name +``` +replacing the default lines where needed. + +For example, to build the extension with the names given above you'd use +``` +[includeBuild] +../qupath-extension-template -Open `settings.gradle` and check the comment lines flagged with `\\TODO`. -These point you towards parts you may well need to change. +[dependencies] +io.github.qupath:qupath-extension-template +``` + +### 3. Run QuPath +Run QuPath from the command line using +``` +gradlew run +``` +If all goes well, QuPath should launch and you can check the *Extensions* mention to confirm the extension is installed. + + +## Set up in an IDE (optional) + +During development, things are likely to be much easier if you work within an IDE. + +QuPath itself is developed using IntelliJ, and you can import the extension template there. + +The setup process is as above, and you'll need a a [Run configuration](https://www.jetbrains.com/help/idea/run-debug-configuration.html) +to call `gradlew run`. + + +## Customize the extension -### Update `build.gradle` +Now you're ready for the creative part. -Open `build.gradle` and follow a similar process to with `settings.gradle`, to update the bits flagged with `\\TODO`. +You can develop the extension using either Java or Groovy - the template includes examples of both. ### Create the extension Java or Groovy file(s) diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 23807b0..0000000 --- a/build.gradle +++ /dev/null @@ -1,170 +0,0 @@ -plugins { - // Main gradle plugin for building a Java library - id 'java-library' - // Support writing the extension in Groovy (remove this if you don't want to) - id 'groovy' - // To create a shadow/fat jar that bundle up all dependencies - id 'com.github.johnrengelman.shadow' version '8.1.1' - // Include this plugin to avoid downloading JavaCPP dependencies for all platforms - id 'org.bytedeco.gradle-javacpp-platform' - id 'org.openjfx.javafxplugin' version '0.1.0' -} - -// TODO: Change the module name -ext.moduleName = 'io.github.qupath.extension.template' - -// TODO: Define the extension name & version, and provide a short description -base { - archivesName = rootProject.name - version = '0.0.1-SNAPSHOT' - description = 'A simple QuPath extension template' -} - -// TODO: Specify the QuPath version, compatible with the extension. -// The default 'gradle.ext.qupathVersion' reads this from settings.gradle. -ext.qupathVersion = gradle.ext.qupathVersion - -// TODO: Specify the Java version compatible with the extension -// Should be Java 17 for QuPath v0.5.0 -ext.qupathJavaVersion = 17 - -/** - * Define dependencies. - * - Using 'shadow' indicates that they are already part of QuPath, so you don't need - * to include them in your extension. If creating a single 'shadow jar' containing your - * extension and all dependencies, these won't be added. - * - Using 'implementation' indicates that you need the dependency for the extension to work, - * and it isn't part of QuPath already. If you are creating a single 'shadow jar', the - * dependency should be bundled up in the extension. - * - Using 'testImplementation' indicates that the dependency is only needed for testing, - * but shouldn't be bundled up for use in the extension. - */ -dependencies { - - // Main QuPath user interface jar. - // Automatically includes other QuPath jars as subdependencies. - shadow "io.github.qupath:qupath-gui-fx:${qupathVersion}" - - // For logging - the version comes from QuPath's version catalog at - // https://github.com/qupath/qupath/blob/main/gradle/libs.versions.toml - // 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 - - testImplementation "io.github.qupath:qupath-gui-fx:${qupathVersion}" - testImplementation libs.junit -} - -/* - * Manifest info - */ -jar { - manifest { - attributes("Implementation-Title": project.name, - "Implementation-Version": archiveVersion, - "Automatic-Module-Name": moduleName) - } -} - -/** - * Copy necessary attributes, see - * - https://github.com/qupath/qupath-extension-template/issues/9 - * - https://github.com/openjfx/javafx-gradle-plugin#variants - */ -configurations.shadow { - def runtimeAttributes = configurations.runtimeClasspath.attributes - runtimeAttributes.keySet().each { key -> - if (key in [Usage.USAGE_ATTRIBUTE, OperatingSystemFamily.OPERATING_SYSTEM_ATTRIBUTE, MachineArchitecture.ARCHITECTURE_ATTRIBUTE]) - attributes.attribute(key, runtimeAttributes.getAttribute(key)) - } -} - -/* - * Copy the LICENSE file into the jar... if we have one (we should!) - */ -processResources { - from ("${projectDir}/LICENSE") { - into 'licenses/' - } -} - -/* - * Define extra 'copyDependencies' task to copy dependencies into the build directory. - */ -tasks.register("copyDependencies", Copy) { - description "Copy dependencies into the build directory for use elsewhere" - group "QuPath" - - from configurations.default - into 'build/libs' -} - -/* - * Ensure Java 17 compatibility, and include sources and javadocs when building. - */ -java { - toolchain { - languageVersion = JavaLanguageVersion.of(qupathJavaVersion) - } - withSourcesJar() - withJavadocJar() -} - -/* - * Create javadocs for all modules/packages in one place. - * Use -PstrictJavadoc=true to fail on error with doclint (which is rather strict). - */ -tasks.withType(Javadoc) { - options.encoding = 'UTF-8' - def strictJavadoc = findProperty('strictJavadoc') - if (!strictJavadoc) { - options.addStringOption('Xdoclint:none', '-quiet') - } -} - -/* - * Specify that the encoding should be UTF-8 for source files - */ -tasks.named('compileJava') { - options.encoding = 'UTF-8' -} - -/* - * Avoid 'Entry .gitkeep is a duplicate but no duplicate handling strategy has been set.' - * when using withSourcesJar() - */ -tasks.withType(org.gradle.jvm.tasks.Jar) { - duplicatesStrategy = DuplicatesStrategy.INCLUDE -} - -/* - * Support tests with JUnit. - */ -tasks.named('test') { - useJUnitPlatform() -} - -// Looks redundant to include this here and in settings.gradle, -// but helps overcome some gradle trouble when including this as a subproject -// within QuPath itself (which is useful during development). -repositories { - // Add this if you need access to dependencies only installed locally - // mavenLocal() - - mavenCentral() - - // Add scijava - which is where QuPath's jars are hosted - maven { - url "https://maven.scijava.org/content/repositories/releases" - } - - maven { - url "https://maven.scijava.org/content/repositories/snapshots" - } - -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..134683f --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + // Support writing the extension in Groovy (remove this if you don't want to) + groovy + // To optionally create a shadow/fat jar that bundle up any non-core dependencies + id("com.gradleup.shadow") version "8.3.5" + // QuPath Gradle extension convention plugin + id("qupath-conventions") +} + +// TODO: Configure your extension here (please change the defaults!) +qupathExtension { + name = "qupath-extension-template" + group = "io.github.qupath" + version = "0.1.0-SNAPSHOT" + description = "A simple QuPath extension" + automaticModule = "io.github.qupath.extension.template" +} + +// TODO: Define your dependencies here +dependencies { + + // Main dependencies for most QuPath extensions + shadow(libs.bundles.qupath) + shadow(libs.bundles.logging) + shadow(libs.qupath.fxtras) + + // If you aren't using Groovy, this can be removed + shadow(libs.bundles.groovy) + + // For testing + testImplementation(libs.bundles.qupath) + testImplementation(libs.junit) + +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd49..a4b76b9 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9355b41..cea7a79 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a4..f5feea6 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 6689b85..9b42019 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/qupath-intellij.png b/qupath-intellij.png deleted file mode 100644 index bcbd526..0000000 Binary files a/qupath-intellij.png and /dev/null differ diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index ed3fa38..0000000 --- a/settings.gradle +++ /dev/null @@ -1,42 +0,0 @@ -pluginManagement { - plugins { - // Gradle is awkward about declaring versions for plugins - // Specifying it here, rather than build.gradle, makes it possible - // to include the extension as a subproject of QuPath itself - // (which is useful during development) - id 'org.bytedeco.gradle-javacpp-platform' version '1.5.9' - } -} - -// TODO: Name your QuPath extension here! -rootProject.name = 'my-qupath-extension' - -// TODO: Define the QuPath version compatible with the extension -// Note that the QuPath API isn't stable; something designed for -// 0.X.a should work with 0.X.b, but not necessarily with 0.Y.a. -gradle.ext.qupathVersion = "0.5.0" - -dependencyResolutionManagement { - - // Access QuPath's version catalog for dependency versions - versionCatalogs { - libs { - from("io.github.qupath:qupath-catalog:${gradle.ext.qupathVersion}") - } - } - - repositories { - - mavenCentral() - - // Add scijava - which is where QuPath's jars are hosted - maven { - url "https://maven.scijava.org/content/repositories/releases" - } - - maven { - url "https://maven.scijava.org/content/repositories/snapshots" - } - - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..5c6ca66 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { + url = uri("https://maven.scijava.org/content/repositories/releases") + } + } +} + +// TODO: Specify which version of QuPath the extension is targeting here +qupath { + version = "0.5.1" +} + +// Apply QuPath Gradle settings plugin to handle configuration +plugins { + id("io.github.qupath.qupath-extension-settings") version "0.2.1" +} diff --git a/src/main/java/qupath/ext/template/ui/InterfaceController.java b/src/main/java/qupath/ext/template/ui/InterfaceController.java index b0ab335..ef919d6 100644 --- a/src/main/java/qupath/ext/template/ui/InterfaceController.java +++ b/src/main/java/qupath/ext/template/ui/InterfaceController.java @@ -2,9 +2,7 @@ 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; @@ -22,6 +20,11 @@ public class InterfaceController extends VBox { @FXML private Spinner threadSpinner; + /** + * Create a new instance of the interface controller. + * @return a new instance of the interface controller + * @throws IOException + */ public static InterfaceController createInstance() throws IOException { return new InterfaceController(); }