Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Batch actions for MeasurementToolList #406

Merged
merged 7 commits into from
Sep 12, 2023
46 changes: 18 additions & 28 deletions src/components/AnnotationsModule.vue
Original file line number Diff line number Diff line change
@@ -1,42 +1,36 @@
<script lang="ts">
import { defineComponent } from 'vue';
import MeasurementsRulerList from './MeasurementsRulerList.vue';
<script setup lang="ts">
import MeasurementsToolList from './MeasurementsToolList.vue';
import LabelmapList from './LabelmapList.vue';
import ToolControls from './ToolControls.vue';
import { usePolygonStore } from '../store/tools/polygons';
import { useRectangleStore } from '../store/tools/rectangles';
import { useRulerStore } from '../store/tools/rulers';
import { AnnotationToolStore } from '../store/tools/useAnnotationTool';
import MeasurementRulerDetails from './MeasurementRulerDetails.vue';

export default defineComponent({
components: {
MeasurementsRulerList,
MeasurementsToolList,
LabelmapList,
ToolControls,
const tools = [
{
store: useRulerStore(),
icon: 'mdi-ruler',
details: MeasurementRulerDetails,
},
setup() {
return {
rectangleStore: useRectangleStore(),
polygonStore: usePolygonStore(),
};
{
store: useRectangleStore() as unknown as AnnotationToolStore<string>,
icon: 'mdi-vector-square',
},
});
{
store: usePolygonStore() as unknown as AnnotationToolStore<string>,
icon: 'mdi-pentagon-outline',
},
];
Comment on lines +11 to +25
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No action for now, but we will revisit this later on, potentially making a lookup table mapping the Tools enum to the stores.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like!

</script>

<template>
<div class="overflow-y-auto mx-2 fill-height">
<tool-controls />
<div class="header">Measurements</div>
<div class="content">
<measurements-ruler-list />
<measurements-tool-list
:tool-store="rectangleStore"
icon="mdi-vector-square"
/>
<measurements-tool-list
:tool-store="polygonStore"
icon="mdi-pentagon-outline"
/>
<measurements-tool-list :tools="tools" />
</div>
<div class="text-caption text-center empty-state">No measurements</div>
<div class="header">Labelmaps</div>
Expand All @@ -48,10 +42,6 @@ export default defineComponent({
</template>

<style scoped>
.annot-subheader {
margin: 8px 0;
}

.empty-state {
display: none;
}
Expand Down
4 changes: 3 additions & 1 deletion src/components/ImageDataBrowser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export default defineComponent({

// --- selection --- //

const { selected, selectedAll, selectedSome } =
const { selected, selectedAll, selectedSome, toggleSelectAll } =
useMultiSelection(nonDICOMImages);

function removeSelection() {
Expand All @@ -137,6 +137,7 @@ export default defineComponent({
selected,
selectedAll,
selectedSome,
toggleSelectAll,
removeSelection,
images,
thumbnails,
Expand All @@ -161,6 +162,7 @@ export default defineComponent({
:indeterminate="selectedSome && !selectedAll"
label="Select All"
v-model="selectedAll"
@click.stop="toggleSelectAll"
density="compact"
hide-details
/>
Expand Down
23 changes: 23 additions & 0 deletions src/components/MeasurementRulerDetails.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script setup lang="ts">
import { useRulerStore } from '@/src/store/tools/rulers';
import { AnnotationTool } from '../types/annotation-tool';

defineProps<{
tool: AnnotationTool<string> & { axis: string };
}>();

const toolStore = useRulerStore();
</script>

<template>
<v-row>
<v-col cols="4">Slice: {{ tool.slice + 1 }}</v-col>
<v-col cols="4">Axis: {{ tool.axis }}</v-col>
<v-col>
Length:
<span class="value">
{{ toolStore.lengthByID[tool.id].toFixed(2) }}mm
</span>
</v-col>
</v-row>
</template>
14 changes: 14 additions & 0 deletions src/components/MeasurementToolDetails.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script setup lang="ts">
import { AnnotationTool } from '../types/annotation-tool';

defineProps<{
tool: AnnotationTool<string> & { axis: string };
}>();
</script>

<template>
<v-row>
<v-col cols="4">Slice: {{ tool.slice + 1 }}</v-col>
<v-col cols="4">Axis: {{ tool.axis }}</v-col>
</v-row>
</template>
23 changes: 0 additions & 23 deletions src/components/MeasurementsRulerList.vue

This file was deleted.

180 changes: 141 additions & 39 deletions src/components/MeasurementsToolList.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
<script setup lang="ts" generic="ToolID extends string">
/* global ToolID:readonly */
<script setup lang="ts">
import { computed } from 'vue';
import { useCurrentImage } from '@/src/composables/useCurrentImage';
import { AnnotationToolStore } from '@/src/store/tools/useAnnotationTool';
import { frameOfReferenceToImageSliceAndAxis } from '@/src/utils/frameOfReference';
import { nonNullable } from '@/src/utils/index';
import MeasurementToolDetails from './MeasurementToolDetails.vue';
import { useMultiSelection } from '../composables/useMultiSelection';
import { AnnotationTool } from '../types/annotation-tool';

const props = defineProps<{
toolStore: AnnotationToolStore<ToolID>;
type AnnotationToolConfig = {
store: AnnotationToolStore<string>;
icon: string;
details?: typeof MeasurementToolDetails;
};

export type AnnotationTools = Array<AnnotationToolConfig>;

const props = defineProps<{
tools: AnnotationTools;
}>();

const { currentImageID, currentImageMetadata } = useCurrentImage();

const tools = computed(() => {
const byID = props.toolStore.toolByID;
return props.toolStore.toolIDs
// Filter and add axis for specific annotation type
const getTools = (toolStore: AnnotationToolStore<string>) => {
const byID = toolStore.toolByID;
return toolStore.toolIDs
.map((id) => byID[id])
.filter((tool) => !tool.placing && tool.imageID === currentImageID.value)
.map((tool) => {
Expand All @@ -30,48 +41,146 @@ const tools = computed(() => {
axis,
};
});
};

// Flatten all tool types and add actions
const tools = computed(() => {
return props.tools.flatMap(
({ store, icon, details = MeasurementToolDetails }) => {
const toolsWithAxis = getTools(store);
return toolsWithAxis.map((tool) => ({
...tool,
icon,
details,
remove: () => store.removeTool(tool.id),
jumpTo: () => store.jumpToTool(tool.id),
toggleHidden: () => {
const toggled = !store.toolByID[tool.id].hidden;
store.updateTool(tool.id, { hidden: toggled });
},
updateTool: (patch: Partial<AnnotationTool<string>>) => {
store.updateTool(tool.id, patch);
},
}));
}
);
});

const remove = (id: ToolID) => {
props.toolStore.removeTool(id);
};
// --- selection and batch actions --- //

const jumpTo = (id: ToolID) => {
props.toolStore.jumpToTool(id);
};
const toolIds = computed(() => tools.value.map((tool) => tool.id));

const toggleHidden = (id: ToolID) => {
const toggled = !props.toolStore.toolByID[id].hidden;
props.toolStore.updateTool(id, { hidden: toggled });
};
const { selected, selectedAll, selectedSome, toggleSelectAll } =
useMultiSelection(toolIds);

const forEachSelectedTool = (
callback: (tool: (typeof tools.value)[number]) => void
) =>
selected.value
.map((id) => tools.value.find((tool) => id === tool.id))
.filter(nonNullable)
.forEach(callback);

function removeAll() {
forEachSelectedTool((tool) => tool.remove());
selected.value = [];
}

// If all selected tools are already hidden, it should be "show".
// If at least one selected tool is visible, it should be "hide".
const allHidden = computed(() => {
return selected.value
.map((id) => tools.value.find((tool) => id === tool.id))
.filter(nonNullable)
.every((tool) => tool.hidden);
});

function toggleGlobalHidden() {
const hidden = !allHidden.value;
forEachSelectedTool((tool) => {
tool.updateTool({ hidden });
});
}
</script>

<template>
<v-row no-gutters justify="space-between" align="center" class="mb-1">
<v-col cols="6">
<v-checkbox
class="ml-3"
:indeterminate="selectedSome && !selectedAll"
label="Select All"
v-model="selectedAll"
@click.stop="toggleSelectAll"
density="compact"
hide-details
/>
</v-col>
<v-col cols="6" align-self="center" class="d-flex justify-end">
<v-btn
icon
variant="text"
:disabled="!selectedSome"
@click.stop="toggleGlobalHidden"
>
<v-icon v-if="allHidden">mdi-eye-off</v-icon>
<v-icon v-else>mdi-eye</v-icon>
<v-tooltip location="top" activator="parent">{{
allHidden ? 'Show' : 'Hide'
}}</v-tooltip>
</v-btn>
<v-btn
icon
variant="text"
:disabled="!selectedSome"
@click.stop="removeAll"
>
<v-icon>mdi-delete</v-icon>
<v-tooltip :disabled="!selectedSome" location="top" activator="parent">
Delete selected
</v-tooltip>
</v-btn>
</v-col>
</v-row>

<v-list-item v-for="tool in tools" :key="tool.id">
<v-container>
<v-row class="align-center main-row">
<v-icon class="tool-icon">{{ icon }}</v-icon>
<div class="color-dot mr-3" :style="{ backgroundColor: tool.color }" />
<v-row class="d-flex align-center main-row">
<v-checkbox
class="no-grow mr-4"
density="compact"
hide-details
:key="tool.id"
:value="tool.id"
v-model="selected"
@click.stop
/>

<v-icon class="tool-icon mr-4">{{ tool.icon }}</v-icon>

<div
class="color-dot flex-shrink-0 mr-2"
:style="{ backgroundColor: tool.color }"
/>
<v-list-item-title v-bind="$attrs">
{{ tool.labelName }}
</v-list-item-title>

<span class="ml-auto actions">
<v-btn icon variant="text" @click="toggleHidden(tool.id)">
<span class="ml-auto flex-shrink-0">
<v-btn icon variant="text" @click="tool.jumpTo()">
<v-icon>mdi-target</v-icon>
<v-tooltip location="top" activator="parent">
Reveal Slice
</v-tooltip>
</v-btn>
<v-btn icon variant="text" @click="tool.toggleHidden()">
<v-icon v-if="tool.hidden">mdi-eye-off</v-icon>
<v-icon v-else>mdi-eye</v-icon>
<v-tooltip location="top" activator="parent">{{
tool.hidden ? 'Show' : 'Hide'
}}</v-tooltip>
</v-btn>
<v-btn icon variant="text" @click="jumpTo(tool.id)">
<v-icon>mdi-target</v-icon>
<v-tooltip location="top" activator="parent">
Reveal Slice
</v-tooltip>
</v-btn>
<v-btn icon variant="text" @click="remove(tool.id)">
<v-btn icon variant="text" @click="tool.remove()">
<v-icon>mdi-delete</v-icon>
<v-tooltip location="top" activator="parent">Delete</v-tooltip>
</v-btn>
Expand All @@ -80,12 +189,7 @@ const toggleHidden = (id: ToolID) => {

<v-row class="mt-4">
<v-list-item-subtitle class="w-100">
<slot name="details" v-bind="{ tool }">
<v-row>
<v-col cols="3">Slice: {{ tool.slice + 1 }}</v-col>
<v-col cols="3">Axis: {{ tool.axis }}</v-col>
</v-row>
</slot>
<component :is="tool.details" :tool="tool" />
</v-list-item-subtitle>
</v-row>
</v-container>
Expand All @@ -104,15 +208,13 @@ const toggleHidden = (id: ToolID) => {
height: 24px;
background: yellow;
border-radius: 16px;
flex-shrink: 0;
}

.tool-icon {
margin-inline-end: 12px;
opacity: var(--v-medium-emphasis-opacity);
}

.actions {
flex-shrink: 0;
.no-grow {
flex: 0 0 auto;
}
</style>
Loading