Search in sources :

Example 1 with ProjectImageEntry

use of qupath.lib.projects.ProjectImageEntry in project qupath by qupath.

the class ProjectImportImagesCommand method promptToImportImages.

/**
 * Prompt to import images to the current project.
 *
 * @param qupath QuPath instance, used to access the current project and stage
 * @param builder if not null, this will be used to create the servers. If null, a combobox will be shown to choose an installed builder.
 * @param defaultPaths URIs to use to prepopulate the list
 * @return
 */
static List<ProjectImageEntry<BufferedImage>> promptToImportImages(QuPathGUI qupath, ImageServerBuilder<BufferedImage> builder, String... defaultPaths) {
    var project = qupath.getProject();
    if (project == null) {
        Dialogs.showNoProjectError(commandName);
        return Collections.emptyList();
    }
    ListView<String> listView = new ListView<>();
    listView.setPrefWidth(480);
    listView.setMinHeight(100);
    listView.getItems().addAll(defaultPaths);
    listView.setPlaceholder(new Label("Drag & drop image or project files for import, \nor choose from the options below"));
    Button btnFile = new Button("Choose files");
    btnFile.setOnAction(e -> loadFromFileChooser(listView.getItems()));
    Button btnURL = new Button("Input URL");
    btnURL.setOnAction(e -> loadFromSingleURL(listView.getItems()));
    Button btnClipboard = new Button("From clipboard");
    btnClipboard.setOnAction(e -> loadFromClipboard(listView.getItems()));
    Button btnFileList = new Button("From path list");
    btnFileList.setOnAction(e -> loadFromTextFile(listView.getItems()));
    TitledPane paneList = new TitledPane("Image paths", listView);
    paneList.setCollapsible(false);
    BorderPane paneImages = new BorderPane();
    class BuilderListCell extends ListCell<ImageServerBuilder<BufferedImage>> {

        @Override
        protected void updateItem(ImageServerBuilder<BufferedImage> item, boolean empty) {
            super.updateItem(item, empty);
            if (empty) {
                setText(null);
            } else {
                if (item == null)
                    setText("Default (let QuPath decide)");
                else
                    setText(item.getName());
            }
        }
    }
    boolean requestBuilder = builder == null;
    ComboBox<ImageServerBuilder<BufferedImage>> comboBuilder = new ComboBox<>();
    Label labelBuilder = new Label("Image provider");
    if (requestBuilder) {
        comboBuilder.setCellFactory(p -> new BuilderListCell());
        comboBuilder.setButtonCell(new BuilderListCell());
        List<ImageServerBuilder<BufferedImage>> availableBuilders = new ArrayList<>(ImageServerProvider.getInstalledImageServerBuilders(BufferedImage.class));
        if (!availableBuilders.contains(null))
            availableBuilders.add(0, null);
        comboBuilder.getItems().setAll(availableBuilders);
        comboBuilder.getSelectionModel().selectFirst();
        labelBuilder.setLabelFor(comboBuilder);
        labelBuilder.setMinWidth(Label.USE_PREF_SIZE);
    }
    ComboBox<ImageType> comboType = new ComboBox<>();
    comboType.getItems().setAll(ImageType.values());
    Label labelType = new Label("Set image type");
    labelType.setLabelFor(comboType);
    labelType.setMinWidth(Label.USE_PREF_SIZE);
    ComboBox<Rotation> comboRotate = new ComboBox<>();
    comboRotate.getItems().setAll(Rotation.values());
    Label labelRotate = new Label("Rotate image");
    labelRotate.setLabelFor(comboRotate);
    labelRotate.setMinWidth(Label.USE_PREF_SIZE);
    TextField tfArgs = new TextField();
    Label labelArgs = new Label("Optional args");
    labelArgs.setLabelFor(tfArgs);
    labelArgs.setMinWidth(Label.USE_PREF_SIZE);
    CheckBox cbPyramidalize = new CheckBox("Auto-generate pyramids");
    cbPyramidalize.setSelected(pyramidalizeProperty.get());
    CheckBox cbImportObjects = new CheckBox("Import objects");
    cbImportObjects.setSelected(importObjectsProperty.get());
    PaneTools.setMaxWidth(Double.MAX_VALUE, comboBuilder, comboType, comboRotate, cbPyramidalize, cbImportObjects, tfArgs);
    PaneTools.setFillWidth(Boolean.TRUE, comboBuilder, comboType, comboRotate, cbPyramidalize, cbImportObjects, tfArgs);
    PaneTools.setHGrowPriority(Priority.ALWAYS, comboBuilder, comboType, comboRotate, cbPyramidalize, cbImportObjects, tfArgs);
    GridPane paneType = new GridPane();
    paneType.setPadding(new Insets(5));
    paneType.setHgap(5);
    paneType.setVgap(5);
    int row = 0;
    if (requestBuilder)
        PaneTools.addGridRow(paneType, row++, 0, "Specify the library used to open images", labelBuilder, comboBuilder);
    PaneTools.addGridRow(paneType, row++, 0, "Specify the default image type for all images being imported (required for analysis, can be changed later under the 'Image' tab)", labelType, comboType);
    PaneTools.addGridRow(paneType, row++, 0, "Optionally rotate images on import", labelRotate, comboRotate);
    PaneTools.addGridRow(paneType, row++, 0, "Optionally pass reader-specific arguments to the image provider.\nUsually this should just be left empty.", labelArgs, tfArgs);
    PaneTools.addGridRow(paneType, row++, 0, "Dynamically create image pyramids for large, single-resolution images", cbPyramidalize, cbPyramidalize);
    PaneTools.addGridRow(paneType, row++, 0, "Read and import objects (e.g. annotations) from the image file, if possible", cbImportObjects, cbImportObjects);
    paneImages.setCenter(paneList);
    paneImages.setBottom(paneType);
    // TilePane paneButtons = new TilePane();
    // paneButtons.getChildren().addAll(btnFile, btnURL, btnClipboard, btnFileList);
    GridPane paneButtons = PaneTools.createColumnGridControls(btnFile, btnURL, btnClipboard, btnFileList);
    paneButtons.setHgap(5);
    paneButtons.setPadding(new Insets(5));
    BorderPane pane = new BorderPane();
    pane.setCenter(paneImages);
    pane.setBottom(paneButtons);
    // Support drag & drop for files
    pane.setOnDragOver(e -> {
        e.acceptTransferModes(TransferMode.COPY);
        e.consume();
    });
    pane.setOnDragDropped(e -> {
        Dragboard dragboard = e.getDragboard();
        if (dragboard.hasFiles()) {
            logger.trace("Files dragged onto project import dialog");
            try {
                var paths = dragboard.getFiles().stream().filter(f -> f.isFile() && !f.isHidden()).map(f -> f.getAbsolutePath()).collect(Collectors.toList());
                paths.removeAll(listView.getItems());
                if (!paths.isEmpty())
                    listView.getItems().addAll(paths);
            } catch (Exception ex) {
                Dialogs.showErrorMessage("Drag & Drop", ex);
            }
        }
        e.setDropCompleted(true);
        e.consume();
    });
    Dialog<ButtonType> dialog = new Dialog<>();
    dialog.setResizable(true);
    dialog.initOwner(qupath.getStage());
    dialog.setTitle("Import images to project");
    ButtonType typeImport = new ButtonType("Import", ButtonData.OK_DONE);
    dialog.getDialogPane().getButtonTypes().addAll(typeImport, ButtonType.CANCEL);
    ScrollPane scroll = new ScrollPane(pane);
    scroll.setFitToHeight(true);
    scroll.setFitToWidth(true);
    dialog.getDialogPane().setContent(scroll);
    Optional<ButtonType> result = dialog.showAndWait();
    if (!result.isPresent() || result.get() != typeImport)
        return Collections.emptyList();
    // // Do the actual import
    // List<String> pathSucceeded = new ArrayList<>();
    // List<String> pathFailed = new ArrayList<>();
    // for (String path : listView.getItems()) {
    // if (qupath.getProject().addImage(path.trim()))
    // pathSucceeded.add(path);
    // else
    // pathFailed.add(path);
    // }
    ImageType type = comboType.getValue();
    Rotation rotation = comboRotate.getValue();
    boolean pyramidalize = cbPyramidalize.isSelected();
    boolean importObjects = cbImportObjects.isSelected();
    pyramidalizeProperty.set(pyramidalize);
    importObjectsProperty.set(importObjects);
    ImageServerBuilder<BufferedImage> requestedBuilder = requestBuilder ? comboBuilder.getSelectionModel().getSelectedItem() : builder;
    List<String> argsList = new ArrayList<>();
    String argsString = tfArgs.getText();
    // TODO: Use a smarter approach to splitting! Currently we support so few arguments that splitting on spaces should be ok... for now.
    String[] argsSplit = argsString == null || argsString.isBlank() ? new String[0] : argsString.split(" ");
    for (var a : argsSplit) {
        argsList.add(a);
    }
    if (rotation != null && rotation != Rotation.ROTATE_NONE) {
        argsList.add("--rotate");
        argsList.add(rotation.toString());
    }
    if (!argsList.isEmpty())
        logger.debug("Args: [{}]", argsList.stream().collect(Collectors.joining(", ")));
    String[] args = argsList.toArray(String[]::new);
    List<String> pathSucceeded = new ArrayList<>();
    List<String> pathFailed = new ArrayList<>();
    List<ProjectImageEntry<BufferedImage>> entries = new ArrayList<>();
    Task<Collection<ProjectImageEntry<BufferedImage>>> worker = new Task<>() {

        @Override
        protected Collection<ProjectImageEntry<BufferedImage>> call() throws Exception {
            AtomicLong counter = new AtomicLong(0L);
            List<String> items = new ArrayList<>(listView.getItems());
            updateMessage("Checking for compatible image readers...");
            // Limit the size of the thread pool
            // The previous use of a cached thread pool caused trouble when importing may large, non-pyramidal images
            var pool = Executors.newFixedThreadPool(ThreadTools.getParallelism(), ThreadTools.createThreadFactory("project-import", true));
            // var pool = Executors.newCachedThreadPool(ThreadTools.createThreadFactory("project-import", true));
            List<Future<List<ServerBuilder<BufferedImage>>>> results = new ArrayList<>();
            List<ProjectImageEntry<BufferedImage>> projectImages = new ArrayList<>();
            for (var item : items) {
                // Try to load items from a project if possible
                if (item.toLowerCase().endsWith(ProjectIO.DEFAULT_PROJECT_EXTENSION)) {
                    try {
                        var tempProject = ProjectIO.loadProject(GeneralTools.toURI(item), BufferedImage.class);
                        projectImages.addAll(tempProject.getImageList());
                    } catch (Exception e) {
                        logger.error("Unable to add images from {} ({})", item, e.getLocalizedMessage());
                    }
                    continue;
                }
                results.add(pool.submit(() -> {
                    try {
                        var uri = GeneralTools.toURI(item);
                        UriImageSupport<BufferedImage> support;
                        if (requestedBuilder == null)
                            support = ImageServers.getImageSupport(uri, args);
                        else
                            support = ImageServers.getImageSupport(requestedBuilder, uri, args);
                        if (support != null)
                            return support.getBuilders();
                    } catch (Exception e) {
                        logger.error("Unable to add {}");
                        logger.error(e.getLocalizedMessage(), e);
                    }
                    return new ArrayList<ServerBuilder<BufferedImage>>();
                }));
            }
            List<ProjectImageEntry<BufferedImage>> failures = Collections.synchronizedList(new ArrayList<>());
            // If we have projects, try adding images from these first
            if (!projectImages.isEmpty()) {
                if (projectImages.size() == 1)
                    updateMessage("Importing 1 image from existing projects");
                else
                    updateMessage("Importing " + projectImages.size() + " images from existing projects");
                for (var temp : projectImages) {
                    try {
                        project.addDuplicate(temp, true);
                    } catch (Exception e) {
                        failures.add(temp);
                    }
                }
            }
            // If we have 'standard' image paths, use these next
            List<ServerBuilder<BufferedImage>> builders = new ArrayList<>();
            for (var result : results) {
                try {
                    builders.addAll(result.get());
                } catch (ExecutionException e) {
                    logger.error("Execution exception importing image", e);
                }
            }
            long max = builders.size();
            if (!builders.isEmpty()) {
                if (max == 1)
                    updateMessage("Adding 1 image to project");
                else
                    updateMessage("Adding " + max + " images to project");
                // Add everything in order first
                List<ProjectImageEntry<BufferedImage>> entries = new ArrayList<>();
                for (var builder : builders) {
                    // if (rotation != null && rotation != Rotation.ROTATE_NONE)
                    // builder = RotatedImageServer.getRotatedBuilder(builder, rotation);
                    // if (swapRedBlue)
                    // builder = RearrangeRGBImageServer.getSwapRedBlueBuilder(builder);
                    entries.add(project.addImage(builder));
                }
                // Initialize (the slow bit)
                int n = builders.size();
                for (var entry : entries) {
                    pool.submit(() -> {
                        try {
                            initializeEntry(entry, type, pyramidalize, importObjects);
                        } catch (Exception e) {
                            failures.add(entry);
                            logger.warn("Exception adding " + entry, e);
                        } finally {
                            long i = counter.incrementAndGet();
                            updateProgress(i, max);
                            String name = entry.getImageName();
                            if (name != null) {
                                updateMessage("Added " + i + "/" + n + " - " + name);
                            }
                        }
                    });
                }
            }
            pool.shutdown();
            try {
                pool.awaitTermination(60, TimeUnit.MINUTES);
            } catch (Exception e) {
                logger.error("Exception waiting for project import to complete: " + e.getLocalizedMessage(), e);
            }
            if (!failures.isEmpty()) {
                String message;
                if (failures.size() == 1)
                    message = "Failed to load one image.";
                else
                    message = "Failed to load " + failures.size() + " images.";
                if (requestedBuilder != null)
                    message += "\nThe image type might not be supported by '" + requestedBuilder.getName() + "'";
                Dialogs.showErrorMessage("Import images", message);
                var toRemove = failures.stream().filter(p -> project.getImageList().contains(p)).collect(Collectors.toList());
                project.removeAllImages(toRemove, true);
            }
            // Now save changes
            project.syncChanges();
            // builders.parallelStream().forEach(builder -> {
            // //				builders.parallelStream().forEach(builder -> {
            // try (var server =  builder.build()) {
            // var entry = addSingleImageToProject(project, server);
            // updateMessage("Added " + entry.getImageName());
            // } catch (Exception e) {
            // logger.warn("Exception adding " + builder, e);
            // } finally {
            // updateProgress(counter.incrementAndGet(), max);
            // }
            // });
            updateProgress(max, max);
            return entries;
        }
    };
    ProgressDialog progress = new ProgressDialog(worker);
    progress.setTitle("Project import");
    qupath.submitShortTask(worker);
    progress.showAndWait();
    try {
        project.syncChanges();
    } catch (IOException e1) {
        Dialogs.showErrorMessage("Sync project", e1);
    }
    qupath.refreshProject();
    StringBuilder sb = new StringBuilder();
    if (!pathSucceeded.isEmpty()) {
        sb.append("Successfully imported " + pathSucceeded.size() + " paths:\n");
        for (String path : pathSucceeded) sb.append("\t" + path + "\n");
        sb.append("\n");
        qupath.refreshProject();
        ProjectBrowser.syncProject(qupath.getProject());
    }
    if (!pathFailed.isEmpty()) {
        sb.append("Unable to import " + pathFailed.size() + " paths:\n");
        for (String path : pathFailed) sb.append("\t" + path + "\n");
        sb.append("\n");
        TextArea textArea = new TextArea();
        textArea.setText(sb.toString());
        if (pathSucceeded.isEmpty())
            Dialogs.showErrorMessage(commandName, textArea);
        else
            Dialogs.showMessageDialog(commandName, textArea);
    }
    // TODO: Add failed and successful paths to pathFailed/pathSucceeded, so the line below prints something
    if (sb.length() > 0)
        logger.info(sb.toString());
    return entries;
}
Also used : Button(javafx.scene.control.Button) ImageServer(qupath.lib.images.servers.ImageServer) ServerTools(qupath.lib.images.servers.ServerTools) PathObjectReader(qupath.lib.objects.PathObjectReader) ListCell(javafx.scene.control.ListCell) LoggerFactory(org.slf4j.LoggerFactory) Scanner(java.util.Scanner) RenderingHints(java.awt.RenderingHints) ProjectBrowser(qupath.lib.gui.panes.ProjectBrowser) Future(java.util.concurrent.Future) Task(javafx.concurrent.Task) ScrollPane(javafx.scene.control.ScrollPane) ComboBox(javafx.scene.control.ComboBox) URI(java.net.URI) ImageServers(qupath.lib.images.servers.ImageServers) ImageType(qupath.lib.images.ImageData.ImageType) QuPathGUI(qupath.lib.gui.QuPathGUI) TextField(javafx.scene.control.TextField) BufferedImage(java.awt.image.BufferedImage) ImageServerProvider(qupath.lib.images.servers.ImageServerProvider) Collection(java.util.Collection) ChannelDisplayInfo(qupath.lib.display.ChannelDisplayInfo) ImageServerBuilder(qupath.lib.images.servers.ImageServerBuilder) Collectors(java.util.stream.Collectors) FileNotFoundException(java.io.FileNotFoundException) Executors(java.util.concurrent.Executors) PathObject(qupath.lib.objects.PathObject) Priority(javafx.scene.layout.Priority) List(java.util.List) BooleanProperty(javafx.beans.property.BooleanProperty) Project(qupath.lib.projects.Project) ProjectIO(qupath.lib.projects.ProjectIO) Clipboard(javafx.scene.input.Clipboard) Optional(java.util.Optional) ThreadTools(qupath.lib.common.ThreadTools) BorderPane(javafx.scene.layout.BorderPane) ButtonData(javafx.scene.control.ButtonBar.ButtonData) ListView(javafx.scene.control.ListView) TextArea(javafx.scene.control.TextArea) ButtonType(javafx.scene.control.ButtonType) ProgressDialog(org.controlsfx.dialog.ProgressDialog) ImageDisplay(qupath.lib.display.ImageDisplay) UriImageSupport(qupath.lib.images.servers.ImageServerBuilder.UriImageSupport) TransferMode(javafx.scene.input.TransferMode) ArrayList(java.util.ArrayList) Dialogs(qupath.lib.gui.dialogs.Dialogs) Dragboard(javafx.scene.input.Dragboard) Insets(javafx.geometry.Insets) Graphics2D(java.awt.Graphics2D) GridPane(javafx.scene.layout.GridPane) ImageData(qupath.lib.images.ImageData) Logger(org.slf4j.Logger) Dialog(javafx.scene.control.Dialog) Label(javafx.scene.control.Label) TitledPane(javafx.scene.control.TitledPane) ProjectImageEntry(qupath.lib.projects.ProjectImageEntry) GeneralTools(qupath.lib.common.GeneralTools) CheckBox(javafx.scene.control.CheckBox) IOException(java.io.IOException) ServerBuilder(qupath.lib.images.servers.ImageServerBuilder.ServerBuilder) File(java.io.File) ExecutionException(java.util.concurrent.ExecutionException) TimeUnit(java.util.concurrent.TimeUnit) AtomicLong(java.util.concurrent.atomic.AtomicLong) WrappedBufferedImageServer(qupath.lib.images.servers.WrappedBufferedImageServer) Collections(java.util.Collections) PathPrefs(qupath.lib.gui.prefs.PathPrefs) Rotation(qupath.lib.images.servers.RotatedImageServer.Rotation) PaneTools(qupath.lib.gui.tools.PaneTools) Task(javafx.concurrent.Task) TextArea(javafx.scene.control.TextArea) Label(javafx.scene.control.Label) ArrayList(java.util.ArrayList) ProgressDialog(org.controlsfx.dialog.ProgressDialog) ImageType(qupath.lib.images.ImageData.ImageType) ListView(javafx.scene.control.ListView) Button(javafx.scene.control.Button) ProgressDialog(org.controlsfx.dialog.ProgressDialog) Dialog(javafx.scene.control.Dialog) TextField(javafx.scene.control.TextField) ButtonType(javafx.scene.control.ButtonType) Dragboard(javafx.scene.input.Dragboard) ImageServerBuilder(qupath.lib.images.servers.ImageServerBuilder) UriImageSupport(qupath.lib.images.servers.ImageServerBuilder.UriImageSupport) GridPane(javafx.scene.layout.GridPane) ComboBox(javafx.scene.control.ComboBox) CheckBox(javafx.scene.control.CheckBox) ScrollPane(javafx.scene.control.ScrollPane) Collection(java.util.Collection) ImageServerBuilder(qupath.lib.images.servers.ImageServerBuilder) ServerBuilder(qupath.lib.images.servers.ImageServerBuilder.ServerBuilder) BorderPane(javafx.scene.layout.BorderPane) Insets(javafx.geometry.Insets) ListCell(javafx.scene.control.ListCell) BufferedImage(java.awt.image.BufferedImage) ProjectImageEntry(qupath.lib.projects.ProjectImageEntry) ExecutionException(java.util.concurrent.ExecutionException) TitledPane(javafx.scene.control.TitledPane) IOException(java.io.IOException) Rotation(qupath.lib.images.servers.RotatedImageServer.Rotation) FileNotFoundException(java.io.FileNotFoundException) IOException(java.io.IOException) ExecutionException(java.util.concurrent.ExecutionException) AtomicLong(java.util.concurrent.atomic.AtomicLong) Future(java.util.concurrent.Future)

