diff --git a/CHANGELOG.md b/CHANGELOG.md index 06711303e..a77fd95fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,6 @@ This is a work-in-progress for the next QuPath release. ### Enhancements * New toolbar buttons for script editor `` and log viewer * *File → Export snapshots* supports PNG, JPEG and TIFF (not just PNG) -* New preference to control whether the 'system menubar' is used globally, or just for the main window - * Makes a difference on macOS, doesn't on Windows... on Linux it depends ### Bugs fixed * Rendered image export does not support opacity (https://github.com/qupath/qupath/issues/1292) @@ -19,6 +17,7 @@ This is a work-in-progress for the next QuPath release. * PathIO doesn't restore backup if writing ImageData fails (https://github.com/qupath/qupath/issues/1252) * Scripts open with the caret at the bottom of the text rather than the top (https://github.com/qupath/qupath/issues/1258) * 'Synchronize viewers' ignores z and t positions (https://github.com/qupath/qupath/issues/1220) +* Menubars and shortcuts are confused when ImageJ is open but QuPath is in focus in macOS (https://github.com/qupath/qupath/issues/6) ### Dependency updates * Bio-Formats 7.0.0 diff --git a/qupath-core-processing/src/main/java/qupath/imagej/detect/cells/SubcellularDetection.java b/qupath-core-processing/src/main/java/qupath/imagej/detect/cells/SubcellularDetection.java index 330289ee9..179b7035c 100644 --- a/qupath-core-processing/src/main/java/qupath/imagej/detect/cells/SubcellularDetection.java +++ b/qupath-core-processing/src/main/java/qupath/imagej/detect/cells/SubcellularDetection.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2020 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -92,9 +92,9 @@ public class SubcellularDetection extends AbstractInteractivePlugin pluginRunner, final String arg) { - boolean success = super.runPlugin(pluginRunner, arg); - getHierarchy(pluginRunner).fireHierarchyChangedEvent(this); + public boolean runPlugin(final PluginRunner pluginRunner, final ImageData imageData, final String arg) { + boolean success = super.runPlugin(pluginRunner, imageData, arg); + imageData.getHierarchy().fireHierarchyChangedEvent(this); return success; } @@ -441,10 +441,10 @@ public String getDescription() { } @Override - protected Collection getParentObjects(final PluginRunner runner) { + protected Collection getParentObjects(final ImageData imageData) { Collection> parentClasses = getSupportedParentObjectClasses(); List parents = new ArrayList<>(); - for (PathObject parent : getHierarchy(runner).getSelectionModel().getSelectedObjects()) { + for (PathObject parent : imageData.getHierarchy().getSelectionModel().getSelectedObjects()) { for (Class cls : parentClasses) { if (cls.isAssignableFrom(parent.getClass())) { parents.add(parent); diff --git a/qupath-core-processing/src/main/java/qupath/imagej/detect/dearray/TMADearrayerPluginIJ.java b/qupath-core-processing/src/main/java/qupath/imagej/detect/dearray/TMADearrayerPluginIJ.java index a840518df..2368fa077 100644 --- a/qupath-core-processing/src/main/java/qupath/imagej/detect/dearray/TMADearrayerPluginIJ.java +++ b/qupath-core-processing/src/main/java/qupath/imagej/detect/dearray/TMADearrayerPluginIJ.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2020 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -55,7 +55,6 @@ import qupath.lib.plugins.AbstractInteractivePlugin; import qupath.lib.plugins.ObjectDetector; import qupath.lib.plugins.PathPlugin; -import qupath.lib.plugins.PluginRunner; import qupath.lib.plugins.parameters.ParameterList; import qupath.lib.plugins.workflow.SimplePluginWorkflowStep; import qupath.lib.plugins.workflow.WorkflowStep; @@ -377,8 +376,8 @@ public String getDescription() { @Override - protected Collection getParentObjects(final PluginRunner runner) { - return Collections.singletonList(runner.getImageData().getHierarchy().getRootObject()); + protected Collection getParentObjects(final ImageData imageData) { + return Collections.singletonList(imageData.getHierarchy().getRootObject()); } diff --git a/qupath-core-processing/src/main/java/qupath/imagej/detect/tissue/SimpleTissueDetection2.java b/qupath-core-processing/src/main/java/qupath/imagej/detect/tissue/SimpleTissueDetection2.java index 881a4cc90..d501a03f7 100644 --- a/qupath-core-processing/src/main/java/qupath/imagej/detect/tissue/SimpleTissueDetection2.java +++ b/qupath-core-processing/src/main/java/qupath/imagej/detect/tissue/SimpleTissueDetection2.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2020 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -63,7 +63,6 @@ import qupath.lib.plugins.AbstractDetectionPlugin; import qupath.lib.plugins.DetectionPluginTools; import qupath.lib.plugins.ObjectDetector; -import qupath.lib.plugins.PluginRunner; import qupath.lib.plugins.parameters.ParameterList; import qupath.lib.regions.ImagePlane; import qupath.lib.regions.RegionRequest; @@ -442,9 +441,9 @@ protected void addRunnableTasks(ImageData imageData, PathObject p @Override - protected Collection getParentObjects(final PluginRunner runner) { + protected Collection getParentObjects(final ImageData imageData) { - PathObjectHierarchy hierarchy = getHierarchy(runner); + PathObjectHierarchy hierarchy = imageData.getHierarchy(); if (hierarchy.getTMAGrid() == null) return Collections.singleton(hierarchy.getRootObject()); diff --git a/qupath-core-processing/src/main/java/qupath/imagej/superpixels/DoGSuperpixelsPlugin.java b/qupath-core-processing/src/main/java/qupath/imagej/superpixels/DoGSuperpixelsPlugin.java index 67c9ef8f5..420d70e72 100644 --- a/qupath-core-processing/src/main/java/qupath/imagej/superpixels/DoGSuperpixelsPlugin.java +++ b/qupath-core-processing/src/main/java/qupath/imagej/superpixels/DoGSuperpixelsPlugin.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2020 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -52,7 +52,6 @@ import qupath.lib.objects.PathObjects; import qupath.lib.plugins.AbstractTileableDetectionPlugin; import qupath.lib.plugins.ObjectDetector; -import qupath.lib.plugins.PluginRunner; import qupath.lib.plugins.parameters.ParameterList; import qupath.lib.regions.RegionRequest; import qupath.lib.roi.RoiTools; @@ -269,8 +268,8 @@ public String getDescription() { @Override - protected synchronized Collection getParentObjects(final PluginRunner runner) { - Collection parents = super.getParentObjects(runner); + protected synchronized Collection getParentObjects(final ImageData imageData) { + Collection parents = super.getParentObjects(imageData); return parents; // Exploring the use of hidden objects... diff --git a/qupath-core-processing/src/main/java/qupath/imagej/superpixels/SLICSuperpixelsPlugin.java b/qupath-core-processing/src/main/java/qupath/imagej/superpixels/SLICSuperpixelsPlugin.java index 126c758f9..56fd683ac 100644 --- a/qupath-core-processing/src/main/java/qupath/imagej/superpixels/SLICSuperpixelsPlugin.java +++ b/qupath-core-processing/src/main/java/qupath/imagej/superpixels/SLICSuperpixelsPlugin.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2020 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -52,7 +52,6 @@ import qupath.lib.objects.PathObjects; import qupath.lib.plugins.AbstractTileableDetectionPlugin; import qupath.lib.plugins.ObjectDetector; -import qupath.lib.plugins.PluginRunner; import qupath.lib.plugins.parameters.ParameterList; import qupath.lib.regions.RegionRequest; import qupath.lib.roi.RoiTools; @@ -549,8 +548,8 @@ public String getDescription() { @Override - protected synchronized Collection getParentObjects(final PluginRunner runner) { - Collection parents = super.getParentObjects(runner); + protected synchronized Collection getParentObjects(final ImageData imageData) { + Collection parents = super.getParentObjects(imageData); return parents; } diff --git a/qupath-core-processing/src/main/java/qupath/lib/algorithms/CoherenceFeaturePlugin.java b/qupath-core-processing/src/main/java/qupath/lib/algorithms/CoherenceFeaturePlugin.java index 74c9704f4..c01b9c3b1 100644 --- a/qupath-core-processing/src/main/java/qupath/lib/algorithms/CoherenceFeaturePlugin.java +++ b/qupath-core-processing/src/main/java/qupath/lib/algorithms/CoherenceFeaturePlugin.java @@ -352,8 +352,8 @@ public String getDescription() { } @Override - protected Collection getParentObjects(final PluginRunner runner) { - return runner.getImageData().getHierarchy().getDetectionObjects(); + protected Collection getParentObjects(final ImageData imageData) { + return imageData.getHierarchy().getDetectionObjects(); } @Override diff --git a/qupath-core-processing/src/main/java/qupath/lib/algorithms/HaralickFeaturesPlugin.java b/qupath-core-processing/src/main/java/qupath/lib/algorithms/HaralickFeaturesPlugin.java index b62fffdb6..e0e8dd907 100644 --- a/qupath-core-processing/src/main/java/qupath/lib/algorithms/HaralickFeaturesPlugin.java +++ b/qupath-core-processing/src/main/java/qupath/lib/algorithms/HaralickFeaturesPlugin.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2020 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -57,7 +57,6 @@ import qupath.lib.objects.PathObject; import qupath.lib.objects.TMACoreObject; import qupath.lib.plugins.AbstractInteractivePlugin; -import qupath.lib.plugins.PluginRunner; import qupath.lib.plugins.parameters.ParameterList; import qupath.lib.regions.RegionRequest; import qupath.lib.roi.interfaces.ROI; @@ -476,8 +475,8 @@ public String getDescription() { } @Override - protected Collection getParentObjects(final PluginRunner runner) { - return runner.getImageData().getHierarchy().getSelectionModel().getSelectedObjects(); + protected Collection getParentObjects(final ImageData imageData) { + return imageData.getHierarchy().getSelectionModel().getSelectedObjects(); // return runner.getImageData().getHierarchy().getSelectionModel().getSelectedObjects(); // return runner.getImageData().getHierarchy().getObjects(null, PathDetectionObject.class); } diff --git a/qupath-core-processing/src/main/java/qupath/lib/algorithms/IntensityFeaturesPlugin.java b/qupath-core-processing/src/main/java/qupath/lib/algorithms/IntensityFeaturesPlugin.java index 1b94354e4..bf981c0ea 100644 --- a/qupath-core-processing/src/main/java/qupath/lib/algorithms/IntensityFeaturesPlugin.java +++ b/qupath-core-processing/src/main/java/qupath/lib/algorithms/IntensityFeaturesPlugin.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2020 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -351,9 +351,9 @@ public float[] getTransformedPixels(final BufferedImage img, int[] buf, final Co @Override - public boolean runPlugin(final PluginRunner pluginRunner, final String arg) { - boolean success = super.runPlugin(pluginRunner, arg); - getHierarchy(pluginRunner).fireHierarchyChangedEvent(this); + public boolean runPlugin(final PluginRunner pluginRunner, final ImageData imageData, final String arg) { + boolean success = super.runPlugin(pluginRunner, imageData, arg); + imageData.getHierarchy().fireHierarchyChangedEvent(this); return success; } @@ -716,8 +716,8 @@ public String getDescription() { } @Override - protected Collection getParentObjects(final PluginRunner runner) { - return runner.getImageData().getHierarchy().getSelectionModel().getSelectedObjects(); + protected Collection getParentObjects(final ImageData imageData) { + return imageData.getHierarchy().getSelectionModel().getSelectedObjects(); } @Override diff --git a/qupath-core-processing/src/main/java/qupath/lib/algorithms/LocalBinaryPatternsPlugin.java b/qupath-core-processing/src/main/java/qupath/lib/algorithms/LocalBinaryPatternsPlugin.java index 331e725ab..7b050ce35 100644 --- a/qupath-core-processing/src/main/java/qupath/lib/algorithms/LocalBinaryPatternsPlugin.java +++ b/qupath-core-processing/src/main/java/qupath/lib/algorithms/LocalBinaryPatternsPlugin.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2020 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -50,7 +50,6 @@ import qupath.lib.objects.PathDetectionObject; import qupath.lib.objects.PathObject; import qupath.lib.plugins.AbstractInteractivePlugin; -import qupath.lib.plugins.PluginRunner; import qupath.lib.plugins.parameters.ParameterList; import qupath.lib.regions.RegionRequest; import qupath.lib.roi.interfaces.ROI; @@ -310,8 +309,8 @@ public String getDescription() { } @Override - protected Collection getParentObjects(final PluginRunner runner) { - return runner.getImageData().getHierarchy().getDetectionObjects(); + protected Collection getParentObjects(final ImageData imageData) { + return imageData.getHierarchy().getDetectionObjects(); } @Override diff --git a/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/DilateAnnotationPlugin.java b/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/DilateAnnotationPlugin.java index 14f976725..cdcac7a3b 100644 --- a/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/DilateAnnotationPlugin.java +++ b/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/DilateAnnotationPlugin.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2020 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -44,7 +44,6 @@ import qupath.lib.objects.PathObjects; import qupath.lib.objects.hierarchy.PathObjectHierarchy; import qupath.lib.plugins.AbstractInteractivePlugin; -import qupath.lib.plugins.PluginRunner; import qupath.lib.plugins.parameters.ParameterList; import qupath.lib.regions.ImagePlane; import qupath.lib.roi.GeometryTools; @@ -135,24 +134,24 @@ public ParameterList getDefaultParameterList(ImageData imageData) { } @Override - protected Collection getParentObjects(PluginRunner runner) { - return getHierarchy(runner).getSelectionModel().getSelectedObjects().stream().filter(p -> p.isAnnotation()).toList(); + protected Collection getParentObjects(ImageData imageData) { + return imageData.getHierarchy().getSelectionModel().getSelectedObjects().stream().filter(p -> p.isAnnotation()).toList(); } @Override protected void addRunnableTasks(ImageData imageData, PathObject parentObject, List tasks) {} @Override - protected Collection getTasks(final PluginRunner runner) { - Collection parentObjects = getParentObjects(runner); + protected Collection getTasks(final ImageData imageData) { + Collection parentObjects = getParentObjects(imageData); if (parentObjects == null || parentObjects.isEmpty()) return Collections.emptyList(); // Add a single task, to avoid multithreading - which may complicate setting parents List tasks = new ArrayList<>(parentObjects.size()); - ImageServer server = getServer(runner); + ImageServer server = imageData.getServer(); Rectangle bounds = new Rectangle(0, 0, server.getWidth(), server.getHeight()); - PathObjectHierarchy hierarchy = getHierarchy(runner); + PathObjectHierarchy hierarchy = imageData.getHierarchy(); double radiusPixels; PixelCalibration cal = server.getPixelCalibration(); diff --git a/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/FillAnnotationHolesPlugin.java b/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/FillAnnotationHolesPlugin.java index 46a01e6d4..54705bcf5 100644 --- a/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/FillAnnotationHolesPlugin.java +++ b/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/FillAnnotationHolesPlugin.java @@ -35,7 +35,6 @@ import qupath.lib.objects.PathROIObject; import qupath.lib.objects.hierarchy.PathObjectHierarchy; import qupath.lib.plugins.AbstractInteractivePlugin; -import qupath.lib.plugins.PluginRunner; import qupath.lib.plugins.parameters.ParameterList; import qupath.lib.roi.RoiTools; import qupath.lib.roi.interfaces.ROI; @@ -80,22 +79,22 @@ public ParameterList getDefaultParameterList(ImageData imageData) { } @Override - protected Collection getParentObjects(PluginRunner runner) { - return getHierarchy(runner).getSelectionModel().getSelectedObjects().stream().filter(p -> p.isAnnotation()).toList(); + protected Collection getParentObjects(ImageData imageData) { + return imageData.getHierarchy().getSelectionModel().getSelectedObjects().stream().filter(p -> p.isAnnotation()).toList(); } @Override protected void addRunnableTasks(ImageData imageData, PathObject parentObject, List tasks) {} @Override - protected Collection getTasks(final PluginRunner runner) { - Collection parentObjects = getParentObjects(runner); + protected Collection getTasks(final ImageData imageData) { + Collection parentObjects = getParentObjects(imageData); if (parentObjects == null || parentObjects.isEmpty()) return Collections.emptyList(); // Add a single task, to avoid multithreading - which may complicate setting parents List tasks = new ArrayList<>(1); - PathObjectHierarchy hierarchy = getHierarchy(runner); + PathObjectHierarchy hierarchy = imageData.getHierarchy(); // Want to reset selection PathObject selected = hierarchy.getSelectionModel().getSelectedObject(); diff --git a/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/FindConvexHullDetectionsPlugin.java b/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/FindConvexHullDetectionsPlugin.java index 93588cf1d..6f17b7fc7 100644 --- a/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/FindConvexHullDetectionsPlugin.java +++ b/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/FindConvexHullDetectionsPlugin.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2020 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -113,8 +113,8 @@ public String getLastResultsDescription() { } @Override - protected Collection getParentObjects(PluginRunner runner) { - PathObjectHierarchy hierarchy = getHierarchy(runner); + protected Collection getParentObjects(ImageData imageData) { + PathObjectHierarchy hierarchy = imageData.getHierarchy(); PathObject selected = hierarchy.getSelectionModel().getSelectedObject(); if (selected instanceof TMACoreObject) return Collections.singleton(selected); @@ -138,9 +138,9 @@ public ParameterList getDefaultParameterList(ImageData imageData) { @Override - public boolean runPlugin(final PluginRunner pluginRunner, final String arg) { + public boolean runPlugin(final PluginRunner pluginRunner, final ImageData imageData, final String arg) { nObjectsRemoved.set(0); - return super.runPlugin(pluginRunner, arg); + return super.runPlugin(pluginRunner, imageData, arg); } diff --git a/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/RefineAnnotationsPlugin.java b/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/RefineAnnotationsPlugin.java index 84a6e6b64..d3067fa12 100644 --- a/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/RefineAnnotationsPlugin.java +++ b/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/RefineAnnotationsPlugin.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2020 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -41,7 +41,6 @@ import qupath.lib.objects.PathROIObject; import qupath.lib.objects.hierarchy.PathObjectHierarchy; import qupath.lib.plugins.AbstractInteractivePlugin; -import qupath.lib.plugins.PluginRunner; import qupath.lib.plugins.parameters.ParameterList; import qupath.lib.roi.RoiTools; import qupath.lib.roi.interfaces.ROI; @@ -93,26 +92,26 @@ public ParameterList getDefaultParameterList(ImageData imageData) { } @Override - protected Collection getParentObjects(PluginRunner runner) { - return getHierarchy(runner).getSelectionModel().getSelectedObjects().stream().filter(p -> p.isAnnotation()).toList(); + protected Collection getParentObjects(ImageData imageData) { + return imageData.getHierarchy().getSelectionModel().getSelectedObjects().stream().filter(p -> p.isAnnotation()).toList(); } @Override protected void addRunnableTasks(ImageData imageData, PathObject parentObject, List tasks) {} @Override - protected Collection getTasks(final PluginRunner runner) { - Collection parentObjects = getParentObjects(runner); + protected Collection getTasks(final ImageData imageData) { + Collection parentObjects = getParentObjects(imageData); if (parentObjects == null || parentObjects.isEmpty()) return Collections.emptyList(); // Add a single task, to avoid multithreading - which may complicate setting parents List tasks = new ArrayList<>(1); - PathObjectHierarchy hierarchy = getHierarchy(runner); + PathObjectHierarchy hierarchy = imageData.getHierarchy(); double minFragmentSize; double maxHoleSize, maxHoleSizeTemp; - ImageServer server = getServer(runner); + ImageServer server = imageData.getServer(); PixelCalibration cal = server.getPixelCalibration(); if (cal.hasPixelSizeMicrons()) { double pixelAreaMicrons = cal.getPixelWidthMicrons() * cal.getPixelHeightMicrons(); diff --git a/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/ShapeFeaturesPlugin.java b/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/ShapeFeaturesPlugin.java index 3f4ea5c49..ab1e9b1b2 100644 --- a/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/ShapeFeaturesPlugin.java +++ b/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/ShapeFeaturesPlugin.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2020 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -39,7 +39,6 @@ import qupath.lib.objects.PathDetectionObject; import qupath.lib.objects.PathObject; import qupath.lib.plugins.AbstractInteractivePlugin; -import qupath.lib.plugins.PluginRunner; import qupath.lib.plugins.parameters.ParameterList; import qupath.lib.roi.RoiTools; import qupath.lib.roi.interfaces.ROI; @@ -170,8 +169,8 @@ public ParameterList getDefaultParameterList(final ImageData imageData) { @Override - protected Collection getParentObjects(PluginRunner runner) { - return runner.getImageData().getHierarchy().getSelectionModel().getSelectedObjects(); + protected Collection getParentObjects(ImageData imageData) { + return imageData.getHierarchy().getSelectionModel().getSelectedObjects(); } diff --git a/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/SmoothFeaturesPlugin.java b/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/SmoothFeaturesPlugin.java index 79dc6231a..982efea0a 100644 --- a/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/SmoothFeaturesPlugin.java +++ b/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/SmoothFeaturesPlugin.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2020 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -47,7 +47,6 @@ import qupath.lib.objects.classes.PathClass; import qupath.lib.objects.hierarchy.PathObjectHierarchy; import qupath.lib.plugins.AbstractInteractivePlugin; -import qupath.lib.plugins.PluginRunner; import qupath.lib.plugins.parameters.ParameterList; import qupath.lib.roi.interfaces.ROI; @@ -398,8 +397,8 @@ public ParameterList getDefaultParameterList(final ImageData imageData) { } @Override - protected Collection getParentObjects(final PluginRunner runner) { - PathObjectHierarchy hierarchy = runner.getImageData().getHierarchy(); + protected Collection getParentObjects(final ImageData imageData) { + PathObjectHierarchy hierarchy = imageData.getHierarchy(); List parents = new ArrayList<>(); if (hierarchy.getTMAGrid() != null) { logger.info("Smoothing using TMA cores"); diff --git a/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/SplitAnnotationsPlugin.java b/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/SplitAnnotationsPlugin.java index 0a16a26ad..13dc4464a 100644 --- a/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/SplitAnnotationsPlugin.java +++ b/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/SplitAnnotationsPlugin.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2020 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -35,7 +35,6 @@ import qupath.lib.objects.PathObjects; import qupath.lib.objects.hierarchy.PathObjectHierarchy; import qupath.lib.plugins.AbstractInteractivePlugin; -import qupath.lib.plugins.PluginRunner; import qupath.lib.plugins.parameters.ParameterList; import qupath.lib.roi.RoiTools; import qupath.lib.roi.interfaces.ROI; @@ -80,22 +79,22 @@ public ParameterList getDefaultParameterList(ImageData imageData) { } @Override - protected Collection getParentObjects(PluginRunner runner) { - return getHierarchy(runner).getSelectionModel().getSelectedObjects().stream().filter(p -> p.isAnnotation()).toList(); + protected Collection getParentObjects(ImageData imageData) { + return imageData.getHierarchy().getSelectionModel().getSelectedObjects().stream().filter(p -> p.isAnnotation()).toList(); } @Override protected void addRunnableTasks(ImageData imageData, PathObject parentObject, List tasks) {} @Override - protected Collection getTasks(final PluginRunner runner) { - Collection parentObjects = getParentObjects(runner); + protected Collection getTasks(final ImageData imageData) { + Collection parentObjects = getParentObjects(imageData); if (parentObjects == null || parentObjects.isEmpty()) return Collections.emptyList(); // Add a single task, to avoid multithreading - which may complicate setting parents List tasks = new ArrayList<>(1); - PathObjectHierarchy hierarchy = getHierarchy(runner); + PathObjectHierarchy hierarchy = imageData.getHierarchy(); // Want to reset selection PathObject selected = hierarchy.getSelectionModel().getSelectedObject(); diff --git a/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/TileClassificationsToAnnotationsPlugin.java b/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/TileClassificationsToAnnotationsPlugin.java index 961ba8553..8b6466b65 100644 --- a/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/TileClassificationsToAnnotationsPlugin.java +++ b/qupath-core-processing/src/main/java/qupath/lib/plugins/objects/TileClassificationsToAnnotationsPlugin.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2020 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -50,7 +50,6 @@ import qupath.lib.objects.hierarchy.PathObjectHierarchy; import qupath.lib.plugins.AbstractDetectionPlugin; import qupath.lib.plugins.PathTask; -import qupath.lib.plugins.PluginRunner; import qupath.lib.plugins.parameters.ParameterList; import qupath.lib.regions.ImagePlane; import qupath.lib.roi.RoiTools; @@ -141,8 +140,8 @@ public int compare(PathClass pc1, PathClass pc2) { } @Override - protected Collection getParentObjects(final PluginRunner runner) { - PathObjectHierarchy hierarchy = runner.getImageData().getHierarchy(); + protected Collection getParentObjects(final ImageData imageData) { + PathObjectHierarchy hierarchy = imageData.getHierarchy(); List parents = new ArrayList<>(hierarchy.getSelectionModel().getSelectedObjects()); // Deal with nested objects - the code is clumsy, but the idea is to take the highest diff --git a/qupath-core-processing/src/main/java/qupath/lib/scripting/QP.java b/qupath-core-processing/src/main/java/qupath/lib/scripting/QP.java index 8bb33f229..bfa25252c 100644 --- a/qupath-core-processing/src/main/java/qupath/lib/scripting/QP.java +++ b/qupath-core-processing/src/main/java/qupath/lib/scripting/QP.java @@ -1461,7 +1461,7 @@ public static boolean runPlugin(final String className, final ImageData image Class cPlugin = QP.class.getClassLoader().loadClass(className); Constructor cons = cPlugin.getConstructor(); final PathPlugin plugin = (PathPlugin)cons.newInstance(); - return plugin.runPlugin(new CommandLinePluginRunner<>(imageData), args); + return plugin.runPlugin(new CommandLinePluginRunner(), imageData, args); } catch (Exception e) { logger.error("Unable to run plugin " + className, e); return false; diff --git a/qupath-core-processing/src/main/java/qupath/opencv/features/DelaunayClusteringPlugin.java b/qupath-core-processing/src/main/java/qupath/opencv/features/DelaunayClusteringPlugin.java index 9f741760e..193b33fe9 100644 --- a/qupath-core-processing/src/main/java/qupath/opencv/features/DelaunayClusteringPlugin.java +++ b/qupath-core-processing/src/main/java/qupath/opencv/features/DelaunayClusteringPlugin.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2020 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -70,16 +70,16 @@ public DelaunayClusteringPlugin() { } @Override - protected void preprocess(PluginRunner pluginRunner) { - super.preprocess(pluginRunner); + protected void preprocess(PluginRunner pluginRunner, ImageData imageData) { + super.preprocess(pluginRunner, imageData); // Reset any previous connections - pluginRunner.getImageData().removeProperty(DefaultPathObjectConnectionGroup.KEY_OBJECT_CONNECTIONS); + imageData.removeProperty(DefaultPathObjectConnectionGroup.KEY_OBJECT_CONNECTIONS); } @Override - protected void postprocess(PluginRunner pluginRunner) { - super.postprocess(pluginRunner); - getHierarchy(pluginRunner).fireHierarchyChangedEvent(this); + protected void postprocess(PluginRunner pluginRunner, ImageData imageData) { + super.postprocess(pluginRunner, imageData); + imageData.getHierarchy().fireHierarchyChangedEvent(this); } @@ -121,8 +121,8 @@ public ParameterList getDefaultParameterList(ImageData imageData) { } @Override - protected Collection getParentObjects(PluginRunner runner) { - PathObjectHierarchy hierarchy = getHierarchy(runner); + protected Collection getParentObjects(ImageData imageData) { + PathObjectHierarchy hierarchy = imageData.getHierarchy(); if (hierarchy == null) return Collections.emptyList(); diff --git a/qupath-core/src/main/java/qupath/lib/plugins/AbstractDetectionPlugin.java b/qupath-core/src/main/java/qupath/lib/plugins/AbstractDetectionPlugin.java index 5177c3bde..900f3304a 100644 --- a/qupath-core/src/main/java/qupath/lib/plugins/AbstractDetectionPlugin.java +++ b/qupath-core/src/main/java/qupath/lib/plugins/AbstractDetectionPlugin.java @@ -28,6 +28,7 @@ import java.util.HashSet; import java.util.Set; +import qupath.lib.images.ImageData; import qupath.lib.objects.PathAnnotationObject; import qupath.lib.objects.PathObject; import qupath.lib.objects.PathObjectTools; @@ -63,9 +64,9 @@ public Collection> getSupportedParentObjectClasses() * Get all selected objects that are instances of a supported class. */ @Override - protected Collection getParentObjects(final PluginRunner runner) { + protected Collection getParentObjects(final ImageData imageData) { Collection> supported = getSupportedParentObjectClasses(); - PathObjectHierarchy hierarchy = getHierarchy(runner); + PathObjectHierarchy hierarchy = imageData.getHierarchy(); Collection selectedObjects = hierarchy .getSelectionModel() .getSelectedObjects(); diff --git a/qupath-core/src/main/java/qupath/lib/plugins/AbstractPlugin.java b/qupath-core/src/main/java/qupath/lib/plugins/AbstractPlugin.java index 54a58d46c..5c79113ce 100644 --- a/qupath-core/src/main/java/qupath/lib/plugins/AbstractPlugin.java +++ b/qupath-core/src/main/java/qupath/lib/plugins/AbstractPlugin.java @@ -32,9 +32,7 @@ import org.slf4j.LoggerFactory; import qupath.lib.images.ImageData; -import qupath.lib.images.servers.ImageServer; import qupath.lib.objects.PathObject; -import qupath.lib.objects.hierarchy.PathObjectHierarchy; import qupath.lib.plugins.workflow.SimplePluginWorkflowStep; import qupath.lib.plugins.workflow.WorkflowStep; @@ -54,16 +52,16 @@ public abstract class AbstractPlugin implements PathPlugin { /** * Get a collection of tasks to perform. * - * This will be called from {@link #runPlugin(PluginRunner, String)} after a call to {@link #parseArgument(ImageData, String)}. - * - * The default implementation simply calls {@link #getParentObjects(PluginRunner)}, then {@link #addRunnableTasks(ImageData, PathObject, List)} + * This will be called from {@link #runPlugin(PluginRunner, ImageData, String)} after a call to {@link #parseArgument(ImageData, String)}. + * + * The default implementation simply calls {@link #getParentObjects(ImageData)}, then {@link #addRunnableTasks(ImageData, PathObject, List)} * for every parent object that was returned. * - * @param runner + * @param imageData * @return */ - protected Collection getTasks(final PluginRunner runner) { - Collection parentObjects = getParentObjects(runner); + protected Collection getTasks(final ImageData imageData) { + Collection parentObjects = getParentObjects(imageData); if (parentObjects == null || parentObjects.isEmpty()) return Collections.emptyList(); @@ -71,33 +69,13 @@ protected Collection getTasks(final PluginRunner runner) { long startTime = System.currentTimeMillis(); for (PathObject pathObject : parentObjects) { - addRunnableTasks(runner.getImageData(), pathObject, tasks); + addRunnableTasks(imageData, pathObject, tasks); } long endTime = System.currentTimeMillis(); logger.debug("Time to add plugin tasks: {} ms", endTime - startTime); return tasks; } - /** - * Get the ImageServer from a PluginRunner, or null if no server is available. - * @param runner - * @return - */ - protected ImageServer getServer(final PluginRunner runner) { - ImageData imageData = runner.getImageData(); - return imageData == null ? null : imageData.getServer(); - } - - /** - * Get the hierarchy from a PluginRunner, or null if no hierarchy is available. - * @param runner - * @return - */ - protected PathObjectHierarchy getHierarchy(final PluginRunner runner) { - ImageData imageData = runner.getImageData(); - return imageData == null ? null : imageData.getHierarchy(); - } - /** * Optionally request a hierarchy update after the tasks have run. * Default implementation returns true. @@ -135,10 +113,10 @@ protected boolean requestHierarchyUpdate() { * * In practice, this method can be overridden to return anything/nothing if getTasks is overridden instead. * - * @param runner + * @param imageData * @return */ - protected abstract Collection getParentObjects(final PluginRunner runner); + protected abstract Collection getParentObjects(final ImageData imageData); /** @@ -155,25 +133,29 @@ protected boolean requestHierarchyUpdate() { @Override - public boolean runPlugin(final PluginRunner pluginRunner, final String arg) { - - if (!parseArgument(pluginRunner.getImageData(), arg)) + public boolean runPlugin(final PluginRunner pluginRunner, ImageData imageData, final String arg) { + + if (!parseArgument(imageData, arg)) return false; - preprocess(pluginRunner); + preprocess(pluginRunner, imageData); - Collection tasks = getTasks(pluginRunner); + Collection tasks = getTasks(imageData); if (tasks.isEmpty()) return false; - pluginRunner.runTasks(tasks, requestHierarchyUpdate()); - postprocess(pluginRunner); + pluginRunner.runTasks(tasks); + + if (requestHierarchyUpdate()) + imageData.getHierarchy().fireHierarchyChangedEvent(this); + + postprocess(pluginRunner, imageData); if (pluginRunner.isCancelled()) return false; // Only add a workflow step if plugin was not cancelled - addWorkflowStep(pluginRunner.getImageData(), arg); + addWorkflowStep(imageData, arg); return true; } @@ -183,17 +165,19 @@ public boolean runPlugin(final PluginRunner pluginRunner, final String arg) { * Called after parsing the argument String, and immediately before creating & running any generated tasks. * * Does nothing by default. - * @param pluginRunner + * @param pluginRunner + * @param imageData */ - protected void preprocess(final PluginRunner pluginRunner) {}; + protected void preprocess(final PluginRunner pluginRunner, final ImageData imageData) {}; /** * Called immediately after running any generated tasks. * * Does nothing by default. - * @param pluginRunner + * @param pluginRunner + * @param imageData */ - protected void postprocess(final PluginRunner pluginRunner) {}; + protected void postprocess(final PluginRunner pluginRunner, final ImageData imageData) {}; /** diff --git a/qupath-core/src/main/java/qupath/lib/plugins/AbstractPluginRunner.java b/qupath-core/src/main/java/qupath/lib/plugins/AbstractPluginRunner.java index 8da3fa85b..f2f516aa9 100644 --- a/qupath-core/src/main/java/qupath/lib/plugins/AbstractPluginRunner.java +++ b/qupath-core/src/main/java/qupath/lib/plugins/AbstractPluginRunner.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2020 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -36,7 +36,6 @@ import org.slf4j.LoggerFactory; import qupath.lib.common.ThreadTools; -import qupath.lib.images.ImageData; /** @@ -46,16 +45,13 @@ * Note! This makes use of a static threadpool, which will be reused by all inheriting classes. * * @author Pete Bankhead - * - * @param */ -public abstract class AbstractPluginRunner implements PluginRunner { +public abstract class AbstractPluginRunner implements PluginRunner { private static final Logger logger = LoggerFactory.getLogger(AbstractPluginRunner.class); private static int counter = 0; -// private static ExecutorService pool; private ExecutorService pool; private ExecutorCompletionService service; @@ -67,16 +63,12 @@ public abstract class AbstractPluginRunner implements PluginRunner { protected AbstractPluginRunner() { super(); -// monitor = makeProgressMonitor(); } protected abstract SimpleProgressMonitor makeProgressMonitor(); @Override - public abstract ImageData getImageData(); - - @Override - public synchronized void runTasks(Collection tasks, boolean updateHierarchy) { + public synchronized void runTasks(Collection tasks) { if (tasks.isEmpty()) return; @@ -106,8 +98,6 @@ public synchronized void runTasks(Collection tasks, boolean updateHier // Post-process any PathTasks postProcess(tasks.stream().filter(t -> t instanceof PathTask).map(t -> (PathTask)t).toList()); - - getImageData().getHierarchy().fireHierarchyChangedEvent(this); } @@ -182,18 +172,11 @@ protected void postProcess(final Collection tasks) { boolean wasCancelled = tasksCancelled || monitor.cancelled(); for (var task : tasks) task.taskComplete(wasCancelled); - - var imageData = getImageData(); - if (imageData != null) - imageData.getHierarchy().fireHierarchyChangedEvent(this); - -// PathTask task = runnable instanceof PathTask ? (PathTask)runnable : null; -// if (task != null) { -// task.taskComplete(); -// } -// runnable.getClass().getSimpleName() -// System.err.println("Updating monitor from post processing"); -// updateMonitor(task); + + // Removed v0.5.0 - TODO: Check if should be reinstated +// var imageData = getImageData(); +// if (imageData != null) +// imageData.getHierarchy().fireHierarchyChangedEvent(this); } private void updateMonitor(final PathTask task) { diff --git a/qupath-core/src/main/java/qupath/lib/plugins/CommandLinePluginRunner.java b/qupath-core/src/main/java/qupath/lib/plugins/CommandLinePluginRunner.java index e776c4f1e..10898bcfa 100644 --- a/qupath-core/src/main/java/qupath/lib/plugins/CommandLinePluginRunner.java +++ b/qupath-core/src/main/java/qupath/lib/plugins/CommandLinePluginRunner.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2020 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -28,40 +28,28 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import qupath.lib.images.ImageData; import qupath.lib.regions.ImageRegion; /** * A PluginRunner that simply logs progress and output. *

