Skip to content

Commit 5aeb9eb

Browse files
committed
feat(SegmentGroupControls): add batch edit checkboxes
1 parent d565bf3 commit 5aeb9eb

File tree

2 files changed

+184
-65
lines changed

2 files changed

+184
-65
lines changed

src/components/SegmentGroupControls.vue

Lines changed: 149 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { useGlobalLayerColorConfig } from '@/src/composables/useGlobalLayerColor
1515
import { usePaintToolStore } from '@/src/store/tools/paint';
1616
import { Maybe } from '@/src/types';
1717
import { reactive, ref, computed, watch, toRaw } from 'vue';
18+
import { useMultiSelection } from '@/src/composables/useMultiSelection';
1819
1920
const UNNAMED_GROUP_NAME = 'Unnamed Segment Group';
2021
@@ -160,6 +161,63 @@ function openSaveDialog(id: string) {
160161
saveId.value = id;
161162
saveDialog.value = true;
162163
}
164+
165+
const segGroupIds = computed(() =>
166+
currentSegmentGroups.value.map((group) => group.id)
167+
);
168+
169+
const { selected, selectedAll, selectedSome } = useMultiSelection(segGroupIds);
170+
171+
// ensure currentSegmentGroupID is always in selected
172+
watch(
173+
currentSegmentGroupID,
174+
(segGroupId) => {
175+
if (segGroupId == null) {
176+
return;
177+
}
178+
selected.value = [segGroupId];
179+
},
180+
{ immediate: true }
181+
);
182+
183+
// ensure currentSegmentGroupID is always in selected
184+
function toggleSelectAll() {
185+
if (selectedAll.value) {
186+
selected.value = currentSegmentGroupID.value
187+
? [currentSegmentGroupID.value]
188+
: [];
189+
} else {
190+
selected.value = [...segGroupIds.value];
191+
}
192+
}
193+
194+
const allHidden = computed(() => {
195+
return selected.value
196+
.map((id) => currentSegmentGroups.value.find((group) => id === group.id))
197+
.filter((group): group is NonNullable<typeof group> => group != null)
198+
.every((group) => !group.visibility);
199+
});
200+
201+
function toggleGlobalVisibility() {
202+
const shouldShow = allHidden.value;
203+
selected.value.forEach((id) => {
204+
const group = currentSegmentGroups.value.find((g) => g.id === id);
205+
if (group) {
206+
const { sampledConfig, updateConfig } = useGlobalLayerColorConfig(id);
207+
const currentBlend = sampledConfig.value!.config!.blendConfig;
208+
updateConfig({
209+
blendConfig: {
210+
...currentBlend,
211+
visibility: shouldShow,
212+
},
213+
});
214+
}
215+
});
216+
}
217+
218+
function deleteSelected() {
219+
selected.value.forEach((id) => deleteGroup(id));
220+
}
163221
</script>
164222