Example 2 with ProjectImageEntry

use of qupath.lib.projects.ProjectImageEntry in project qupath by qupath.

the class ProjectMetadataEditorCommand method showProjectMetadataEditor.

public static void showProjectMetadataEditor(Project<?> project) {
    if (project == null) {
        logger.warn("No project available!");
        return;
    }
    Set<String> metadataNameSet = new TreeSet<>();
    List<ImageEntryWrapper> entries = new ArrayList<>();
    for (ProjectImageEntry<?> entry : project.getImageList()) {
        entries.add(new ImageEntryWrapper(entry));
        metadataNameSet.addAll(entry.getMetadataKeys());
    }
    TableView<ImageEntryWrapper> table = new TableView<>();
    TableColumn<ImageEntryWrapper, String> colName = new TableColumn<>(IMAGE_NAME);
    colName.setCellValueFactory(v -> v.getValue().getNameBinding());
    table.getColumns().add(colName);
    table.setEditable(true);
    for (String metadataName : metadataNameSet) {
        TableColumn<ImageEntryWrapper, String> col = new TableColumn<>(metadataName);
        col.setCellFactory(TextFieldTableCell.<ImageEntryWrapper>forTableColumn());
        col.setOnEditCommit(e -> {
            ImageEntryWrapper entry = e.getRowValue();
            String n = e.getNewValue();
            if (n == null || n.isEmpty())
                entry.removeMetadataValue(e.getTableColumn().getText());
            else
                entry.putMetadataValue(e.getTableColumn().getText(), n);
        });
        col.setCellValueFactory(v -> v.getValue().getProperty(metadataName));
        col.setEditable(true);
        table.getColumns().add(col);
    }
    table.getItems().setAll(entries);
    table.getSelectionModel().setCellSelectionEnabled(true);
    table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
    // Handle deleting entries
    table.addEventHandler(KeyEvent.KEY_RELEASED, e -> {
        if (e.getCode() == KeyCode.BACK_SPACE || e.getCode() == KeyCode.DELETE) {
            var positions = table.getSelectionModel().getSelectedCells().stream().filter(p -> !IMAGE_NAME.equals(p.getTableColumn().getText())).collect(Collectors.toList());
            if (positions.isEmpty())
                return;
            if (positions.size() == 1) {
                setTextForSelectedCells(positions, null);
            } else {
                if (Dialogs.showConfirmDialog("Project metadata", "Clear metadata for " + positions.size() + " selected cells?")) {
                    setTextForSelectedCells(positions, null);
                }
            }
            table.refresh();
        }
    });
    BooleanBinding selectedCells = Bindings.createBooleanBinding(() -> table.getSelectionModel().selectedItemProperty() == null, table.getSelectionModel().selectedItemProperty());
    MenuBar menubar = new MenuBar();
    Menu menuEdit = new Menu("Edit");
    MenuItem miCopy = new MenuItem("Copy selected cells");
    miCopy.disableProperty().bind(selectedCells);
    miCopy.setAccelerator(new KeyCodeCombination(KeyCode.C, KeyCombination.SHORTCUT_DOWN));
    miCopy.setOnAction(e -> copySelectedCellsToClipboard(table, true));
    MenuItem miCopyFull = new MenuItem("Copy full table");
    miCopyFull.setOnAction(e -> copyEntireTableToClipboard(table));
    MenuItem miPaste = new MenuItem("Paste");
    miPaste.setAccelerator(new KeyCodeCombination(KeyCode.V, KeyCombination.SHORTCUT_DOWN));
    miPaste.disableProperty().bind(selectedCells);
    miPaste.setOnAction(e -> pasteClipboardContentsToTable(table));
    MenuItem miSet = new MenuItem("Set cell contents");
    miSet.disableProperty().bind(selectedCells);
    miSet.setOnAction(e -> {
        String input = Dialogs.showInputDialog("Set metadata cells", "Metadata text", "");
        if (input == null)
            return;
        setTextForSelectedCells(table.getSelectionModel().getSelectedCells(), input);
        table.refresh();
    });
    menuEdit.getItems().addAll(miCopy, miCopyFull, miPaste, miSet);
    menubar.getMenus().add(menuEdit);
    BorderPane pane = new BorderPane();
    pane.setTop(menubar);
    pane.setCenter(table);
    // menubar.setUseSystemMenuBar(true);
    menubar.useSystemMenuBarProperty().bindBidirectional(PathPrefs.useSystemMenubarProperty());
    Dialog<ButtonType> dialog = new Dialog<>();
    var qupath = QuPathGUI.getInstance();
    if (qupath != null)
        dialog.initOwner(qupath.getStage());
    dialog.setTitle("Project metadata");
    dialog.setHeaderText(null);
    dialog.setResizable(true);
    dialog.getDialogPane().setContent(pane);
    dialog.getDialogPane().getButtonTypes().setAll(ButtonType.OK, ButtonType.CANCEL);
    dialog.getDialogPane().setPrefWidth(500);
    Optional<ButtonType> result = dialog.showAndWait();
    if (result.isPresent() && result.get().getButtonData() == ButtonData.OK_DONE) {
        // Make the changes
        for (ImageEntryWrapper wrapper : entries) {
            wrapper.commitChanges();
        }
        // Write the project
        ProjectBrowser.syncProject(project);
    }
// Stage stage = new Stage();
// stage.initOwner(qupath.getStage());
// stage.setTitle("Project metadata");
// stage.setScene(new Scene(pane, 500, 400));
// stage.showAndWait();
}
Also used : ButtonData(javafx.scene.control.ButtonBar.ButtonData) BooleanBinding(javafx.beans.binding.BooleanBinding) ButtonType(javafx.scene.control.ButtonType) LoggerFactory(org.slf4j.LoggerFactory) TextFieldTableCell(javafx.scene.control.cell.TextFieldTableCell) Bindings(javafx.beans.binding.Bindings) TreeSet(java.util.TreeSet) ArrayList(java.util.ArrayList) TableColumn(javafx.scene.control.TableColumn) KeyCombination(javafx.scene.input.KeyCombination) ProjectBrowser(qupath.lib.gui.panes.ProjectBrowser) Dialogs(qupath.lib.gui.dialogs.Dialogs) Map(java.util.Map) TableView(javafx.scene.control.TableView) QuPathGUI(qupath.lib.gui.QuPathGUI) KeyCode(javafx.scene.input.KeyCode) Logger(org.slf4j.Logger) Dialog(javafx.scene.control.Dialog) MenuBar(javafx.scene.control.MenuBar) MenuItem(javafx.scene.control.MenuItem) ProjectImageEntry(qupath.lib.projects.ProjectImageEntry) Set(java.util.Set) TablePosition(javafx.scene.control.TablePosition) ObservableStringValue(javafx.beans.value.ObservableStringValue) KeyEvent(javafx.scene.input.KeyEvent) Collectors(java.util.stream.Collectors) Menu(javafx.scene.control.Menu) KeyCodeCombination(javafx.scene.input.KeyCodeCombination) List(java.util.List) Project(qupath.lib.projects.Project) SelectionMode(javafx.scene.control.SelectionMode) TreeMap(java.util.TreeMap) Clipboard(javafx.scene.input.Clipboard) Entry(java.util.Map.Entry) Optional(java.util.Optional) ClipboardContent(javafx.scene.input.ClipboardContent) BorderPane(javafx.scene.layout.BorderPane) PathPrefs(qupath.lib.gui.prefs.PathPrefs) BorderPane(javafx.scene.layout.BorderPane) ArrayList(java.util.ArrayList) MenuBar(javafx.scene.control.MenuBar) MenuItem(javafx.scene.control.MenuItem) KeyCodeCombination(javafx.scene.input.KeyCodeCombination) TableColumn(javafx.scene.control.TableColumn) BooleanBinding(javafx.beans.binding.BooleanBinding) TreeSet(java.util.TreeSet) Dialog(javafx.scene.control.Dialog) Menu(javafx.scene.control.Menu) ButtonType(javafx.scene.control.ButtonType) TableView(javafx.scene.control.TableView)

