Skip to content

Commit

Permalink
Add button to select all trees and all segments that match a search (#…
Browse files Browse the repository at this point in the history
…8123)

* add button to select all trees that match a search

* add function for segments and improve icon

* remove console log

* add ts-expect-error tag again

* focus first search result and only allow select all matches for leaves

* fix select segment group as search result

* expand parent groups and fix mixed tree and tree group selection

* changelog

* lint

* address review

* add placeholder and disable field if all matches all selected

* fix case where group is selected

---------

Co-authored-by: MichaelBuessemeyer <[email protected]>
  • Loading branch information
dieknolle3333 and MichaelBuessemeyer authored Oct 29, 2024
1 parent 9b5a12e commit 684a588
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 16 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- Most sliders have been improved: Wheeling above a slider now changes its value and double-clicking its knob resets it to its default value. [#8095](https://github.com/scalableminds/webknossos/pull/8095)
- It is now possible to search for unnamed segments with the full default name instead of only their id. [#8133](https://github.com/scalableminds/webknossos/pull/8133)
- Increased loading speed for precomputed meshes. [#8110](https://github.com/scalableminds/webknossos/pull/8110)
- Added a button to the search popover in the skeleton and segment tab to select all matching non-group results. [#8123](https://github.com/scalableminds/webknossos/pull/8123)
- Unified wording in UI and code: “Magnification”/“mag” is now used in place of “Resolution“ most of the time, compare [https://docs.webknossos.org/webknossos/terminology.html](terminology document). [#8111](https://github.com/scalableminds/webknossos/pull/8111)
- Added support for adding remote OME-Zarr NGFF version 0.5 datasets. [#8122](https://github.com/scalableminds/webknossos/pull/8122)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { Input, Tooltip, Popover, Space, type InputRef } from "antd";
import { DownOutlined, UpOutlined } from "@ant-design/icons";
import { CheckSquareOutlined, DownOutlined, UpOutlined } from "@ant-design/icons";
import * as React from "react";
import memoizeOne from "memoize-one";
import ButtonComponent from "oxalis/view/components/button_component";
import Shortcut from "libs/shortcut_component";
import DomVisibilityObserver from "oxalis/view/components/dom_visibility_observer";
import { mod } from "libs/utils";

const PRIMARY_COLOR = "var(--ant-color-primary)";

type Props<S> = {
data: S[];
searchKey: keyof S | ((item: S) => string);
onSelect: (arg0: S) => void;
onSelectAllMatches?: (arg0: S[]) => void;
children: React.ReactNode;
provideShortcut?: boolean;
targetId: string;
Expand All @@ -20,6 +23,7 @@ type State = {
isVisible: boolean;
searchQuery: string;
currentPosition: number | null | undefined;
areAllMatchesSelected: boolean;
};

export default class AdvancedSearchPopover<
Expand All @@ -29,6 +33,7 @@ export default class AdvancedSearchPopover<
isVisible: false,
searchQuery: "",
currentPosition: null,
areAllMatchesSelected: false,
};

getAvailableOptions = memoizeOne(
Expand Down Expand Up @@ -69,6 +74,7 @@ export default class AdvancedSearchPopover<
currentPosition = mod(currentPosition + offset, numberOfAvailableOptions);
this.setState({
currentPosition,
areAllMatchesSelected: false,
});
this.props.onSelect(availableOptions[currentPosition]);
};
Expand Down Expand Up @@ -101,21 +107,25 @@ export default class AdvancedSearchPopover<

render() {
const { data, searchKey, provideShortcut, children, targetId } = this.props;
const { searchQuery, isVisible } = this.state;
const { searchQuery, isVisible, areAllMatchesSelected } = this.state;
let { currentPosition } = this.state;
const availableOptions = this.getAvailableOptions(data, searchQuery, searchKey);
const numberOfAvailableOptions = availableOptions.length;
// Ensure that currentPosition to not higher than numberOfAvailableOptions.
currentPosition =
currentPosition == null ? -1 : Math.min(currentPosition, numberOfAvailableOptions - 1);
const hasNoResults = numberOfAvailableOptions === 0;
const hasMultipleResults = numberOfAvailableOptions > 1;
const availableOptionsToSelectAllMatches = availableOptions.filter(
(result) => result.type === "Tree" || result.type === "segment",
);
const isSelectAllMatchesDisabled = availableOptionsToSelectAllMatches.length < 2;
const additionalInputStyle =
hasNoResults && searchQuery !== ""
? {
color: "red",
}
: {};
const selectAllMatchesButtonColor = areAllMatchesSelected ? PRIMARY_COLOR : undefined;
return (
<React.Fragment>
{provideShortcut ? (
Expand Down Expand Up @@ -171,9 +181,23 @@ export default class AdvancedSearchPopover<
this.setState({
searchQuery: evt.target.value,
currentPosition: null,
areAllMatchesSelected: false,
})
}
addonAfter={`${currentPosition + 1}/${numberOfAvailableOptions}`}
addonAfter={
<div
style={{
minWidth: 25,
color: areAllMatchesSelected
? "var(--ant-color-text-disabled)"
: undefined,
}}
>
{areAllMatchesSelected
? "all"
: `${currentPosition + 1}/${numberOfAvailableOptions}`}
</div>
}
ref={this.autoFocus}
autoFocus
/>
Expand All @@ -183,7 +207,7 @@ export default class AdvancedSearchPopover<
width: 40,
}}
onClick={this.selectPreviousOption}
disabled={!hasMultipleResults}
disabled={hasNoResults}
>
<UpOutlined />
</ButtonComponent>
Expand All @@ -194,11 +218,32 @@ export default class AdvancedSearchPopover<
width: 40,
}}
onClick={this.selectNextOption}
disabled={!hasMultipleResults}
disabled={hasNoResults}
>
<DownOutlined />
</ButtonComponent>
</Tooltip>
<Tooltip title="Select all matches (except groups)">
<ButtonComponent
style={{
width: 40,
color: selectAllMatchesButtonColor,
borderColor: selectAllMatchesButtonColor,
}}
onClick={
this.props.onSelectAllMatches != null
? () => {
this.props.onSelectAllMatches!(availableOptionsToSelectAllMatches);
if (!areAllMatchesSelected)
this.setState({ areAllMatchesSelected: true });
}
: undefined
}
disabled={isSelectAllMatchesDisabled}
>
<CheckSquareOutlined />
</ButtonComponent>
</Tooltip>
</Space.Compact>
</React.Fragment>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ import { SegmentStatisticsModal } from "./segment_statistics_modal";
import type { ItemType } from "antd/lib/menu/interface";
import { InputWithUpdateOnBlur } from "oxalis/view/components/input_with_update_on_blur";

const SCROLL_DELAY_MS = 50;

const { confirm } = Modal;
const { Option } = Select;
// Interval in ms to check for running mesh file computation jobs for this dataset
Expand Down Expand Up @@ -1590,7 +1592,7 @@ class SegmentsView extends React.Component<Props, State> {
this.setState(({ renamingCounter }) => ({ renamingCounter: renamingCounter - 1 }));
};

handleSearchSelect = (selectedElement: SegmentHierarchyNode) => {
maybeExpandParentGroup = (selectedElement: SegmentHierarchyNode) => {
if (this.tree?.current == null) {
return;
}
Expand All @@ -1606,16 +1608,47 @@ class SegmentsView extends React.Component<Props, State> {
if (expandedGroups) {
this.setExpandedGroupsFromSet(expandedGroups);
}
};

handleSearchSelect = (selectedElement: SegmentHierarchyNode) => {
this.maybeExpandParentGroup(selectedElement);
// As parent groups might still need to expand, we need to wait for this to finish.
setTimeout(() => {
if (this.tree.current) this.tree.current.scrollTo({ key: selectedElement.key });
}, 50);
}, SCROLL_DELAY_MS);
const isASegment = "color" in selectedElement;
if (isASegment) {
this.onSelectSegment(selectedElement);
} else {
if (this.props.visibleSegmentationLayer == null) return;
Store.dispatch(
setSelectedSegmentsOrGroupAction(
[],
selectedElement.id,
this.props.visibleSegmentationLayer?.name,
),
);
}
};

handleSelectAllMatchingSegments = (allMatches: SegmentHierarchyNode[]) => {
if (this.props.visibleSegmentationLayer == null) return;
const allMatchingSegmentIds = allMatches.map((match) => {
this.maybeExpandParentGroup(match);
return match.id;
});
Store.dispatch(
setSelectedSegmentsOrGroupAction(
allMatchingSegmentIds,
null,
this.props.visibleSegmentationLayer.name,
),
);
setTimeout(() => {
this.tree.current?.scrollTo({ key: allMatches[0].key });
}, SCROLL_DELAY_MS);
};

getSegmentStatisticsModal = (groupId: number) => {
const visibleSegmentationLayer = this.props.visibleSegmentationLayer;
if (visibleSegmentationLayer == null) {
Expand Down Expand Up @@ -1833,6 +1866,7 @@ class SegmentsView extends React.Component<Props, State> {
searchKey={(item) => getSegmentName(item)}
provideShortcut
targetId={segmentsTabId}
onSelectAllMatches={this.handleSelectAllMatchingSegments}
>
<ButtonComponent
size="small"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,7 @@ class SkeletonTabView extends React.PureComponent<Props, State> {
});
};

handleSearchSelect = (selectedElement: TreeOrTreeGroup) => {
maybeExpandParentGroups = (selectedElement: TreeOrTreeGroup) => {
const { skeletonTracing } = this.props;
if (!skeletonTracing) {
return;
Expand All @@ -682,13 +682,26 @@ class SkeletonTabView extends React.PureComponent<Props, State> {
if (expandedGroups) {
this.props.onSetExpandedGroups(expandedGroups);
}
};

handleSearchSelect = (selectedElement: TreeOrTreeGroup) => {
this.maybeExpandParentGroups(selectedElement);
if (selectedElement.type === GroupTypeEnum.TREE) {
this.props.onSetActiveTree(selectedElement.id);
} else {
this.props.onSetActiveTreeGroup(selectedElement.id);
}
};

handleSelectAllMatchingTrees = (matchingTrees: TreeOrTreeGroup[]) => {
this.props.onDeselectActiveGroup();
const treeIds = matchingTrees.map((tree) => {
this.maybeExpandParentGroups(tree);
return tree.id;
});
this.setState({ selectedTreeIds: treeIds });
};

getTreesComponents(sortBy: string) {
if (!this.props.skeletonTracing) {
return null;
Expand Down Expand Up @@ -864,6 +877,7 @@ class SkeletonTabView extends React.PureComponent<Props, State> {
searchKey="name"
provideShortcut
targetId={treeTabId}
onSelectAllMatches={this.handleSelectAllMatchingTrees}
>
<ButtonComponent title="Open the search via CTRL + Shift + F">
<SearchOutlined />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,7 @@ function TreeHierarchyView(props: Props) {
}
}

function onSelectGroupNode(node: TreeNode) {
const groupId = node.id;
function onSelectGroupNode(groupId: number) {
const numberOfSelectedTrees = props.selectedTreeIds.length;

if (numberOfSelectedTrees > 1) {
Expand Down Expand Up @@ -254,11 +253,14 @@ function TreeHierarchyView(props: Props) {
const checkedKeys = deepFlatFilter(UITreeData, (node) => node.isChecked).map((node) => node.key);

// selectedKeys is mainly used for highlighting, i.e. blueish background color
const selectedKeys = props.selectedTreeIds.map((treeId) =>
getNodeKey(GroupTypeEnum.TREE, treeId),
);
const selectedKeys = props.activeGroupId
? [getNodeKey(GroupTypeEnum.GROUP, props.activeGroupId)]
: props.selectedTreeIds.map((treeId) => getNodeKey(GroupTypeEnum.TREE, treeId));

if (props.activeGroupId) selectedKeys.push(getNodeKey(GroupTypeEnum.GROUP, props.activeGroupId));
useEffect(
() => treeRef.current?.scrollTo({ key: selectedKeys[0], align: "auto" }),
[selectedKeys[0]],
);

return (
<>
Expand Down Expand Up @@ -297,7 +299,7 @@ function TreeHierarchyView(props: Props) {
onSelect={(_selectedKeys, info: { node: TreeNode; nativeEvent: MouseEvent }) =>
info.node.type === GroupTypeEnum.TREE
? onSelectTreeNode(info.node, info.nativeEvent)
: onSelectGroupNode(info.node)
: onSelectGroupNode(info.node.id)
}
onDrop={onDrop}
onCheck={onCheck}
Expand Down

0 comments on commit 684a588

Please sign in to comment.