165223
<template>
@@ -203,63 +261,100 @@ function openSaveDialog(id: string) {
203261
</v-list>
204262
</v-menu>
205263
</div>
206-
<v-divider class="my-4" />
207264

208265
<segment-group-opacity
209266
v-if="currentSegmentGroupID"
210267
:group-id="currentSegmentGroupID"
268+
:selected="selected"
211269
/>
212-
<v-radio-group
213-
v-model="currentSegmentGroupID"
214-
hide-details
215-
density="comfortable"
216-
class="my-1 segment-group-list"
217-
>
218-
<v-radio
270+
271+
<div class="d-flex align-center">
272+
<v-checkbox
273+
class="ml-3"
274+
:indeterminate="selectedSome && !selectedAll"
275+
label="Select All"
276+
:model-value="selectedAll"
277+
@update:model-value="toggleSelectAll"
278+
density="compact"
279+
hide-details
280+
/>
281+
<v-btn
282+
icon
283+
variant="text"
284+
:disabled="selected.length === 0"
285+
@click.stop="toggleGlobalVisibility"
286+
>
287+
<v-icon v-if="allHidden">mdi-eye-off</v-icon>
288+
<v-icon v-else>mdi-eye</v-icon>
289+
<v-tooltip location="top" activator="parent">
290+
{{ allHidden ? 'Show' : 'Hide' }} selected
291+
</v-tooltip>
292+
</v-btn>
293+
<v-btn
294+
icon
295+
variant="text"
296+
:disabled="selected.length === 0"
297+
@click.stop="deleteSelected"
298+
>
299+
<v-icon>mdi-delete</v-icon>
300+
<v-tooltip location="top" activator="parent">
301+
Delete selected
302+
</v-tooltip>
303+
</v-btn>
304+
</div>
305+
<v-list density="comfortable" class="my-1 segment-group-list">
306+
<v-list-item
219307
v-for="group in currentSegmentGroups"
220308
:key="group.id"
221-
:value="group.id"
309+
:active="currentSegmentGroupID === group.id"
310+
@click="currentSegmentGroupID = group.id"
222311
>
223-
<template #label>
224-
<div class="d-flex flex-row align-center w-100" :title="group.name">
225-
<span class="group-name">{{ group.name }}</span>
226-
<v-spacer />
227-
<v-btn
228-
icon
229-
variant="flat"
230-
size="small"
231-
@click.stop="group.toggleVisibility"
312+
<div class="d-flex flex-row align-center w-100" :title="group.name">
313+
<v-checkbox
314+
class="no-grow mr-4"
315+
density="compact"
316+
hide-details
317+
@click.stop
318+
:value="group.id"
319+
v-model="selected"
320+
/>
321+
<span class="group-name">{{ group.name }}</span>
322+
<v-spacer />
323+
<v-btn
324+
icon
325+
variant="text"
326+
size="small"
327+
@click.stop="group.toggleVisibility"
328+
>
329+
<v-icon v-if="group.visibility" style="pointer-events: none"
330+
>mdi-eye</v-icon
232331
>
233-
<v-icon v-if="group.visibility" style="pointer-events: none"
234-
>mdi-eye</v-icon
235-
>
236-
<v-icon v-else style="pointer-events: none">mdi-eye-off</v-icon>
237-
<v-tooltip location="left" activator="parent">{{
238-
group.visibility ? 'Hide' : 'Show'
239-
}}</v-tooltip>
240-
</v-btn>
241-
<v-btn
242-
icon="mdi-content-save"
243-
size="small"
244-
variant="flat"
245-
@click.stop="openSaveDialog(group.id)"
246-
></v-btn>
247-
<v-btn
248-
icon="mdi-pencil"
249-
size="small"
250-
variant="flat"
251-
@click.stop="startEditing(group.id)"
252-
></v-btn>
253-
<v-btn
254-
icon="mdi-delete"
255-
size="small"
256-
variant="flat"
257-
@click.stop="deleteGroup(group.id)"
258-
></v-btn>
259-
</div>
260-
</template>
261-
</v-radio>
262-
</v-radio-group>
332+
<v-icon v-else style="pointer-events: none">mdi-eye-off</v-icon>
333+
<v-tooltip location="left" activator="parent">
334+
{{ group.visibility ? 'Hide' : 'Show' }}
335+
</v-tooltip>
336+
</v-btn>
337+
<v-btn
338+
icon="mdi-content-save"
339+
size="small"
340+
variant="text"
341+
@click.stop="openSaveDialog(group.id)"
342+
/>
343+
<v-btn
344+
icon="mdi-pencil"
345+
size="small"
346+
variant="text"
347+
@click.stop="startEditing(group.id)"
348+
/>
349+
<v-btn
350+
icon="mdi-delete"
351+
size="small"
352+
variant="text"
353+
@click.stop="deleteGroup(group.id)"
354+
/>
355+
</div>
356+
</v-list-item>
357+
</v-list>
263358
<v-divider class="my-4" />
264359
</div>
265360
<div v-else class="text-center text-caption">No selected image</div>
@@ -306,5 +401,11 @@ function openSaveDialog(id: string) {
306401
white-space: nowrap;
307402
overflow: hidden;
308403
text-overflow: ellipsis;
404+
padding-right: 10px;
405+
text-align: left;
406+
}
407+
408+
.no-grow {
409+
flex: 0 0 auto;
309410
}
310411
</style>

src/components/SegmentGroupOpacity.vue

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,59 @@ import { useGlobalSegmentGroupConfig } from '@/src/store/view-configs/segmentGro
55
66
const props = defineProps<{
77
groupId: string;
8+
selected: string[];
89
}>();
910
10-
const { groupId } = toRefs(props);
11+
const { groupId, selected } = toRefs(props);
1112
12-
const { sampledConfig, updateConfig } = useGlobalLayerColorConfig(groupId);
13+
const { sampledConfig } = useGlobalLayerColorConfig(groupId);
1314
1415
const blendConfig = computed(() => sampledConfig.value!.config!.blendConfig);
1516
17+
const layerUpdateFunctions = computed(() =>
18+
selected.value.map((id) => {
19+
return useGlobalLayerColorConfig(id).updateConfig;
20+
})
21+
);
22+
1623
const setOpacity = (opacity: number) => {
17-
updateConfig({
18-
blendConfig: {
19-
...blendConfig.value,
20-
// 1.0 puts us in Opaque render pass which changes stack order.
21-
opacity: Math.min(opacity, 0.9999),
22-
},
24+
layerUpdateFunctions.value.forEach((updateFn) => {
25+
updateFn({
26+
blendConfig: {
27+
...blendConfig.value,
28+
// 1.0 puts us in Opaque render pass which changes stack order.
29+
opacity: Math.min(opacity, 0.9999),
30+
},
31+
});
2332
});
2433
};
2534
26-
const { config, updateConfig: updateSegmentGroupConfig } =
27-
useGlobalSegmentGroupConfig(groupId);
35+
const { config } = useGlobalSegmentGroupConfig(groupId);
36+
37+
const groupUpdateFunctions = computed(() =>
38+
selected.value.map((id) => {
39+
return useGlobalSegmentGroupConfig(id).updateConfig;
40+
})
41+
);
2842
2943
const outlineOpacity = computed({
3044
get: () => config.value!.config!.outlineOpacity,
3145
set: (opacity: number) => {
32-
updateSegmentGroupConfig({
33-
outlineOpacity: opacity,
46+
groupUpdateFunctions.value.forEach((updateFn) => {
47+
updateFn({
48+
outlineOpacity: opacity,
49+
});
3450
});
3551
},
3652
});
3753
3854
const outlineThickness = computed({
3955
get: () => config.value!.config!.outlineThickness,
4056
set: (thickness: number) => {
41-
updateSegmentGroupConfig({
42-
outlineThickness: thickness,
57+
groupUpdateFunctions.value.forEach((updateFn) => {
58+
updateFn({
59+
outlineThickness: thickness,
60+
});
4361
});
4462
},
4563
});
@@ -48,7 +66,7 @@ const outlineThickness = computed({
4866
<template>
4967
<v-slider
5068
class="mx-4"
51-
label="Segment Group Fill Opacity"
69+
label="Fill Opacity"
5270
min="0"
5371
max="1"
5472
step="0.01"
@@ -60,7 +78,7 @@ const outlineThickness = computed({
6078
/>
6179
<v-slider
6280
class="mx-4"
63-
label="Segment Group Outline Opacity"
81+
label="Outline Opacity"
6482
min="0"
6583
max="1"
6684
step="0.01"
@@ -71,7 +89,7 @@ const outlineThickness = computed({
7189
/>
7290
<v-slider
7391
class="mx-4"
74-
label="Segment Group Outline Thickness"
92+
label="Outline Thickness"
7593
min="0"
7694
max="10"
7795
step="1"

0 commit comments

Comments
 (0)