Example 3 with ProjectImageEntry

use of qupath.lib.projects.ProjectImageEntry in project qupath by qupath.

the class QuPathGUI method openImage.

/**
 * Open a new whole slide image server, or ImageData.
 * If the path is the same as a currently-open server, do nothing.
 *
 * @param viewer the viewer into which the image should be opened
 * @param pathNew
 * @param prompt if true, give the user the opportunity to cancel opening if a whole slide server is already set
 * @param includeURLs if true, any prompt should support URL input and not only a file chooser
 * @return true if the server was set for this GUI, false otherwise
 * @throws IOException
 */
public boolean openImage(QuPathViewer viewer, String pathNew, boolean prompt, boolean includeURLs) throws IOException {
    if (viewer == null) {
        if (getViewers().size() == 1)
            viewer = getViewer();
        else {
            Dialogs.showErrorMessage("Open image", "Please specify the viewer where the image should be opened!");
            return false;
        }
    }
    ImageServer<BufferedImage> server = viewer.getServer();
    String pathOld = null;
    File fileBase = null;
    if (server != null) {
        var uris = server.getURIs();
        if (uris.size() == 1) {
            var uri = uris.iterator().next();
            pathOld = uri.toString();
            try {
                var path = GeneralTools.toPath(uri);
                if (path != null)
                    fileBase = path.toFile().getParentFile();
            } catch (Exception e) {
            }
            ;
        }
    // pathOld = server.getPath();
    // try {
    // fileBase = new File(pathOld).getParentFile();
    // } catch (Exception e) {};
    }
    // Prompt for a path, if required
    File fileNew = null;
    if (pathNew == null) {
        if (includeURLs) {
            pathNew = Dialogs.promptForFilePathOrURL("Choose path", pathOld, fileBase, null);
            if (pathNew == null)
                return false;
            fileNew = new File(pathNew);
        } else {
            fileNew = Dialogs.promptForFile(null, fileBase, null);
            if (fileNew == null)
                return false;
            pathNew = fileNew.getAbsolutePath();
        }
    } else
        fileNew = new File(pathNew);
    // If we have a file, check if it is a data file - if so, handle differently
    if (fileNew.isFile() && GeneralTools.checkExtensions(pathNew, PathPrefs.getSerializationExtension()))
        return openSavedData(viewer, fileNew, false, true);
    // Check for project file
    if (fileNew.isFile() && GeneralTools.checkExtensions(pathNew, ProjectIO.getProjectExtension())) {
        logger.info("Trying to load project {}", fileNew.getAbsolutePath());
        try {
            Project<BufferedImage> project = ProjectIO.loadProject(fileNew, BufferedImage.class);
            if (project != null) {
                setProject(project);
                return true;
            }
        } catch (Exception e) {
            Dialogs.showErrorMessage("Open project", e);
            logger.error("Error opening project " + fileNew.getAbsolutePath(), e);
            return false;
        }
    }
    // Try opening an image, unless it's the same as the image currently open
    if (!pathNew.equals(pathOld)) {
        // If we have a project, show the import dialog
        if (getProject() != null) {
            List<ProjectImageEntry<BufferedImage>> entries = ProjectCommands.promptToImportImages(this, pathNew);
            if (entries.isEmpty())
                return false;
            return openImageEntry(entries.get(0));
        }
        ImageServer<BufferedImage> serverNew = null;
        UriImageSupport<BufferedImage> support = ImageServerProvider.getPreferredUriImageSupport(BufferedImage.class, pathNew);
        List<ServerBuilder<BufferedImage>> builders = support == null ? Collections.emptyList() : support.getBuilders();
        if (builders.isEmpty()) {
            String message = "Unable to build ImageServer for " + pathNew + ".\nSee View > Show log for more details";
            Dialogs.showErrorMessage("Unable to build server", message);
            return false;
        } else if (builders.size() == 1) {
            try {
                serverNew = builders.get(0).build();
            } catch (Exception e) {
                logger.error("Error building server: " + e.getLocalizedMessage(), e);
            }
        } else {
            var selector = new ServerSelector(builders);
            serverNew = selector.promptToSelectServer();
            if (serverNew == null)
                return false;
        }
        if (serverNew != null) {
            if (pathOld != null && prompt && !viewer.getHierarchy().isEmpty()) {
                if (!Dialogs.showYesNoDialog("Replace open image", "Close " + ServerTools.getDisplayableImageName(server) + "?"))
                    return false;
            }
            ImageData<BufferedImage> imageData = null;
            if (serverNew != null) {
                int minSize = PathPrefs.minPyramidDimensionProperty().get();
                if (serverNew.nResolutions() == 1 && Math.max(serverNew.getWidth(), serverNew.getHeight()) > minSize) {
                    // Check if we have any hope at all with the current settings
                    long estimatedBytes = (long) serverNew.getWidth() * (long) serverNew.getHeight() * (long) serverNew.nChannels() * (long) serverNew.getPixelType().getBytesPerPixel();
                    double requiredBytes = estimatedBytes * (4.0 / 3.0);
                    if (prompt && imageRegionStore != null && requiredBytes >= imageRegionStore.getTileCacheSize()) {
                        logger.warn("Selected image is {} x {} x {} pixels ({})", serverNew.getWidth(), serverNew.getHeight(), serverNew.nChannels(), serverNew.getPixelType());
                        Dialogs.showErrorMessage("Image too large", "Non-pyramidal image is too large for the available tile cache!\n" + "Try converting the image to a pyramidal file format, or increasing the memory available to QuPath.");
                        return false;
                    }
                    // Offer to pyramidalize
                    var serverWrapped = ImageServers.pyramidalize(serverNew);
                    if (serverWrapped.nResolutions() > 1) {
                        if (prompt) {
                            var response = Dialogs.showYesNoCancelDialog("Auto pyramidalize", "QuPath works best with large images saved in a pyramidal format.\n\n" + "Do you want to generate a pyramid dynamically from " + ServerTools.getDisplayableImageName(serverNew) + "?" + "\n(This requires more memory, but is usually worth it)");
                            if (response == DialogButton.CANCEL)
                                return false;
                            if (response == DialogButton.YES)
                                serverNew = serverWrapped;
                        }
                    }
                }
                imageData = createNewImageData(serverNew);
            }
            viewer.setImageData(imageData);
            if (imageData.getImageType() == ImageType.UNSET && PathPrefs.imageTypeSettingProperty().get() == ImageTypeSetting.PROMPT) {
                var type = GuiTools.estimateImageType(serverNew, imageRegionStore.getThumbnail(serverNew, 0, 0, true));
                ImageDetailsPane.promptToSetImageType(imageData, type);
            }
            return true;
        } else {
            // Show an error message if we can't open the file
            Dialogs.showErrorNotification("Open image", "Sorry, I can't open " + pathNew);
        // logger.error("Unable to build whole slide server for path '{}'", pathNew);
        }
    }
    return false;
}
Also used : BufferedImage(java.awt.image.BufferedImage) IOException(java.io.IOException) URISyntaxException(java.net.URISyntaxException) ScriptException(javax.script.ScriptException) FileNotFoundException(java.io.FileNotFoundException) ProjectImageEntry(qupath.lib.projects.ProjectImageEntry) File(java.io.File) ServerBuilder(qupath.lib.images.servers.ImageServerBuilder.ServerBuilder) ImageServerBuilder(qupath.lib.images.servers.ImageServerBuilder)

