From 54a131eaaf5b7a61206dc26ce68a0786a6424453 Mon Sep 17 00:00:00 2001 From: David Tsay Date: Fri, 25 Apr 2025 16:32:38 -0700 Subject: [PATCH 1/2] sync containers on load allow for fixed containers WIP move existing functionality into setup --- src/plugins/timeline/Container.js | 44 +++++ src/plugins/timeline/TimelineViewLayout.vue | 71 ++++---- src/plugins/timeline/configuration.js | 28 +++ src/utils/vue/useFlexContainers.js | 191 +++++++++++++------- 4 files changed, 231 insertions(+), 103 deletions(-) diff --git a/src/plugins/timeline/Container.js b/src/plugins/timeline/Container.js index 3f729d56da..8832556bdc 100644 --- a/src/plugins/timeline/Container.js +++ b/src/plugins/timeline/Container.js @@ -1,8 +1,52 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2024, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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. + *****************************************************************************/ + +/** + * a sizing container for objects in a layout + */ class Container { constructor(domainObject, size) { + /** + * the identifier of the associated domain object + * @type {import('@/api/objects/ObjectAPI.js').Identifier} + */ this.domainObjectIdentifier = domainObject.identifier; + /** + * the size in percentage or pixels + * @type {number} + */ this.size = size; + /** + * the default percentage scale of an object + * @type {number} + */ this.scale = getContainerScale(domainObject); + /** + * true if the container should be a fixed pixel size + * false if the container should be a flexible percentage size + * containers are added as flex + * @type {boolean} + */ + this.fixed = false; } } diff --git a/src/plugins/timeline/TimelineViewLayout.vue b/src/plugins/timeline/TimelineViewLayout.vue index b1ccf20de4..a41c89a5d5 100644 --- a/src/plugins/timeline/TimelineViewLayout.vue +++ b/src/plugins/timeline/TimelineViewLayout.vue @@ -76,7 +76,7 @@ import _ from 'lodash'; import { useDragResizer } from 'utils/vue/useDragResizer.js'; import { useFlexContainers } from 'utils/vue/useFlexContainers.js'; -import { inject, onBeforeUnmount, onMounted, provide, ref } from 'vue'; +import { inject, onBeforeUnmount, provide, ref, toRaw } from 'vue'; import SwimLane from '@/ui/components/swim-lane/SwimLane.vue'; import ResizeHandle from '@/ui/layout/ResizeHandle/ResizeHandle.vue'; @@ -109,11 +109,17 @@ export default { const openmct = inject('openmct'); const domainObject = inject('domainObject'); const path = inject('path'); - const composition = inject('composition'); const extendedLinesBus = inject('extendedLinesBus'); + const composition = ref(null); + let isCompositionLoaded = false; + const existingContainers = []; + const compositionCollection = openmct.composition.get(toRaw(domainObject)); + compositionCollection.on('add', addItem); + compositionCollection.on('remove', removeItem); + compositionCollection.on('reorder', reorder); + const items = ref([]); - const loadedComposition = ref(null); const extendedLinesPerKey = ref({}); const { alignment: alignmentData, reset: resetAlignment } = useAlignment( @@ -146,6 +152,7 @@ export default { addContainer, removeContainer, reorderContainers, + setContainers, containers, startContainerResizing, containerResizing, @@ -156,6 +163,28 @@ export default { callback: mutateContainers }); + compositionCollection.load().then((loadedComposition) => { + composition.value = loadedComposition; + isCompositionLoaded = true; + + // sync containers to composition, + // in case composition modified outside of view + // but do not mutate until user makes a change + composition.value.forEach((object) => { + const containerIndex = domainObject.configuration.containers.findIndex((container) => + openmct.objects.areIdsEqual(container.domainObjectIdentifier, object.identifier) + ); + if (containerIndex > -1) { + existingContainers.push(domainObject.configuration.containers[containerIndex]); + } else { + const container = new Container(object); + existingContainers.push(container); + } + }); + + setContainers(existingContainers); + }); + function addItem(_domainObject) { let rowCount = 0; @@ -187,14 +216,7 @@ export default { items.value.push(item); - if ( - !containers.value.some((container) => - openmct.objects.areIdsEqual( - container.domainObjectIdentifier, - item.domainObject.identifier - ) - ) - ) { + if (isCompositionLoaded) { const container = new Container(domainObject); addContainer(container); } @@ -245,30 +267,10 @@ export default { openmct.objects.mutate(domainObject, 'configuration.containers', containers.value); } - onMounted(async () => { - if (composition) { - composition.on('add', addItem); - composition.on('remove', removeItem); - composition.on('reorder', reorder); - - loadedComposition.value = await composition.load(); - - const containersToRemove = containers.value.filter( - (container) => - !items.value.some((item) => - openmct.objects.areIdsEqual( - container.domainObjectIdentifier, - item.domainObject.identifier - ) - ) - ); - } - }); - onBeforeUnmount(() => { - composition.off('add', addItem); - composition.off('remove', removeItem); - composition.off('reorder', reorder); + compositionCollection.off('add', addItem); + compositionCollection.off('remove', removeItem); + compositionCollection.off('reorder', reorder); }); return { @@ -281,7 +283,6 @@ export default { containers, getContainerSize, timelineHolder, - loadedComposition, items, addContainer, removeContainer, diff --git a/src/plugins/timeline/configuration.js b/src/plugins/timeline/configuration.js index 0128f990dd..cb31653da7 100644 --- a/src/plugins/timeline/configuration.js +++ b/src/plugins/timeline/configuration.js @@ -1,3 +1,31 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2024, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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. + *****************************************************************************/ + +/** + * @typedef {Object} TimeStripConfig configuration for Time Strip views + * @property {boolean} useIndependentTime true for independent time, false for global time + * @property {Array} containers + * @property {number} swimLaneLabelWidth + */ export const configuration = { useIndependentTime: false, containers: [], diff --git a/src/utils/vue/useFlexContainers.js b/src/utils/vue/useFlexContainers.js index 4a949b606d..0475c70fd2 100644 --- a/src/utils/vue/useFlexContainers.js +++ b/src/utils/vue/useFlexContainers.js @@ -1,5 +1,26 @@ -import _ from 'lodash'; -import { ref } from 'vue'; +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2024, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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. + *****************************************************************************/ + +import { computed, ref } from 'vue'; /** * @typedef {Object} configuration @@ -22,25 +43,29 @@ export function useFlexContainers( ) { const containers = ref(existingContainers); const maxMoveSize = ref(null); - let sizingContainers = 0; + + const fixedContainersSize = computed(() => { + return containers.value + .filter((container) => container.fixed === true) + .reduce((size, currentContainer) => size + currentContainer.size, 0); + }); function addContainer(container) { containers.value.push(container); - if (container.scale) { - sizingContainers += container.scale; - } else { - sizingContainers++; - } - - sizeItems(containers.value, container); + sizeItems(containers.value); callback?.(); } function removeContainer(index) { + const isFlexContainer = !containers.value[index].fixed; + containers.value.splice(index, 1); - sizeToFill(containers.value); + + if (isFlexContainer) { + sizeItems(containers.value); + } callback?.(); } @@ -55,17 +80,49 @@ export function useFlexContainers( callback?.(); } + function setContainers(_containers) { + containers.value = _containers; + sizeItems(containers.value); + } + function startContainerResizing(index) { - const beforeContainer = containers.value[index]; - const afterContainer = containers.value[index + 1]; + const beforeContainer = getBeforeContainer(index); + const afterContainer = getAfterContainer(index); maxMoveSize.value = beforeContainer.size + afterContainer.size; } + function getBeforeContainer(index) { + return containers.value + .slice(0, index + 1) + .filter((container) => !container.fixed === true) + .at(-1); + } + + function getAfterContainer(index) { + return containers.value.slice(index + 1).filter((container) => !container.fixed === true)[0]; + } + function containerResizing(index, delta, event) { - let percentageMoved = Math.round((delta / getElSize()) * 100); - let beforeContainer = containers.value[index]; - let afterContainer = containers.value[index + 1]; + const beforeContainer = getBeforeContainer(index); + const afterContainer = getAfterContainer(index); + const percentageMoved = Math.round((delta / getElSize()) * 100); + + if (beforeContainer === undefined || afterContainer === undefined) { + console.warn( + 'Cannot drag to resize a single flex sized containers. Use Elements Tab in Inspector to resize fixed containers.' + ); + + return; + } + + if (beforeContainer.fixed === true && afterContainer.fixed === true) { + console.warn( + 'Cannot drag to resize two fixed sized containers. Use Elements Tab in Inspector to resize.' + ); + + return; + } beforeContainer.size = getContainerSize(beforeContainer.size + percentageMoved); afterContainer.size = getContainerSize(afterContainer.size - percentageMoved); @@ -76,11 +133,9 @@ export function useFlexContainers( } function getElSize() { - if (rowsLayout) { - return element.value.offsetHeight; - } else { - return element.value.offsetWidth; - } + const elSize = rowsLayout === true ? element.value.offsetHeight : element.value.offsetWidth; + + return elSize - fixedContainersSize.value; } function getContainerSize(size) { @@ -94,59 +149,58 @@ export function useFlexContainers( } /** - * Resize items so that newItem fits proportionally (newItem must be an element of items). - * If newItem does not have a size or is sized at 100%, - * newItem will have size set to (1 * newItemScale) / n * 100, - * where n is the total number of items and newItemScale is the scale of newItem. - * All other items will be resized to fit inside the remaining space. - * @param {*} items - * @param {*} newItem - */ - function sizeItems(items, newItem) { - const newItemScale = newItem.scale || 1; - - if (!newItem.size && items.length === 1) { - newItem.size = 100; - } else { - if (!newItem.size || newItem.size === 100) { - newItem.size = Math.round((100 * newItemScale) / sizingContainers); - - // Resize oldItems to fit inside remaining space; - const oldItems = items.filter((item) => !_.isEqual(item, newItem)); - const remainingSize = 100 - newItem.size; - - oldItems.forEach((item) => { - const itemScale = item.scale || 1; - item.size = Math.round((item.size * itemScale * remainingSize) / 100); - }); - - // Ensure items add up to 100 in case of rounding error. - const total = items.reduce((t, item) => t + item.size, 0); - const excess = Math.round(100 - total); - oldItems[oldItems.length - 1].size += excess; - } - } - } - - /** - * Scales items proportionally so total is equal to 100. - * Assumes that an item was removed from array. + * Resize flexible sized items so they fit proportionally within a viewport + * 1. add size to 0 sized items based on scale proportional to total scale + * 2. resize item sizes to equal 100 + * if total size < 100, resize all items + * if total size > 100, resize only items not resized in step 1 (newly added) + * 3. round excess and apply to last item + * + * Items may have a scale (ie. items with composition) + * + * Handles single add or removal, as well as atypical use cases, + * such as composition out of sync with containers config + * due to composition edits outside of view + * * @param {*} items */ - function sizeToFill(items) { - if (items.length === 0) { - return; - } + function sizeItems(items) { + let totalSize; + const itemsWithSize = items.filter((item) => item.size); + const itemsWithoutSize = items.filter((item) => !item.size); + // total number of items, adjusted by each item scale + const totalScale = items.reduce((total, item) => { + const scale = item.scale ?? 1; + return total + scale; + }, 0); - const oldTotal = items.reduce((total, item) => total + item.size, 0); - items.forEach((item) => { - const itemScale = item.scale || 1; - item.size = Math.round((item.size * itemScale * 100) / oldTotal); + itemsWithoutSize.forEach((item) => { + const scale = item.scale ?? 1; + item.size = Math.round((100 * scale) / totalScale); }); + totalSize = items.reduce((total, item) => total + item.size, 0); + + if (totalSize > 100) { + const addedSize = itemsWithoutSize.reduce((total, item) => total + item.size, 0); + const remainingSize = 100 - addedSize; + + itemsWithSize.forEach((item) => { + const scale = item.scale ?? 1; + item.size = Math.round((item.size * scale * remainingSize) / 100); + }); + } else if (totalSize < 100) { + const sizeToFill = 100 - totalSize; + + items.forEach((item) => { + const scale = item.scale ?? 1; + item.size = Math.round((item.size * scale * 100) / sizeToFill); + }); + } + // Ensure items add up to 100 in case of rounding error. - const total = items.reduce((t, item) => t + item.size, 0); - const excess = Math.round(100 - total); + totalSize = items.reduce((total, item) => total + item.size, 0); + const excess = Math.round(100 - totalSize); items[items.length - 1].size += excess; } @@ -154,6 +208,7 @@ export function useFlexContainers( addContainer, removeContainer, reorderContainers, + setContainers, containers, startContainerResizing, containerResizing, From c5ee817c81b50e14fc161df2c2279efc3ef728f5 Mon Sep 17 00:00:00 2001 From: David Tsay Date: Fri, 2 May 2025 15:55:45 -0700 Subject: [PATCH 2/2] WIP: allow fixed pixel heights --- .../inspectorViews/elements/ElementItem.vue | 1 + .../inspectorViews/elements/ElementsPool.vue | 6 +- .../elements/ElementsViewProvider.js | 1 + .../timeline/TimelineElementsContent.vue | 82 +++++++++++++++ src/plugins/timeline/TimelineElementsPool.vue | 21 ++++ .../timeline/TimelineElementsViewProvider.js | 76 ++++++++++++++ src/plugins/timeline/TimelineObjectView.vue | 22 +++-- src/plugins/timeline/TimelineViewLayout.vue | 39 +++++++- src/plugins/timeline/TimelineViewProvider.js | 8 +- src/plugins/timeline/plugin.js | 3 +- src/plugins/timeline/timeline.scss | 12 +++ src/utils/vue/useFlexContainers.js | 99 +++++++++++++------ 12 files changed, 327 insertions(+), 43 deletions(-) create mode 100644 src/plugins/timeline/TimelineElementsContent.vue create mode 100644 src/plugins/timeline/TimelineElementsPool.vue create mode 100644 src/plugins/timeline/TimelineElementsViewProvider.js diff --git a/src/plugins/inspectorViews/elements/ElementItem.vue b/src/plugins/inspectorViews/elements/ElementItem.vue index f668f6b8b2..e2770b1646 100644 --- a/src/plugins/inspectorViews/elements/ElementItem.vue +++ b/src/plugins/inspectorViews/elements/ElementItem.vue @@ -45,6 +45,7 @@ :object-path="[elementObject, domainObject]" @context-click-active="setContextClickState" /> + diff --git a/src/plugins/inspectorViews/elements/ElementsPool.vue b/src/plugins/inspectorViews/elements/ElementsPool.vue index 06ee6de757..b465c9cbf0 100644 --- a/src/plugins/inspectorViews/elements/ElementsPool.vue +++ b/src/plugins/inspectorViews/elements/ElementsPool.vue @@ -42,7 +42,11 @@ :allow-drop="allowDrop" @dragstart-custom="moveFrom(index)" @drop-custom="moveTo(index)" - /> + > + +
  • No contained elements
    diff --git a/src/plugins/inspectorViews/elements/ElementsViewProvider.js b/src/plugins/inspectorViews/elements/ElementsViewProvider.js index ce9748861a..dc66f3a3b1 100644 --- a/src/plugins/inspectorViews/elements/ElementsViewProvider.js +++ b/src/plugins/inspectorViews/elements/ElementsViewProvider.js @@ -29,6 +29,7 @@ export default function ElementsViewProvider(openmct) { key: 'elementsView', name: 'Elements', canView: function (selection) { + // TODO - only use this view provider if another custom provider has not been applied const hasValidSelection = selection?.length; const isOverlayPlot = selection?.[0]?.[0]?.context?.item?.type === 'telemetry.plot.overlay'; diff --git a/src/plugins/timeline/TimelineElementsContent.vue b/src/plugins/timeline/TimelineElementsContent.vue new file mode 100644 index 0000000000..4c38750e1b --- /dev/null +++ b/src/plugins/timeline/TimelineElementsContent.vue @@ -0,0 +1,82 @@ + + diff --git a/src/plugins/timeline/TimelineElementsPool.vue b/src/plugins/timeline/TimelineElementsPool.vue new file mode 100644 index 0000000000..43c9e3df2a --- /dev/null +++ b/src/plugins/timeline/TimelineElementsPool.vue @@ -0,0 +1,21 @@ + + diff --git a/src/plugins/timeline/TimelineElementsViewProvider.js b/src/plugins/timeline/TimelineElementsViewProvider.js new file mode 100644 index 0000000000..73b5023915 --- /dev/null +++ b/src/plugins/timeline/TimelineElementsViewProvider.js @@ -0,0 +1,76 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2024, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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. + *****************************************************************************/ + +import mount from 'utils/mount'; + +import TimelineElementsPool from './TimelineElementsPool.vue'; + +export default function TimelineElementsViewProvider(openmct) { + return { + key: 'timelineElementsView', + name: 'Elements', + canView: function (selection) { + return selection?.[0]?.[0]?.context?.item?.type === 'time-strip'; + }, + view: function (selection) { + let _destroy = null; + + const domainObject = selection?.[0]?.[0]?.context?.item; + + return { + show: function (element) { + const { destroy } = mount( + { + el: element, + components: { + TimelineElementsPool + }, + provide: { + openmct, + domainObject + }, + template: `` + }, + { + app: openmct.app, + element + } + ); + _destroy = destroy; + }, + showTab: function (isEditing) { + const hasComposition = Boolean(domainObject && openmct.composition.get(domainObject)); + + return hasComposition && isEditing; + }, + priority: function () { + return openmct.priority.HIGH - 1; + }, + destroy: function () { + if (_destroy) { + _destroy(); + } + } + }; + } + }; +} diff --git a/src/plugins/timeline/TimelineObjectView.vue b/src/plugins/timeline/TimelineObjectView.vue index 7d3db098d5..e1afbc3035 100644 --- a/src/plugins/timeline/TimelineObjectView.vue +++ b/src/plugins/timeline/TimelineObjectView.vue @@ -32,7 +32,8 @@ :hide-button="!item.isEventTelemetry" :button-click-on="enableExtendEventLines" :button-click-off="disableExtendEventLines" - :style="[{ 'flex-basis': sizeString }]" + :class="sizeClass" + :style="sizeStyle" >