diff options
| author | Terry Truong <terry06890@gmail.com> | 2023-01-06 23:58:25 +1100 |
|---|---|---|
| committer | Terry Truong <terry06890@gmail.com> | 2023-01-06 23:58:25 +1100 |
| commit | 3a66879b889dce85d7498e216980a2c08288b36f (patch) | |
| tree | b6967865e64bf3d61f80feea00b6a180553331bb /src | |
| parent | d5b2c3d55b614914331525801ffb38ce824a5e8f (diff) | |
Keep track of a 'current' timeline
Make timeline addition and searching use the current timeline.
Add keyboard controls for timeline panning, zooming, switching, opening, and closing.
Fix zoomTimeline() bug when not centering zoom on pointer.
Diffstat (limited to 'src')
| -rw-r--r-- | src/App.vue | 68 | ||||
| -rw-r--r-- | src/components/TimeLine.vue | 47 |
2 files changed, 96 insertions, 19 deletions
diff --git a/src/App.vue b/src/App.vue index 960e648..803b53a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,7 +2,7 @@ <div class="absolute left-0 top-0 w-screen h-screen overflow-hidden flex flex-col"> <!-- Title bar --> <div class="flex gap-2 p-2" :style="{backgroundColor: store.color.bgDark2}"> - <h1 class="my-auto ml-2 text-4xl" :style="{color: store.color.altDark}">Histplorer</h1> + <h1 class="my-auto sm:ml-2 text-3xl sm:text-4xl" :style="{color: store.color.altDark}">Histplorer</h1> <div class="mx-auto"/> <!-- Spacer --> <!-- Icons --> <icon-button :size="45" :style="buttonStyles" @click="helpOpen = true" title="Show help info"> @@ -23,10 +23,11 @@ :style="{backgroundColor: store.color.bg}" ref="contentAreaRef"> <time-line v-for="(state, idx) in timelines" :key="state.id" :vert="vert" :initialState="state" :closeable="timelines.length > 1" - :eventTree="eventTree" :unitCountMaps="unitCountMaps" :searchTarget="timelineTargets[idx]" + :eventTree="eventTree" :unitCountMaps="unitCountMaps" + :current="idx == currentTimelineIdx && !modalOpen" :searchTarget="timelineTargets[idx]" class="grow basis-full min-h-0 outline outline-1" @close="onTimelineClose(idx)" @state-chg="onTimelineChg($event, idx)" @event-display="onEventDisplay" - @info-click="onInfoClick"/> + @info-click="onInfoClick" @pointerenter="currentTimelineIdx = idx"/> <base-line :vert="vert" :timelines="timelines" class='m-1 sm:m-2'/> </div> <!-- Modals --> @@ -87,23 +88,24 @@ onMounted(updateAreaDims); // Timeline data const timelines: Ref<TimelineState[]> = ref([]); +const currentTimelineIdx = ref(0); let nextTimelineId = 1; function addTimeline(){ if (timelines.value.length == 0){ timelines.value.push(new TimelineState(nextTimelineId, store.initialStartDate, store.initialEndDate)); } else { - let last = timelines.value[timelines.value.length - 1]; - timelines.value.push(new TimelineState( - nextTimelineId, last.startDate, - last.endDate, last.startOffset, last.endOffset, last.scaleIdx - )); + let state = timelines.value[currentTimelineIdx.value]; + timelines.value.splice(currentTimelineIdx.value, 0, new TimelineState( + nextTimelineId, state.startDate, state.endDate, state.startOffset, state.endOffset, state.scaleIdx)); } - timelineTargets.value.push([null, false]); - nextTimelineId++; + timelineTargets.value.splice(currentTimelineIdx.value, 0, [null, false]); + currentTimelineIdx.value += 1; + nextTimelineId += 1; } onMounted(addTimeline); function onTimelineChg(state: TimelineState, idx: number){ timelines.value[idx] = state; + currentTimelineIdx.value = idx; } // For timeline addition/removal @@ -123,6 +125,9 @@ function onTimelineClose(idx: number){ } timelines.value.splice(idx, 1); timelineTargets.value.splice(idx, 1); + if (currentTimelineIdx.value >= idx){ + currentTimelineIdx.value = Math.max(0, idx - 1); + } } // For storing and looking up events @@ -319,9 +324,8 @@ const timelineTargets = ref([] as [HistEvent | null, boolean][]); // For communi function onSearch(event: HistEvent){ searchOpen.value = false; // Trigger jump in endmost timeline - let timelineIdx = timelineTargets.value.length - 1; - let oldFlag = timelineTargets.value[timelineIdx]; - timelineTargets.value.splice(timelineIdx, 1, [event, !oldFlag[1]]); + let oldVal = timelineTargets.value[currentTimelineIdx.value]; + timelineTargets.value.splice(currentTimelineIdx.value, 1, [event, !oldVal[1]]); } // For settings modal @@ -330,6 +334,10 @@ const settingsOpen = ref(false); // For help modal const helpOpen = ref(false); +// +const modalOpen = computed(() => + (infoModalData.value != null || searchOpen.value || settingsOpen.value || helpOpen.value)); + // For resize handling let lastResizeHdlrTime = 0; // Used to throttle resize handling let afterResizeHdlr = 0; // Used to trigger handler after ending a run of resize events @@ -366,6 +374,10 @@ function onKeyDown(evt: KeyboardEvent){ infoModalData.value = null; } else if (searchOpen.value){ searchOpen.value = false; + } else if (settingsOpen.value){ + settingsOpen.value = false; + } else if (helpOpen.value){ + helpOpen.value = false; } } else if (evt.key == 'f' && evt.ctrlKey){ evt.preventDefault(); @@ -373,6 +385,36 @@ function onKeyDown(evt: KeyboardEvent){ if (!searchOpen.value){ searchOpen.value = true; } + } else if (evt.key.startsWith('Arrow') && !modalOpen.value && !evt.shiftKey){ + if (evt.key == 'ArrowUp'){ + if (!vert.value){ + if (currentTimelineIdx.value > 0){ + currentTimelineIdx.value -= 1; + } + } + } else if (evt.key == 'ArrowDown'){ + if (!vert.value){ + if (currentTimelineIdx.value < timelines.value.length - 1){ + currentTimelineIdx.value += 1; + } + } + } else if (evt.key == 'ArrowLeft'){ + if (vert.value){ + if (currentTimelineIdx.value > 0){ + currentTimelineIdx.value -= 1; + } + } + } else if (evt.key == 'ArrowRight'){ + if (vert.value){ + if (currentTimelineIdx.value < timelines.value.length - 1){ + currentTimelineIdx.value += 1; + } + } + } + } else if (evt.key == '+' && !modalOpen.value){ + onTimelineAdd(); + } else if (evt.key == 'Delete' && !modalOpen.value){ + onTimelineClose(currentTimelineIdx.value); } } onMounted(() => { diff --git a/src/components/TimeLine.vue b/src/components/TimeLine.vue index 1583aef..d6bb36a 100644 --- a/src/components/TimeLine.vue +++ b/src/components/TimeLine.vue @@ -58,7 +58,7 @@ </div> </div> <!-- Timeline position label --> - <div class="absolute top-2 left-2 z-20 text-lg text-stone-50"> + <div class="absolute top-2 left-2 z-20 text-lg" :class="[current ? 'text-yellow-300' : 'text-stone-50']"> {{timelinePosStr}} </div> <!-- Buttons --> @@ -71,7 +71,7 @@ </template> <script setup lang="ts"> -import {ref, onMounted, computed, watch, watchEffect, PropType, Ref, shallowRef, ShallowRef} from 'vue'; +import {ref, onMounted, onUnmounted, computed, watch, watchEffect, PropType, Ref, shallowRef, ShallowRef} from 'vue'; // Components import IconButton from './IconButton.vue'; // Icons @@ -98,6 +98,7 @@ const props = defineProps({ initialState: {type: Object as PropType<TimelineState>, required: true}, eventTree: {type: Object as PropType<RBTree<HistEvent>>, required: true}, unitCountMaps: {type: Object as PropType<Map<number, number>[]>, required: true}, + current: {type: Boolean, required: true}, searchTarget: {type: Object as PropType<[null | HistEvent, boolean]>, required: true}, }); const emit = defineEmits(['close', 'state-chg', 'event-display', 'info-click']); @@ -820,7 +821,7 @@ function panTimeline(scrollRatio: number){ startOffset.value = newStartOffset; endOffset.value = newEndOffset; } -function zoomTimeline(zoomRatio: number){ +function zoomTimeline(zoomRatio: number, ignorePointer=false){ if (zoomRatio > 1 && startDate.value.equals(MIN_DATE, scale.value) && endDate.value.equals(MAX_DATE, scale.value)){ @@ -833,9 +834,9 @@ function zoomTimeline(zoomRatio: number){ let startChg: number; let endChg: number; let ptrOffset = props.vert ? pointerY : pointerX; - if (ptrOffset == null){ + if (ptrOffset == null || ignorePointer){ let unitChg = newNumUnits - numUnits; - startChg = unitChg / 2; + startChg = -unitChg / 2; endChg = unitChg / 2; } else { // Pointer-centered zoom // Get element-relative ptrOffset @@ -1122,7 +1123,7 @@ function onStateChg(){ ID, startDate.value, endDate.value, startOffset.value, endOffset.value, scaleIdx.value )); } -watch(firstDate, onStateChg); +watch(startDate, onStateChg); // For jumping to search result const searchEvent = ref(null as null | HistEvent); // Holds most recent search result @@ -1163,6 +1164,40 @@ watch(() => props.searchTarget, () => { searchEvent.value = event; }); +// For keyboard shortcuts +function onKeyDown(evt: KeyboardEvent){ + if (!props.current || store.disableShortcuts){ + return; + } + if (evt.key == 'ArrowUp'){ + if (evt.shiftKey){ + zoomTimeline(1/store.zoomRatio, true); + } else if (props.vert){ + panTimeline(-store.scrollRatio); + } + } else if (evt.key == 'ArrowDown'){ + if (evt.shiftKey){ + zoomTimeline(store.zoomRatio, true); + } else if (props.vert){ + panTimeline(store.scrollRatio); + } + } else if (evt.key == 'ArrowLeft'){ + if (!props.vert){ + panTimeline(-store.scrollRatio); + } + } else if (evt.key == 'ArrowRight'){ + if (!props.vert){ + panTimeline(store.scrollRatio); + } + } +} +onMounted(() => { + window.addEventListener('keydown', onKeyDown); +}); +onUnmounted(() => { + window.removeEventListener('keydown', onKeyDown); +}); + // For skipping transitions on startup (and on horz/vert swap) const skipTransition = ref(true); onMounted(() => setTimeout(() => {skipTransition.value = false}, 100)); |