Example 4 with ProjectImageEntry

use of qupath.lib.projects.ProjectImageEntry in project qupath by qupath.

the class MeasurementExporter method exportMeasurements.

/**
 * Exports the measurements of one or more entries in the project.
 * This function first opens all the images in the project to store
 * all the column names and values of the measurements.
 * Then, it loops through the maps containing the values to write
 * them to the given output stream.
 * @param stream
 */
public void exportMeasurements(OutputStream stream) {
    long startTime = System.currentTimeMillis();
    Map<ProjectImageEntry<?>, String[]> imageCols = new HashMap<>();
    Map<ProjectImageEntry<?>, Integer> nImageEntries = new HashMap<>();
    List<String> allColumns = new ArrayList<>();
    Multimap<String, String> valueMap = LinkedListMultimap.create();
    String pattern = "(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)";
    for (ProjectImageEntry<?> entry : imageList) {
        try {
            ImageData<?> imageData = entry.readImageData();
            ObservableMeasurementTableData model = new ObservableMeasurementTableData();
            Collection<PathObject> pathObjects = imageData == null ? Collections.emptyList() : imageData.getHierarchy().getObjects(null, type);
            if (filter != null)
                pathObjects = pathObjects.stream().filter(filter).collect(Collectors.toList());
            model.setImageData(imageData, pathObjects);
            List<String> data = SummaryMeasurementTableCommand.getTableModelStrings(model, separator, excludeColumns);
            // Get header
            String[] header;
            String headerString = data.get(0);
            if (headerString.chars().filter(e -> e == '"').count() > 1)
                header = headerString.split(separator.equals("\t") ? "\\" + separator : separator + pattern, -1);
            else
                header = headerString.split(separator);
            imageCols.put(entry, header);
            nImageEntries.put(entry, data.size() - 1);
            for (String col : header) {
                if (!allColumns.contains(col) && !excludeColumns.contains(col))
                    allColumns.add(col);
            }
            // To keep the same column order, just delete non-relevant columns
            if (!includeOnlyColumns.isEmpty())
                allColumns.removeIf(n -> !includeOnlyColumns.contains(n));
            for (int i = 1; i < data.size(); i++) {
                String[] row;
                String rowString = data.get(i);
                // Check if some values in the row are escaped
                if (rowString.chars().filter(e -> e == '"').count() > 1)
                    row = rowString.split(separator.equals("\t") ? "\\" + separator : separator + pattern, -1);
                else
                    row = rowString.split(separator);
                // Put value in map
                for (int elem = 0; elem < row.length; elem++) {
                    if (allColumns.contains(header[elem]))
                        valueMap.put(header[elem], row[elem]);
                }
            }
        } catch (Exception e) {
            logger.error(e.getLocalizedMessage(), e);
        }
    }
    try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(stream, StandardCharsets.UTF_8))) {
        writer.write(String.join(separator, allColumns));
        writer.write(System.lineSeparator());
        Iterator[] its = new Iterator[allColumns.size()];
        for (int col = 0; col < allColumns.size(); col++) {
            its[col] = valueMap.get(allColumns.get(col)).iterator();
        }
        for (ProjectImageEntry<?> entry : imageList) {
            for (int nObject = 0; nObject < nImageEntries.get(entry); nObject++) {
                for (int nCol = 0; nCol < allColumns.size(); nCol++) {
                    if (Arrays.stream(imageCols.get(entry)).anyMatch(allColumns.get(nCol)::equals)) {
                        String val = (String) its[nCol].next();
                        // NaN values -> blank
                        if (val.equals("NaN"))
                            val = "";
                        writer.write(val);
                    }
                    if (nCol < allColumns.size() - 1)
                        writer.write(separator);
                }
                writer.write(System.lineSeparator());
            }
        }
    } catch (Exception e) {
        logger.error("Error writing to file: " + e.getLocalizedMessage(), e);
    }
    long endTime = System.currentTimeMillis();
    long timeMillis = endTime - startTime;
    String time = null;
    if (timeMillis > 1000 * 60)
        time = String.format("Total processing time: %.2f minutes", timeMillis / (1000.0 * 60.0));
    else if (timeMillis > 1000)
        time = String.format("Total processing time: %.2f seconds", timeMillis / (1000.0));
    else
        time = String.format("Total processing time: %d milliseconds", timeMillis);
    logger.info("Processed {} images", imageList.size());
    logger.info(time);
}
Also used : Arrays(java.util.Arrays) LoggerFactory(org.slf4j.LoggerFactory) HashMap(java.util.HashMap) Multimap(com.google.common.collect.Multimap) ArrayList(java.util.ArrayList) PathRootObject(qupath.lib.objects.PathRootObject) ObservableMeasurementTableData(qupath.lib.gui.measure.ObservableMeasurementTableData) Map(java.util.Map) OutputStreamWriter(java.io.OutputStreamWriter) OutputStream(java.io.OutputStream) PrintWriter(java.io.PrintWriter) LinkedListMultimap(com.google.common.collect.LinkedListMultimap) ImageData(qupath.lib.images.ImageData) Logger(org.slf4j.Logger) Iterator(java.util.Iterator) ProjectImageEntry(qupath.lib.projects.ProjectImageEntry) BufferedImage(java.awt.image.BufferedImage) Predicate(java.util.function.Predicate) Collection(java.util.Collection) FileOutputStream(java.io.FileOutputStream) Collectors(java.util.stream.Collectors) File(java.io.File) StandardCharsets(java.nio.charset.StandardCharsets) PathObject(qupath.lib.objects.PathObject) List(java.util.List) SummaryMeasurementTableCommand(qupath.lib.gui.commands.SummaryMeasurementTableCommand) Collections(java.util.Collections) PathPrefs(qupath.lib.gui.prefs.PathPrefs) HashMap(java.util.HashMap) ArrayList(java.util.ArrayList) ObservableMeasurementTableData(qupath.lib.gui.measure.ObservableMeasurementTableData) PathObject(qupath.lib.objects.PathObject) Iterator(java.util.Iterator) ProjectImageEntry(qupath.lib.projects.ProjectImageEntry) OutputStreamWriter(java.io.OutputStreamWriter) PrintWriter(java.io.PrintWriter)

