Skip to content

Commit

Permalink
Support sort by name/ID in project browser
Browse files Browse the repository at this point in the history
Addresses one of the more minor requests in qupath#1289
  • Loading branch information
petebankhead committed Sep 11, 2023
1 parent 42c2aec commit 339d066
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 29 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ This is a work-in-progress for the next QuPath release.
### Enhancements
* New toolbar buttons for script editor `</>` and log viewer
* *File &rarr; Export snapshots* supports PNG, JPEG and TIFF (not just PNG)
* Support sorting project entries by name, ID, and URI
* Right-click on the project list to access the *Sort by...* menu

### Bugs fixed
* Rendered image export does not support opacity (https://github.com/qupath/qupath/issues/1292)
Expand Down
108 changes: 79 additions & 29 deletions qupath-gui-fx/src/main/java/qupath/lib/gui/panes/ProjectBrowser.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import org.controlsfx.control.MasterDetailPane;
import org.controlsfx.control.action.Action;
Expand Down Expand Up @@ -152,11 +153,27 @@ public class ProjectBrowser implements ChangeListener<ImageData<BufferedImage>>
/**
* Metadata keys that will always be present
*/
private static final String URI = "URI";
private enum BaseMetadataKeys {
IMAGE_NAME("Image name"), ENTRY_ID("Entry ID"), URI("URI");

private final String displayName;

BaseMetadataKeys(String displayName) {
this.displayName = displayName;
}

String getDisplayName() {
return displayName;
}

String getKey() {
return "SORT_KEY[" + toString() + "]";
}

}
private static final String UNASSIGNED_NODE = "(Unassigned)";
private static final String UNDEFINED_VALUE = "Undefined";
private static String[] baseMetadataKeys = new String[] {URI};


/**
* To load thumbnails in the background
*/
Expand Down Expand Up @@ -484,7 +501,6 @@ ContextMenu getPopup() {
Action actionOpenImageServerDirectory = createBrowsePathAction("Image...", () -> getImageServerPath());


Menu menuSort = new Menu("Sort by...");
ContextMenu menu = new ContextMenu();

var hasProjectBinding = qupath.projectProperty().isNotNull();
Expand All @@ -503,8 +519,10 @@ ContextMenu getPopup() {
MenuItem miEditDescription = ActionUtils.createMenuItem(actionEditDescription);
MenuItem miAddMetadata = ActionUtils.createMenuItem(actionAddMetadataValue);
MenuItem miMaskImages = ActionUtils.createCheckMenuItem(actionMaskImageNames);



// Create menu for sorting by metadata
Menu menuSort = createSortByMenu();

// Set visibility as menu being displayed
menu.setOnShowing(e -> {
TreeItem<ProjectTreeRow> selected = tree.getSelectionModel().getSelectedItem();
Expand All @@ -529,29 +547,12 @@ ContextMenu getPopup() {
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())
Expand Down Expand Up @@ -586,6 +587,34 @@ ContextMenu getPopup() {

return menu;
}


Menu createSortByMenu() {
var menuSort = new Menu("Sort by...");
Map<String, MenuItem> newItems = new TreeMap<>();
if (project != null) {
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)));
}
}
}
menuSort.getItems().setAll(newItems.values());

// Add all additional keys
for (var key : BaseMetadataKeys.values()) {
if (!newItems.containsKey(key.getKey()))
menuSort.getItems().add(ActionUtils.createMenuItem(createSortByKeyAction(key.getDisplayName(), key.getKey())));
}

menuSort.getItems().add(0, ActionUtils.createMenuItem(createSortByKeyAction("None", null)));
menuSort.getItems().add(1, new SeparatorMenuItem());

return menuSort;
}


Path getProjectPath() {
return project == null ? null : project.getPath();
Expand Down Expand Up @@ -896,7 +925,7 @@ private Set<String> getAllMetadataValues(String metadataKey) {
* @throws IOException
*/
private static <T> String getDefaultValue(ProjectImageEntry<T> entry, String key) throws IOException {
if (key.equals(URI)) {
if (key.equals(BaseMetadataKeys.URI.getKey())) {
var URIs = entry.getURIs();
var it = URIs.iterator();

Expand All @@ -911,6 +940,10 @@ private static <T> String getDefaultValue(ProjectImageEntry<T> entry, String key
return fullURI.substring(fullURI.lastIndexOf("/")+1, fullURI.length());
}
return "Multiple URIs";
} else if (key.equals(BaseMetadataKeys.IMAGE_NAME.getKey())) {
return entry.getImageName();
} else if (key.equals(BaseMetadataKeys.ENTRY_ID.getKey())) {
return entry.getID();
}
var value = entry.getMetadataValue(key);
return value == null ? UNASSIGNED_NODE : value;
Expand Down Expand Up @@ -1131,10 +1164,12 @@ public void updateItem(ProjectTreeRow item, boolean empty) {
var children = getTreeItem().getChildren();
setText(item.getDisplayableString() + (children.size() > 0 ? " (" + children.size() + ")" : ""));
setGraphic(null);
// TODO: Extract styles to external CSS
setStyle("-fx-font-weight: normal; -fx-font-family: arial");
return;
} else if (item.getType() == ProjectTreeRow.Type.METADATA) {
var children = getTreeItem().getChildren();
// TODO: Try not to display count when grouping by ID
setText(item.getDisplayableString() + (children.size() > 0 ? " (" + children.size() + ")" : ""));
setGraphic(null);
setStyle("-fx-font-weight: normal; -fx-font-family: arial");
Expand Down Expand Up @@ -1264,9 +1299,24 @@ public ObservableList<TreeItem<ProjectTreeRow>> getChildren() {
} else {
var values = new ArrayList<>(getAllMetadataValues(metadataKey));
GeneralTools.smartStringSort(values);
children.addAll(values.stream()
var potentialChildren = values.stream()
.map(value -> new ProjectTreeRowItem(new MetadataRow(value)))
.toList());
.toList();
// When sorting by name, we don't want to show grouped by name - since it looks weird,
// with the name effectively being repeated twice
if (metadataKey.equals(BaseMetadataKeys.IMAGE_NAME.getKey()))
potentialChildren = potentialChildren.stream().flatMap(c -> {
if (c.isLeaf())
return Stream.empty();
else
return c.getChildren().stream();
}).map(t -> (ProjectTreeRowItem)t).toList();
// When sorting by entry ID, we want to expand everything - since there should only be one
// entry per ID
if (metadataKey.equals(BaseMetadataKeys.ENTRY_ID.getKey()))
potentialChildren.forEach(c -> c.setExpanded(true));

children.addAll(potentialChildren);
}
break;
case METADATA:
Expand All @@ -1281,7 +1331,7 @@ public ObservableList<TreeItem<ProjectTreeRow>> getChildren() {
if (value != null && value.equals(((MetadataRow)getValue()).getDisplayableString()))
children.add(new ProjectTreeRowItem(row));
} catch (IOException ex) {
logger.warn("Could not get URIs from: " + row.getDisplayableString(), ex.getLocalizedMessage());
logger.warn("Could not get {} from {}", metadataKey, row.getDisplayableString(), ex);
}
}
case IMAGE:
Expand Down

0 comments on commit 339d066

Please sign in to comment.