webglimpse
Version:
Webglimpse is a data visualization library for the web.
900 lines • 230 kB
JavaScript
/*
* Copyright (c) 2014, Metron, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import { Pane, xFrac, isLeftMouseDown } from '../core';
import { TimelineUi } from './timeline_ui';
import { newVerticalScrollLayout, newVerticalScrollbar, attachTimelineVerticalScrollMouseListeners } from '../scroll';
import { white, rgb, gray, rgba } from '../color';
import { newTimeAxisPainter } from './time_axis_painter';
import { newInsets, newInsetPane, newInsetLayout } from '../layout/inset_layout';
import { rowPaneFactoryChooser_DEFAULT } from './timeline_styles';
import { axisZoomStep } from '../plot/axis';
import { newColumnLayout } from '../layout/column_layout';
import { newCardLayout } from '../layout/card_layout';
import { newRowLayout } from '../layout/row_layout';
import { Side, newBackgroundPainter, xyFrac_VERTSHADER, solid_FRAGSHADER, putQuadXys, newSolidPane, Background, Highlight } from '../misc';
import { newOverlayLayout } from '../layout/overlay_layout';
import { alwaysTrue, GL, hasval, isNumber } from '../util/util';
import { Program, Attribute, UniformColor } from '../shader';
import { newDynamicBuffer } from '../buffer';
import { TextureRenderer } from '../texture';
import { Label, fitToLabel, newLabelPainter } from '../label';
import { newTimeGridPainter } from './time_grid_painter';
import { newBorderPainter } from '../painter/border_painter';
import { createTextTextureFactory } from '../text';
export class TimelinePane extends Pane {
constructor(layout, model, ui) {
super(layout, true);
this._model = model;
this._ui = ui;
}
get model() { return this._model; }
get ui() { return this._ui; }
}
export function newTimelinePane(drawable, timeAxis, model, options, ui) {
// Misc
const font = (hasval(options) && hasval(options.font) ? options.font : '11px verdana,sans-serif');
const rowPaneFactoryChooser = (hasval(options) && hasval(options.rowPaneFactoryChooser) ? options.rowPaneFactoryChooser : rowPaneFactoryChooser_DEFAULT);
const cursor = (hasval(options) && hasval(options.cursor) ? options.cursor : 'auto');
// Scroll
const showScrollbar = (hasval(options) && hasval(options.showScrollbar) ? options.showScrollbar : true);
const scrollbarOptions = (hasval(options) ? options.scrollbarOptions : null);
// Colors
const fgColor = (hasval(options) && hasval(options.fgColor) ? options.fgColor : white);
const bgColor = (hasval(options) && hasval(options.bgColor) ? options.bgColor : rgb(0.098, 0.165, 0.243));
const rowLabelColor = (hasval(options) && hasval(options.rowLabelColor) ? options.rowLabelColor : fgColor);
const rowLabelBgColor = (hasval(options) && hasval(options.rowLabelBgColor) ? options.rowLabelBgColor : bgColor);
const rowHighlightColor = (hasval(options) && hasval(options.rowHighlightColor) ? options.rowHighlightColor : rgb(1, 0.623, 0));
const groupLabelColor = (hasval(options) && hasval(options.groupLabelColor) ? options.groupLabelColor : fgColor);
const groupHighlightColor = (hasval(options) && hasval(options.groupHighlightColor) ? options.groupHighlightColor : rgb(0, 1, 1));
const axisLabelColor = (hasval(options) && hasval(options.axisLabelColor) ? options.axisLabelColor : fgColor);
const rowBgColor = (hasval(options) && hasval(options.rowBgColor) ? options.rowBgColor : rgb(0.020, 0.086, 0.165));
const rowAltBgColor = (hasval(options) && hasval(options.rowAltBgColor) ? options.rowAltBgColor : rgb(0.020, 0.086, 0.165));
const gridColor = (hasval(options) && hasval(options.gridColor) ? options.gridColor : gray(0.5));
const selectedIntervalFillColor = (hasval(options) && hasval(options.selectedIntervalFillColor) ? options.selectedIntervalFillColor : rgba(0, 0.6, 0.8, 0.157));
const selectedIntervalBorderColor = (hasval(options) && hasval(options.selectedIntervalBorderColor) ? options.selectedIntervalBorderColor : rgb(0, 0.2, 1.0));
// Axes
const showTopAxis = (hasval(options) && hasval(options.showTopAxis) ? options.showTopAxis : true);
const showBottomAxis = (hasval(options) && hasval(options.showBottomAxis) ? options.showBottomAxis : true);
const topTimeZone = (hasval(options) && hasval(options.topTimeZone) ? options.topTimeZone : '+0000');
const bottomTimeZone = (hasval(options) && hasval(options.bottomTimeZone) ? options.bottomTimeZone : '+0000');
const tickSpacing = (hasval(options) && hasval(options.tickSpacing) ? options.tickSpacing : 60);
const axisLabelAlign = (hasval(options) && hasval(options.axisLabelAlign) ? options.axisLabelAlign : 0.5);
// Sizing
const groupLabelInsets = (hasval(options) && hasval(options.groupLabelInsets) ? options.groupLabelInsets : newInsets(6, 10));
const groupHighlightWidth = (hasval(options) && hasval(options.groupHighlightWidth) ? options.groupHighlightWidth : 2);
const groupHighlightInsets = (hasval(options) && hasval(options.groupHighlightInsets) ? options.groupHighlightInsets : newInsets(6, 0, 2, 4));
const rowLabelInsets = (hasval(options) && hasval(options.rowLabelInsets) ? options.rowLabelInsets : newInsets(0, 35));
const rowLabelPaneWidth = (hasval(options) && hasval(options.rowLabelPaneWidth) ? options.rowLabelPaneWidth : 140);
const rowHighlightWidth = (hasval(options) && hasval(options.rowHighlightWidth) ? options.rowHighlightWidth : 2);
const rowHighlightInsets = (hasval(options) && hasval(options.rowHighlightInsets) ? options.rowHighlightInsets : newInsets(1, 0, 2, 10));
const rowSeparatorHeight = (hasval(options) && hasval(options.rowSeparatorHeight) ? options.rowSeparatorHeight : 2);
let scrollbarWidth = (hasval(options) && hasval(options.scrollbarWidth) ? options.scrollbarWidth : 16);
scrollbarWidth = showScrollbar ? scrollbarWidth : 0; // if the scrollbar is not showing, set its width to 0
const axisPaneHeight = (hasval(options) && hasval(options.axisPaneHeight) ? options.axisPaneHeight : 40);
const draggableEdgeWidth = (hasval(options) && hasval(options.draggableEdgeWidth) ? options.draggableEdgeWidth : 6);
const snapToDistance = (hasval(options) && hasval(options.snapToDistance) ? options.snapToDistance : 10);
// Event / Selection
const allowEventMultiSelection = (hasval(options) && hasval(options.allowEventMultiSelection) ? options.allowEventMultiSelection : true);
const selectedIntervalMode = (hasval(options) && hasval(options.selectedIntervalMode) ? options.selectedIntervalMode : 'range');
const centerSelectedIntervalOnDoubleClick = (hasval(options) && hasval(options.centerSelectedIntervalOnDoubleClick) ? options.centerSelectedIntervalOnDoubleClick : true);
const defaultMouseWheelListener = function (ev) {
const zoomFactor = Math.pow(axisZoomStep, ev.wheelSteps);
timeAxis.zoom(zoomFactor, timeAxis.vAtFrac(xFrac(ev)));
};
const mouseWheelListener = (hasval(options) && hasval(options.mouseWheelListener) ? options.mouseWheelListener : defaultMouseWheelListener);
let outsideManagedUi = false;
if (!ui) {
outsideManagedUi = false;
ui = new TimelineUi(model, { allowEventMultiSelection: allowEventMultiSelection });
}
else {
// remove old panes (if the ui is being reused)
outsideManagedUi = true;
ui.panes.removeAll();
}
const selection = ui.selection;
const redraw = function () {
drawable.redraw();
};
selection.selectedInterval.changed.on(redraw);
selection.hoveredEvent.changed.on(redraw);
selection.selectedEvents.valueAdded.on(redraw);
selection.selectedEvents.valueRemoved.on(redraw);
// even if the model defines cursors, we may need to redraw when the mouse position changes
// (we might not actually need to if: none of the rows actually use the cursor, or if the
// cursor doesn't show a vertical or horizontal line)
// this check just avoids redrawing unncessarily in the easy-to-verify common case where
// no cursors are defined
const redrawCursor = function () {
if (!model.cursors.isEmpty) {
drawable.redraw();
}
};
selection.hoveredY.changed.on(redrawCursor);
selection.hoveredTime_PMILLIS.changed.on(redrawCursor);
// Scroll Pane and Maximized Row Pane
//
// setup Pane which either shows timeline content, or only maximized rows
// able to switch between the two depending on model.root.maximizedRowGuids.isEmpty
// Scroll Pane
const tickTimeZone = (showTopAxis ? topTimeZone : bottomTimeZone);
const contentPaneOpts = { selectedIntervalMode: selectedIntervalMode, rowPaneFactoryChooser: rowPaneFactoryChooser, font: font, fgColor: fgColor, rowLabelColor: rowLabelColor, rowLabelBgColor: rowLabelBgColor, rowHighlightColor: rowHighlightColor, groupLabelColor: groupLabelColor, groupHighlightColor: groupHighlightColor, axisLabelColor: axisLabelColor, bgColor: bgColor, rowBgColor: rowBgColor, rowAltBgColor: rowAltBgColor, gridColor: gridColor, gridTickSpacing: tickSpacing, gridTimeZone: tickTimeZone, referenceDate: options.referenceDate, groupLabelInsets: groupLabelInsets, groupHighlightWidth: groupHighlightWidth, groupHighlightInsets: groupHighlightInsets, rowLabelInsets: rowLabelInsets, rowLabelPaneWidth: rowLabelPaneWidth, rowHighlightWidth: rowHighlightWidth, rowHighlightInsets: rowHighlightInsets, rowSeparatorHeight: rowSeparatorHeight, draggableEdgeWidth: draggableEdgeWidth, snapToDistance: snapToDistance, mouseWheelListener: mouseWheelListener };
let contentPaneArgs;
let contentPane = null;
if (showScrollbar) {
const scrollLayout = newVerticalScrollLayout();
const scrollable = new Pane(scrollLayout, false);
ui.addPane('scroll-content-pane', scrollable);
contentPaneArgs = { drawable: drawable, scrollLayout: scrollLayout, timeAxis: timeAxis, model: model, ui: ui, options: contentPaneOpts };
const scrollContentPane = newTimelineContentPane(contentPaneArgs);
ui.addPane('content-pane', scrollContentPane);
scrollable.addPane(scrollContentPane, 0);
const scrollbar = newVerticalScrollbar(scrollLayout, drawable, scrollbarOptions);
ui.addPane('scrollbar', scrollbar);
contentPane = new Pane(newColumnLayout(false), false);
ui.addPane('scroll-outer-pane', contentPane);
contentPane.addPane(scrollbar, 0, { width: scrollbarWidth, ignoreHeight: true });
contentPane.addPane(scrollable, 1);
}
else {
contentPaneArgs = { drawable: drawable, scrollLayout: null, timeAxis: timeAxis, model: model, ui: ui, options: contentPaneOpts };
contentPane = newTimelineContentPane(contentPaneArgs);
ui.addPane('content-pane', contentPane);
}
// Card Pane Switching Logic
const timelineCardPane = new Pane(newCardLayout());
ui.addPane('switch-content-pane', timelineCardPane);
const maximizedContentPane = new Pane(newRowLayout());
ui.addPane('maximize-content-pane', maximizedContentPane);
const insetMaximizedContentPane = newInsetPane(maximizedContentPane, newInsets(0, scrollbarWidth, 0, 0));
ui.addPane('inset-maximize-content-pane', insetMaximizedContentPane);
const contentActive = model.root.maximizedRowGuids.isEmpty;
timelineCardPane.addPane(insetMaximizedContentPane, !contentActive);
timelineCardPane.addPane(contentPane, contentActive);
setupRowContainerPane(contentPaneArgs, maximizedContentPane, model.root.maximizedRowGuids, true, 'maximized');
const updateMaximizedRows = function (rowGuid, rowIndex) {
const contentActiveInner = model.root.maximizedRowGuids.isEmpty;
timelineCardPane.setLayoutArg(insetMaximizedContentPane, !contentActiveInner);
timelineCardPane.setLayoutArg(contentPane, contentActiveInner);
drawable.redraw();
};
model.root.maximizedRowGuids.valueAdded.on(updateMaximizedRows);
model.root.maximizedRowGuids.valueRemoved.on(updateMaximizedRows);
// Overlay and Underlay Panes
//
const underlayPane = new Pane(newRowLayout());
ui.addPane('underlay-pane', underlayPane);
const axisInsets = newInsets(0, scrollbarWidth, 0, rowLabelPaneWidth);
// top time axis pane
const axisOpts = { tickSpacing: tickSpacing, font: font, textColor: axisLabelColor, tickColor: axisLabelColor, labelAlign: axisLabelAlign, referenceDate: options.referenceDate, isFuturePositive: options.isFuturePositive, timeAxisFormat: options.timeAxisFormat };
if (showTopAxis) {
const topAxisPane = newTimeAxisPane(contentPaneArgs, null, cursor);
ui.addPane('top-axis-pane', topAxisPane);
topAxisPane.addPainter(newTimeAxisPainter(timeAxis, Side.TOP, topTimeZone, tickTimeZone, axisOpts));
underlayPane.addPane(newInsetPane(topAxisPane, axisInsets), 0, { height: axisPaneHeight, width: null });
}
// pane containing pinned rows specified in TimelineRoot.topPinnedRowGuids
const topPinnedPane = new Pane(newRowLayout());
ui.addPane('top-pinned-pane', topPinnedPane);
const insetTopPinnedPane = newInsetPane(topPinnedPane, newInsets(0, scrollbarWidth, 0, 0));
ui.addPane('inset-top-pinned-pane', insetTopPinnedPane);
setupRowContainerPane(contentPaneArgs, topPinnedPane, model.root.topPinnedRowGuids, false, 'toppinned');
underlayPane.addPane(insetTopPinnedPane, 1);
// main pane containing timeline groups and rows
underlayPane.addPane(timelineCardPane, 2, { height: 'pref-max', width: null });
// pane containing pinned rows specified in TimelineRoot.bottomPinnedRowGuids
const bottomPinnedPane = new Pane(newRowLayout());
ui.addPane('bottom-pinned-pane', bottomPinnedPane);
const insetBottomPinnedPane = newInsetPane(bottomPinnedPane, newInsets(0, scrollbarWidth, 0, 0));
ui.addPane('inset-bottom-pinned-pane', insetBottomPinnedPane);
setupRowContainerPane(contentPaneArgs, bottomPinnedPane, model.root.bottomPinnedRowGuids, false, 'bottompinned');
underlayPane.addPane(insetBottomPinnedPane, 3);
// bottom time axis pane
if (showBottomAxis) {
const bottomAxisPane = newTimeAxisPane(contentPaneArgs, null, cursor);
ui.addPane('bottom-axis-pane', bottomAxisPane);
bottomAxisPane.addPainter(newTimeAxisPainter(timeAxis, Side.BOTTOM, bottomTimeZone, tickTimeZone, axisOpts));
underlayPane.addPane(newInsetPane(bottomAxisPane, axisInsets), 4, { height: axisPaneHeight, width: null });
}
const updateMillisPerPx = function () {
const w = underlayPane.viewport.w - axisInsets.left - axisInsets.right;
ui.millisPerPx.value = timeAxis.tSize_MILLIS / w;
};
underlayPane.viewportChanged.on(updateMillisPerPx);
timeAxis.limitsChanged.on(updateMillisPerPx);
const timelinePane = new TimelinePane(newOverlayLayout(), model, ui);
ui.addPane('timeline-pane', timelinePane);
timelinePane.addPainter(newBackgroundPainter(bgColor));
timelinePane.addPane(underlayPane, true);
if (selectedIntervalMode === 'single' || selectedIntervalMode === 'single-unmodifiable') {
const overlayPane = new Pane(null, false, alwaysTrue);
ui.addPane('overlay-pane', overlayPane);
overlayPane.addPainter(newTimelineSingleSelectionPainter(timeAxis, selection.selectedInterval, selectedIntervalBorderColor, selectedIntervalFillColor));
timelinePane.addPane(newInsetPane(overlayPane, axisInsets, null, false));
}
else if (selectedIntervalMode === 'range' || selectedIntervalMode === 'range-unmodifiable') {
const overlayPane = new Pane(null, false, alwaysTrue);
ui.addPane('overlay-pane', overlayPane);
overlayPane.addPainter(newTimelineRangeSelectionPainter(timeAxis, selection.selectedInterval, selectedIntervalBorderColor, selectedIntervalFillColor));
timelinePane.addPane(newInsetPane(overlayPane, axisInsets, null, false));
}
// Enable double click to center selection on mouse
if (centerSelectedIntervalOnDoubleClick) {
const doubleClick = function (ev) {
if (selectedIntervalMode === 'single') {
if (ev.clickCount > 1) {
const time_PMILLIS = timeAtPointer_PMILLIS(timeAxis, ev);
selection.selectedInterval.setInterval(time_PMILLIS, time_PMILLIS);
}
}
else if (selectedIntervalMode === 'range') {
if (ev.clickCount > 1) {
const time_PMILLIS = timeAtPointer_PMILLIS(timeAxis, ev);
const offset_PMILLIS = selection.selectedInterval.start_PMILLIS + 0.5 * selection.selectedInterval.duration_MILLIS;
selection.selectedInterval.pan(time_PMILLIS - offset_PMILLIS);
}
}
};
ui.input.mouseDown.on(doubleClick);
}
timelinePane.dispose.on(function () {
// only dispose the ui if we created it (and this manage its lifecycle)
if (!outsideManagedUi) {
ui.dispose.fire();
}
selection.selectedInterval.changed.off(redraw);
selection.hoveredEvent.changed.off(redraw);
selection.hoveredY.changed.off(redrawCursor);
selection.hoveredTime_PMILLIS.changed.off(redrawCursor);
selection.selectedEvents.valueAdded.off(redraw);
selection.selectedEvents.valueRemoved.off(redraw);
underlayPane.viewportChanged.off(updateMillisPerPx);
timeAxis.limitsChanged.off(updateMillisPerPx);
model.root.maximizedRowGuids.valueAdded.off(updateMaximizedRows);
model.root.maximizedRowGuids.valueRemoved.off(updateMaximizedRows);
});
return timelinePane;
}
function newTimeIntervalMask(timeAxis, interval, selectedIntervalMode) {
if (selectedIntervalMode === 'range') {
return function (viewport, i, j) {
const time_PMILLIS = timeAxis.tAtFrac_PMILLIS(viewport.xFrac(i));
// allow a 10 pixel selection buffer to make it easier to grab ends of the selection
const buffer_MILLIS = timeAxis.tSize_MILLIS / viewport.w * 10;
return interval.overlaps(time_PMILLIS - buffer_MILLIS, time_PMILLIS + buffer_MILLIS);
};
}
else if (selectedIntervalMode === 'single') {
return function (viewport, i, j) {
const time_PMILLIS = timeAxis.tAtFrac_PMILLIS(viewport.xFrac(i));
// allow a 10 pixel selection buffer to make it easier to grab the selection
const buffer_MILLIS = timeAxis.tSize_MILLIS / viewport.w * 10;
return time_PMILLIS < interval.cursor_PMILLIS + buffer_MILLIS && time_PMILLIS > interval.cursor_PMILLIS - buffer_MILLIS;
};
}
}
function attachTimeAxisMouseListeners(pane, axis, args) {
let vGrab = null;
pane.mouseDown.on(function (ev) {
if (isLeftMouseDown(ev.mouseEvent) && !hasval(vGrab)) {
vGrab = axis.vAtFrac(xFrac(ev));
}
});
pane.mouseMove.on(function (ev) {
if (isLeftMouseDown(ev.mouseEvent) && hasval(vGrab)) {
axis.pan(vGrab - axis.vAtFrac(xFrac(ev)));
}
});
pane.mouseUp.on(function (ev) {
vGrab = null;
});
pane.mouseWheel.on(args.options.mouseWheelListener);
}
function newTimeAxisPane(args, row, cursor) {
const timeAxis = args.timeAxis;
const ui = args.ui;
const draggableEdgeWidth = args.options.draggableEdgeWidth;
const scrollLayout = args.scrollLayout;
const drawable = args.drawable;
const selectedIntervalMode = args.options.selectedIntervalMode;
const input = ui.input;
const axisPane = new Pane(newOverlayLayout());
if (scrollLayout) {
attachTimelineVerticalScrollMouseListeners(axisPane, scrollLayout, drawable);
}
attachTimeAxisMouseListeners(axisPane, timeAxis, args);
const onMouseMove = function (ev) {
const time_PMILLIS = timeAxis.tAtFrac_PMILLIS(xFrac(ev));
input.mouseMove.fire(ev);
input.timeHover.fire(time_PMILLIS, ev);
if (row) {
input.rowHover.fire(row, ev);
}
};
axisPane.mouseMove.on(onMouseMove);
const onMouseExit = function (ev) {
input.mouseExit.fire(ev);
input.timeHover.fire(null, ev);
if (row) {
input.rowHover.fire(null, ev);
}
};
axisPane.mouseExit.on(onMouseExit);
const onMouseDown = function (ev) {
input.mouseDown.fire(ev);
};
axisPane.mouseDown.on(onMouseDown);
const onMouseUp = function (ev) {
input.mouseUp.fire(ev);
};
axisPane.mouseUp.on(onMouseUp);
const onContextMenu = function (ev) {
input.contextMenu.fire(ev);
};
axisPane.contextMenu.on(onContextMenu);
if (cursor) {
axisPane.mouseCursor = cursor;
}
if (selectedIntervalMode === 'single' || selectedIntervalMode === 'range') {
const selection = ui.selection;
const selectedIntervalPane = new Pane(null, true, newTimeIntervalMask(timeAxis, selection.selectedInterval, selectedIntervalMode));
attachTimeSelectionMouseListeners(selectedIntervalPane, timeAxis, selection.selectedInterval, input, draggableEdgeWidth, selectedIntervalMode, args.options.mouseWheelListener);
axisPane.addPane(selectedIntervalPane, false);
selectedIntervalPane.mouseMove.on(onMouseMove);
selectedIntervalPane.mouseExit.on(onMouseExit);
selectedIntervalPane.mouseDown.on(onMouseDown);
selectedIntervalPane.mouseUp.on(onMouseUp);
selectedIntervalPane.contextMenu.on(onContextMenu);
}
// Dispose
//
// mouse listeners are disposed of automatically by Pane
return axisPane;
}
function timeAtPointer_PMILLIS(timeAxis, ev) {
return timeAxis.tAtFrac_PMILLIS(ev.paneViewport.xFrac(ev.i));
}
function attachTimeSelectionMouseListeners(pane, timeAxis, interval, input, draggableEdgeWidth, selectedIntervalMode, mouseWheelListener) {
if (selectedIntervalMode === 'single') {
const chooseDragMode = function (ev) {
return 'center';
};
attachTimeIntervalSelectionMouseListeners(pane, timeAxis, interval, input, draggableEdgeWidth, selectedIntervalMode, chooseDragMode, mouseWheelListener);
}
else if (selectedIntervalMode === 'range') {
// Edges are draggable when interval is at least this wide
const minIntervalWidthForEdgeDraggability = 3 * draggableEdgeWidth;
// When dragging an edge, the interval cannot be made narrower than this
//
// Needs to be greater than minIntervalWidthForEdgeDraggability -- by enough to
// cover floating-point precision loss -- so a user can't accidentally make
// the interval so narrow that it can't easily be widened again.
//
const minIntervalWidthWhenDraggingEdge = minIntervalWidthForEdgeDraggability + 1;
const chooseDragMode = function (ev) {
const intervalWidth = (interval.duration_MILLIS) * ev.paneViewport.w / timeAxis.vSize;
if (intervalWidth < minIntervalWidthForEdgeDraggability) {
// If interval isn't very wide, don't try to allow edge dragging
return 'center';
}
else {
const time_PMILLIS = timeAtPointer_PMILLIS(timeAxis, ev);
const mouseOffset = (time_PMILLIS - interval.start_PMILLIS) * ev.paneViewport.w / timeAxis.vSize;
if (mouseOffset < draggableEdgeWidth) {
// If mouse is near the left edge, drag the interval's start-time
return 'start';
}
else if (mouseOffset < intervalWidth - draggableEdgeWidth) {
// If mouse is in the center, drag the whole interval
return 'center';
}
else {
// If mouse is near the right edge, drag the interval's end-time
return 'end';
}
}
};
attachTimeIntervalSelectionMouseListeners(pane, timeAxis, interval, input, draggableEdgeWidth, selectedIntervalMode, chooseDragMode, mouseWheelListener);
}
}
function attachTimeIntervalSelectionMouseListeners(pane, timeAxis, interval, input, draggableEdgeWidth, selectedIntervalMode, chooseDragMode, mouseWheelListener) {
// see comments in attachTimeSelectionMouseListeners( ... )
const minIntervalWidthForEdgeDraggability = 3 * draggableEdgeWidth;
const minIntervalWidthWhenDraggingEdge = minIntervalWidthForEdgeDraggability + 1;
// Hook up input notifications
//
pane.mouseWheel.on(mouseWheelListener);
pane.contextMenu.on(function (ev) {
input.contextMenu.fire(ev);
});
// Begin interval-drag
//
let dragMode = null;
let dragOffset_MILLIS = null;
pane.mouseMove.on(function (ev) {
if (!dragMode) {
const mouseCursors = { 'center': 'move', 'start': 'w-resize', 'end': 'e-resize' };
pane.mouseCursor = mouseCursors[chooseDragMode(ev)];
}
});
pane.mouseDown.on(function (ev) {
dragMode = isLeftMouseDown(ev.mouseEvent) ? chooseDragMode(ev) : null;
if (!hasval(dragMode)) {
dragOffset_MILLIS = null;
}
});
// Compute (and remember) the pointer time, for use by the drag listeners below
//
let dragPointer_PMILLIS = null;
const updateDragPointer = function (ev) {
if (hasval(dragMode)) {
dragPointer_PMILLIS = timeAtPointer_PMILLIS(timeAxis, ev);
}
};
pane.mouseDown.on(updateDragPointer);
pane.mouseMove.on(updateDragPointer);
// Dragging interval-center
//
const grabCenter = function () {
if (dragMode === 'center') {
dragOffset_MILLIS = dragPointer_PMILLIS - interval.start_PMILLIS;
}
};
pane.mouseDown.on(grabCenter);
const dragCenter = function () {
if (dragMode === 'center') {
const newStart_PMILLIS = (dragPointer_PMILLIS - dragOffset_MILLIS);
const newEnd_PMILLIS = interval.end_PMILLIS + (newStart_PMILLIS - interval.start_PMILLIS);
interval.setInterval(newStart_PMILLIS, newEnd_PMILLIS);
}
};
pane.mouseMove.on(dragCenter);
// Dragging interval-start
//
const grabStart = function () {
if (dragMode === 'start') {
dragOffset_MILLIS = dragPointer_PMILLIS - interval.start_PMILLIS;
}
};
pane.mouseDown.on(grabStart);
const dragStart = function () {
if (dragMode === 'start') {
const wMin_MILLIS = minIntervalWidthWhenDraggingEdge * timeAxis.vSize / pane.viewport.w;
const newStart_PMILLIS = dragPointer_PMILLIS - dragOffset_MILLIS;
interval.start_PMILLIS = Math.min(interval.end_PMILLIS - wMin_MILLIS, newStart_PMILLIS);
}
};
pane.mouseMove.on(dragStart);
// Dragging interval-end
//
const grabEnd = function () {
if (dragMode === 'end') {
dragOffset_MILLIS = dragPointer_PMILLIS - interval.end_PMILLIS;
}
};
pane.mouseDown.on(grabEnd);
const dragEnd = function () {
if (dragMode === 'end') {
const wMin_MILLIS = minIntervalWidthWhenDraggingEdge * timeAxis.vSize / pane.viewport.w;
const newEnd_PMILLIS = dragPointer_PMILLIS - dragOffset_MILLIS;
interval.end_PMILLIS = Math.max(interval.start_PMILLIS + wMin_MILLIS, newEnd_PMILLIS);
interval.cursor_PMILLIS = interval.end_PMILLIS;
}
};
pane.mouseMove.on(dragEnd);
// Finish interval-drag
//
pane.mouseUp.on(function (ev) {
dragOffset_MILLIS = null;
dragPointer_PMILLIS = null;
dragMode = null;
});
}
export function newTimelineSingleSelectionPainter(timeAxis, interval, borderColor, fillColor) {
const program = new Program(xyFrac_VERTSHADER, solid_FRAGSHADER);
const a_XyFrac = new Attribute(program, 'a_XyFrac');
const u_Color = new UniformColor(program, 'u_Color');
// holds vertices for fill and border
const coords = new Float32Array(12 + 8);
const coordsBuffer = newDynamicBuffer();
return function (gl, viewport) {
if (hasval(interval.cursor_PMILLIS)) {
const fracSelection = timeAxis.tFrac(interval.cursor_PMILLIS);
const fracWidth = 1 / viewport.w;
const fracHeight = 1 / viewport.h;
const thickWidth = 3 / viewport.w;
const highlightWidth = 7 / viewport.w;
let index = 0;
// fill vertices
coords[index++] = fracSelection - highlightWidth;
coords[index++] = 1;
coords[index++] = fracSelection + highlightWidth;
coords[index++] = 1;
coords[index++] = fracSelection - highlightWidth;
coords[index++] = 0;
coords[index++] = fracSelection + highlightWidth;
coords[index++] = 0;
// selection vertices
index = putQuadXys(coords, index, fracSelection - thickWidth / 2, fracSelection + thickWidth / 2, 1, 0 + fracHeight); // selection
gl.blendFuncSeparate(GL.SRC_ALPHA, GL.ONE_MINUS_SRC_ALPHA, GL.ONE, GL.ONE_MINUS_SRC_ALPHA);
gl.enable(GL.BLEND);
program.use(gl);
coordsBuffer.setData(coords);
a_XyFrac.setDataAndEnable(gl, coordsBuffer, 2, GL.FLOAT);
u_Color.setData(gl, fillColor);
gl.drawArrays(GL.TRIANGLE_STRIP, 0, 4);
u_Color.setData(gl, borderColor);
gl.drawArrays(GL.TRIANGLES, 4, 6);
a_XyFrac.disable(gl);
program.endUse(gl);
}
};
}
function newTimelineRangeSelectionPainter(timeAxis, interval, borderColor, fillColor) {
const program = new Program(xyFrac_VERTSHADER, solid_FRAGSHADER);
const a_XyFrac = new Attribute(program, 'a_XyFrac');
const u_Color = new UniformColor(program, 'u_Color');
// holds vertices for fill and border
const coords = new Float32Array(12 + 8 + 48);
const coordsBuffer = newDynamicBuffer();
return function (gl, viewport) {
if (hasval(interval.start_PMILLIS) && hasval(interval.end_PMILLIS)) {
const fracStart = timeAxis.tFrac(interval.start_PMILLIS);
const fracEnd = timeAxis.tFrac(interval.end_PMILLIS);
const fracSelection = timeAxis.tFrac(interval.cursor_PMILLIS);
const fracWidth = 1 / viewport.w;
const fracHeight = 1 / viewport.h;
const thickWidth = 3 / viewport.w;
let index = 0;
// fill vertices
coords[index++] = fracStart;
coords[index++] = 1;
coords[index++] = fracEnd;
coords[index++] = 1;
coords[index++] = fracStart;
coords[index++] = 0;
coords[index++] = fracEnd;
coords[index++] = 0;
// border vertices
index = putQuadXys(coords, index, fracStart, fracEnd - fracWidth, +1, +1 - fracHeight); // top
index = putQuadXys(coords, index, fracStart + fracWidth, fracEnd, 0 + fracHeight, 0); // bottom
index = putQuadXys(coords, index, fracStart, fracStart + fracWidth, 1 - fracHeight, 0); // left
index = putQuadXys(coords, index, fracEnd - fracWidth, fracEnd, 1, 0 + fracHeight); // right
// selection vertices
index = putQuadXys(coords, index, fracSelection - thickWidth, fracSelection, 1, 0 + fracHeight); // selection
gl.blendFuncSeparate(GL.SRC_ALPHA, GL.ONE_MINUS_SRC_ALPHA, GL.ONE, GL.ONE_MINUS_SRC_ALPHA);
gl.enable(GL.BLEND);
program.use(gl);
coordsBuffer.setData(coords);
a_XyFrac.setDataAndEnable(gl, coordsBuffer, 2, GL.FLOAT);
u_Color.setData(gl, fillColor);
gl.drawArrays(GL.TRIANGLE_STRIP, 0, 4);
u_Color.setData(gl, borderColor);
gl.drawArrays(GL.TRIANGLES, 4, 30);
a_XyFrac.disable(gl);
program.endUse(gl);
}
};
}
function newGroupCollapseExpandArrowPainter(group) {
const program = new Program(xyFrac_VERTSHADER, solid_FRAGSHADER);
const a_XyFrac = new Attribute(program, 'a_XyFrac');
const u_Color = new UniformColor(program, 'u_Color');
// holds vertices for triangle
const coords = new Float32Array(6);
const coordsBuffer = newDynamicBuffer();
return function (gl, viewport) {
let sizeFracX = 0.5;
const sizeX = sizeFracX * viewport.w;
const sizeY = sizeX * Math.sqrt(3) / 2;
let sizeFracY = sizeY / viewport.h;
const bufferFracX = 0.05;
const bufferSize = bufferFracX * viewport.w;
const bufferFracY = bufferSize / viewport.h;
const centerFracX = 0.5;
const centerFracY = bufferFracY + sizeFracY / 2;
if (group.collapsed) {
sizeFracX = sizeY / viewport.w;
sizeFracY = sizeX / viewport.h;
const fracStartX = centerFracX - sizeFracX / 2;
const fracEndX = centerFracX + sizeFracX / 2;
const fracStartY = 1 - (centerFracY - sizeFracY / 2);
const fracEndY = 1 - (centerFracY + sizeFracY / 2);
let index = 0;
coords[index++] = fracStartX;
coords[index++] = fracStartY;
coords[index++] = fracEndX;
coords[index++] = (fracStartY + fracEndY) / 2;
coords[index++] = fracStartX;
coords[index++] = fracEndY;
}
else {
const fracStartX = centerFracX - sizeFracX / 2;
const fracEndX = centerFracX + sizeFracX / 2;
const fracStartY = 1 - (centerFracY - sizeFracY / 2);
const fracEndY = 1 - (centerFracY + sizeFracY / 2);
let index = 0;
coords[index++] = fracStartX;
coords[index++] = fracStartY;
coords[index++] = fracEndX;
coords[index++] = fracStartY;
coords[index++] = (fracStartX + fracEndX) / 2;
coords[index++] = fracEndY;
}
program.use(gl);
coordsBuffer.setData(coords);
a_XyFrac.setDataAndEnable(gl, coordsBuffer, 2, GL.FLOAT);
u_Color.setData(gl, white);
gl.drawArrays(GL.TRIANGLES, 0, 3);
a_XyFrac.disable(gl);
program.endUse(gl);
};
}
function newTimelineContentPane(args) {
const drawable = args.drawable;
const scrollLayout = args.scrollLayout;
const timeAxis = args.timeAxis;
const model = args.model;
const ui = args.ui;
const options = args.options;
const root = model.root;
const selectedIntervalMode = options.selectedIntervalMode;
const rowPaneFactoryChooser = options.rowPaneFactoryChooser;
const font = options.font;
const fgColor = options.fgColor;
const rowLabelColor = options.rowLabelColor;
const groupLabelColor = options.groupLabelColor;
const groupHighlightColor = options.groupHighlightColor;
const groupHighlightWidth = options.groupHighlightWidth;
const groupHighlightInsets = options.groupHighlightInsets;
const axisLabelColor = options.axisLabelColor;
const bgColor = options.bgColor;
const rowBgColor = options.rowBgColor;
const rowAltBgColor = options.rowAltBgColor;
const gridColor = options.gridColor;
const gridTimeZone = options.gridTimeZone;
const gridTickSpacing = options.gridTickSpacing;
const groupLabelInsets = options.groupLabelInsets;
const rowLabelInsets = options.rowLabelInsets;
const rowLabelPaneWidth = options.rowLabelPaneWidth;
const rowSeparatorHeight = options.rowSeparatorHeight;
const draggableEdgeWidth = options.draggableEdgeWidth;
const snapToDistance = options.snapToDistance;
const textureRenderer = new TextureRenderer();
const createGroupLabelTexture = createTextTextureFactory(font);
const createRowLabelTexture = createTextTextureFactory(font);
// Group panes
//
const timelineContentPane = new Pane(newRowLayout());
const groupHeaderPanes = {};
const groupContentPanes = {};
const groupContainerPanes = {};
const addGroup = function (groupGuid, groupIndex) {
const group = model.group(groupGuid);
const groupLabelFont = hasval(group.labelFont) ? group.labelFont : font;
const groupLabel = new Label(group.label, groupLabelFont, groupLabelColor);
const groupLabelPane = new Pane({ updatePrefSize: fitToLabel(groupLabel) }, false);
groupLabelPane.addPainter(newLabelPainter(groupLabel, 0, 1, 0, 1));
const groupArrowPane = new Pane({
updatePrefSize: function (parentPrefSize) {
parentPrefSize.w = 16;
parentPrefSize.h = 0;
}
}, false);
groupArrowPane.addPainter(newGroupCollapseExpandArrowPainter(group));
const groupPane = new Pane(newColumnLayout(), false);
groupPane.addPane(groupArrowPane, 0);
groupPane.addPane(groupLabelPane, 1);
const groupButton = newInsetPane(groupPane, groupLabelInsets, bgColor);
const redrawLabel = function () {
groupLabel.text = group.label;
drawable.redraw();
};
group.attrsChanged.on(redrawLabel);
/// handle rollup group row ///
const groupHeaderStripe = new Pane(newRowLayout());
groupHeaderStripe.addPane(new Pane(null), 0, { height: null });
groupHeaderStripe.addPane(newSolidPane(groupLabelColor), 1, { height: 1 });
groupHeaderStripe.addPane(new Pane(null), 2, { height: null });
const rollupRow = model.row(group.rollupGuid);
let groupHeaderPane = null;
let groupHeaderUnderlay = null;
if (rollupRow) {
const rowBackgroundPanes = newRowBackgroundPanes(args, group.rowGuids, rollupRow);
const rowBackgroundPane = rowBackgroundPanes.rowBackgroundPane;
const rowInsetPane = rowBackgroundPanes.rowInsetPane;
const rollupUi = ui.rowUi(rollupRow.rowGuid);
// expose panes in api via TimelineRowUi
rollupUi.addPane('background', rowBackgroundPane);
rollupUi.addPane('inset', rowInsetPane);
const rollupDataAxis = rollupRow.dataAxis;
let rollupContentPane = null;
let rollupPaneFactory = null;
const rollupContentOptions = { timelineFont: font, timelineFgColor: fgColor, draggableEdgeWidth: draggableEdgeWidth, snapToDistance: snapToDistance, isMaximized: false, mouseWheelListener: args.options.mouseWheelListener };
const refreshRollupContentPane = function () {
const newRollupPaneFactory = (rollupUi.paneFactory || rowPaneFactoryChooser(rollupRow));
if (newRollupPaneFactory !== rollupPaneFactory) {
if (rollupContentPane) {
rollupContentPane.dispose.fire();
rowInsetPane.removePane(rollupContentPane);
}
rollupPaneFactory = newRollupPaneFactory;
rollupContentPane = (rollupPaneFactory && rollupPaneFactory(drawable, timeAxis, rollupDataAxis, model, rollupRow, ui, rollupContentOptions));
if (rollupContentPane) {
rowInsetPane.addPane(rollupContentPane);
}
drawable.redraw();
}
};
rollupUi.paneFactoryChanged.on(refreshRollupContentPane);
rollupRow.attrsChanged.on(refreshRollupContentPane);
rollupRow.eventGuids.valueAdded.on(refreshRollupContentPane);
rollupRow.eventGuids.valueRemoved.on(refreshRollupContentPane);
rollupRow.timeseriesGuids.valueAdded.on(refreshRollupContentPane);
rollupRow.timeseriesGuids.valueRemoved.on(refreshRollupContentPane);
refreshRollupContentPane();
const groupButtonHeaderUnderlay = new Pane(newColumnLayout());
groupButtonHeaderUnderlay.addPane(groupButton, 0);
groupButtonHeaderUnderlay.addPane(groupHeaderStripe, 1, { ignoreHeight: true });
groupHeaderUnderlay = new Pane(newColumnLayout());
groupHeaderUnderlay.addPainter(newBackgroundPainter(bgColor));
groupHeaderUnderlay.addPane(groupButtonHeaderUnderlay, 0, { width: rowLabelPaneWidth });
groupHeaderUnderlay.addPane(rowBackgroundPane, 1, { width: null });
groupHeaderPane = groupHeaderUnderlay;
}
else {
groupHeaderUnderlay = new Pane(newColumnLayout());
groupHeaderUnderlay.addPainter(newBackgroundPainter(bgColor));
groupHeaderUnderlay.addPane(groupButton, 0);
groupHeaderUnderlay.addPane(groupHeaderStripe, 1, { ignoreHeight: true });
const groupHeaderOverlay = newTimeAxisPane(args, null, group.cursor);
const groupHeaderOverlayInsets = newInsets(0, 0, 0, rowLabelPaneWidth);
groupHeaderPane = new Pane(newOverlayLayout());
groupHeaderPane.addPane(groupHeaderUnderlay, true);
groupHeaderPane.addPane(newInsetPane(groupHeaderOverlay, groupHeaderOverlayInsets, null, false), false);
}
const groupHeaderHighlight = new Pane(newColumnLayout(), false);
const groupHighlight = new Highlight(group.highlightColor || groupHighlightColor);
const highlightInnerPane = new Pane(null);
highlightInnerPane.addPainter(groupHighlight.newPainter());
const insets = group.highlightInsets || groupHighlightInsets;
const width = group.highlightWidth || groupHighlightWidth;
const containerWidth = insets ? insets.left + insets.right + width : width;
const highlightPane = newInsetPane(highlightInnerPane, insets);
groupHeaderHighlight.addPane(highlightPane, 0, { width: containerWidth, height: null });
const groupContainerOverlayPane = new Pane(newOverlayLayout());
const groupContainerRowPane = new Pane(newRowLayout());
const groupContentPane = new Pane(newRowLayout());
groupContainerOverlayPane.addPane(groupContainerRowPane, true, { width: null, height: null });
groupContainerOverlayPane.addPane(groupHeaderHighlight, false, { width: null, height: null });
groupContainerRowPane.updateLayoutArgs(function (layoutArg) {
const shift = (isNumber(layoutArg) && layoutArg >= 2 * groupIndex);
return (shift ? layoutArg + 2 : layoutArg);
});
groupContainerRowPane.addPane(groupHeaderPane, 2 * groupIndex);
groupContainerRowPane.addPane(groupContentPane, 2 * groupIndex + 1, { hide: group.collapsed });
timelineContentPane.addPane(groupContainerOverlayPane, groupIndex);
groupHeaderPanes[groupGuid] = groupHeaderPane;
groupContentPanes[groupGuid] = groupContentPane;
groupContainerPanes[groupGuid] = groupContainerOverlayPane;
const groupAttrsChanged = function (timelineGroup) {
const groupContentLayoutOpts = groupContainerRowPane.layoutOptions(groupContentPane);
const groupHighlightLayoutOpts = groupHeaderHighlight.layoutOptions(highlightPane);
let redraw = false;
if (timelineGroup.highlighted !== (!groupHighlightLayoutOpts.hide)) {
groupHighlightLayoutOpts.hide = !timelineGroup.highlighted;
redraw = true;
}
if (timelineGroup.highlightColor !== groupHighlight.color) {
groupHighlight.color = timelineGroup.highlightColor;
redraw = true;
}
if (timelineGroup.dashPattern !== groupHighlight.dashPattern) {
groupHighlight.dashPattern = timelineGroup.dashPattern;
redraw = true;
}
if (timelineGroup.dashLength !== groupHighlight.dashLength) {
groupHighlight.dashLength = timelineGroup.dashLength;
redraw = true;
}
if (timelineGroup.collapsed !== groupContentLayoutOpts.hide) {
groupContentLayoutOpts.hide = timelineGroup.collapsed;
redraw = true;
}
if (timelineGroup.labelFont !== groupContentLayoutOpts.labelFont) {
groupLabel.font = timelineGroup.labelFont;
redraw = true;
}
if (redraw) {
drawable.redraw();
}
}.bind(this, group);
group.attrsChanged.on(groupAttrsChanged);
groupAttrsChanged();
groupButton.mouseDown.on(function (ev) {
if (isLeftMouseDown(ev.mouseEvent)) {
group.collapsed = !group.collapsed;
}
});
// Handle hidden property
//
groupContainerRowPane.layoutOptions(groupContentPane).hide = group.hidden;
groupContainerRowPane.layoutOptions(groupHeaderPane).hide = group.hidden;
setupRowContainerPane(args, groupContentPane, group.rowGuids, false, group.groupGuid);
groupContentPane.dispose.on(function () {
group.attrsChanged.off(redrawLabel);
group.attrsChanged.off(groupAttrsChanged);
});
};
root.groupGuids.forEach(addGroup);
root.groupGuids.valueAdded.on(addGroup);
const moveGroup = function (groupGuid, groupOldIndex, groupNewIndex) {
const nMin = Math.min(groupOldIndex, groupNewIndex);
const nMax = Math.max(groupOldIndex, groupNewIndex);
for (let n = nMin; n <= nMax; n++) {
const groupGuidTemp = root.groupGuids.valueAt(n);
timelineContentPane.setLayoutArg(groupContainerPanes[groupGuidTemp], n);
// timelineContentPane.setLayoutArg(groupHeaderPanes[groupGuidTemp], 2 * n);
// timelineContentPane.setLayoutArg(groupContentPanes[groupGuidTemp], 2 * n + 1);
}
drawable.redraw();
};
root.groupGuids.valueMoved.on(moveGroup);
const removeGroup = function (groupGuid, groupIndex) {
// const contentPane: Pane = groupContentPanes[groupGuid];
// const headerPane: Pane = groupHeaderPanes[groupGuid];
const groupPane = groupContainerPanes[groupGuid];
// contentPane.dispose.fire();
// headerPane.dispose.fire();
groupPane.dispose.fire();
// timelineContentPane.removePane(contentPane);
// timelineContentPane.removePane(headerPane);
timelineContentPane.removePane(groupPane);
timelineContentPane.updateLayoutArgs(function (layoutArg) {
// const shift = (isNumber(layoutArg) && layoutArg > 2 * groupIndex + 1);
// return (shift ? layoutArg - 2 : layoutArg);
const shift = (isNumber(layoutArg) && layoutArg > groupIndex);
return (shift ? layoutArg - 1 : layoutArg);
});
// delete groupHeaderPanes[groupGuid];
// delete groupContentPanes[groupGuid];
delete groupContainerPanes[groupGuid];
drawable.redraw();
};
root.groupGuids.valueRemoved.on(removeGroup);
// Handle listing for hidden property
//
const groupAttrsChangedListeners = {};
const attachGroupAttrsChangedListener = function (groupGuid, groupIndex) {
const group = model.group(groupGuid);
const groupAttrsChangedListener = function () {
if (hasval(group.hidden) && hasval(groupContentPanes[groupGuid])) {
timelineContentPane.layoutOptions(groupContentPanes[groupGuid]).hide = group.hidden;
timelineContentPane.layoutOptions(group