Example 5 with ProjectImageEntry

use of qupath.lib.projects.ProjectImageEntry in project qupath by qupath.

the class ProjectBrowser method getPopup.

ContextMenu getPopup() {
    Action actionOpenImage = new Action("Open image", e -> qupath.openImageEntry(getSelectedEntry()));
    Action actionRemoveImage = new Action("Remove image(s)", e -> {
        Collection<ImageRow> imageRows = getSelectedImageRowsRecursive();
        Collection<ProjectImageEntry<BufferedImage>> entries = ProjectTreeRow.getEntries(imageRows);
        if (entries.isEmpty())
            return;
        // Don't allow us to remove any entries that are currently open (in any viewer)
        for (var viewer : qupath.getViewers()) {
            var imageData = viewer.getImageData();
            var entry = imageData == null ? null : getProject().getEntry(imageData);
            if (entry != null && entries.contains(entry)) {
                Dialogs.showErrorMessage("Remove project entries", "Please close all images you want to remove!");
                return;
            }
        }
        if (entries.size() == 1) {
            if (!Dialogs.showConfirmDialog("Remove project entry", "Remove " + entries.iterator().next().getImageName() + " from project?"))
                return;
        } else if (!Dialogs.showYesNoDialog("Remove project entries", String.format("Remove %d entries?", entries.size())))
            return;
        var result = Dialogs.showYesNoCancelDialog("Remove project entries", "Delete all associated data?");
        if (result == DialogButton.CANCEL)
            return;
        project.removeAllImages(entries, result == DialogButton.YES);
        refreshTree(null);
        syncProject(project);
        if (tree != null) {
            boolean isExpanded = tree.getRoot() != null && tree.getRoot().isExpanded();
            tree.setRoot(model.getRoot());
            tree.getRoot().setExpanded(isExpanded);
        }
    });
    Action actionDuplicateImages = new Action("Duplicate image(s)", e -> {
        Collection<ImageRow> imageRows = getSelectedImageRowsRecursive();
        if (imageRows.isEmpty()) {
            logger.debug("Nothing to duplicate - no entries selected");
            return;
        }
        boolean singleImage = false;
        String name = "";
        String title = "Duplicate images";
        String namePrompt = "Append to image name";
        String nameHelp = "Specify text to append to the image name to distinguish duplicated images";
        if (imageRows.size() == 1) {
            title = "Duplicate image";
            namePrompt = "Duplicate image name";
            nameHelp = "Specify name for the duplicated image";
            singleImage = true;
            name = imageRows.iterator().next().getDisplayableString();
            name = GeneralTools.generateDistinctName(name, project.getImageList().stream().map(p -> p.getImageName()).collect(Collectors.toSet()));
        }
        var params = new ParameterList().addStringParameter("name", namePrompt, name, nameHelp).addBooleanParameter("copyData", "Also duplicate data files", true, "Duplicate any associated data files along with the image");
        if (!Dialogs.showParameterDialog(title, params))
            return;
        boolean copyData = params.getBooleanParameterValue("copyData");
        name = params.getStringParameterValue("name");
        // Ensure we have a single space and then the text to append, with extra whitespace removed
        if (!singleImage && !name.isBlank())
            name = " " + name.strip();
        for (var imageRow : imageRows) {
            try {
                var newEntry = project.addDuplicate(ProjectTreeRow.getEntry(imageRow), copyData);
                if (newEntry != null && !name.isBlank()) {
                    if (singleImage)
                        newEntry.setImageName(name);
                    else
                        newEntry.setImageName(newEntry.getImageName() + name);
                }
            } catch (Exception ex) {
                Dialogs.showErrorNotification("Duplicating image", "Error duplicating " + ProjectTreeRow.getEntry(imageRow).getImageName());
                logger.error(ex.getLocalizedMessage(), ex);
            }
        }
        try {
            project.syncChanges();
        } catch (Exception ex) {
            logger.error("Error synchronizing project changes: " + ex.getLocalizedMessage(), ex);
        }
        refreshProject();
        if (imageRows.size() == 1)
            logger.debug("Duplicated 1 image entry");
        else
            logger.debug("Duplicated {} image entries");
    });
    Action actionSetImageName = new Action("Rename image", e -> {
        TreeItem<ProjectTreeRow> path = tree.getSelectionModel().getSelectedItem();
        if (path == null)
            return;
        if (path.getValue().getType() == ProjectTreeRow.Type.IMAGE) {
            if (setProjectEntryImageName(ProjectTreeRow.getEntry(path.getValue())) && project != null)
                syncProject(project);
        }
    });
    // Add a metadata value
    Action actionAddMetadataValue = new Action("Add metadata", e -> {
        Project<BufferedImage> project = getProject();
        Collection<ImageRow> imageRows = getSelectedImageRowsRecursive();
        if (project != null && !imageRows.isEmpty()) {
            TextField tfMetadataKey = new TextField();
            var suggestions = project.getImageList().stream().map(p -> p.getMetadataKeys()).flatMap(Collection::stream).distinct().sorted().collect(Collectors.toList());
            TextFields.bindAutoCompletion(tfMetadataKey, suggestions);
            TextField tfMetadataValue = new TextField();
            Label labKey = new Label("New key");
            Label labValue = new Label("New value");
            labKey.setLabelFor(tfMetadataKey);
            labValue.setLabelFor(tfMetadataValue);
            tfMetadataKey.setTooltip(new Tooltip("Enter the name for the metadata entry"));
            tfMetadataValue.setTooltip(new Tooltip("Enter the value for the metadata entry"));
            ProjectImageEntry<BufferedImage> entry = imageRows.size() == 1 ? ProjectTreeRow.getEntry(imageRows.iterator().next()) : null;
            int nMetadataValues = entry == null ? 0 : entry.getMetadataKeys().size();
            GridPane pane = new GridPane();
            pane.setVgap(5);
            pane.setHgap(5);
            pane.add(labKey, 0, 0);
            pane.add(tfMetadataKey, 1, 0);
            pane.add(labValue, 0, 1);
            pane.add(tfMetadataValue, 1, 1);
            String name = imageRows.size() + " images";
            if (entry != null) {
                name = entry.getImageName();
                if (nMetadataValues > 0) {
                    Label labelCurrent = new Label("Current metadata");
                    TextArea textAreaCurrent = new TextArea();
                    textAreaCurrent.setEditable(false);
                    String keyString = entry.getMetadataSummaryString();
                    if (keyString.isEmpty())
                        textAreaCurrent.setText("No metadata entries yet");
                    else
                        textAreaCurrent.setText(keyString);
                    textAreaCurrent.setPrefRowCount(3);
                    labelCurrent.setLabelFor(textAreaCurrent);
                    pane.add(labelCurrent, 0, 2);
                    pane.add(textAreaCurrent, 1, 2);
                }
            }
            Dialog<ButtonType> dialog = new Dialog<>();
            dialog.setTitle("Metadata");
            dialog.getDialogPane().getButtonTypes().setAll(ButtonType.OK, ButtonType.CANCEL);
            dialog.getDialogPane().setHeaderText("Set metadata for " + name);
            dialog.getDialogPane().setContent(pane);
            Optional<ButtonType> result = dialog.showAndWait();
            if (result.isPresent() && result.get() == ButtonType.OK) {
                String key = tfMetadataKey.getText().trim();
                String value = tfMetadataValue.getText();
                if (key.isEmpty()) {
                    logger.warn("Attempted to set metadata value for {}, but key was empty!", name);
                } else {
                    // Set metadata for all entries
                    for (var temp : imageRows) ProjectTreeRow.getEntry(temp).putMetadataValue(key, value);
                    syncProject(project);
                    tree.refresh();
                }
            }
        } else {
            Dialogs.showErrorMessage("Edit image description", "No entry is selected!");
        }
    });
    // Edit the description for the image
    Action actionEditDescription = new Action("Edit description", e -> {
        Project<?> project = getProject();
        ProjectImageEntry<?> entry = getSelectedEntry();
        if (project != null && entry != null) {
            if (showDescriptionEditor(entry)) {
                descriptionText.set(entry.getDescription());
                syncProject(project);
            }
        } else {
            Dialogs.showErrorMessage("Edit image description", "No entry is selected!");
        }
    });
    // Mask the name of the images and shuffle the entry
    Action actionMaskImageNames = ActionTools.createSelectableAction(PathPrefs.maskImageNamesProperty(), "Mask image names");
    // Refresh thumbnail according to current display settings
    Action actionRefreshThumbnail = new Action("Refresh thumbnail", e -> {
        TreeItem<ProjectTreeRow> path = tree.getSelectionModel().getSelectedItem();
        if (path == null)
            return;
        if (path.getValue().getType() == ProjectTreeRow.Type.IMAGE) {
            ProjectImageEntry<BufferedImage> entry = ProjectTreeRow.getEntry(path.getValue());
            if (!isCurrentImage(entry)) {
                logger.warn("Cannot refresh entry for image that is not open!");
                return;
            }
            BufferedImage imgThumbnail = qupath.getViewer().getRGBThumbnail();
            imgThumbnail = resizeForThumbnail(imgThumbnail);
            try {
                entry.setThumbnail(imgThumbnail);
            } catch (IOException e1) {
                logger.error("Error writing thumbnail", e1);
            }
            tree.refresh();
        }
    });
    // Open the project directory using Explorer/Finder etc.
    Action actionOpenProjectDirectory = createBrowsePathAction("Project...", () -> getProjectPath());
    Action actionOpenProjectEntryDirectory = createBrowsePathAction("Project entry...", () -> getProjectEntryPath());
    Action actionOpenImageServerDirectory = createBrowsePathAction("Image server...", () -> getImageServerPath());
    Menu menuSort = new Menu("Sort by...");
    ContextMenu menu = new ContextMenu();
    var hasProjectBinding = qupath.projectProperty().isNotNull();
    var menuOpenDirectories = MenuTools.createMenu("Open directory...", actionOpenProjectDirectory, actionOpenProjectEntryDirectory, actionOpenImageServerDirectory);
    menuOpenDirectories.visibleProperty().bind(hasProjectBinding);
    // MenuItem miOpenProjectDirectory = ActionUtils.createMenuItem(actionOpenProjectDirectory);
    MenuItem miOpenImage = ActionUtils.createMenuItem(actionOpenImage);
    MenuItem miRemoveImage = ActionUtils.createMenuItem(actionRemoveImage);
    MenuItem miDuplicateImage = ActionUtils.createMenuItem(actionDuplicateImages);
    MenuItem miSetImageName = ActionUtils.createMenuItem(actionSetImageName);
    MenuItem miRefreshThumbnail = ActionUtils.createMenuItem(actionRefreshThumbnail);
    MenuItem miEditDescription = ActionUtils.createMenuItem(actionEditDescription);
    MenuItem miAddMetadata = ActionUtils.createMenuItem(actionAddMetadataValue);
    MenuItem miMaskImages = ActionUtils.createCheckMenuItem(actionMaskImageNames);
    // Set visibility as menu being displayed
    menu.setOnShowing(e -> {
        TreeItem<ProjectTreeRow> selected = tree.getSelectionModel().getSelectedItem();
        ProjectImageEntry<BufferedImage> selectedEntry = selected == null ? null : ProjectTreeRow.getEntry(selected.getValue());
        var entries = getSelectedImageRowsRecursive();
        boolean isImageEntry = selectedEntry != null;
        // miOpenProjectDirectory.setVisible(project != null && project.getBaseDirectory().exists());
        miOpenImage.setVisible(isImageEntry);
        miDuplicateImage.setVisible(isImageEntry);
        miSetImageName.setVisible(isImageEntry);
        miAddMetadata.setVisible(!entries.isEmpty());
        miEditDescription.setVisible(isImageEntry);
        miRefreshThumbnail.setVisible(isImageEntry && isCurrentImage(selectedEntry));
        miRemoveImage.setVisible(selected != null && project != null && !project.getImageList().isEmpty());
        if (project == null) {
            menuSort.setVisible(false);
            return;
        }
        Map<String, MenuItem> newItems = new TreeMap<>();
        for (ProjectImageEntry<?> entry : project.getImageList()) {
            // Add all entry metadata keys
            for (String key : entry.getMetadataKeys()) {
                if (!newItems.containsKey(key))
                    newItems.put(key, ActionUtils.createMenuItem(createSortByKeyAction(key, key)));
            }
            // Add all additional keys
            for (String key : baseMetadataKeys) {
                if (!newItems.containsKey(key))
                    newItems.put(key, ActionUtils.createMenuItem(createSortByKeyAction(key, key)));
            }
        }
        menuSort.getItems().setAll(newItems.values());
        menuSort.getItems().add(0, ActionUtils.createMenuItem(createSortByKeyAction("None", null)));
        menuSort.getItems().add(1, new SeparatorMenuItem());
        menuSort.setVisible(true);
        if (menu.getItems().isEmpty())
            e.consume();
    });
    SeparatorMenuItem separator = new SeparatorMenuItem();
    separator.visibleProperty().bind(menuSort.visibleProperty());
    menu.getItems().addAll(miOpenImage, miRemoveImage, miDuplicateImage, new SeparatorMenuItem(), miSetImageName, miAddMetadata, miEditDescription, miMaskImages, miRefreshThumbnail, separator, menuSort);
    separator = new SeparatorMenuItem();
    separator.visibleProperty().bind(menuOpenDirectories.visibleProperty());
    if (Desktop.isDesktopSupported()) {
        menu.getItems().addAll(separator, menuOpenDirectories);
    }
    return menu;
}
Also used : Button(javafx.scene.control.Button) ImageServer(qupath.lib.images.servers.ImageServer) DoubleBinding(javafx.beans.binding.DoubleBinding) ActionUtils(org.controlsfx.control.action.ActionUtils) LoggerFactory(org.slf4j.LoggerFactory) RenderingHints(java.awt.RenderingHints) StackPane(javafx.scene.layout.StackPane) Side(javafx.geometry.Side) ParameterList(qupath.lib.plugins.parameters.ParameterList) MasterDetailPane(org.controlsfx.control.MasterDetailPane) ContextMenu(javafx.scene.control.ContextMenu) Map(java.util.Map) URI(java.net.URI) Path(java.nio.file.Path) QuPathGUI(qupath.lib.gui.QuPathGUI) Pane(javafx.scene.layout.Pane) TextField(javafx.scene.control.TextField) MenuItem(javafx.scene.control.MenuItem) BufferedImage(java.awt.image.BufferedImage) IconFactory(qupath.lib.gui.tools.IconFactory) Collection(java.util.Collection) Set(java.util.Set) Canvas(javafx.scene.canvas.Canvas) Collectors(java.util.stream.Collectors) Executors(java.util.concurrent.Executors) TreeView(javafx.scene.control.TreeView) Objects(java.util.Objects) Platform(javafx.application.Platform) SeparatorMenuItem(javafx.scene.control.SeparatorMenuItem) List(java.util.List) Project(qupath.lib.projects.Project) GuiTools(qupath.lib.gui.tools.GuiTools) Optional(java.util.Optional) ThreadTools(qupath.lib.common.ThreadTools) ObservableList(javafx.collections.ObservableList) BorderPane(javafx.scene.layout.BorderPane) StringProperty(javafx.beans.property.StringProperty) IntStream(java.util.stream.IntStream) TextArea(javafx.scene.control.TextArea) SimpleStringProperty(javafx.beans.property.SimpleStringProperty) ButtonType(javafx.scene.control.ButtonType) TreeItem(javafx.scene.control.TreeItem) Action(org.controlsfx.control.action.Action) ImageRow(qupath.lib.gui.panes.ProjectTreeRow.ImageRow) FXCollections(javafx.collections.FXCollections) PathIcons(qupath.lib.gui.tools.IconFactory.PathIcons) Supplier(java.util.function.Supplier) Bindings(javafx.beans.binding.Bindings) HashSet(java.util.HashSet) Dialogs(qupath.lib.gui.dialogs.Dialogs) TextFields(org.controlsfx.control.textfield.TextFields) Insets(javafx.geometry.Insets) Graphics2D(java.awt.Graphics2D) ActionTools(qupath.lib.gui.ActionTools) Tooltip(javafx.scene.control.Tooltip) ExecutorService(java.util.concurrent.ExecutorService) GridPane(javafx.scene.layout.GridPane) ImageData(qupath.lib.images.ImageData) Desktop(java.awt.Desktop) KeyCode(javafx.scene.input.KeyCode) ObjectProperty(javafx.beans.property.ObjectProperty) Logger(org.slf4j.Logger) Dialog(javafx.scene.control.Dialog) Label(javafx.scene.control.Label) TitledPane(javafx.scene.control.TitledPane) ProjectImageEntry(qupath.lib.projects.ProjectImageEntry) GeneralTools(qupath.lib.common.GeneralTools) IOException(java.io.IOException) MetadataRow(qupath.lib.gui.panes.ProjectTreeRow.MetadataRow) ProjectCommands(qupath.lib.gui.commands.ProjectCommands) MenuTools(qupath.lib.gui.tools.MenuTools) Menu(javafx.scene.control.Menu) SelectionMode(javafx.scene.control.SelectionMode) TreeMap(java.util.TreeMap) Type(qupath.lib.gui.panes.ProjectTreeRow.Type) ImageView(javafx.scene.image.ImageView) SwingFXUtils(javafx.embed.swing.SwingFXUtils) TreeCell(javafx.scene.control.TreeCell) ObservableValue(javafx.beans.value.ObservableValue) ChangeListener(javafx.beans.value.ChangeListener) Collections(java.util.Collections) Image(javafx.scene.image.Image) PathPrefs(qupath.lib.gui.prefs.PathPrefs) ImageServerMetadata(qupath.lib.images.servers.ImageServerMetadata) DialogButton(qupath.lib.gui.dialogs.Dialogs.DialogButton) PaneTools(qupath.lib.gui.tools.PaneTools) Action(org.controlsfx.control.action.Action) ImageRow(qupath.lib.gui.panes.ProjectTreeRow.ImageRow) TextArea(javafx.scene.control.TextArea) Label(javafx.scene.control.Label) ContextMenu(javafx.scene.control.ContextMenu) BufferedImage(java.awt.image.BufferedImage) Dialog(javafx.scene.control.Dialog) TextField(javafx.scene.control.TextField) ProjectImageEntry(qupath.lib.projects.ProjectImageEntry) ContextMenu(javafx.scene.control.ContextMenu) Menu(javafx.scene.control.Menu) ButtonType(javafx.scene.control.ButtonType) GridPane(javafx.scene.layout.GridPane) Tooltip(javafx.scene.control.Tooltip) MenuItem(javafx.scene.control.MenuItem) SeparatorMenuItem(javafx.scene.control.SeparatorMenuItem) IOException(java.io.IOException) TreeMap(java.util.TreeMap) SeparatorMenuItem(javafx.scene.control.SeparatorMenuItem) IOException(java.io.IOException) ParameterList(qupath.lib.plugins.parameters.ParameterList)

Aggregations

ProjectImageEntry (qupath.lib.projects.ProjectImageEntry)8 BufferedImage (java.awt.image.BufferedImage)6 ArrayList (java.util.ArrayList)4 List (java.util.List)4 Collectors (java.util.stream.Collectors)4 ButtonType (javafx.scene.control.ButtonType)4 Dialog (javafx.scene.control.Dialog)4 File (java.io.File)3 IOException (java.io.IOException)3 Collection (java.util.Collection)3 Collections (java.util.Collections)3 Map (java.util.Map)3 Optional (java.util.Optional)3 Logger (org.slf4j.Logger)3 LoggerFactory (org.slf4j.LoggerFactory)3 PathPrefs (qupath.lib.gui.prefs.PathPrefs)3 Graphics2D (java.awt.Graphics2D)2 RenderingHints (java.awt.RenderingHints)2 FileNotFoundException (java.io.FileNotFoundException)2 URI (java.net.URI)2