diff options
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)); |