- * This doesn't need to be run on any particular thread (e.g. Platform or EDT). + * This doesn't need to be run on any particular thread (e.g. the JavaFX Application thread). * * @author Pete Bankhead - * - * @param */ -public class CommandLinePluginRunner extends AbstractPluginRunner { +public class CommandLinePluginRunner extends AbstractPluginRunner { - private ImageData imageData; - /** * Constructor for a PluginRunner that send progress to a log. - * @param imageData the ImageData for the current plugin */ - public CommandLinePluginRunner(final ImageData imageData) { + public CommandLinePluginRunner() { super(); - this.imageData = imageData; } @Override public SimpleProgressMonitor makeProgressMonitor() { return new CommandLineProgressMonitor(); } - - @Override - public ImageData getImageData() { - return imageData; - } diff --git a/qupath-core/src/main/java/qupath/lib/plugins/PathInteractivePlugin.java b/qupath-core/src/main/java/qupath/lib/plugins/PathInteractivePlugin.java index 9e2820e81..4fc9c6371 100644 --- a/qupath-core/src/main/java/qupath/lib/plugins/PathInteractivePlugin.java +++ b/qupath-core/src/main/java/qupath/lib/plugins/PathInteractivePlugin.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2020 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as diff --git a/qupath-core/src/main/java/qupath/lib/plugins/PathPlugin.java b/qupath-core/src/main/java/qupath/lib/plugins/PathPlugin.java index d9d79457d..e0fe82668 100644 --- a/qupath-core/src/main/java/qupath/lib/plugins/PathPlugin.java +++ b/qupath-core/src/main/java/qupath/lib/plugins/PathPlugin.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2020 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -23,6 +23,8 @@ package qupath.lib.plugins; +import qupath.lib.images.ImageData; + /** * Primary interface for defining a 'plugin' command. *

@@ -68,7 +70,7 @@ public interface PathPlugin { * @param arg * @return */ - public boolean runPlugin(PluginRunner pluginRunner, String arg); + public boolean runPlugin(PluginRunner pluginRunner, ImageData imageData, String arg); /** * (Optional) short one-line description of the results, e.g. to say how many objects detected. diff --git a/qupath-core/src/main/java/qupath/lib/plugins/PluginRunner.java b/qupath-core/src/main/java/qupath/lib/plugins/PluginRunner.java index 51cae8f8f..23caa5c37 100644 --- a/qupath-core/src/main/java/qupath/lib/plugins/PluginRunner.java +++ b/qupath-core/src/main/java/qupath/lib/plugins/PluginRunner.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2020 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -25,27 +25,15 @@ import java.util.Collection; -import qupath.lib.images.ImageData; - /** - * Implementing classes encapsulate the data and functionality needed to run a plugin on a single image. - *

- * This means access to an ImageData object (along with helper methods to access its server, hierarchy & - * selected objects), as well as the ability to run a collection of tasks - possibly in parallel. + * A minimal interface for a class capable of running tasks in parallel, giving feedback to the user. *

- * This implementation may also (optionally) provide useful feedback on progress when running tasks. + * This is intended for use with {@link PathPlugin}s, but may be used elsewhere. + *

* * @author Pete Bankhead - * - * @param */ -public interface PluginRunner { - - /** - * Get the current {@link ImageData} upon which the plugin should operate. - * @return - */ - ImageData getImageData(); +public interface PluginRunner { /** * Query if the plugin can be cancelled while running. @@ -58,10 +46,7 @@ public interface PluginRunner { * Pass a collection of parallelizable tasks to run. * @param tasks the tasks to run. If these are instances of {@link PathTask} then * an optional postprocessing may be applied after all tasks are complete. - * @param fireHierarchyUpdate if true, a hierarchy update should be fired on completion. - * This means that individual tasks do not need to fire their own updates, - * which can be a performance bottleneck. */ - void runTasks(Collection tasks, boolean fireHierarchyUpdate); + void runTasks(Collection tasks); } \ No newline at end of file diff --git a/qupath-extension-processing/src/main/java/qupath/imagej/gui/AwtMenuBarBlocker.java b/qupath-extension-processing/src/main/java/qupath/imagej/gui/AwtMenuBarBlocker.java new file mode 100644 index 000000000..1e6f82b01 --- /dev/null +++ b/qupath-extension-processing/src/main/java/qupath/imagej/gui/AwtMenuBarBlocker.java @@ -0,0 +1,138 @@ +/*- + * #%L + * This file is part of QuPath. + * %% + * Copyright (C) 2023 QuPath developers, The University of Edinburgh + * %% + * QuPath is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * QuPath is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QuPath. If not, see . + * #L% + */ + + +package qupath.imagej.gui; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Frame; +import java.awt.KeyboardFocusManager; +import java.awt.MenuBar; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.lang.ref.WeakReference; + +/** + * Helper class for managing the fact that JavaFX and AWT system MenuBars don't play nicely together. + *

+ * It works my listening to the active window on the AWT side, and checking when no AWT window is active. + * It then looks to see if the previously active window had a menubar and, if so, calls MenuBar.removeNotify(). + * This temporarily blocks the menubar from responding to keyboard input, and allows JavaFX to take over. + * When an AWT window then receives focus later, MenuBar.addNotify() is called to restore the menubar (unless + * it has been garbage collected in the meantime). + *

+ *

+ * The behavior seems to work acceptably on macOS, but not on Windows. + * But on Windows the JavaFX default behavior doesn't seem to be problematic, so the workaround isn't needed. + *

+ * + * @implNote This assumes that the {@link KeyboardFocusManager} is never changed. + */ +class AwtMenuBarBlocker implements PropertyChangeListener { + + private static final Logger logger = LoggerFactory.getLogger(AwtMenuBarBlocker.class); + + private WeakReference blockedMenuBarRef = null; + + private boolean isBlocking = false; + + /** + * Request that AWT MenuBars are blocked whenever no AWT window is active. + */ + synchronized void startBlocking() { + if (isBlocking) + return; + KeyboardFocusManager.getCurrentKeyboardFocusManager().addPropertyChangeListener("activeWindow", this); + isBlocking = true; + } + + /** + * Stop blocking AWT MenuBars whenever no AWT window is active. + */ + synchronized void stopBlocking() { + if (!isBlocking) + return; + KeyboardFocusManager.getCurrentKeyboardFocusManager().removePropertyChangeListener("activeWindow", this); + var blockedMenuBar = blockedMenuBarRef == null ? null : blockedMenuBarRef.get(); + if (blockedMenuBar != null) { + restoreMenuBar(blockedMenuBar); + } + blockedMenuBarRef = null; + isBlocking = false; + } + + /** + * Check if AWT MenuBars are currently being blocked. + * @return + */ + public synchronized boolean isBlocking() { + return isBlocking; + } + + + @Override + public void propertyChange(PropertyChangeEvent evt) { + var activeWindow = evt.getNewValue(); + MenuBar blockedMenuBar = blockedMenuBarRef == null ? null : blockedMenuBarRef.get(); + if (activeWindow != null) { + // We have an active AWT window - so we should revive any previous menubar + if (blockedMenuBar != null) { + restoreMenuBar(blockedMenuBar); + blockedMenuBarRef = null; + } + } else { + // We no longer have an active window, so we should make sure to block any menubar + var currentMenuBar = getMenuBar(evt.getOldValue()); + if (currentMenuBar != null) { + if (blockedMenuBar != currentMenuBar) { + blockMenuBar(currentMenuBar); + restoreMenuBar(blockedMenuBar); + blockedMenuBarRef = new WeakReference<>(currentMenuBar); + } + } + } + } + + + private void blockMenuBar(MenuBar menuBar) { + if (menuBar == null) + return; + logger.debug("Removing menubar notifications {}", menuBar); + menuBar.removeNotify(); + } + + private void restoreMenuBar(MenuBar menuBar) { + if (menuBar == null) + return; + logger.debug("Adding menubar notifications {}", menuBar); + menuBar.removeNotify(); + } + + + private static MenuBar getMenuBar(Object o) { + if (o instanceof Frame frame) + return frame.getMenuBar(); + return null; + } + +} diff --git a/qupath-extension-processing/src/main/java/qupath/imagej/gui/IJExtension.java b/qupath-extension-processing/src/main/java/qupath/imagej/gui/IJExtension.java index 08ece6d80..e9ecf6981 100644 --- a/qupath-extension-processing/src/main/java/qupath/imagej/gui/IJExtension.java +++ b/qupath-extension-processing/src/main/java/qupath/imagej/gui/IJExtension.java @@ -34,9 +34,9 @@ import javafx.application.Platform; import javafx.beans.property.StringProperty; import javafx.geometry.Orientation; -import javafx.scene.control.Button; -import javafx.scene.control.ContextMenu; import javafx.scene.control.Menu; +import javafx.scene.control.MenuBar; +import javafx.scene.control.MenuButton; import javafx.scene.control.Separator; import javafx.scene.control.Tooltip; import javafx.scene.image.Image; @@ -86,6 +86,7 @@ import qupath.lib.gui.extensions.QuPathExtension; import qupath.lib.gui.localization.QuPathResources; import qupath.lib.gui.prefs.PathPrefs; +import qupath.lib.gui.prefs.SystemMenuBar; import qupath.lib.gui.tools.ColorToolsFX; import qupath.lib.gui.tools.IconFactory.PathIcons; import qupath.lib.gui.viewer.OverlayOptions; @@ -122,6 +123,16 @@ public class IJExtension implements QuPathExtension { // Path to ImageJ - used to determine plugins directory private static StringProperty imageJPath = null; + /** + * It is necessary to block MenuBars created with AWT on macOS, otherwise shortcuts + * can be fired twice and menus confused. + * But using the same strategy on Windows causes ImageJ menus not to display. + * So we need to handle both cases (and make it possible to override). + */ + private static final boolean blockAwtMenuBars = "true".equals(System.getProperty("qupath.block.awt.menubars", + GeneralTools.isMac() ? "true" : "false")); + private static final AwtMenuBarBlocker menuBarBlocker = new AwtMenuBarBlocker(); + static { // Try to default to the most likely ImageJ path on a Mac if (GeneralTools.isMac() && new File("/Applications/ImageJ/").isDirectory()) @@ -173,7 +184,7 @@ public static synchronized ImageJ getImageJInstance() { // Try getting ImageJ without resorting to the EDT? return getImageJInstanceOnEDT(); } - + private static Set installedPlugins = Collections.newSetFromMap(new WeakHashMap<>()); /** @@ -194,16 +205,14 @@ private static synchronized void ensurePluginsInstalled(ImageJ imageJ) { Menus.installPlugin(QuPath_Send_Overlay_to_QuPath.class.getName() + "(\"manager\")", Menus.PLUGINS_MENU, "Send RoiManager ROIs to QuPath", "", imageJ); installedPlugins.add(imageJ); } - + + private static synchronized ImageJ getImageJInstanceOnEDT() { ImageJ ijTemp = IJ.getInstance(); Prefs.setThreads(1); // Turn off ImageJ's multithreading, since we do our own if (ijTemp == null) { logger.info("Creating a new standalone ImageJ instance..."); - // List menusPrevious = new ArrayList<>(QuPathGUI.getInstance().getMenuBar().getMenus()); try { - // Class cls = IJ.getClassLoader().loadClass("MacAdapter"); - // System.err.println("I LOADED: " + cls); // See http://rsb.info.nih.gov/ij/docs/menus/plugins.html for setting the plugins directory String ijPath = getImageJPath(); if (ijPath != null) @@ -224,54 +233,17 @@ private static synchronized ImageJ getImageJInstanceOnEDT() { // so here ensure that all remaining displayed images are closed final ImageJ ij = ijTemp; ij.exitWhenQuitting(false); - ij.addWindowListener(new WindowAdapter() { - - @Override - public void windowDeactivated(WindowEvent e) { - // ij.setMenuBar(null); - } - - @Override - public void windowLostFocus(WindowEvent e) { - // ij.setMenuBar(null); - } - - @Override - public void windowClosing(WindowEvent e) { - // Spoiler alert: it *is* the EDT (as one would expect) - // System.err.println("EDT: " + SwingUtilities.isEventDispatchThread()); - // System.err.println("Application thread: " + Platform.isFxApplicationThread()); - ij.requestFocus(); - for (Frame frame : Frame.getFrames()) { - // Close any images we have open - if (frame instanceof ImageWindow) { - ImageWindow win = (ImageWindow)frame; - ImagePlus imp = win.getImagePlus(); - if (imp != null) - imp.setIJMenuBar(false); - win.setVisible(false); - if (imp != null) { - // Save message still appears... - imp.changes = false; - // Initially tried to close, but then ImageJ hung - // Flush was ok, unless it was selected to save changes - in which case that didn't work out - // imp.flush(); - // imp.close(); - // imp.flush(); - } else - win.dispose(); - } - } - ij.removeWindowListener(this); - IJ.wait(10); - ij.setMenuBar(null); - } - - @Override - public void windowClosed(WindowEvent e) {} - - }); - + var windowListener = new ImageJWindowListener(ij); + ij.addWindowListener(windowListener); + + // Attempt to block the AWT menu bar when ImageJ is not in focus. + // Also try to work around a macOS issue where ImageJ's menubar and QuPath's don't work nicely together, + // by ensuring that any system menubar request by QuPath is (temporarily) overridden. + if (blockAwtMenuBars) + menuBarBlocker.startBlocking(); + if (ij.isShowing()) + SystemMenuBar.setOverrideSystemMenuBar(true); + logger.debug("Created ImageJ instance: {}", ijTemp); } @@ -281,6 +253,42 @@ public void windowClosed(WindowEvent e) {} return ijTemp; } + + private static class ImageJWindowListener extends WindowAdapter { + + private final ImageJ ij; + + private ImageJWindowListener(ImageJ ij) { + this.ij = ij; + } + + @Override + public void windowClosing(WindowEvent e) { + ij.requestFocus(); + for (Frame frame : Frame.getFrames()) { + // Close any images we have open + if (frame instanceof ImageWindow) { + ImageWindow win = (ImageWindow) frame; + ImagePlus imp = win.getImagePlus(); + if (imp != null) + imp.setIJMenuBar(false); + win.setVisible(false); + if (imp != null) { + // Save message still appears... + imp.changes = false; + // Initially tried to close, but then ImageJ hung + // Flush was ok, unless it was selected to save changes - in which case that didn't work out + // imp.flush(); + // imp.close(); + // imp.flush(); + } else + win.dispose(); + } + } + SystemMenuBar.setOverrideSystemMenuBar(false); + } + + } /** @@ -605,17 +613,13 @@ private void addQuPathCommands(final QuPathGUI qupath) { try { ImageView imageView = new ImageView(getImageJIcon(QuPathGUI.TOOLBAR_ICON_SIZE, QuPathGUI.TOOLBAR_ICON_SIZE)); - Button btnImageJ = new Button(); + MenuButton btnImageJ = new MenuButton(); btnImageJ.setGraphic(imageView); btnImageJ.setTooltip(new Tooltip("ImageJ commands")); - ContextMenu popup = new ContextMenu(); - popup.getItems().addAll( + btnImageJ.getItems().addAll( ActionTools.createMenuItem(commands.actionExtractRegion), ActionTools.createMenuItem(commands.actionSnapshot) - ); - btnImageJ.setOnMouseClicked(e -> { - popup.show(btnImageJ, e.getScreenX(), e.getScreenY()); - }); + ); toolbar.getItems().add(btnImageJ); } catch (Exception e) { logger.error("Error adding toolbar buttons", e); @@ -721,7 +725,6 @@ public void installExtension(QuPathGUI qupath) { extensionInstalled = true; Prefs.setThreads(1); // We always want a single thread, due to QuPath's multithreading -// Prefs.setIJMenuBar = false; addQuPathCommands(qupath); } diff --git a/qupath-extension-processing/src/main/java/qupath/imagej/gui/ImageJMacroRunner.java b/qupath-extension-processing/src/main/java/qupath/imagej/gui/ImageJMacroRunner.java index dea37b332..606c179ff 100644 --- a/qupath-extension-processing/src/main/java/qupath/imagej/gui/ImageJMacroRunner.java +++ b/qupath-extension-processing/src/main/java/qupath/imagej/gui/ImageJMacroRunner.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2020 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -116,8 +116,8 @@ public String getDescription() { } @Override - public boolean runPlugin(final PluginRunner runner, final String arg) { - if (!parseArgument(runner.getImageData(), arg)) + public boolean runPlugin(final PluginRunner runner, final ImageData imageData, final String arg) { + if (!parseArgument(imageData, arg)) return false; if (dialog == null) { @@ -143,7 +143,7 @@ public boolean runPlugin(final PluginRunner runner, final String panelMacro.setCenter(textArea); - ParameterPanelFX parameterPanel = new ParameterPanelFX(getParameterList(runner.getImageData())); + ParameterPanelFX parameterPanel = new ParameterPanelFX(getParameterList(imageData)); panelMacro.setBottom(parameterPanel.getPane()); @@ -155,38 +155,36 @@ public boolean runPlugin(final PluginRunner runner, final String if (macroText.length() == 0) return; - PathObjectHierarchy hierarchy = getHierarchy(runner); + // TODO: Consider that we're requesting a new ImageData here (probably right, but need to check) + var viewer = qupath.getViewer(); + var imageDataLocal = viewer.getImageData(); + PathObjectHierarchy hierarchy = imageDataLocal.getHierarchy(); PathObject pathObject = hierarchy.getSelectionModel().singleSelection() ? hierarchy.getSelectionModel().getSelectedObject() : null; if (pathObject instanceof PathAnnotationObject || pathObject instanceof TMACoreObject) { SwingUtilities.invokeLater(() -> { - runMacro(params, - qupath.getViewer().getImageData(), - qupath.getViewer().getImageDisplay(), pathObject, macroText); + runMacro(params, + imageDataLocal, + viewer.getImageDisplay(), pathObject, macroText); }); } else { // DisplayHelpers.showErrorMessage(getClass().getSimpleName(), "Sorry, ImageJ macros can only be run for single selected images"); // logger.warn("ImageJ macro being run in current thread"); // runPlugin(runner, arg); // TODO: Consider running in a background thread? // Run in a background thread - Collection parents = getParentObjects(runner); + Collection parents = getParentObjects(imageDataLocal); if (parents.isEmpty()) { - Dialogs.showErrorMessage("ImageJ macro runner", "No annotation or TMA core objects selected!"); + Dialogs.showErrorNotification("ImageJ macro runner", "No annotation or TMA core objects selected!"); return; } List tasks = new ArrayList<>(); for (PathObject parent : parents) - addRunnableTasks(qupath.getViewer().getImageData(), parent, tasks); + addRunnableTasks(imageDataLocal, parent, tasks); - qupath.getThreadPoolManager().submitShortTask(() -> runner.runTasks(tasks, true)); -// runner.runTasks(tasks); - -// Runnable r = new Runnable() { -// public void run() { -// runPlugin(runner, arg); -// } -// }; -// new Thread(r).start(); + qupath.getThreadPoolManager().submitShortTask(() -> { + runner.runTasks(tasks); + imageDataLocal.getHierarchy().fireHierarchyChangedEvent(ImageJMacroRunner.this); + }); } }); Button btnClose = new Button("Close"); @@ -208,11 +206,7 @@ public boolean runPlugin(final PluginRunner runner, final String static void runMacro(final ParameterList params, final ImageData imageData, final ImageDisplay imageDisplay, final PathObject pathObject, final String macroText) { -// if (!SwingUtilities.isEventDispatchThread()) { -// SwingUtilities.invokeLater(() -> runMacro(params, imageData, imageDisplay, pathObject, macroText)); -// return; -// } - + // Don't try if interrupted if (Thread.currentThread().isInterrupted()) { logger.warn("Skipping macro for {} - thread interrupted", pathObject); @@ -249,12 +243,6 @@ static void runMacro(final ParameterList params, final ImageData return; } - - // IJHelpers.getImageJInstance(); - // ImageJ ij = IJHelpers.getImageJInstance(); - // if (ij != null && WindowManager.getIDList() == null) - // ij.setVisible(false); - // Determine a sensible argument to pass String argument; if (pathObject instanceof TMACoreObject || !pathObject.hasROI()) @@ -434,13 +422,13 @@ public void run() { } @Override - protected Collection getParentObjects(final PluginRunner runner) { + protected Collection getParentObjects(final ImageData imageData) { // Try to get currently-selected objects - PathObjectHierarchy hierarchy = getHierarchy(runner); + PathObjectHierarchy hierarchy = imageData.getHierarchy(); List pathObjects = hierarchy.getSelectionModel().getSelectedObjects().stream() .filter(p -> p.isAnnotation() || p.isTMACore()).toList(); if (pathObjects.isEmpty()) { - if (GuiTools.promptForParentObjects(this.getName(), runner.getImageData(), false, getSupportedParentObjectClasses())) + if (GuiTools.promptForParentObjects(this.getName(), imageData, false, getSupportedParentObjectClasses())) pathObjects = new ArrayList<>(hierarchy.getSelectionModel().getSelectedObjects()); } return pathObjects; diff --git a/qupath-gui-fx/src/main/java/qupath/lib/gui/ParameterDialogWrapper.java b/qupath-gui-fx/src/main/java/qupath/lib/gui/ParameterDialogWrapper.java index cd1b2d2be..714d928c1 100644 --- a/qupath-gui-fx/src/main/java/qupath/lib/gui/ParameterDialogWrapper.java +++ b/qupath-gui-fx/src/main/java/qupath/lib/gui/ParameterDialogWrapper.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2021 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -70,7 +70,7 @@ class ParameterDialogWrapper { * @param params parameters to display * @param pluginRunner the {@link PluginRunner} that may be used to run this plugin if necessary */ - public ParameterDialogWrapper(final PathInteractivePlugin plugin, final ParameterList params, final PluginRunner pluginRunner) { + public ParameterDialogWrapper(final PathInteractivePlugin plugin, final ParameterList params, final PluginRunner pluginRunner) { dialog = createDialog(plugin, params, pluginRunner); } @@ -111,36 +111,10 @@ public ParameterList getParameterList() { return panel.getParameters(); } - private Stage createDialog(final PathInteractivePlugin plugin, final ParameterList params, final PluginRunner pluginRunner) { + private Stage createDialog(final PathInteractivePlugin plugin, final ParameterList params, final PluginRunner pluginRunner) { panel = new ParameterPanelFX(params); panel.getPane().setPadding(new Insets(5, 5, 5, 5)); - // panel.addParameterChangeListener(new ParameterChangeListener() { - // - // @Override - // public void parameterChanged(ParameterList parameterList, String key, boolean isAdjusting) { - // - // if (!plugin.requestLiveUpdate()) - // return; - // - // PathObjectHierarchy hierarchy = pluginRunner.getHierarchy(); - // if (hierarchy == null) - // return; - // - // Collection> supportedParents = plugin.getSupportedParentObjectClasses(); - // - // PathObject selectedObject = pluginRunner.getSelectedObject(); - // if (selectedObject == null) { - // if (supportedParents.contains(PathRootObject.class)) - // Collections.singleton(hierarchy.getRootObject()); - // } else if (supportedParents.contains(selectedObject.getClass())) - // Collections.singleton(selectedObject); - // } - // - // }); - - -// final Button btnRun = new Button("Run " + plugin.getName()); final Button btnRun = new Button("Run"); btnRun.textProperty().bind(Bindings.createStringBinding(() -> { if (btnRun.isDisabled()) @@ -164,6 +138,9 @@ private Stage createDialog(final PathInteractivePlugin plugin, final Paramete btnRun.setOnAction(e -> { + // Return the current ImageData + var imageData = qupath.getImageData(); // v0.5.0 change - previously pluginRunner.getImageData(); + // Check if we have the parent objects available to make this worthwhile if (plugin instanceof PathInteractivePlugin) { @@ -171,11 +148,10 @@ private Stage createDialog(final PathInteractivePlugin plugin, final Paramete // params.removeParameter(KEY_REGIONS); boolean alwaysPrompt = plugin.alwaysPromptForObjects(); - ImageData imageData = pluginRunner.getImageData(); Collection selected = imageData == null ? Collections.emptyList() : imageData.getHierarchy().getSelectionModel().getSelectedObjects(); Collection parents = PathObjectTools.getSupportedObjects(selected, plugin.getSupportedParentObjectClasses()); if (alwaysPrompt || parents == null || parents.isEmpty()) { - if (!ParameterDialogWrapper.promptForParentObjects(pluginRunner, plugin, alwaysPrompt && !parents.isEmpty())) + if (!ParameterDialogWrapper.promptForParentObjects(imageData, plugin, alwaysPrompt && !parents.isEmpty())) return; } // promptForParentObjects @@ -188,9 +164,10 @@ private Stage createDialog(final PathInteractivePlugin plugin, final Paramete @Override public void run() { try { - WorkflowStep lastStep = pluginRunner.getImageData().getHistoryWorkflow().getLastStep(); - boolean success = plugin.runPlugin(pluginRunner, ParameterList.convertToJson(params)); - WorkflowStep lastStepNew = pluginRunner.getImageData().getHistoryWorkflow().getLastStep(); + var historyWorkflow = imageData.getHistoryWorkflow(); + WorkflowStep lastStep = historyWorkflow.getLastStep(); + boolean success = plugin.runPlugin(pluginRunner, (ImageData)imageData, ParameterList.convertToJson(params)); + WorkflowStep lastStepNew = historyWorkflow.getLastStep(); if (success && lastStep != lastStepNew) lastWorkflowStep = lastStepNew; else @@ -252,13 +229,13 @@ public WorkflowStep getLastWorkflowStep() { * Get the parent objects to use when running the plugin, or null if no suitable parent objects are found. * This involves prompting the user if multiple options are possible. * - * @param runner + * @param imageData * @param plugin * @param includeSelected * @return */ - public static boolean promptForParentObjects(final PluginRunner runner, final PathInteractivePlugin plugin, final boolean includeSelected) { - return GuiTools.promptForParentObjects(plugin.getName(), runner.getImageData(), includeSelected, plugin.getSupportedParentObjectClasses()); + public static boolean promptForParentObjects(final ImageData imageData, final PathInteractivePlugin plugin, final boolean includeSelected) { + return GuiTools.promptForParentObjects(plugin.getName(), imageData, includeSelected, plugin.getSupportedParentObjectClasses()); } } \ No newline at end of file diff --git a/qupath-gui-fx/src/main/java/qupath/lib/gui/PluginRunnerFX.java b/qupath-gui-fx/src/main/java/qupath/lib/gui/PluginRunnerFX.java index aebea9c4e..5e03e5292 100644 --- a/qupath-gui-fx/src/main/java/qupath/lib/gui/PluginRunnerFX.java +++ b/qupath-gui-fx/src/main/java/qupath/lib/gui/PluginRunnerFX.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2021 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -60,7 +60,7 @@ * @author Pete Bankhead * */ -class PluginRunnerFX extends AbstractPluginRunner { +class PluginRunnerFX extends AbstractPluginRunner { private static final Logger logger = LoggerFactory.getLogger(PluginRunnerFX.class); @@ -68,7 +68,10 @@ class PluginRunnerFX extends AbstractPluginRunner { private static long repaintDelayMillis = 1000; private QuPathGUI qupath; - // private ImageData imageData; // Consider reinstating - at least as an option + + // Snapshot of the current image data, used only within runTasks() + // Because the method is synchronized, it is not expected to cause trouble + private ImageData currentImageData; // Consider reinstating - at least as an option /** * Constructor. @@ -77,7 +80,6 @@ class PluginRunnerFX extends AbstractPluginRunner { public PluginRunnerFX(final QuPathGUI qupath) { super(); this.qupath = qupath; - // this.imageData = qupath.getImageData(); } @Override @@ -90,12 +92,12 @@ public SimpleProgressMonitor makeProgressMonitor() { } @Override - public synchronized void runTasks(Collection tasks, boolean updateHierarchy) { + public synchronized void runTasks(Collection tasks) { var viewer = qupath == null || repaintDelayMillis <= 0 ? null : qupath.getViewer(); if (viewer != null) viewer.setMinimumRepaintSpacingMillis(repaintDelayMillis); try { - super.runTasks(tasks, updateHierarchy); + super.runTasks(tasks); } catch (Exception e) { throw(e); } finally { @@ -104,12 +106,6 @@ public synchronized void runTasks(Collection tasks, boolean updateHier } } - @Override - public ImageData getImageData() { - // return imageData; - return qupath.getImageData(); - } - @Override protected void postProcess(final Collection tasks) { @@ -118,9 +114,7 @@ protected void postProcess(final Collection tasks) { // Failing to do this leads to issues such as intermittent concurrent modification exceptions, or commands needing // to be run twice // This aims to ensure that can't happen - FutureTask postProcessTask = new FutureTask<>(() -> { - super.postProcess(tasks); - }, Boolean.TRUE); + FutureTask postProcessTask = new FutureTask<>(() -> super.postProcess(tasks), Boolean.TRUE); Platform.runLater(postProcessTask); try { postProcessTask.get(); @@ -129,10 +123,9 @@ protected void postProcess(final Collection tasks) { } catch (ExecutionException e) { logger.error("Exception during post-processing", e); } -// Platform.runLater(() -> postProcess(runnable)); return; - } - super.postProcess(tasks); + } else + super.postProcess(tasks); } diff --git a/qupath-gui-fx/src/main/java/qupath/lib/gui/QuPathGUI.java b/qupath-gui-fx/src/main/java/qupath/lib/gui/QuPathGUI.java index 84876cf73..f75cf74b6 100644 --- a/qupath-gui-fx/src/main/java/qupath/lib/gui/QuPathGUI.java +++ b/qupath-gui-fx/src/main/java/qupath/lib/gui/QuPathGUI.java @@ -55,6 +55,8 @@ import javax.script.ScriptException; import javax.swing.SwingUtilities; +import javafx.beans.value.ObservableValue; +import javafx.scene.Node; import javafx.scene.control.*; import javafx.stage.Window; import org.controlsfx.control.action.Action; @@ -124,7 +126,7 @@ import qupath.lib.gui.prefs.PathPrefs; import qupath.lib.gui.prefs.PathPrefs.ImageTypeSetting; import qupath.lib.gui.prefs.QuPathStyleManager; -import qupath.lib.gui.prefs.SystemMenubar; +import qupath.lib.gui.prefs.SystemMenuBar; import qupath.lib.gui.scripting.ScriptEditor; import qupath.lib.gui.scripting.languages.GroovyLanguage; import qupath.lib.gui.scripting.languages.ScriptLanguageProvider; @@ -327,7 +329,7 @@ private QuPathGUI(final Stage stage, final HostServices hostServices, boolean sh // Populating the scripting menu is slower, so delay it until now populateScriptingMenu(getMenu(QuPathResources.getString("Menu.Automate"), false)); - SystemMenubar.manageMainMenubar(menuBar); + SystemMenuBar.manageMainMenuBar(menuBar); logger.debug("{}", timeit.stop()); } @@ -723,6 +725,15 @@ private Pane initializeMainComponent() { } + /** + * Observable value indicating that the user interface is/should be blocked. + * This happens when a plugin or script is running. + * @return + */ + public ObservableValue uiBlocked() { + return uiBlocked; + } + private void syncDefaultImageDataAndProjectForScripting() { imageDataProperty().addListener((v, o, n) -> QP.setDefaultImageData(n)); @@ -774,6 +785,7 @@ public void handleQuitRequestWith(QuitEvent e, QuitResponse response) { } } + private void ensureQuPathInstanceSet() { @@ -820,10 +832,21 @@ class MainSceneKeyEventHandler implements EventHandler { public void handle(KeyEvent e) { if (e.getEventType() != KeyEvent.KEY_RELEASED) return; - + + // For detachable viewers, we can have events passed from the other viewer + // but which should be handled here + var target = e.getTarget(); + boolean propagatedFromAnotherScene = false; + if (target instanceof Node node) { + if (node.getScene() != stage.getScene()) + propagatedFromAnotherScene = true; + } + // It seems if using the system menubar on Mac, we can sometimes need to mop up missed keypresses - if (e.isConsumed() || e.isShortcutDown() || !(GeneralTools.isMac() && getMenuBar().isUseSystemMenuBar()) || e.getTarget() instanceof TextInputControl) { - return; + if (!propagatedFromAnotherScene) { + if (e.isConsumed() || e.isShortcutDown() || !(GeneralTools.isMac() && getMenuBar().isUseSystemMenuBar()) || e.getTarget() instanceof TextInputControl) { + return; + } } for (var entry : comboMap.entrySet()) { @@ -1436,7 +1459,14 @@ private boolean checkSaveChanges(ImageData imageData) { return true; ProjectImageEntry entry = getProjectImageEntry(imageData); String name = entry == null ? ServerTools.getDisplayableImageName(imageData.getServer()) : entry.getImageName(); - var response = Dialogs.showYesNoCancelDialog("Save changes", "Save changes to " + name + "?"); + var owner = FXUtils.getWindow(getViewer().getView()); + var response = Dialogs.builder() + .title("Save changes") + .owner(owner) + .contentText("Save changes to " + name + "?") + .buttons(ButtonType.YES, ButtonType.NO, ButtonType.CANCEL) + .showAndWait() + .orElse(ButtonType.CANCEL); if (response == ButtonType.CANCEL) return false; if (response == ButtonType.NO) @@ -2116,8 +2146,9 @@ private static Action createPluginAction(final String name, final Class plugin, final String arg, final boolean doInteractive) throws Exception { + var imageData = getImageData(); if (doInteractive && plugin instanceof PathInteractivePlugin pluginInteractive) { - ParameterList params = pluginInteractive.getDefaultParameterList(getImageData()); + ParameterList params = pluginInteractive.getDefaultParameterList(imageData); // Update parameter list, if necessary if (arg != null) { Map map = GeneralTools.parseArgStringValues(arg); @@ -2134,7 +2165,7 @@ public boolean runPlugin(final PathPlugin plugin, final String ar pluginRunning.set(true); var runner = new PluginRunnerFX(this); @SuppressWarnings("unused") - var completed = plugin.runPlugin(runner, arg); + var completed = plugin.runPlugin(runner, imageData, arg); return !runner.isCancelled(); } finally { pluginRunning.set(false); @@ -2326,11 +2357,12 @@ public Stage getStage() { * Refresh the title bar in the main QuPath window. */ public void refreshTitle() { - if (Platform.isFxApplicationThread()) + if (Platform.isFxApplicationThread()) { titleBinding.invalidate(); - else + viewerManager.refreshTitles(); + } else Platform.runLater(() -> refreshTitle()); - } + } private StringBinding createTitleBinding() { return Bindings.createStringBinding(() -> createTitleFromCurrentImage(), @@ -2347,8 +2379,14 @@ private String createTitleFromCurrentImage() { return name; return name + " - " + getDisplayedImageName(imageData); } - - private String getDisplayedImageName(ImageData imageData) { + + /** + * Get the image name to display for a specified image. + * This can be used to determine a name to display in the title bar, for example. + * @param imageData + * @return + */ + public String getDisplayedImageName(ImageData imageData) { if (imageData == null) return null; var project = getProject(); @@ -2542,7 +2580,7 @@ public boolean closeViewer(final QuPathViewer viewer) { * * @param dialogTitle Name to use within any displayed dialog box. * @param viewer - * @return True if the viewer no longer contains an open image (either because it never did contain one, or + * @return true if the viewer no longer contains an open image (either because it never did contain one, or * the image was successfully closed), false otherwise (e.g. if the user thwarted the close request) */ private boolean requestToCloseViewer(final QuPathViewer viewer, final String dialogTitle) { diff --git a/qupath-gui-fx/src/main/java/qupath/lib/gui/actions/ViewerActions.java b/qupath-gui-fx/src/main/java/qupath/lib/gui/actions/ViewerActions.java index 0e37d5b9f..d41378b6a 100644 --- a/qupath-gui-fx/src/main/java/qupath/lib/gui/actions/ViewerActions.java +++ b/qupath-gui-fx/src/main/java/qupath/lib/gui/actions/ViewerActions.java @@ -57,11 +57,38 @@ public class ViewerActions { @ActionAccelerator("shortcut+alt+s") @ActionConfig("ViewerActions.synchronize") + public final Action TOGGLE_SYNCHRONIZE_VIEWERS; - + + @ActionConfig("ViewerActions.grid1x1") + @ActionIcon(PathIcons.VIEWER_GRID_1x1) + public final Action VIEWER_GRID_1x1; + + @ActionConfig("ViewerActions.grid1x2") + @ActionIcon(PathIcons.VIEWER_GRID_1x2) + public final Action VIEWER_GRID_1x2; + + @ActionConfig("ViewerActions.grid2x1") + @ActionIcon(PathIcons.VIEWER_GRID_2x1) + public final Action VIEWER_GRID_2x1; + + @ActionConfig("ViewerActions.grid2x2") + @ActionIcon(PathIcons.VIEWER_GRID_2x2) + public final Action VIEWER_GRID_2x2; + + @ActionConfig("ViewerActions.grid3x3") + @ActionIcon(PathIcons.VIEWER_GRID_3x3) + public final Action VIEWER_GRID_3x3; + @ActionConfig("ViewerActions.matchResolutions") public final Action MATCH_VIEWER_RESOLUTIONS; - + + @ActionConfig("ViewerActions.detachViewer") + public final Action DETACH_VIEWER; + + @ActionConfig("ViewerActions.attachViewer") + public final Action ATTACH_VIEWER; + private ViewerManager viewerManager; public ViewerActions(ViewerManager viewerManager) { @@ -73,7 +100,16 @@ public ViewerActions(ViewerManager viewerManager) { TOGGLE_SYNCHRONIZE_VIEWERS = ActionTools.createSelectableAction(viewerManager.synchronizeViewersProperty()); MATCH_VIEWER_RESOLUTIONS = new Action(e -> viewerManager.matchResolutions()); ZOOM_TO_FIT = ActionTools.createSelectableAction(viewerManager.zoomToFitProperty()); - + + VIEWER_GRID_1x1 = ActionTools.createAction(() -> viewerManager.setGridSize(1, 1)); + VIEWER_GRID_2x1 = ActionTools.createAction(() -> viewerManager.setGridSize(2, 1)); + VIEWER_GRID_1x2 = ActionTools.createAction(() -> viewerManager.setGridSize(1, 2)); + VIEWER_GRID_2x2 = ActionTools.createAction(() -> viewerManager.setGridSize(2, 2)); + VIEWER_GRID_3x3 = ActionTools.createAction(() -> viewerManager.setGridSize(3, 3)); + + DETACH_VIEWER = ActionTools.createAction(() -> viewerManager.detachActiveViewerFromGrid()); + ATTACH_VIEWER = ActionTools.createAction(() -> viewerManager.attachActiveViewerToGrid()); + ActionTools.getAnnotatedActions(this); } diff --git a/qupath-gui-fx/src/main/java/qupath/lib/gui/actions/menus/ObjectsMenuActions.java b/qupath-gui-fx/src/main/java/qupath/lib/gui/actions/menus/ObjectsMenuActions.java index 0e90f81f3..16cedc961 100644 --- a/qupath-gui-fx/src/main/java/qupath/lib/gui/actions/menus/ObjectsMenuActions.java +++ b/qupath-gui-fx/src/main/java/qupath/lib/gui/actions/menus/ObjectsMenuActions.java @@ -180,29 +180,36 @@ public class AnnotationActions { @ActionConfig("Action.Objects.Annotation.specify") public final Action SPECIFY_ANNOTATION = Commands.createSingleStageAction(() -> Commands.createSpecifyAnnotationDialog(qupath)); - + + @ActionAccelerator("shortcut+shift+a") @ActionConfig("Action.Objects.Annotation.fullImage") public final Action SELECT_ALL_ANNOTATION = qupath.createImageDataAction(imageData -> Commands.createFullImageAnnotation(qupath.getViewer())); public final Action SEP_5 = ActionTools.createSeparator(); - + + @ActionAccelerator("shortcut+shift+i") @ActionConfig("Action.Objects.Annotation.hierarchyInsert") public final Action INSERT_INTO_HIERARCHY = qupath.createImageDataAction(imageData -> Commands.insertSelectedObjectsInHierarchy(imageData)); - + + @ActionAccelerator("shortcut+shift+r") @ActionConfig("Action.Objects.Annotation.hierarchyResolve") public final Action RESOLVE_HIERARCHY = qupath.createImageDataAction(imageData -> Commands.promptToResolveHierarchy(imageData)); public final Action SEP_6 = ActionTools.createSeparator(); + @ActionAccelerator("shortcut+shift+t") @ActionConfig("Action.Objects.Annotation.transform") public final Action RIGID_OBJECT_EDITOR = qupath.createImageDataAction(imageData -> Commands.editSelectedAnnotation(qupath)); - + + @ActionAccelerator("shift+d") @ActionConfig("Action.Objects.Annotation.duplicate") public final Action ANNOTATION_DUPLICATE = qupath.createImageDataAction(imageData -> Commands.duplicateSelectedAnnotations(imageData)); + @ActionAccelerator("shortcut+shift+v") @ActionConfig("Action.Objects.Annotation.copyToCurrentPlane") public final Action ANNOTATION_COPY_TO_PLANE = qupath.createViewerAction(viewer -> Commands.copySelectedAnnotationsToCurrentPlane(viewer)); + @ActionAccelerator("shift+e") @ActionConfig("Action.Objects.Annotation.transferLast") public final Action TRANSFER_ANNOTATION = qupath.createImageDataAction(imageData -> qupath.getViewerManager().applyLastAnnotationToActiveViewer()); diff --git a/qupath-gui-fx/src/main/java/qupath/lib/gui/actions/menus/ViewMenuActions.java b/qupath-gui-fx/src/main/java/qupath/lib/gui/actions/menus/ViewMenuActions.java index 2f6fc14bc..9032f3740 100644 --- a/qupath-gui-fx/src/main/java/qupath/lib/gui/actions/menus/ViewMenuActions.java +++ b/qupath-gui-fx/src/main/java/qupath/lib/gui/actions/menus/ViewMenuActions.java @@ -163,14 +163,18 @@ public class Actions { public final MultitouchActions multitouchActions = new MultitouchActions(); } - - - - public class MultiviewActions { - - public final Action MULTIVIEW_SYNCHRONIZE_VIEWERS = viewerActions.TOGGLE_SYNCHRONIZE_VIEWERS; - - public final Action MULTIVIEW_MATCH_RESOLUTIONS = viewerActions.MATCH_VIEWER_RESOLUTIONS; + + public class MultiviewGridActions { + + public final Action MULTIVIEW_GRID_1x1 = viewerActions.VIEWER_GRID_1x1; + + public final Action MULTIVIEW_GRID_1x2 = viewerActions.VIEWER_GRID_1x2; + + public final Action MULTIVIEW_GRID_2x1 = viewerActions.VIEWER_GRID_2x1; + + public final Action MULTIVIEW_GRID_2x2 = viewerActions.VIEWER_GRID_2x2; + + public final Action MULTIVIEW_GRID_3x3 = viewerActions.VIEWER_GRID_3x3; public final Action SEP_00 = ActionTools.createSeparator(); @@ -181,7 +185,7 @@ public class MultiviewActions { public final Action MULTIVIEW_ADD_COLUMN = qupath.createViewerAction(viewer -> qupath.getViewerManager().addColumn(viewer)); public final Action SEP_01 = ActionTools.createSeparator(); - + @ActionConfig("Action.View.Multiview.removeRow") public final Action MULTIVIEW_REMOVE_ROW = qupath.createViewerAction(viewer -> qupath.getViewerManager().removeRow(viewer)); @@ -191,13 +195,35 @@ public class MultiviewActions { public final Action SEP_02 = ActionTools.createSeparator(); @ActionConfig("Action.View.Multiview.resetGridSize") - public final Action MULTIVIEW_RESET_GRID = qupath.createViewerAction(viewer -> qupath.getViewerManager().resetGridSize()); - - public final Action SEP_03 = ActionTools.createSeparator(); + public final Action MULTIVIEW_RESET_GRID = qupath.createViewerAction(viewer -> qupath.getViewerManager().resetGridSize()); + + } + + public class MultiviewActions { + + @ActionMenu("Action.View.Multiview.gridMenu") + public final MultiviewGridActions MULTIVIEW_GRID_ACTIONS = new MultiviewGridActions(); + + public final Action SEP_00 = ActionTools.createSeparator(); + + public final Action MULTIVIEW_SYNCHRONIZE_VIEWERS = viewerActions.TOGGLE_SYNCHRONIZE_VIEWERS; + public final Action MULTIVIEW_MATCH_RESOLUTIONS = viewerActions.MATCH_VIEWER_RESOLUTIONS; + + public final Action SEP_01 = ActionTools.createSeparator(); + @ActionConfig("Action.View.Multiview.closeViewer") public final Action MULTIVIEW_CLOSE_VIEWER = qupath.createViewerAction(viewer -> qupath.closeViewer(viewer)); - + + public final Action SEP_02 = ActionTools.createSeparator(); + + // Refined here to take the active viewer from QuPath itself + @ActionConfig("ViewerActions.detachViewer") + public final Action DETACH_VIEWER = qupath.createViewerAction(viewer -> qupath.getViewerManager().detachViewerFromGrid(viewer)); + + @ActionConfig("ViewerActions.attachViewer") + public final Action ATTACH_VIEWER = qupath.createViewerAction(viewer -> qupath.getViewerManager().attachViewerToGrid(viewer)); + } diff --git a/qupath-gui-fx/src/main/java/qupath/lib/gui/commands/LogViewerCommand.java b/qupath-gui-fx/src/main/java/qupath/lib/gui/commands/LogViewerCommand.java index 9285dee3b..f86017742 100644 --- a/qupath-gui-fx/src/main/java/qupath/lib/gui/commands/LogViewerCommand.java +++ b/qupath-gui-fx/src/main/java/qupath/lib/gui/commands/LogViewerCommand.java @@ -32,7 +32,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import qupath.fx.utils.FXUtils; -import qupath.lib.gui.prefs.SystemMenubar; +import qupath.lib.gui.prefs.SystemMenuBar; import qupath.ui.logviewer.ui.main.LogViewer; import qupath.ui.logviewer.ui.textarea.TextAreaLogViewer; @@ -62,7 +62,7 @@ public LogViewerCommand(Window parent) { this.parent = parent; try { LogViewer logviewer = new LogViewer(); - SystemMenubar.manageChildMenubar(logviewer.getMenubar()); + SystemMenuBar.manageChildMenuBar(logviewer.getMenubar()); // Fix cell size for better performance var table = logviewer.getTable(); table.setFixedCellSize(25); diff --git a/qupath-gui-fx/src/main/java/qupath/lib/gui/commands/ProjectMetadataEditorCommand.java b/qupath-gui-fx/src/main/java/qupath/lib/gui/commands/ProjectMetadataEditorCommand.java index 52fa7fa77..8e64ae2c0 100644 --- a/qupath-gui-fx/src/main/java/qupath/lib/gui/commands/ProjectMetadataEditorCommand.java +++ b/qupath-gui-fx/src/main/java/qupath/lib/gui/commands/ProjectMetadataEditorCommand.java @@ -58,7 +58,7 @@ import qupath.lib.gui.QuPathGUI; import qupath.fx.dialogs.Dialogs; import qupath.lib.gui.panes.ProjectBrowser; -import qupath.lib.gui.prefs.SystemMenubar; +import qupath.lib.gui.prefs.SystemMenuBar; import qupath.lib.projects.Project; import qupath.lib.projects.ProjectImageEntry; @@ -175,7 +175,7 @@ public static void showProjectMetadataEditor(Project project) { pane.setTop(menubar); pane.setCenter(table); // menubar.setUseSystemMenuBar(true); - SystemMenubar.manageChildMenubar(menubar); + SystemMenuBar.manageChildMenuBar(menubar); Dialog dialog = new Dialog<>(); var qupath = QuPathGUI.getInstance(); diff --git a/qupath-gui-fx/src/main/java/qupath/lib/gui/commands/ScriptInterpreter.java b/qupath-gui-fx/src/main/java/qupath/lib/gui/commands/ScriptInterpreter.java index 72f26cd76..2522dd428 100644 --- a/qupath-gui-fx/src/main/java/qupath/lib/gui/commands/ScriptInterpreter.java +++ b/qupath-gui-fx/src/main/java/qupath/lib/gui/commands/ScriptInterpreter.java @@ -88,8 +88,7 @@ import qupath.lib.gui.QuPathGUI; import qupath.fx.dialogs.Dialogs; import qupath.lib.gui.panes.ObjectTreeBrowser; -import qupath.lib.gui.prefs.PathPrefs; -import qupath.lib.gui.prefs.SystemMenubar; +import qupath.lib.gui.prefs.SystemMenuBar; import qupath.lib.gui.scripting.QPEx; import qupath.lib.gui.tools.WebViews; import qupath.lib.images.ImageData; @@ -537,7 +536,7 @@ else if (allNames.contains("javascript")) paneMasterDetail.setDetailNode(tabPane); pane.setTop(menuBar); pane.setCenter(paneMasterDetail); - SystemMenubar.manageChildMenubar(menuBar); + SystemMenuBar.manageChildMenuBar(menuBar); stage.setScene(new Scene(pane, 800, 600)); textAreaInput.requestFocus(); diff --git a/qupath-gui-fx/src/main/java/qupath/lib/gui/panes/ImageDetailsPane.java b/qupath-gui-fx/src/main/java/qupath/lib/gui/panes/ImageDetailsPane.java index 6bd607bc6..9a550e4a7 100644 --- a/qupath-gui-fx/src/main/java/qupath/lib/gui/panes/ImageDetailsPane.java +++ b/qupath-gui-fx/src/main/java/qupath/lib/gui/panes/ImageDetailsPane.java @@ -117,7 +117,7 @@ import qupath.lib.gui.prefs.PathPrefs; import qupath.lib.gui.prefs.PathPrefs.ImageTypeSetting; import qupath.fx.utils.GridPaneUtils; -import qupath.lib.gui.prefs.SystemMenubar; +import qupath.lib.gui.prefs.SystemMenuBar; import qupath.lib.gui.tools.GuiTools; import qupath.lib.images.ImageData; import qupath.lib.images.ImageData.ImageType; @@ -286,7 +286,7 @@ private void handleAssociatedImagesMouseClick(MouseEvent event) { Scene scene = new Scene(pane); pane.prefWidthProperty().bind(scene.widthProperty()); pane.prefHeightProperty().bind(scene.heightProperty()); - SystemMenubar.manageChildMenubar(menubar); + SystemMenuBar.manageChildMenuBar(menubar); // menubar.setUseSystemMenuBar(true); pane.setBackground(new Background(new BackgroundFill(Color.BLACK, null, null))); diff --git a/qupath-gui-fx/src/main/java/qupath/lib/gui/panes/PreferencePane.java b/qupath-gui-fx/src/main/java/qupath/lib/gui/panes/PreferencePane.java index aa0a71e26..624f72aef 100644 --- a/qupath-gui-fx/src/main/java/qupath/lib/gui/panes/PreferencePane.java +++ b/qupath-gui-fx/src/main/java/qupath/lib/gui/panes/PreferencePane.java @@ -73,7 +73,7 @@ import qupath.lib.gui.prefs.PathPrefs.ImageTypeSetting; import qupath.lib.gui.prefs.QuPathStyleManager; import qupath.lib.gui.prefs.QuPathStyleManager.StyleOption; -import qupath.lib.gui.prefs.SystemMenubar; +import qupath.lib.gui.prefs.SystemMenuBar; import qupath.lib.gui.tools.CommandFinderTools; import qupath.lib.gui.tools.CommandFinderTools.CommandBarDisplay; @@ -119,7 +119,7 @@ private void initializePane() { private PropertySheet createPropertySheet() { var factory = new PropertyEditorFactory(); factory.setReformatEnums( - SystemMenubar.SystemMenubarOption.class, + SystemMenuBar.SystemMenuBarOption.class, FontWeight.class, FontSize.class, LogLevel.class, @@ -222,8 +222,8 @@ public static class GeneralPreferences { @Pref(value = "Prefs.General.checkForUpdates", type = AutoUpdateType.class) public final ObjectProperty autoUpdate = PathPrefs.autoUpdateCheckProperty(); - @Pref(value = "Prefs.General.systemMenubar", type = SystemMenubar.SystemMenubarOption.class) - public final ObjectProperty systemMenubar = SystemMenubar.systemMenubarProperty(); + @Pref(value = "Prefs.General.systemMenubar", type = SystemMenuBar.SystemMenuBarOption.class) + public final ObjectProperty systemMenubar = SystemMenuBar.systemMenubarProperty(); @DoublePref("Prefs.General.maxMemory") public final DoubleProperty maxMemoryGB = PathPrefs.hasJavaPreferences() ? createMaxMemoryProperty() : null; diff --git a/qupath-gui-fx/src/main/java/qupath/lib/gui/panes/ProjectBrowser.java b/qupath-gui-fx/src/main/java/qupath/lib/gui/panes/ProjectBrowser.java index ac443d863..c5f93069e 100644 --- a/qupath-gui-fx/src/main/java/qupath/lib/gui/panes/ProjectBrowser.java +++ b/qupath-gui-fx/src/main/java/qupath/lib/gui/panes/ProjectBrowser.java @@ -1053,7 +1053,7 @@ private List getAllImageRows() { // If 'mask names' is ticked, shuffle the image list for less biased analyses var imageList = project.getImageList(); - var indices = IntStream.range(0, imageList.size()).boxed().toList(); + var indices = IntStream.range(0, imageList.size()).boxed().collect(Collectors.toCollection(ArrayList::new)); Collections.shuffle(indices); return indices.stream().map(index -> new ImageRow(imageList.get(index))).toList(); } diff --git a/qupath-gui-fx/src/main/java/qupath/lib/gui/prefs/PathPrefs.java b/qupath-gui-fx/src/main/java/qupath/lib/gui/prefs/PathPrefs.java index 13e7edc36..677a1a123 100644 --- a/qupath-gui-fx/src/main/java/qupath/lib/gui/prefs/PathPrefs.java +++ b/qupath-gui-fx/src/main/java/qupath/lib/gui/prefs/PathPrefs.java @@ -113,13 +113,13 @@ private static PreferenceManager createPreferenceManager() { * Legacy property used to specify whether the system menubar should be used for the main QuPath stage. * This should be bound bidirectionally to the corresponding property of any menubars created. * @return a bound boolean property, which is true whenever systemMenubarProperty() is set to ALL_WINDOWS. - * @deprecated use {@link SystemMenubar#systemMenubarProperty()} instead + * @deprecated use {@link SystemMenuBar#systemMenubarProperty()} instead */ @Deprecated public static BooleanProperty useSystemMenubarProperty() { if (!useSystemMenubar.isBound()) { logger.warn("PathPrefs.useSystemMenubarProperty() is deprecated - please use PathPrefs.systemMenubarProperty() instead"); - useSystemMenubar.bind(SystemMenubar.systemMenubarProperty().isEqualTo(SystemMenubar.SystemMenubarOption.ALL_WINDOWS)); + useSystemMenubar.bind(SystemMenuBar.systemMenubarProperty().isEqualTo(SystemMenuBar.SystemMenuBarOption.ALL_WINDOWS)); } return useSystemMenubar; } diff --git a/qupath-gui-fx/src/main/java/qupath/lib/gui/prefs/SystemMenuBar.java b/qupath-gui-fx/src/main/java/qupath/lib/gui/prefs/SystemMenuBar.java new file mode 100644 index 000000000..46bedfae7 --- /dev/null +++ b/qupath-gui-fx/src/main/java/qupath/lib/gui/prefs/SystemMenuBar.java @@ -0,0 +1,201 @@ +/*- + * #%L + * This file is part of QuPath. + * %% + * Copyright (C) 2023 QuPath developers, The University of Edinburgh + * %% + * QuPath is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * QuPath is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QuPath. If not, see . + * #L% + */ + +package qupath.lib.gui.prefs; + +import javafx.application.Platform; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.value.ObservableValue; +import javafx.scene.control.MenuBar; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.Set; +import java.util.WeakHashMap; + +/** + * Helper class for managing {@link MenuBar#useSystemMenuBarProperty()} values based upon a property value. + *

+ * The original plan was to make it easier to control if all windows, the main window only, or no windows should + * use the system menubar. + *

+ *

+ * Alas, that doesn't work well - at least on macOS. + * If a system menubar is set for the main window only, then its accelerators are still triggered even when + * another window with a non-system menubar is active. + *

+ *

+ * For this reason, there is no option to use the system menubar for the main window only. + * This will be reinstated in the future, if a workaround can be found. + *

+ * + * @since v0.5.0 + * @implNote Currently, this avoids binding to the MenuBar's property directly, as that would require a bidirectional + * binding. + */ +public class SystemMenuBar { + + private static final Logger logger = LoggerFactory.getLogger(SystemMenuBar.class); + + /** + * Enum specifying when and where the system menubar should be used. + * This matters whenever the system menubar differs from the regular JavaFX behavior of adding a menubar to the + * top of every window, e.g. on macOS where the menubar is generally at the top of the screen. + */ + public enum SystemMenuBarOption { + /** + * Use the system menubar for all windows. + */ + ALL_WINDOWS, +// /** +// * Use the system menubar for the main window only. +// */ +// MAIN_WINDOW, + /** + * Don't use the system menubar for any windows. + */ + NEVER + } + + private static BooleanProperty overrideSystemMenuBar = new SimpleBooleanProperty(false); + + private static Set mainMenuBars = Collections.newSetFromMap(new WeakHashMap<>()); + + private static Set childMenuBars = Collections.newSetFromMap(new WeakHashMap<>()); + + private static ObjectProperty systemMenuBar = PathPrefs.createPersistentPreference( + "systemMenubar", SystemMenuBarOption.ALL_WINDOWS, SystemMenuBarOption.class); + + static { + systemMenuBar.addListener(SystemMenuBar::updateMenuBars); + overrideSystemMenuBar.addListener(SystemMenuBar::updateOverrideMenubars); + } + + private static void updateOverrideMenubars(ObservableValue value, Boolean old, Boolean newValue) { + updateMenuBars(systemMenuBar, systemMenuBar.get(), systemMenuBar.get()); + } + + private static void updateMenuBars(ObservableValue value, SystemMenuBarOption old, SystemMenuBarOption newValue) { + if (Platform.isFxApplicationThread()) { + for (var mb : mainMenuBars) { + updateMainMenuBar(mb, newValue); + } + for (var mb : childMenuBars) { + updateChildMenuBar(mb, newValue); + } + } else { + Platform.runLater(() -> updateMenuBars(value, old, newValue)); + } + } + + private static void updateMainMenuBar(MenuBar menuBar, SystemMenuBarOption option) { + if (menuBar.useSystemMenuBarProperty().isBound()) + logger.warn("MenuBar.useSystemMenuBarProperty() is already bound for {}", menuBar); + else if (overrideSystemMenuBar.get()) + menuBar.setUseSystemMenuBar(false); + else + menuBar.setUseSystemMenuBar(option == SystemMenuBarOption.ALL_WINDOWS); +// menuBar.setUseSystemMenuBar(option == SystemMenuBarOption.MAIN_WINDOW || option == SystemMenuBarOption.ALL_WINDOWS); + } + + private static void updateChildMenuBar(MenuBar menuBar, SystemMenuBarOption option) { + if (menuBar.useSystemMenuBarProperty().isBound()) + logger.warn("MenuBar.useSystemMenuBarProperty() is already bound for {}", menuBar); + else if (overrideSystemMenuBar.get()) + menuBar.setUseSystemMenuBar(false); + else + menuBar.setUseSystemMenuBar(option == SystemMenuBarOption.ALL_WINDOWS); + } + + /** + * Property used to specify whether the system menubar should be used for the main QuPath stage. + * This should be bound bidirectionally to the corresponding property of any menubars created. + * @return + * @since v0.5.0 + */ + public static ObjectProperty systemMenubarProperty() { + return systemMenuBar; + } + + /** + * Request that a menubar is managed as a main menubar. + * This means it is treated as a system menubar if #systemMenubarProperty() is set to ALL_WINDOWS or MAIN_WINDOW. + * @param menuBar + */ + public static void manageMainMenuBar(MenuBar menuBar) { + mainMenuBars.add(menuBar); + updateMainMenuBar(menuBar, systemMenuBar.get()); + } + + /** + * Request that a menubar is managed as a child menubar. + * This means it is treated as a system menubar if #systemMenubarProperty() is set to ALL_WINDOWS only. + * @param menuBar + */ + public static void manageChildMenuBar(MenuBar menuBar) { + childMenuBars.add(menuBar); + updateChildMenuBar(menuBar, systemMenuBar.get()); + } + + /** + * Do not manage the system menubar status for the given menubar. + * @param menuBar + */ + public static void unmanageMenuBar(MenuBar menuBar) { + mainMenuBars.remove(menuBar); + childMenuBars.remove(menuBar); + } + + /** + * Property requesting that the system menubar should never be used for managed menubars. + * This is useful if another window requires access to the system menubar. + * In particular, it helps in a macOS application if a Java AWT window is being used (e.g. ImageJ), + * since the conflicting attempts to get the system menubar can cause confusing behavior. + * @return + */ + public static BooleanProperty overrideSystemMenuBarProperty() { + return overrideSystemMenuBar; + } + + /** + * Get the current value of the override property, which specifies whether the system menubar should not be used + * by any window - no matter what the value of {@link #systemMenubarProperty()}. + * @return + * @see #overrideSystemMenuBarProperty() + */ + public static boolean getOverrideSystemMenuBar() { + return overrideSystemMenuBar.get(); + } + + /** + * Set the current value of the override property, which optionally specifies whether the system menubar should not + * be used by any window - no matter what the value of {@link #systemMenubarProperty()}. + * @param doOverride + * @see #overrideSystemMenuBarProperty() + */ + public static void setOverrideSystemMenuBar(boolean doOverride) { + overrideSystemMenuBar.set(doOverride); + } + +} diff --git a/qupath-gui-fx/src/main/java/qupath/lib/gui/prefs/SystemMenubar.java b/qupath-gui-fx/src/main/java/qupath/lib/gui/prefs/SystemMenubar.java deleted file mode 100644 index 7c86a66a3..000000000 --- a/qupath-gui-fx/src/main/java/qupath/lib/gui/prefs/SystemMenubar.java +++ /dev/null @@ -1,129 +0,0 @@ -/*- - * #%L - * This file is part of QuPath. - * %% - * Copyright (C) 2023 QuPath developers, The University of Edinburgh - * %% - * QuPath is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * QuPath is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with QuPath. If not, see . - * #L% - */ - -package qupath.lib.gui.prefs; - -import javafx.beans.property.ObjectProperty; -import javafx.beans.value.ObservableValue; -import javafx.scene.control.MenuBar; - -import java.util.Collections; -import java.util.Set; -import java.util.WeakHashMap; - -/** - * Helper class for managing {@link MenuBar#useSystemMenuBarProperty()} values based upon a property value. - * This makes it easier to control if all windows, the main window only, or no windows should use the system menubar. - * - * @since v0.5.0 - * @implNote Currently, this avoids binding to the MenuBar's property directly, as that would require a bidirectional - * binding. - */ -public class SystemMenubar { - - /** - * Enum specifying when and where the system menubar should be used. - * This matters whenever the system menubar differs from the regular JavaFX behavior of adding a menubar to the - * top of every window, e.g. on macOS where the menubar is generally at the top of the screen. - */ - public enum SystemMenubarOption { - /** - * Use the system menubar for all windows. - */ - ALL_WINDOWS, - /** - * Use the system menubar for the main window only. - */ - MAIN_WINDOW, - /** - * Don't use the system menubar for any windows. - */ - NEVER - } - - private static Set mainMenubars = Collections.newSetFromMap(new WeakHashMap<>()); - - private static Set childMenubars = Collections.newSetFromMap(new WeakHashMap<>()); - - private static ObjectProperty systemMenubar = PathPrefs.createPersistentPreference( - "systemMenubar", SystemMenubarOption.MAIN_WINDOW, SystemMenubarOption.class); - - static { - systemMenubar.addListener(SystemMenubar::updateMenubars); - } - - private static void updateMenubars(ObservableValue value, SystemMenubarOption old, SystemMenubarOption newValue) { - for (var mb : mainMenubars) { - updateMainMenubar(mb, newValue); - } - for (var mb : childMenubars) { - updateChildMenubar(mb, newValue); - } - } - - private static void updateMainMenubar(MenuBar menuBar, SystemMenubarOption option) { - menuBar.setUseSystemMenuBar(option == SystemMenubarOption.MAIN_WINDOW || option == SystemMenubarOption.ALL_WINDOWS); - } - - private static void updateChildMenubar(MenuBar menuBar, SystemMenubarOption option) { - menuBar.setUseSystemMenuBar(option == SystemMenubarOption.ALL_WINDOWS); - } - - /** - * Property used to specify whether the system menubar should be used for the main QuPath stage. - * This should be bound bidirectionally to the corresponding property of any menubars created. - * @return - * @since v0.5.0 - */ - public static ObjectProperty systemMenubarProperty() { - return systemMenubar; - } - - /** - * Request that a menubar is managed as a main menubar. - * This means it is treated as a system menubar if #systemMenubarProperty() is set to ALL_WINDOWS or MAIN_WINDOW. - * @param menuBar - */ - public static void manageMainMenubar(MenuBar menuBar) { - mainMenubars.add(menuBar); - updateMainMenubar(menuBar, systemMenubar.get()); - } - - /** - * Request that a menubar is managed as a child menubar. - * This means it is treated as a system menubar if #systemMenubarProperty() is set to ALL_WINDOWS only. - * @param menuBar - */ - public static void manageChildMenubar(MenuBar menuBar) { - childMenubars.add(menuBar); - updateChildMenubar(menuBar, systemMenubar.get()); - } - - /** - * Do not manage the system menubar status for the given menubar. - * @param menuBar - */ - public static void unmanageMenubar(MenuBar menuBar) { - mainMenubars.remove(menuBar); - childMenubars.remove(menuBar); - } - -} diff --git a/qupath-gui-fx/src/main/java/qupath/lib/gui/scripting/DefaultScriptEditor.java b/qupath-gui-fx/src/main/java/qupath/lib/gui/scripting/DefaultScriptEditor.java index 54ae2b35e..ce6b823a4 100644 --- a/qupath-gui-fx/src/main/java/qupath/lib/gui/scripting/DefaultScriptEditor.java +++ b/qupath-gui-fx/src/main/java/qupath/lib/gui/scripting/DefaultScriptEditor.java @@ -121,7 +121,7 @@ import qupath.lib.gui.dialogs.ProjectDialogs; import qupath.lib.gui.logging.LogManager; import qupath.lib.gui.prefs.PathPrefs; -import qupath.lib.gui.prefs.SystemMenubar; +import qupath.lib.gui.prefs.SystemMenuBar; import qupath.lib.gui.scripting.languages.DefaultScriptLanguage; import qupath.lib.gui.scripting.languages.GroovyLanguage; import qupath.lib.gui.scripting.languages.HtmlRenderer; @@ -905,7 +905,7 @@ public ListCell call(ListView list) { dialog.getScene().setOnDragDropped(dragDropListener); splitMain.setDividerPosition(0, 0.25); - SystemMenubar.manageChildMenubar(menubar); + SystemMenuBar.manageChildMenuBar(menubar); // menubar.setUseSystemMenuBar(true); updateUndoActionState(); updateCutCopyActionState(); diff --git a/qupath-gui-fx/src/main/java/qupath/lib/gui/scripting/QPEx.java b/qupath-gui-fx/src/main/java/qupath/lib/gui/scripting/QPEx.java index c6a0fe9ec..6e1478bc8 100644 --- a/qupath-gui-fx/src/main/java/qupath/lib/gui/scripting/QPEx.java +++ b/qupath-gui-fx/src/main/java/qupath/lib/gui/scripting/QPEx.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2022 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -231,8 +231,8 @@ public static boolean runPlugin(final String className, final ImageData image // TODO: Give potential of passing a plugin runner var qupath = getQuPath(); if (isBatchMode() || imageData != qupath.getImageData()) { - runner = new CommandLinePluginRunner(imageData); - completed = plugin.runPlugin(runner, args); + runner = new CommandLinePluginRunner(); + completed = plugin.runPlugin(runner, imageData, args); cancelled = runner.isCancelled(); } else { diff --git a/qupath-gui-fx/src/main/java/qupath/lib/gui/tma/TMADataIO.java b/qupath-gui-fx/src/main/java/qupath/lib/gui/tma/TMADataIO.java index 7eb6d478c..c34c24a55 100644 --- a/qupath-gui-fx/src/main/java/qupath/lib/gui/tma/TMADataIO.java +++ b/qupath-gui-fx/src/main/java/qupath/lib/gui/tma/TMADataIO.java @@ -4,7 +4,7 @@ * %% * Copyright (C) 2014 - 2016 The Queen's University of Belfast, Northern Ireland * Contact: IP Management (ipmanagement@qub.ac.uk) - * Copyright (C) 2018 - 2020 QuPath developers, The University of Edinburgh + * Copyright (C) 2018 - 2023 QuPath developers, The University of Edinburgh * %% * QuPath is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -59,7 +59,6 @@ import qupath.lib.objects.hierarchy.TMAGrid; import qupath.lib.plugins.AbstractPlugin; import qupath.lib.plugins.CommandLinePluginRunner; -import qupath.lib.plugins.PluginRunner; import qupath.lib.regions.RegionRequest; import qupath.lib.roi.interfaces.ROI; @@ -199,11 +198,9 @@ public static void writeTMAData(File file, final ImageData imageD .downsamples(downsample) .build(); ExportCoresPlugin plugin = new ExportCoresPlugin(dirData, renderedImageServer, downsample, coreExt); - PluginRunner runner; var qupath = QuPathGUI.getInstance(); if (qupath == null || qupath.getImageData() != imageData) { - runner = new CommandLinePluginRunner<>(imageData); - plugin.runPlugin(runner, null); + plugin.runPlugin(new CommandLinePluginRunner(), imageData,null); } else { try { qupath.runPlugin(plugin, null, false); @@ -338,8 +335,8 @@ protected boolean parseArgument(ImageData imageData, String arg) } @Override - protected Collection getParentObjects(PluginRunner runner) { - return PathObjectTools.getTMACoreObjects(getHierarchy(runner), true); + protected Collection getParentObjects(ImageData imageData) { + return PathObjectTools.getTMACoreObjects(imageData.getHierarchy(), true); } @Override diff --git a/qupath-gui-fx/src/main/java/qupath/lib/gui/tma/TMASummaryViewer.java b/qupath-gui-fx/src/main/java/qupath/lib/gui/tma/TMASummaryViewer.java index cbcc967bd..896770f0d 100644 --- a/qupath-gui-fx/src/main/java/qupath/lib/gui/tma/TMASummaryViewer.java +++ b/qupath-gui-fx/src/main/java/qupath/lib/gui/tma/TMASummaryViewer.java @@ -146,7 +146,7 @@ import qupath.lib.gui.measure.ObservableMeasurementTableData; import qupath.lib.gui.measure.PathTableData; import qupath.lib.gui.prefs.PathPrefs; -import qupath.lib.gui.prefs.SystemMenubar; +import qupath.lib.gui.prefs.SystemMenuBar; import qupath.lib.gui.tma.TMAEntries.TMAEntry; import qupath.lib.gui.tma.TMAEntries.TMAObjectEntry; import qupath.lib.gui.tools.GuiTools; @@ -421,7 +421,7 @@ private void initialize() { BorderPane pane = new BorderPane(); pane.setTop(menuBar); - SystemMenubar.manageMainMenubar(menuBar); + SystemMenuBar.manageMainMenuBar(menuBar); // menuBar.setUseSystemMenuBar(true); // Create options diff --git a/qupath-gui-fx/src/main/java/qupath/lib/gui/tools/GuiTools.java b/qupath-gui-fx/src/main/java/qupath/lib/gui/tools/GuiTools.java index bb100d079..9864c68ed 100644 --- a/qupath-gui-fx/src/main/java/qupath/lib/gui/tools/GuiTools.java +++ b/qupath-gui-fx/src/main/java/qupath/lib/gui/tools/GuiTools.java @@ -1240,7 +1240,7 @@ public static Menu createRecentItemsMenu(String menuTitle, ObservableList r * @param supportedParents collection of valid parent objects * @return */ - public static boolean promptForParentObjects(final String name, final ImageData imageData, final boolean includeSelected, final Collection> supportedParents) { + public static boolean promptForParentObjects(final String name, final ImageData imageData, final boolean includeSelected, final Collection> supportedParents) { PathObjectHierarchy hierarchy = imageData == null ? null : imageData.getHierarchy(); if (hierarchy == null) diff --git a/qupath-gui-fx/src/main/java/qupath/lib/gui/tools/IconFactory.java b/qupath-gui-fx/src/main/java/qupath/lib/gui/tools/IconFactory.java index c2ecf687a..75eb59d00 100644 --- a/qupath-gui-fx/src/main/java/qupath/lib/gui/tools/IconFactory.java +++ b/qupath-gui-fx/src/main/java/qupath/lib/gui/tools/IconFactory.java @@ -26,6 +26,8 @@ import java.awt.geom.AffineTransform; import java.awt.geom.Path2D; import java.awt.geom.PathIterator; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.WeakHashMap; import java.util.function.IntFunction; @@ -226,6 +228,10 @@ static IntFunction showNamesIcon() { return i -> new DuplicatableNode(() -> drawShowNamesIcon(i)); } + static IntFunction createViewerGridIcon(int rows, int cols) { + return i -> new DuplicatableNode(() -> drawViewerGridIcon(i, rows, cols)); + } + } @@ -300,6 +306,12 @@ public enum PathIcons { ACTIVE_SERVER(IconSuppliers.icoMoon('\ue915', ColorTools TABLE(IconSuppliers.icoMoon('\ue91a')), TMA_GRID(IconSuppliers.icoMoon('\ue91b', PathPrefs.colorTMAProperty())), + VIEWER_GRID_1x1(IconSuppliers.createViewerGridIcon(1, 1)), + VIEWER_GRID_1x2(IconSuppliers.createViewerGridIcon(1, 2)), + VIEWER_GRID_2x1(IconSuppliers.createViewerGridIcon(2, 1)), + VIEWER_GRID_2x2(IconSuppliers.createViewerGridIcon(2, 2)), + VIEWER_GRID_3x3(IconSuppliers.createViewerGridIcon(3, 3)), + WAND_TOOL(IconSuppliers.icoMoon('\ue91c', PathPrefs.colorDefaultObjectsProperty())), WARNING(IconSuppliers.fontAwesome(FontAwesome.Glyph.WARNING)), @@ -535,6 +547,46 @@ private static Node drawPixelClassificationIcon(int size) { return label; } + + private static Node drawViewerGridIcon(int size, int rows, int cols) { + double pad = 2.0; + List nodes = new ArrayList<>(); + double h = (size - pad*2) / rows; + double w = (size - pad*2) / cols; + + var rect = new Rectangle(pad, pad, size-pad*2, size-pad*2); + double arcSize = size / 6.0; + rect.setArcHeight(arcSize); + rect.setArcWidth(arcSize); + styleIconShape(rect); + nodes.add(rect); + + for (int r = 1; r < rows; r++) { + double y = pad + r * h; + var line = new Line(pad, y, size - pad, y); + styleIconShape(line); + nodes.add(line); + } + + for (int c = 1; c < cols; c++) { + double x = pad + c * w; + var line = new Line(x, pad, x, size - pad); + styleIconShape(line); + nodes.add(line); + } + + var group = wrapInGroup(size, nodes.toArray(Node[]::new)); + group.getStyleClass().add("qupath-icon"); + return group; + } + + private static void styleIconShape(Shape shape) { + shape.setFill(Color.TRANSPARENT); + shape.setStrokeWidth(1.0); + shape.setSmooth(true); + shape.setStyle("-fx-stroke: -fx-text-fill;"); + } + private static Node drawSelectionModeIcon(int size) { var text = new Text("S"); diff --git a/qupath-gui-fx/src/main/java/qupath/lib/gui/viewer/ViewerManager.java b/qupath-gui-fx/src/main/java/qupath/lib/gui/viewer/ViewerManager.java index 6cbc3af33..b9ae72f6f 100644 --- a/qupath-gui-fx/src/main/java/qupath/lib/gui/viewer/ViewerManager.java +++ b/qupath-gui-fx/src/main/java/qupath/lib/gui/viewer/ViewerManager.java @@ -31,11 +31,19 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.WeakHashMap; - +import java.util.stream.Collectors; + +import javafx.beans.binding.Bindings; +import javafx.beans.binding.StringBinding; +import javafx.scene.Scene; +import javafx.scene.layout.BorderPane; +import javafx.stage.Stage; +import javafx.stage.WindowEvent; import org.controlsfx.control.action.Action; import org.controlsfx.control.action.ActionUtils; import org.slf4j.Logger; @@ -60,7 +68,6 @@ import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.SplitPane; import javafx.scene.control.ToggleGroup; -import javafx.scene.control.SplitPane.Divider; import javafx.scene.effect.DropShadow; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; @@ -74,6 +81,7 @@ import javafx.scene.shape.Rectangle; import javafx.util.Duration; import jfxtras.scene.menu.CirclePopupMenu; +import qupath.fx.utils.FXUtils; import qupath.lib.common.GeneralTools; import qupath.lib.gui.QuPathGUI; import qupath.lib.gui.ToolManager; @@ -135,6 +143,9 @@ public class ViewerManager implements QuPathViewerListener { private Map lastViewerPosition = new WeakHashMap<>(); + // Hacky solution to needing a mechanism to refresh the titles of detached viewers + private BooleanProperty refreshTitleProperty = new SimpleBooleanProperty(); + private ViewerManager(final QuPathGUI qupath) { this.qupath = qupath; } @@ -147,7 +158,14 @@ private ViewerManager(final QuPathGUI qupath) { public static ViewerManager create(final QuPathGUI qupath) { return new ViewerManager(qupath); } - + + /** + * Request that viewers refresh their titles. + * This is only really needed for detached viewers, so that they are notified of any changes to the image name. + */ + public void refreshTitles() { + refreshTitleProperty.set(!refreshTitleProperty.get()); + } /** * Get an observable list of viewers. @@ -337,20 +355,20 @@ public boolean removeRow(final QuPathViewer viewer) { // if (viewer.getServer() != null) // System.err.println(viewer.getServer().getShortServerName()); // Note: These are the internal row numbers... these don't necessarily match with the displayed row (?) - int row = splitPaneGrid.getRow(viewer.getView()); + int row = splitPaneGrid.getRow(viewer); if (row < 0) { // Shouldn't occur... - Dialogs.showErrorMessage("Multiview error", "Cannot find " + viewer + " in the grid!"); + Dialogs.showErrorMessage("Multiview", "Cannot find " + viewer + " in the grid!"); return false; } if (splitPaneGrid.nRows() == 1) { - Dialogs.showErrorMessage("Close row error", "The last row can't be removed!"); + Dialogs.showWarningNotification("Close row", "The last row can't be removed!"); return false; } int nOpen = splitPaneGrid.countOpenViewersForRow(row); if (nOpen > 0) { - Dialogs.showErrorMessage("Close row error", "Please close all open viewers in the selected row, then try again"); + Dialogs.showWarningNotification("Close row", "Please close all open viewers in the selected row, then try again"); return false; } splitPaneGrid.removeRow(row); @@ -369,7 +387,8 @@ private void refreshViewerList() { // Easiest way is to check for a scene Iterator iter = viewers.iterator(); while (iter.hasNext()) { - if (iter.next().getView().getScene() == null) + var view = iter.next().getView(); + if (view.getScene() == null) iter.remove(); } } @@ -389,13 +408,13 @@ public boolean removeColumn(final QuPathViewer viewer) { } if (splitPaneGrid.nCols() == 1) { - Dialogs.showErrorMessage("Close row error", "The last row can't be removed!"); + Dialogs.showWarningNotification("Close column", "The last columns can't be removed!"); return false; } int nOpen = splitPaneGrid.countOpenViewersForColumn(col); if (nOpen > 0) { - Dialogs.showErrorMessage("Close column error", "Please close all open viewers in selected column, then try again"); + Dialogs.showWarningNotification("Close column", "Please close all open viewers in selected column, then try again"); // DisplayHelpers.showErrorMessage("Close column error", "Please close all open viewers in column " + col + ", then try again"); return false; } @@ -407,6 +426,81 @@ public boolean removeColumn(final QuPathViewer viewer) { } + /** + * Set the grid to have a specific number of rows and columns. + * @param nRows + * @param nCols + * @return + */ + public boolean setGridSize(int nRows, int nCols) { + if (nRows < 1 || nCols < 1) { + Dialogs.showErrorMessage("Multiview grid", "There must be at least one viewer in the grid!"); + return false; + } + // Easiest case - no resizing to do + if (nRows == splitPaneGrid.nRows() && nCols == splitPaneGrid.nCols()) { + logger.warn("Viewer grid is already {} x {} - nothing to change!", nRows, nCols); + return true; + } + // Get all the open viewers currently in the grid & check it fits with what we want + refreshViewerList(); + + var openViewers = getAllViewers().stream().filter( + v -> !splitPaneGrid.isDetached(v) && v.hasServer()).toList(); + if (openViewers.size() > nRows * nCols) { + Dialogs.showWarningNotification("Multiview grid", "There are too many viewers open! Please close some, then set the grid size."); + return false; + } + // Adding is easy too + while (splitPaneGrid.nRows() < nRows) + splitPaneGrid.addRow(splitPaneGrid.nRows()); + while (splitPaneGrid.nCols() < nCols) + splitPaneGrid.addColumn(splitPaneGrid.nCols()); + if (nRows == splitPaneGrid.nRows() && nCols == splitPaneGrid.nCols()) { + return true; + } + // Removing is trickier - we first need to move any open viewers to the top-left, + // replacing any closed viewers we find there + var activeViewer = getActiveViewer(); + var allViewers = getAllViewers(); + var closedViewers = allViewers.stream().filter( + v -> !splitPaneGrid.isDetached(v) + && !v.hasServer() + && splitPaneGrid.getRow(v) < nRows + && splitPaneGrid.getColumn(v) < nCols) + .collect(Collectors.toCollection(ArrayList::new)); + for (var viewer : openViewers) { + var view = viewer.getView(); + int r = splitPaneGrid.getRow(view); + int c = splitPaneGrid.getColumn(view); + if (r >= nRows || c >= nCols) { + var nextClosedViewer = closedViewers.remove(0); + int rClosed = splitPaneGrid.getRow(nextClosedViewer); + int cClosed = splitPaneGrid.getColumn(nextClosedViewer); + splitPaneGrid.splitPaneRows.get(rClosed).getItems().set(cClosed, view); + viewers.remove(nextClosedViewer); + } + } + while (splitPaneGrid.nRows() > nRows) { + splitPaneGrid.removeRow(splitPaneGrid.nRows()-1); + } + while (splitPaneGrid.nCols() > nCols) { + splitPaneGrid.removeColumn(splitPaneGrid.nCols()-1); + } + refreshViewerList(); + if (activeViewer != null && viewers.contains(activeViewer)) + setActiveViewer(activeViewer); + else if (!viewers.isEmpty()) + setActiveViewer(viewers.get(0)); + else + logger.warn("No viewers remaining, cannot set active viewer"); + + // Distribute the divider positions + resetGridSize(); + return true; + } + + public void addRow(final QuPathViewer viewer) { splitViewer(viewer, false); splitPaneGrid.resetGridSize(); @@ -423,12 +517,13 @@ public void splitViewer(final QuPathViewer viewer, final boolean splitVertical) return; if (splitVertical) { - splitPaneGrid.addColumn(splitPaneGrid.getColumn(viewer.getView())); + splitPaneGrid.addColumn(splitPaneGrid.getColumn(viewer.getView())+1); } else { - splitPaneGrid.addRow(splitPaneGrid.getRow(viewer.getView())); + splitPaneGrid.addRow(splitPaneGrid.getRow(viewer.getView())+1); } } + public void resetGridSize() { splitPaneGrid.resetGridSize(); } @@ -743,11 +838,54 @@ private void setupViewer(final QuPathViewerPlus viewer) { } } }); - } + /** + * Detach the currently active viewer from the viewer grid, if possible. + */ + public void detachActiveViewerFromGrid() { + detachViewerFromGrid(getActiveViewer()); + } + + /** + * Insert the currently active viewer back into the viewer grid. + * @see #attachViewerToGrid(QuPathViewer) + */ + public void attachActiveViewerToGrid() { + attachViewerToGrid(getActiveViewer()); + } + + + /** + * Detach the specified viewer from the viewer grid, if possible. + * This will remove the viewer from the grid, and create a new window to contain it. + * @param viewer + * @see #detachViewerFromGrid(QuPathViewer) + */ + public void detachViewerFromGrid(QuPathViewer viewer) { + if (viewer == null) + Dialogs.showWarningNotification("Attach viewer", "Viewer is null - cannot detach from the viewer grid"); + else if (splitPaneGrid.isDetached(viewer)) + Dialogs.showWarningNotification("Attach viewer", "Viewer is already detached from the viewer grid"); + else + splitPaneGrid.detachViewer(viewer); + } + /** + * Attach the specified viewer to the viewer grid, if possible. + * It will be inserted in place of the first available empty viewer slot. + * If no empty slots are available, an error will be shown. + * @param viewer + */ + public void attachViewerToGrid(QuPathViewer viewer) { + if (viewer == null) + Dialogs.showWarningNotification("Attach viewer", "Viewer is null - cannot attach to the viewer grid"); + else if (splitPaneGrid.isDetached(viewer)) + splitPaneGrid.attachViewer(viewer); + else + Dialogs.showWarningNotification("Attach viewer", "Viewer can't be added to the viewer grid"); + } @@ -772,25 +910,24 @@ SplitPane getMainSplitPane() { void addRow(final int position) { - SplitPane splitRow = new SplitPane(); - splitRow.setOrientation(Orientation.HORIZONTAL); + // The new row we will add + SplitPane newRow = new SplitPane(); + newRow.setOrientation(Orientation.HORIZONTAL); // For now, we create a row with the same number of columns in every row // Create viewers & bind dividers - splitRow.getItems().clear(); + newRow.getItems().clear(); SplitPane firstRow = splitPaneRows.get(0); - splitRow.getItems().add(createViewer().getView()); - for (int i = 0; i < firstRow.getDividers().size(); i++) { - splitRow.getItems().add(createViewer().getView()); + for (int i = 0; i < firstRow.getItems().size(); i++) { + newRow.getItems().add(createViewer().getView()); } - // Ensure the new divider takes up half the space - double lastDividerPosition = position == 0 ? 0 : splitPaneMain.getDividers().get(position-1).getPosition(); - double nextDividerPosition = position >= splitPaneRows.size()-1 ? 1 : splitPaneMain.getDividers().get(position).getPosition(); - splitPaneRows.add(position, splitRow); - splitPaneMain.getItems().add(position+1, splitRow); - splitPaneMain.setDividerPosition(position, (lastDividerPosition + nextDividerPosition)/2); + // Insert the row + splitPaneRows.add(position, newRow); + splitPaneMain.getItems().add(position, newRow); + // Redistribute the positions & ensure column dividers bind to the first row + resetDividers(splitPaneMain); refreshDividerBindings(); } @@ -880,22 +1017,22 @@ void refreshDividerBindings() { void addColumn(final int position) { - SplitPane firstRow = splitPaneRows.get(0); - double lastDividerPosition = position == 0 ? 0 : firstRow.getDividers().get(position-1).getPosition(); - double nextDividerPosition = position >= firstRow.getItems().size()-1 ? 1 : firstRow.getDividers().get(position).getPosition(); - - firstRow.getItems().add(position+1, createViewer().getView()); - Divider firstDivider = firstRow.getDividers().get(position); - firstDivider.setPosition((lastDividerPosition + nextDividerPosition)/2); - for (int i = 1; i < splitPaneRows.size(); i++) { - SplitPane splitRow = splitPaneRows.get(i); - splitRow.getItems().add(position+1, createViewer().getView()); + // Add a new column at the same position to each row + for (var splitRow : splitPaneRows) { + splitRow.getItems().add(position, createViewer().getView()); } + // Redistribute the positions & ensure column dividers bind to the first row + resetDividers(splitPaneRows.get(0)); refreshDividerBindings(); } + public int getRow(final QuPathViewer viewer) { + return getRow(viewer.getView()); + } + + public int getRow(final Node node) { int count = 0; for (SplitPane row : splitPaneRows) { @@ -907,6 +1044,10 @@ public int getRow(final Node node) { return -1; } + public int getColumn(final QuPathViewer viewer) { + return getColumn(viewer.getView()); + } + public int getColumn(final Node node) { for (SplitPane row : splitPaneRows) { int ind = row.getItems().indexOf(node); @@ -924,8 +1065,115 @@ public int nCols() { return splitPaneRows.get(0).getDividers().size() + 1; } + + public boolean isDetached(QuPathViewer viewer) { + return getRow(viewer) < 0; + } + + public boolean attachViewer(QuPathViewer viewer) { + var closedViewer = getAllViewers() + .stream() + .filter(v -> !v.hasServer() && !isDetached(v)) + .sorted(Comparator.comparingInt((QuPathViewer vv) -> getRow(vv)).thenComparing(vv -> getColumn(vv))) + .findFirst() + .orElse(null); + if (closedViewer == null) { + Dialogs.showErrorMessage("Attach viewer", "Cannot attach viewer - " + + "please close an existing viewer in the grid first"); + return false; + } + var row = getRow(closedViewer); + int col = getColumn(closedViewer); + var stage = FXUtils.getWindow(viewer.getView()); + stage.hide(); + stage.getScene().setRoot(new BorderPane()); + stage.fireEvent(new WindowEvent(stage, WindowEvent.WINDOW_CLOSE_REQUEST)); + splitPaneRows.get(row).getItems().set(col, viewer.getView()); + refreshViewerList(); + setActiveViewer(viewer); + return true; + } + + public boolean detachViewer(QuPathViewer viewer) { + int row = getRow(viewer.getView()); + int col = getColumn(viewer.getView()); + if (row >= 0 && col >= 0) { + SplitPane splitRow = splitPaneRows.get(row); + splitRow.getItems().set(col, createViewer().getView()); + var stage = new Stage(); + var pane = new BorderPane(viewer.getView()); + var scene = new Scene(pane); + stage.setScene(scene); + stage.initOwner(qupath.getStage()); + stage.titleProperty().bind(createDetachedViewerTitleBinding(viewer)); + // It's messy... but we need to propagate key presses to the main window somehow, + // otherwise the viewer is non-responsive to key presses + stage.addEventFilter(KeyEvent.ANY, this::keyEventFilter); + stage.addEventFilter(MouseEvent.ANY, this::mouseEventFilter); + stage.addEventHandler(KeyEvent.ANY, this::propagateKeyEventToMainWindow); + stage.setOnCloseRequest(e -> { + if (FXUtils.getWindow(viewer.getView()) == null) { + logger.debug("Closing stage after viewer has been removed"); + return; + } + + if (viewers.size() == 1 && viewers.contains(viewer)) { + // This shouldn't occur if we always replace detached viewers + logger.error("The last viewer can't be closed!"); + e.consume(); + return; + } + if (qupath.closeViewer(viewer)) { + // Ensure we have an active viewer + // (If there isn't one, something has gone badly wrong) + var allOtherViewers = new ArrayList<>(getAllViewers()); + allOtherViewers.remove(viewer); + if (!allOtherViewers.isEmpty()) + setActiveViewer(allOtherViewers.get(0)); + stage.close(); + pane.getChildren().clear(); + refreshViewerList(); + } + e.consume(); + }); + stage.show(); + return true; + } else { + logger.warn("Viewer is already detached!"); + return false; + } + } + + private StringBinding createDetachedViewerTitleBinding(QuPathViewer viewer) { + return Bindings.createStringBinding(() -> { + return qupath.getDisplayedImageName(viewer.getImageData()); + }, viewer.imageDataProperty(), PathPrefs.maskImageNamesProperty(), qupath.projectProperty(), refreshTitleProperty); + } + + private void keyEventFilter(KeyEvent e) { + if (qupath.uiBlocked().getValue()) + e.consume(); + } + + private void mouseEventFilter(MouseEvent e) { + if (qupath.uiBlocked().getValue()) + e.consume(); + } + + private void propagateKeyEventToMainWindow(KeyEvent e) { + if (!e.isConsumed()) { + if (e.getEventType() == KeyEvent.KEY_RELEASED) { + var handler = qupath.getStage().getScene().getOnKeyReleased(); + if (handler != null) + handler.handle(e); + } + } + } + + } + private void setViewerPopupMenu(final QuPathViewerPlus viewer) { @@ -934,7 +1182,7 @@ private void setViewerPopupMenu(final QuPathViewerPlus viewer) { var commonActions = qupath.getCommonActions(); var viewerManagerActions = qupath.getViewerActions(); - + MenuItem miAddRow = new MenuItem(QuPathResources.getString("Action.View.Multiview.addRow")); miAddRow.setOnAction(e -> addRow(viewer)); MenuItem miAddColumn = new MenuItem(QuPathResources.getString("Action.View.Multiview.addColumn")); @@ -949,22 +1197,40 @@ private void setViewerPopupMenu(final QuPathViewerPlus viewer) { miCloseViewer.setOnAction(e -> qupath.closeViewer(viewer)); MenuItem miResizeGrid = new MenuItem(QuPathResources.getString("Action.View.Multiview.resetGridSize")); miResizeGrid.setOnAction(e -> resetGridSize()); - + + var menuGrid = MenuTools.createMenu( + "Action.View.Multiview.gridMenu", + viewerManagerActions.VIEWER_GRID_1x1, + viewerManagerActions.VIEWER_GRID_1x2, + viewerManagerActions.VIEWER_GRID_2x1, + viewerManagerActions.VIEWER_GRID_2x2, + viewerManagerActions.VIEWER_GRID_3x3, + null, + miAddRow, + miAddColumn, + null, + miRemoveRow, + miRemoveColumn, + null, + miResizeGrid + ); + + var miDetachViewer = ActionTools.createMenuItem(viewerManagerActions.DETACH_VIEWER); + var miAttachViewer = ActionTools.createMenuItem(viewerManagerActions.ATTACH_VIEWER); + MenuItem miToggleSync = ActionTools.createCheckMenuItem(viewerManagerActions.TOGGLE_SYNCHRONIZE_VIEWERS, null); MenuItem miMatchResolutions = ActionTools.createMenuItem(viewerManagerActions.MATCH_VIEWER_RESOLUTIONS); Menu menuMultiview = MenuTools.createMenu( "Menu.View.Multiview", + menuGrid, + null, miToggleSync, miMatchResolutions, - miCloseViewer, null, - miResizeGrid, - null, - miAddRow, - miAddColumn, + miCloseViewer, null, - miRemoveRow, - miRemoveColumn + miDetachViewer, + miAttachViewer ); Menu menuView = MenuTools.createMenu( @@ -1040,7 +1306,7 @@ private void setViewerPopupMenu(final QuPathViewerPlus viewer) { // Create a standard annotations menu Menu menuAnnotations = GuiTools.populateAnnotationsMenu(qupath, MenuTools.createMenu("General.objects.annotations")); - + SeparatorMenuItem topSeparator = new SeparatorMenuItem(); popup.setOnShowing(e -> { // Check if we have any cells @@ -1086,8 +1352,19 @@ private void setViewerPopupMenu(final QuPathViewerPlus viewer) { topSeparator.setVisible(hasAnnotations || pathObject instanceof TMACoreObject); // Occasionally, the newly-visible top part of a popup menu can have the wrong size? popup.setWidth(popup.getPrefWidth()); + + if (viewer == null || splitPaneGrid == null) { + miDetachViewer.setVisible(false); + miAttachViewer.setVisible(false); + } else if (splitPaneGrid.isDetached(viewer)) { + miDetachViewer.setVisible(false); + miAttachViewer.setVisible(true); + } else { + miDetachViewer.setVisible(true); + miAttachViewer.setVisible(false); + } }); - + popup.getItems().addAll( miClearSelectedObjects, menuTMA, @@ -1099,7 +1376,8 @@ private void setViewerPopupMenu(final QuPathViewerPlus viewer) { menuView, menuTools ); - + + popup.setAutoHide(true); // Enable circle pop-up for quick classification on right-click diff --git a/qupath-gui-fx/src/main/resources/qupath/lib/gui/localization/qupath-gui-strings.properties b/qupath-gui-fx/src/main/resources/qupath/lib/gui/localization/qupath-gui-strings.properties index c619ea9a2..54bd13bea 100644 --- a/qupath-gui-fx/src/main/resources/qupath/lib/gui/localization/qupath-gui-strings.properties +++ b/qupath-gui-fx/src/main/resources/qupath/lib/gui/localization/qupath-gui-strings.properties @@ -145,8 +145,22 @@ ViewerActions.synchronize = Synchronize viewers ViewerActions.synchronize.description = Synchronize panning and zooming when working with images open in multiple viewers ViewerActions.matchResolutions = Match viewer resolutions ViewerActions.matchResolutions.description = Adjust zoom factors to match the resolutions of images open in multiple viewers +ViewerActions.detachViewer = Detach viewer from grid +ViewerActions.detachViewer.description = Detach the selected viewer from the viewer grid, into its own window +ViewerActions.attachViewer = Attach viewer to grid +ViewerActions.attachViewer.description = Add the detached viewer back to the viewer grid (if there is space for it) ViewerActions.zoomToFit = Zoom to fit ViewerActions.zoomToFit.description = Adjust zoom for all images to fit the entire image in the viewer +ViewerActions.grid1x1 = Grid 1 x 1 (single viewer) +ViewerActions.grid1x1.description = Set the viewer grid to contain a single viewer +ViewerActions.grid2x1 = Grid 2 x 1 (vertical) +ViewerActions.grid2x1.description = Set the viewer grid to contain 2 viewers, arranged vertically +ViewerActions.grid1x2 = Grid 1 x 2 (horizontal) +ViewerActions.grid1x2.description = Set the viewer grid to contain 2 viewers, arranged horizontally +ViewerActions.grid2x2 = Grid 2 x 2 +ViewerActions.grid2x2.description = Set the viewer grid to contain 4 viewers, arranged in a 2x2 grid +ViewerActions.grid3x3 = Grid 3 x 3 +ViewerActions.grid3x3.description = Set the viewer grid to contain 9 viewers, arranged in a 3x3 grid # Shared commands for a QuPath instance CommonActions.objectDescriptions = Show object descriptions @@ -325,6 +339,7 @@ Action.View.commandList = Show command list Action.View.commandList.description = Show the command list (much easier than navigating menus...) Action.View.recentCommands = Show recent commands Action.View.recentCommands.description = Show a list containing recently-used commands +Action.View.Multiview.gridMenu = Set grid size Action.View.Multiview.addRow = Add row Action.View.Multiview.addRow.description = Add a new row of viewers to the multi-view grid.\nThis makes it possible to view two or more images side-by-side (vertically). Action.View.Multiview.addColumn = Add column @@ -333,7 +348,7 @@ Action.View.Multiview.removeRow = Remove row Action.View.Multiview.removeRow.description = Remove the row containing the current viewer from the multi-view grid, if possible.\nThe last row cannot be removed. Action.View.Multiview.removeColumn = Remove column Action.View.Multiview.removeColumn.description = Remove the column containing the current viewer from the multi-view grid, if possible.\nThe last column cannot be removed. -Action.View.Multiview.resetGridSize = Reset grid size +Action.View.Multiview.resetGridSize = Reset viewer sizes Action.View.Multiview.resetGridSize.description = Reset the multi-view grid so that all viewers have the same size Action.View.Multiview.closeViewer = Close viewer Action.View.Multiview.closeViewer.description = Close the image in the current viewer.\nThis is needed before it's possible to remove a viewer from the multi-view grid.