diff --git a/client/dive-common/components/Review/ReviewCell.vue b/client/dive-common/components/Review/ReviewCell.vue index 5b968c70b..cf99063f4 100644 --- a/client/dive-common/components/Review/ReviewCell.vue +++ b/client/dive-common/components/Review/ReviewCell.vue @@ -81,6 +81,10 @@ export default defineComponent({ type: Boolean, default: true, }, + activateOnHover: { + type: Boolean, + default: false, + }, cycleIntervalMs: { type: Number, default: 400, @@ -152,6 +156,8 @@ export default defineComponent({ setup(props, { emit }) { /** Chips currently in edit mode, to outline the whole entry. */ const editingCount = ref(0); + const hovered = ref(false); + const cycling = computed(() => props.animate && (!props.activateOnHover || hovered.value)); const viewList = computed(() => props.views ?? [{ key: 'single', @@ -196,7 +202,7 @@ export default defineComponent({ } function syncSharedTimer() { - const shouldRun = shared.value && props.animate && sequenceLength.value > 1 + const shouldRun = shared.value && cycling.value && sequenceLength.value > 1 && !sharedPaused.value && editingCount.value === 0; if (shouldRun && timer === null) { timer = window.setInterval(() => advanceShared(1), props.cycleIntervalMs); @@ -206,7 +212,7 @@ export default defineComponent({ } } watch( - [shared, () => props.animate, sequenceLength, sharedPaused, editingCount, () => props.cycleIntervalMs], + [shared, cycling, sequenceLength, sharedPaused, editingCount, () => props.cycleIntervalMs], () => { if (timer !== null) { window.clearInterval(timer); @@ -233,9 +239,16 @@ export default defineComponent({ if (next !== props.type) emit('assign', next); } + function onHoverChange(active: boolean) { + hovered.value = active; + emit('hover-change', active); + } + return { viewList, editingCount, + hovered, + onHoverChange, commitType, shared, sharedSlot, @@ -258,6 +271,8 @@ export default defineComponent({ 'cell-negative': highlight === 'negative', }" :style="{ '--cell-scale': scale }" + @mouseenter="onHoverChange(true)" + @mouseleave="onHoverChange(false)" >
({ scale: 1, x: 0, y: 0 }); const view = computed(() => props.controlledView ?? ownView.value); const wrap = ref(null); + const hovered = ref(false); + const cycling = computed(() => props.animate && (!props.activateOnHover || hovered.value)); let pan: { startX: number; startY: number; originX: number; originY: number; pointerId: number; capture: Element | null; } | null = null; @@ -210,10 +217,10 @@ export default defineComponent({ }, { immediate: true }); /** Sequence slot on screen; 0 while only the primary chip is available. */ - const currentSlot = computed(() => (hasSequence.value && props.animate ? cycleIndex.value : 0)); + const currentSlot = computed(() => (hasSequence.value && cycling.value ? cycleIndex.value : 0)); const displaySrc = computed(() => { - if (hasSequence.value && props.animate) { + if (hasSequence.value && cycling.value) { return props.srcs?.[cycleIndex.value] ?? props.src; } return props.src; @@ -221,7 +228,7 @@ export default defineComponent({ /** Transform of the chip on screen (the primary's when a slot is still loading). */ const displayTransform = computed(() => { - if (hasSequence.value && props.animate) { + if (hasSequence.value && cycling.value) { const slotSrc = props.srcs?.[cycleIndex.value]; if (slotSrc) return props.transforms?.[cycleIndex.value] ?? null; } @@ -253,7 +260,7 @@ export default defineComponent({ } function syncTimer() { - const shouldRun = props.animate && hasSequence.value && !editing.value && !paused.value + const shouldRun = cycling.value && hasSequence.value && !editing.value && !paused.value && !controlled.value; if (shouldRun && timer === null) { timer = window.setInterval(advance, props.cycleIntervalMs); @@ -263,7 +270,7 @@ export default defineComponent({ if (!editing.value && !paused.value) cycleIndex.value = 0; } } - watch([() => props.animate, () => props.srcs, () => props.cycleIntervalMs, editing, paused], () => { + watch([cycling, () => props.srcs, () => props.cycleIntervalMs, editing, paused], () => { if (timer !== null) { window.clearInterval(timer); timer = null; @@ -722,6 +729,7 @@ export default defineComponent({ onWrapPointerUp, missing, addBox, + hovered, }; }, }); @@ -731,8 +739,10 @@ export default defineComponent({
0); const pageTypeInput = ref(''); const showSettings = ref(false); + const hoveredEntryKeys = ref(new Set()); const typeField = ref<{ isMenuActive: boolean; activateMenu(): void; blur(): void } | null>(null); /** The type field's arrow opens its list, and closes it again on a second press. */ @@ -117,7 +118,18 @@ export default defineComponent({ outline: '', // Deleting or editing an entry keeps the page; only a new query resets it. retainPage: true, + activateOnHover: computed(() => review.settings.activateOnHover), + hoverEntryKeys: hoveredEntryKeys, + entryKeyOf: (entry) => entry.key, }); + + function setEntryHovered(key: string, hovered: boolean) { + const next = new Set(hoveredEntryKeys.value); + if (hovered) next.add(key); + else next.delete(key); + hoveredEntryKeys.value = next; + grid.ensureVisible(); + } watch(review.queryGeneration, () => grid.goToPage(0)); const showDatasetNames = computed(() => review.datasets.value.length > 1); @@ -163,7 +175,12 @@ export default defineComponent({ title: `${review.datasetName(parent)} · track ${item.trackId} · frame ${item.primary.frame}`, subtitle: subtitleBits.join(' · '), attributeText, - cycleIntervalMs: cycleIntervalFor(item.frames, fps, review.grid.cycleIntervalMs), + cycleIntervalMs: chipCycleIntervalMs( + item.frames, + fps, + review.settings.playbackFps, + review.grid.cycleIntervalMs, + ), }; }); }); @@ -424,6 +441,7 @@ export default defineComponent({ countLabel, pageTypeInput, showSettings, + setEntryHovered, setView, openItem, openDataset, @@ -806,7 +824,9 @@ export default defineComponent({ :key="cell.entry.key" :views="cell.views" :animate="true" + :activate-on-hover="review.settings.activateOnHover" :cycle-interval-ms="cell.cycleIntervalMs" + @hover-change="setEntryHovered(cell.entry.key, $event)" :scale="cellScale" :color="review.colorFor(cell.type)" :type="cell.type" @@ -829,6 +849,7 @@ export default defineComponent({ diff --git a/client/dive-common/components/UserSettingsDialog.vue b/client/dive-common/components/UserSettingsDialog.vue index 93ca3e37e..a7aa037b5 100644 --- a/client/dive-common/components/UserSettingsDialog.vue +++ b/client/dive-common/components/UserSettingsDialog.vue @@ -1,8 +1,9 @@