diff --git a/build.gradle b/build.gradle index e644da4..a774493 100644 --- a/build.gradle +++ b/build.gradle @@ -37,8 +37,8 @@ dependencies { // Main QuPath user interface jar. // Automatically includes other QuPath jars as subdependencies. implementation "io.github.qupath:qupath-gui-fx:${qupathVersion}" - implementation 'io.github.qupath:qupath-fxtras:0.1.0' - implementation 'org.commonmark:commonmark:0.21.0' + implementation libs.qupath.fxtras + implementation libs.bundles.markdown // For logging - the version comes from QuPath's version catalog at // https://github.com/qupath/qupath/blob/main/gradle/libs.versions.toml diff --git a/src/main/java/qupath/ext/wsinfer/models/WSInferModel.java b/src/main/java/qupath/ext/wsinfer/models/WSInferModel.java index e7239c8..10c3f5a 100644 --- a/src/main/java/qupath/ext/wsinfer/models/WSInferModel.java +++ b/src/main/java/qupath/ext/wsinfer/models/WSInferModel.java @@ -54,6 +54,14 @@ public String getName() { return hfRepoId; } + /** + * Get a description, if available (may be null). + * @return + */ + public String getDescription() { + return description; + } + /** * Get the configuration. Note that this may be null. * @return the model configuration, or null. @@ -69,8 +77,20 @@ public WSInferModelConfiguration getConfiguration() { * Remove the cached model files. */ public synchronized void removeCache() { - getTorchScriptFile().delete(); - getConfigFile().delete(); + removeIfFound(getTorchScriptFile(), getConfigFile(), getReadMeFile()); + } + + private static void removeIfFound(File... files) { + for (var file : files) { + if (file != null && file.isFile()) { + try { + logger.debug("Deleting file {}", file); + file.delete(); + } catch (Exception e) { + logger.error("Unable to delete file {}", file, e); + } + } + } } /** diff --git a/src/main/java/qupath/ext/wsinfer/ui/WSInferController.java b/src/main/java/qupath/ext/wsinfer/ui/WSInferController.java index c453cc0..3520ed9 100644 --- a/src/main/java/qupath/ext/wsinfer/ui/WSInferController.java +++ b/src/main/java/qupath/ext/wsinfer/ui/WSInferController.java @@ -42,6 +42,8 @@ import javafx.scene.web.WebView; import javafx.stage.Stage; import javafx.util.StringConverter; +import org.commonmark.ext.front.matter.YamlFrontMatterExtension; +import org.commonmark.ext.front.matter.YamlFrontMatterVisitor; import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; import org.controlsfx.control.PopOver; @@ -75,7 +77,7 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; -import java.nio.file.Path; +import java.util.Arrays; import java.util.Collection; import java.util.Objects; import java.util.ResourceBundle; @@ -127,8 +129,9 @@ public class WSInferController { private TextField tfModelDirectory; @FXML private TextField localModelDirectory; - @FXML - private PopOver infoPopover; + + private WebView infoWebView = WebViews.create(true); + private PopOver infoPopover = new PopOver(infoWebView); private final static ResourceBundle resources = ResourceBundle.getBundle("qupath.ext.wsinfer.ui.strings"); @@ -187,11 +190,15 @@ private void configureModelChoices() { modelChoiceBox.getSelectionModel().selectedItemProperty().addListener( (v, o, n) -> { downloadButton.setDisable((n == null) || n.isValid()); - infoButton.setDisable((n == null) || (!n.isValid()) || n instanceof WSInferModelLocal); + infoButton.setDisable((n == null) || (!n.isValid()) || !checkFileExists(n.getReadMeFile())); infoPopover.hide(); }); } + private static boolean checkFileExists(File file) { + return file != null && file.isFile(); + } + private void configureAvailableDevices() { var available = PytorchManager.getAvailableDevices(); deviceChoices.getItems().setAll(available); @@ -393,14 +400,59 @@ public void showInfo() throws IOException { return; } WSInferModel model = modelChoiceBox.getSelectionModel().getSelectedItem(); - Path mdFile = model.getReadMeFile().toPath(); - var doc = Parser.builder().build().parse(Files.readString(mdFile)); - WebView webView = WebViews.create(true); - webView.getEngine().loadContent(HtmlRenderer.builder().build().render(doc)); - infoPopover.setContentNode(webView); - infoPopover.show(infoButton); + var file = model.getReadMeFile(); + if (!checkFileExists(file)) { + logger.warn("ReadMe file not available: {}", file); + return; + } + try { + var markdown = Files.readString(file.toPath()); + + // Parse the initial markdown only, to extract any YAML front matter + var parser = Parser.builder() + .extensions( + Arrays.asList(YamlFrontMatterExtension.create()) + ).build(); + var doc = parser.parse(markdown); + var visitor = new YamlFrontMatterVisitor(); + doc.accept(visitor); + var metadata = visitor.getData(); + + // If we have YAML metadata, remove from the start (since it renders weirdly) and append at the end + if (!metadata.isEmpty()) { + doc.getFirstChild().unlink(); + var sb = new StringBuilder(); + sb.append("----\n\n"); + sb.append("### Metadata\n\n"); + for (var entry : metadata.entrySet()) { + sb.append("\n* **").append(entry.getKey()).append("**: ").append(entry.getValue()); + } + doc.appendChild(parser.parse(sb.toString())); + } + + // If the markdown doesn't start with a title, pre-pending the model title & description (if available) + if (!markdown.startsWith("#")) { + var sb = new StringBuilder(); + sb.append("## ").append(model.getName()).append("\n\n"); + var description = model.getDescription(); + if (description != null && !description.isEmpty()) { + sb.append("_").append(description).append("_").append("\n\n"); + } + sb.append("----\n\n"); + doc.prependChild(parser.parse(sb.toString())); + } + + infoWebView.getEngine().loadContent( + HtmlRenderer.builder().build().render(doc)); + infoPopover.show(infoButton); + } catch (IOException e) { + logger.error("Error parsing readme file", e); + } } + + + @FXML private void selectAllAnnotations() { Commands.selectObjectsByClass(imageDataProperty.get(), PathAnnotationObject.class); diff --git a/src/main/resources/qupath/ext/wsinfer/ui/wsinfer_control.fxml b/src/main/resources/qupath/ext/wsinfer/ui/wsinfer_control.fxml index a4e09b1..461bc9d 100644 --- a/src/main/resources/qupath/ext/wsinfer/ui/wsinfer_control.fxml +++ b/src/main/resources/qupath/ext/wsinfer/ui/wsinfer_control.fxml @@ -19,7 +19,6 @@ - @@ -50,10 +49,6 @@ - - - -