diff --git a/index.html b/index.html index fd12104746..1034cd506e 100644 --- a/index.html +++ b/index.html @@ -34,8 +34,8 @@ <body> </body> <script> - const FIVE_MINUTES = 5 * 60 * 1000; - const THIRTY_MINUTES = 30 * 60 * 1000; + const THIRTY_SECONDS = 30 * 1000; + const THIRTY_MINUTES = THIRTY_SECONDS * 60; [ 'example/eventGenerator' @@ -63,7 +63,39 @@ bounds: { start: Date.now() - THIRTY_MINUTES, end: Date.now() - } + }, + // commonly used bounds can be stored in history + // bounds (start and end) can accept either a milliseconds number + // or a callback function returning a milliseconds number + // a function is useful for invoking Date.now() at exact moment of preset selection + presets: [ + { + label: 'Last Day', + bounds: { + start: () => Date.now() - 1000 * 60 * 60 * 24, + end: () => Date.now() + } + }, + { + label: 'Last 2 hours', + bounds: { + start: () => Date.now() - 1000 * 60 * 60 * 2, + end: () => Date.now() + } + }, + { + label: 'Last hour', + bounds: { + start: () => Date.now() - 1000 * 60 * 60, + end: () => Date.now() + } + } + ], + // maximum recent bounds to retain in conductor history + records: 10, + // maximum duration between start and end bounds + // for utc-based time systems this is in milliseconds + limit: 1000 * 60 * 60 * 24 }, { name: "Realtime", @@ -71,7 +103,7 @@ clock: 'local', clockOffsets: { start: - THIRTY_MINUTES, - end: FIVE_MINUTES + end: THIRTY_SECONDS } } ] diff --git a/src/plugins/localTimeSystem/LocalTimeSystem.js b/src/plugins/localTimeSystem/LocalTimeSystem.js index fd37b334df..3f08d07fe4 100644 --- a/src/plugins/localTimeSystem/LocalTimeSystem.js +++ b/src/plugins/localTimeSystem/LocalTimeSystem.js @@ -41,7 +41,7 @@ define([], function () { this.timeFormat = 'local-format'; this.durationFormat = 'duration'; - this.isUTCBased = false; + this.isUTCBased = true; } return LocalTimeSystem; diff --git a/src/plugins/timeConductor/Conductor.vue b/src/plugins/timeConductor/Conductor.vue index 63c3ced731..eaa9ce3d9b 100644 --- a/src/plugins/timeConductor/Conductor.vue +++ b/src/plugins/timeConductor/Conductor.vue @@ -22,7 +22,12 @@ <template> <div class="c-conductor" - :class="[isFixed ? 'is-fixed-mode' : 'is-realtime-mode']" + :class="[ + { 'is-zooming': isZooming }, + { 'is-panning': isPanning }, + { 'alt-pressed': altPressed }, + isFixed ? 'is-fixed-mode' : 'is-realtime-mode' + ]" > <form ref="conductorForm" @@ -52,7 +57,7 @@ type="text" autocorrect="off" spellcheck="false" - @change="validateAllBounds(); submitForm()" + @change="validateAllBounds('startDate'); submitForm()" > <date-picker v-if="isFixed && isUTCBased" @@ -92,7 +97,7 @@ autocorrect="off" spellcheck="false" :disabled="!isFixed" - @change="validateAllBounds(); submitForm()" + @change="validateAllBounds('endDate'); submitForm()" > <date-picker v-if="isFixed && isUTCBased" @@ -122,14 +127,25 @@ <conductor-axis class="c-conductor__ticks" - :bounds="rawBounds" - @panAxis="setViewFromBounds" + :view-bounds="viewBounds" + :is-fixed="isFixed" + :alt-pressed="altPressed" + @endPan="endPan" + @endZoom="endZoom" + @panAxis="pan" + @zoomAxis="zoom" /> + </div> <div class="c-conductor__controls"> - <!-- Mode, time system menu buttons and duration slider --> <ConductorMode class="c-conductor__mode-select" /> <ConductorTimeSystem class="c-conductor__time-system-select" /> + <ConductorHistory + v-if="isFixed" + class="c-conductor__history-select" + :bounds="openmct.time.bounds()" + :time-system="timeSystem" + /> </div> <input type="submit" @@ -145,6 +161,7 @@ import ConductorTimeSystem from './ConductorTimeSystem.vue'; import DatePicker from './DatePicker.vue'; import ConductorAxis from './ConductorAxis.vue'; import ConductorModeIcon from './ConductorModeIcon.vue'; +import ConductorHistory from './ConductorHistory.vue' const DEFAULT_DURATION_FORMATTER = 'duration'; @@ -155,7 +172,8 @@ export default { ConductorTimeSystem, DatePicker, ConductorAxis, - ConductorModeIcon + ConductorModeIcon, + ConductorHistory }, data() { let bounds = this.openmct.time.bounds(); @@ -165,6 +183,7 @@ export default { let durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); return { + timeSystem: timeSystem, timeFormatter: timeFormatter, durationFormatter: durationFormatter, offsets: { @@ -175,29 +194,68 @@ export default { start: timeFormatter.format(bounds.start), end: timeFormatter.format(bounds.end) }, - rawBounds: { + viewBounds: { start: bounds.start, end: bounds.end }, isFixed: this.openmct.time.clock() === undefined, isUTCBased: timeSystem.isUTCBased, - showDatePicker: false + showDatePicker: false, + altPressed: false, + isPanning: false, + isZooming: false } }, mounted() { + document.addEventListener('keydown', this.handleKeyDown); + document.addEventListener('keyup', this.handleKeyUp); this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem()))); - this.openmct.time.on('bounds', this.setViewFromBounds); this.openmct.time.on('timeSystem', this.setTimeSystem); this.openmct.time.on('clock', this.setViewFromClock); this.openmct.time.on('clockOffsets', this.setViewFromOffsets) }, + beforeDestroy() { + document.removeEventListener('keydown', this.handleKeyDown); + document.removeEventListener('keyup', this.handleKeyUp); + }, methods: { + handleKeyDown(event) { + if (event.key === 'Alt') { + this.altPressed = true; + } + }, + handleKeyUp(event) { + if (event.key === 'Alt') { + this.altPressed = false; + } + }, + pan(bounds) { + this.isPanning = true; + this.setViewFromBounds(bounds); + }, + endPan(bounds) { + this.isPanning = false; + if (bounds) { + this.openmct.time.bounds(bounds); + } + }, + zoom(bounds) { + this.isZooming = true; + this.formattedBounds.start = this.timeFormatter.format(bounds.start); + this.formattedBounds.end = this.timeFormatter.format(bounds.end); + }, + endZoom(bounds) { + const _bounds = bounds ? bounds : this.openmct.time.bounds(); + this.isZooming = false; + + this.openmct.time.bounds(_bounds); + }, setTimeSystem(timeSystem) { + this.timeSystem = timeSystem this.timeFormatter = this.getFormatter(timeSystem.timeFormat); this.durationFormatter = this.getFormatter( timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); - this.isUTCBased = timeSystem.isUTCBased; }, setOffsetsFromView($event) { @@ -237,8 +295,8 @@ export default { setViewFromBounds(bounds) { this.formattedBounds.start = this.timeFormatter.format(bounds.start); this.formattedBounds.end = this.timeFormatter.format(bounds.end); - this.rawBounds.start = bounds.start; - this.rawBounds.end = bounds.end; + this.viewBounds.start = bounds.start; + this.viewBounds.end = bounds.end; }, setViewFromOffsets(offsets) { this.offsets.start = this.durationFormatter.format(Math.abs(offsets.start)); @@ -251,6 +309,15 @@ export default { this.setOffsetsFromView(); } }, + getBoundsLimit() { + const configuration = this.configuration.menuOptions + .filter(option => option.timeSystem === this.timeSystem.key) + .find(option => option.limit); + + const limit = configuration ? configuration.limit : undefined; + + return limit; + }, clearAllValidation() { if (this.isFixed) { [this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput); @@ -262,36 +329,52 @@ export default { input.setCustomValidity(''); input.title = ''; }, - validateAllBounds() { - return [this.$refs.startDate, this.$refs.endDate].every((input) => { - let validationResult = true; - let formattedDate; + validateAllBounds(ref) { + if (!this.areBoundsFormatsValid()) { + return false; + } - if (input === this.$refs.startDate) { - formattedDate = this.formattedBounds.start; + let validationResult = true; + const currentInput = this.$refs[ref]; + + return [this.$refs.startDate, this.$refs.endDate].every((input) => { + let boundsValues = { + start: this.timeFormatter.parse(this.formattedBounds.start), + end: this.timeFormatter.parse(this.formattedBounds.end) + }; + const limit = this.getBoundsLimit(); + + if ( + this.timeSystem.isUTCBased + && limit + && boundsValues.end - boundsValues.start > limit + ) { + if (input === currentInput) { + validationResult = "Start and end difference exceeds allowable limit"; + } } else { - formattedDate = this.formattedBounds.end; + if (input === currentInput) { + validationResult = this.openmct.time.validateBounds(boundsValues); + } } + return this.handleValidationResults(input, validationResult); + }); + }, + areBoundsFormatsValid() { + let validationResult = true; + + return [this.$refs.startDate, this.$refs.endDate].every((input) => { + const formattedDate = input === this.$refs.startDate + ? this.formattedBounds.start + : this.formattedBounds.end + ; + if (!this.timeFormatter.validate(formattedDate)) { validationResult = 'Invalid date'; - } else { - let boundsValues = { - start: this.timeFormatter.parse(this.formattedBounds.start), - end: this.timeFormatter.parse(this.formattedBounds.end) - }; - validationResult = this.openmct.time.validateBounds(boundsValues); } - if (validationResult !== true) { - input.setCustomValidity(validationResult); - input.title = validationResult; - return false; - } else { - input.setCustomValidity(''); - input.title = ''; - return true; - } + return this.handleValidationResults(input, validationResult); }); }, validateAllOffsets(event) { @@ -315,17 +398,20 @@ export default { validationResult = this.openmct.time.validateOffsets(offsetValues); } - if (validationResult !== true) { - input.setCustomValidity(validationResult); - input.title = validationResult; - return false; - } else { - input.setCustomValidity(''); - input.title = ''; - return true; - } + return this.handleValidationResults(input, validationResult); }); }, + handleValidationResults(input, validationResult) { + if (validationResult !== true) { + input.setCustomValidity(validationResult); + input.title = validationResult; + return false; + } else { + input.setCustomValidity(''); + input.title = ''; + return true; + } + }, submitForm() { // Allow Vue model to catch up to user input. // Submitting form will cause validation messages to display (but only if triggered by button click) @@ -338,12 +424,12 @@ export default { }, startDateSelected(date) { this.formattedBounds.start = this.timeFormatter.format(date); - this.validateAllBounds(); + this.validateAllBounds('startDate'); this.submitForm(); }, endDateSelected(date) { this.formattedBounds.end = this.timeFormatter.format(date); - this.validateAllBounds(); + this.validateAllBounds('endDate'); this.submitForm(); } } diff --git a/src/plugins/timeConductor/ConductorAxis.vue b/src/plugins/timeConductor/ConductorAxis.vue index da7871b045..afe14d6bc5 100644 --- a/src/plugins/timeConductor/ConductorAxis.vue +++ b/src/plugins/timeConductor/ConductorAxis.vue @@ -24,7 +24,12 @@ ref="axisHolder" class="c-conductor-axis" @mousedown="dragStart($event)" -></div> +> + <div + class="c-conductor-axis__zoom-indicator" + :style="zoomStyle" + ></div> +</div> </template> <script> @@ -43,52 +48,81 @@ const PIXELS_PER_TICK_WIDE = 200; export default { inject: ['openmct'], props: { - bounds: { + viewBounds: { type: Object, required: true + }, + isFixed: { + type: Boolean, + required: true + }, + altPressed: { + type: Boolean, + required: true + } + }, + data() { + return { + inPanMode: false, + dragStartX: undefined, + dragX: undefined, + zoomStyle: {} + } + }, + computed: { + inZoomMode() { + return !this.inPanMode; } }, watch: { - bounds: { - handler(bounds) { + viewBounds: { + handler() { this.setScale(); }, deep: true } }, mounted() { - let axisHolder = this.$refs.axisHolder; - let height = axisHolder.offsetHeight; - let vis = d3Selection.select(axisHolder) - .append("svg:svg") - .attr("width", "100%") - .attr("height", height); + let vis = d3Selection.select(this.$refs.axisHolder).append("svg:svg"); - this.width = this.$refs.axisHolder.clientWidth; this.xAxis = d3Axis.axisTop(); this.dragging = false; // draw x axis with labels. CSS is used to position them. - this.axisElement = vis.append("g"); + this.axisElement = vis.append("g") + .attr("class", "axis"); this.setViewFromTimeSystem(this.openmct.time.timeSystem()); + this.setAxisDimensions(); this.setScale(); //Respond to changes in conductor this.openmct.time.on("timeSystem", this.setViewFromTimeSystem); setInterval(this.resize, RESIZE_POLL_INTERVAL); }, - destroyed() { - }, methods: { + setAxisDimensions() { + const axisHolder = this.$refs.axisHolder; + const rect = axisHolder.getBoundingClientRect(); + + this.left = Math.round(rect.left); + this.width = axisHolder.clientWidth; + }, setScale() { + if (!this.width) { + return; + } + let timeSystem = this.openmct.time.timeSystem(); - let bounds = this.bounds; if (timeSystem.isUTCBased) { - this.xScale.domain([new Date(bounds.start), new Date(bounds.end)]); + this.xScale.domain( + [new Date(this.viewBounds.start), new Date(this.viewBounds.end)] + ); } else { - this.xScale.domain([bounds.start, bounds.end]); + this.xScale.domain( + [this.viewBounds.start, this.viewBounds.end] + ); } this.xAxis.scale(this.xScale); @@ -102,7 +136,7 @@ export default { this.xAxis.ticks(this.width / PIXELS_PER_TICK); } - this.msPerPixel = (bounds.end - bounds.start) / this.width; + this.msPerPixel = (this.viewBounds.end - this.viewBounds.start) / this.width; }, setViewFromTimeSystem(timeSystem) { //The D3 scale used depends on the type of time system as d3 @@ -120,9 +154,8 @@ export default { }, getActiveFormatter() { let timeSystem = this.openmct.time.timeSystem(); - let isFixed = this.openmct.time.clock() === undefined; - if (isFixed) { + if (this.isFixed) { return this.getFormatter(timeSystem.timeFormat); } else { return this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); @@ -134,45 +167,131 @@ export default { }).formatter; }, dragStart($event) { - let isFixed = this.openmct.time.clock() === undefined; - if (isFixed) { + if (this.isFixed) { this.dragStartX = $event.clientX; + if (this.altPressed) { + this.inPanMode = true; + } + document.addEventListener('mousemove', this.drag); document.addEventListener('mouseup', this.dragEnd, { once: true }); + + if (this.inZoomMode) { + this.startZoom(); + } } }, drag($event) { if (!this.dragging) { this.dragging = true; - requestAnimationFrame(()=>{ - let deltaX = $event.clientX - this.dragStartX; - let percX = deltaX / this.width; - let bounds = this.openmct.time.bounds(); - let deltaTime = bounds.end - bounds.start; - let newStart = bounds.start - percX * deltaTime; - this.$emit('panAxis',{ - start: newStart, - end: newStart + deltaTime - }); + + requestAnimationFrame(() => { + this.dragX = $event.clientX; + this.inPanMode ? this.pan() : this.zoom(); this.dragging = false; - }) - } else { - console.log('Rejected drag due to RAF cap'); + }); } }, dragEnd() { + this.inPanMode ? this.endPan() : this.endZoom(); + document.removeEventListener('mousemove', this.drag); - this.openmct.time.bounds({ - start: this.bounds.start, - end: this.bounds.end + this.dragStartX = undefined; + this.dragX = undefined; + }, + pan() { + const panBounds = this.getPanBounds(); + this.$emit('panAxis', panBounds); + }, + endPan() { + const panBounds = this.dragStartX && this.dragX && this.dragStartX !== this.dragX + ? this.getPanBounds() + : undefined; + this.$emit('endPan', panBounds); + this.inPanMode = false; + }, + getPanBounds() { + const bounds = this.openmct.time.bounds(); + const deltaTime = bounds.end - bounds.start; + const deltaX = this.dragX - this.dragStartX; + const percX = deltaX / this.width; + const panStart = bounds.start - percX * deltaTime; + + return { + start: panStart, + end: panStart + deltaTime + }; + }, + startZoom() { + const x = this.scaleToBounds(this.dragStartX); + + this.zoomStyle = { + left: `${this.dragStartX - this.left}px` + }; + + this.$emit('zoomAxis', { + start: x, + end: x }); }, + zoom() { + const zoomRange = this.getZoomRange(); + + this.zoomStyle = { + left: `${zoomRange.start - this.left}px`, + width: `${zoomRange.end - zoomRange.start}px` + }; + + this.$emit('zoomAxis', { + start: this.scaleToBounds(zoomRange.start), + end: this.scaleToBounds(zoomRange.end) + }); + }, + endZoom() { + const zoomRange = this.dragStartX && this.dragX && this.dragStartX !== this.dragX + ? this.getZoomRange() + : undefined; + + const zoomBounds = zoomRange + ? { + start: this.scaleToBounds(zoomRange.start), + end: this.scaleToBounds(zoomRange.end) + } + : this.openmct.time.bounds(); + + this.zoomStyle = {}; + this.$emit('endZoom', zoomBounds); + }, + getZoomRange() { + const leftBound = this.left; + const rightBound = this.left + this.width; + + const zoomStart = this.dragX < leftBound + ? leftBound + : Math.min(this.dragX, this.dragStartX); + + const zoomEnd = this.dragX > rightBound + ? rightBound + : Math.max(this.dragX, this.dragStartX); + + return { + start: zoomStart, + end: zoomEnd + }; + }, + scaleToBounds(value) { + const bounds = this.openmct.time.bounds(); + const timeDelta = bounds.end - bounds.start; + const valueDelta = value - this.left; + const offset = valueDelta / this.width * timeDelta; + return bounds.start + offset; + }, resize() { if (this.$refs.axisHolder.clientWidth !== this.width) { - this.width = this.$refs.axisHolder.clientWidth; + this.setAxisDimensions(); this.setScale(); } } diff --git a/src/plugins/timeConductor/ConductorHistory.vue b/src/plugins/timeConductor/ConductorHistory.vue new file mode 100644 index 0000000000..bcebf9dd5e --- /dev/null +++ b/src/plugins/timeConductor/ConductorHistory.vue @@ -0,0 +1,200 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2018, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +<template> +<div class="c-ctrl-wrapper c-ctrl-wrapper--menus-up"> + <button class="c-button--menu c-history-button icon-history" + @click.prevent="toggle" + > + <span class="c-button__label">History</span> + </button> + <div v-if="open" + class="c-menu c-conductor__history-menu" + > + <ul v-if="hasHistoryPresets"> + <li + v-for="preset in presets" + :key="preset.label" + class="icon-clock" + @click="selectPresetBounds(preset.bounds)" + > + {{ preset.label }} + </li> + </ul> + + <div + v-if="hasHistoryPresets" + class="c-menu__section-separator" + ></div> + + <div class="c-menu__section-hint"> + Past timeframes, ordered by latest first + </div> + + <ul> + <li + v-for="(timespan, index) in historyForCurrentTimeSystem" + :key="index" + class="icon-history" + @click="selectTimespan(timespan)" + > + {{ formatTime(timespan.start) }} - {{ formatTime(timespan.end) }} + </li> + </ul> + </div> +</div> +</template> + +<script> +import toggleMixin from '../../ui/mixins/toggle-mixin'; + +const LOCAL_STORAGE_HISTORY_KEY = 'tcHistory'; +const DEFAULT_RECORDS = 10; + +export default { + inject: ['openmct', 'configuration'], + mixins: [toggleMixin], + props: { + bounds: { + type: Object, + required: true + }, + timeSystem: { + type: Object, + required: true + } + }, + data() { + return { + history: {}, // contains arrays of timespans {start, end}, array key is time system key + presets: [] + } + }, + computed: { + hasHistoryPresets() { + return this.timeSystem.isUTCBased && this.presets.length; + }, + historyForCurrentTimeSystem() { + const history = this.history[this.timeSystem.key]; + + return history; + } + }, + watch: { + bounds: { + handler() { + this.addTimespan(); + }, + deep: true + }, + timeSystem: { + handler() { + this.loadConfiguration(); + this.addTimespan(); + }, + deep: true + }, + history: { + handler() { + this.persistHistoryToLocalStorage(); + }, + deep: true + } + }, + mounted() { + this.getHistoryFromLocalStorage(); + }, + methods: { + getHistoryFromLocalStorage() { + if (localStorage.getItem(LOCAL_STORAGE_HISTORY_KEY)) { + this.history = JSON.parse(localStorage.getItem(LOCAL_STORAGE_HISTORY_KEY)) + } else { + this.history = {}; + this.persistHistoryToLocalStorage(); + } + }, + persistHistoryToLocalStorage() { + localStorage.setItem(LOCAL_STORAGE_HISTORY_KEY, JSON.stringify(this.history)); + }, + addTimespan() { + const key = this.timeSystem.key; + let [...currentHistory] = this.history[key] || []; + const timespan = { + start: this.bounds.start, + end: this.bounds.end + }; + + const isNotEqual = function (entry) { + const start = entry.start !== this.start; + const end = entry.end !== this.end; + + return start || end; + }; + currentHistory = currentHistory.filter(isNotEqual, timespan); + + while (currentHistory.length >= this.records) { + currentHistory.pop(); + } + + currentHistory.unshift(timespan); + this.history[key] = currentHistory; + }, + selectTimespan(timespan) { + this.openmct.time.bounds(timespan); + }, + selectPresetBounds(bounds) { + const start = typeof bounds.start === 'function' ? bounds.start() : bounds.start; + const end = typeof bounds.end === 'function' ? bounds.end() : bounds.end; + + this.selectTimespan({ + start: start, + end: end + }); + }, + loadConfiguration() { + const configurations = this.configuration.menuOptions + .filter(option => option.timeSystem === this.timeSystem.key); + + this.presets = this.loadPresets(configurations); + this.records = this.loadRecords(configurations); + }, + loadPresets(configurations) { + const configuration = configurations.find(option => option.presets); + const presets = configuration ? configuration.presets : []; + + return presets; + }, + loadRecords(configurations) { + const configuration = configurations.find(option => option.records); + const records = configuration ? configuration.records : DEFAULT_RECORDS; + + return records; + }, + formatTime(time) { + const formatter = this.openmct.telemetry.getValueFormatter({ + format: this.timeSystem.timeFormat + }).formatter; + + return formatter.format(time); + } + } +} +</script> diff --git a/src/plugins/timeConductor/ConductorMode.vue b/src/plugins/timeConductor/ConductorMode.vue index 070a711cc5..20d8d30c53 100644 --- a/src/plugins/timeConductor/ConductorMode.vue +++ b/src/plugins/timeConductor/ConductorMode.vue @@ -110,7 +110,7 @@ export default { if (clock === undefined) { return { key: 'fixed', - name: 'Fixed Timespan Mode', + name: 'Fixed Timespan', description: 'Query and explore data that falls between two fixed datetimes.', cssClass: 'icon-tabular' } diff --git a/src/plugins/timeConductor/conductor-axis.scss b/src/plugins/timeConductor/conductor-axis.scss index 23ded63eaf..b16a58f718 100644 --- a/src/plugins/timeConductor/conductor-axis.scss +++ b/src/plugins/timeConductor/conductor-axis.scss @@ -13,7 +13,7 @@ text-rendering: geometricPrecision; width: 100%; height: 100%; - > g { + > g.axis { // Overall Tick holder transform: translateY($tickYPos); path { @@ -44,7 +44,6 @@ } body.desktop .is-fixed-mode & { - @include cursorGrab(); background-size: 3px 30%; background-color: $colorBodyBgSubtle; box-shadow: inset rgba(black, 0.4) 0 1px 1px; @@ -55,17 +54,6 @@ stroke: $colorBodyBgSubtle; transition: $transOut; } - - &:hover, - &:active { - $c: $colorKeySubtle; - background-color: $c; - transition: $transIn; - svg text { - stroke: $c; - transition: $transIn; - } - } } .is-realtime-mode & { diff --git a/src/plugins/timeConductor/conductor.scss b/src/plugins/timeConductor/conductor.scss index dc5c308477..4ecba9e3f2 100644 --- a/src/plugins/timeConductor/conductor.scss +++ b/src/plugins/timeConductor/conductor.scss @@ -57,6 +57,65 @@ } } + &.is-fixed-mode { + .c-conductor-axis { + &__zoom-indicator { + border: 1px solid transparent; + display: none; // Hidden by default + } + } + + &:not(.is-panning), + &:not(.is-zooming) { + .c-conductor-axis { + &:hover, + &:active { + cursor: col-resize; + filter: $timeConductorAxisHoverFilter; + } + } + } + + &.is-panning, + &.is-zooming { + .c-conductor-input input { + // Styles for inputs while zooming or panning + background: rgba($timeConductorActiveBg, 0.4); + } + } + + &.alt-pressed { + .c-conductor-axis:hover { + // When alt is being pressed and user is hovering over the axis, set the cursor + @include cursorGrab(); + } + } + + &.is-panning { + .c-conductor-axis { + @include cursorGrab(); + background-color: $timeConductorActivePanBg; + transition: $transIn; + + svg text { + stroke: $timeConductorActivePanBg; + transition: $transIn; + } + } + } + + &.is-zooming { + .c-conductor-axis__zoom-indicator { + display: block; + position: absolute; + background: rgba($timeConductorActiveBg, 0.4); + border-left-color: $timeConductorActiveBg; + border-right-color: $timeConductorActiveBg; + top: 0; bottom: 0; + } + } + } + &.is-realtime-mode { .c-conductor__time-bounds { grid-template-columns: 20px auto 1fr auto auto; diff --git a/src/styles/_constants-espresso.scss b/src/styles/_constants-espresso.scss index 1ea130b891..3e3bbaf2db 100644 --- a/src/styles/_constants-espresso.scss +++ b/src/styles/_constants-espresso.scss @@ -142,6 +142,9 @@ $colorTimeHov: pullForward($colorTime, 10%); $colorTimeSubtle: pushBack($colorTime, 20%); $colorTOI: $colorBodyFg; // was $timeControllerToiLineColor $colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov +$timeConductorAxisHoverFilter: brightness(1.2); +$timeConductorActiveBg: $colorKey; +$timeConductorActivePanBg: #226074; /************************************************** BROWSING */ $browseFrameColor: pullForward($colorBodyBg, 10%); diff --git a/src/styles/_constants-maelstrom.scss b/src/styles/_constants-maelstrom.scss index 81a9cf3eae..d72abb7f8e 100644 --- a/src/styles/_constants-maelstrom.scss +++ b/src/styles/_constants-maelstrom.scss @@ -146,6 +146,9 @@ $colorTimeHov: pullForward($colorTime, 10%); $colorTimeSubtle: pushBack($colorTime, 20%); $colorTOI: $colorBodyFg; // was $timeControllerToiLineColor $colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov +$timeConductorAxisHoverFilter: brightness(1.2); +$timeConductorActiveBg: $colorKey; +$timeConductorActivePanBg: #226074; /************************************************** BROWSING */ $browseFrameColor: pullForward($colorBodyBg, 10%); diff --git a/src/styles/_constants-snow.scss b/src/styles/_constants-snow.scss index dcc4998f1a..7f44128019 100644 --- a/src/styles/_constants-snow.scss +++ b/src/styles/_constants-snow.scss @@ -132,7 +132,7 @@ $colorPausedFg: #fff; // Base variations $colorBodyBgSubtle: pullForward($colorBodyBg, 5%); $colorBodyBgSubtleHov: pushBack($colorKey, 50%); -$colorKeySubtle: pushBack($colorKey, 10%); +$colorKeySubtle: pushBack($colorKey, 20%); // Time Colors $colorTime: #618cff; @@ -142,6 +142,9 @@ $colorTimeHov: pushBack($colorTime, 5%); $colorTimeSubtle: pushBack($colorTime, 20%); $colorTOI: $colorBodyFg; // was $timeControllerToiLineColor $colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov +$timeConductorAxisHoverFilter: brightness(0.8); +$timeConductorActiveBg: $colorKey; +$timeConductorActivePanBg: #A0CDE1; /************************************************** BROWSING */ $browseFrameColor: pullForward($colorBodyBg, 10%); diff --git a/src/styles/_controls.scss b/src/styles/_controls.scss index 22a6497c05..565295d6dd 100644 --- a/src/styles/_controls.scss +++ b/src/styles/_controls.scss @@ -462,9 +462,17 @@ select { text-shadow: $shdwMenuText; padding: $interiorMarginSm; box-shadow: $shdwMenu; - display: block; + display: flex; + flex-direction: column; position: absolute; z-index: 100; + + > * { + flex: 0 0 auto; + //+ * { + // margin-top: $interiorMarginSm; + //} + } } @mixin menuInner() { @@ -502,6 +510,23 @@ select { .c-menu { @include menuOuter(); @include menuInner(); + + &__section-hint { + $m: $interiorMargin; + margin: $m 0; + padding: $m nth($menuItemPad, 2) 0 nth($menuItemPad, 2); + + opacity: 0.6; + font-size: 0.9em; + font-style: italic; + } + + &__section-separator { + $m: $interiorMargin; + border-top: 1px solid $colorInteriorBorder; + margin: $m 0; + padding: $m nth($menuItemPad, 2) 0 nth($menuItemPad, 2); + } } .c-super-menu {