From b857014a380d5e1f43396248dc07bda5255afbe8 Mon Sep 17 00:00:00 2001 From: Silvan Verhoeven Date: Thu, 27 May 2021 18:08:32 +0200 Subject: [PATCH 01/18] Add custom layout --- editor.js | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/editor.js b/editor.js index 30e50d7e8..0c5eac395 100644 --- a/editor.js +++ b/editor.js @@ -118,6 +118,8 @@ export class InteractivesEditor extends QinoqMorph { } async initializePanels () { + this.ui.preview = this.addMorph(new Preview({ _editor: this })); + this.ui.interactiveGraph = this.addMorph(new InteractiveGraph({ position: pt(0, 0), extent: pt(CONSTANTS.SIDEBAR_WIDTH, CONSTANTS.SUBWINDOW_HEIGHT), @@ -131,8 +133,6 @@ export class InteractivesEditor extends QinoqMorph { } })); - this.ui.preview = this.addMorph(new Preview({ _editor: this })); - this.ui.inspector = new InteractiveMorphInspector({ position: pt(CONSTANTS.PREVIEW_WIDTH + CONSTANTS.SIDEBAR_WIDTH, 0), extent: pt(CONSTANTS.SIDEBAR_WIDTH, CONSTANTS.SUBWINDOW_HEIGHT), @@ -203,12 +203,32 @@ export class InteractivesEditor extends QinoqMorph { } initializeLayout () { - this.layout = new ProportionalLayout({ - lastExtent: this.extent - }); + connect(this, 'extent', this, 'relayout'); this.extent = pt(CONSTANTS.EDITOR_WIDTH, CONSTANTS.EDITOR_HEIGHT); } + relayout (extent) { + this.ui.inspector.position = pt(extent.x - this.ui.inspector.width, 0); + + this.ui.menuBar.position = + pt(this.ui.interactiveGraph.left, this.ui.interactiveGraph.bottom); + this.ui.menuBar.extent = pt(extent.x, this.ui.menuBar.height); + + this.ui.tabContainer.position = pt(this.ui.menuBar.left, this.ui.menuBar.bottom); + this.ui.tabContainer.extent = + pt(extent.x, extent.y - this.ui.interactiveGraph.height - + this.ui.menuBar.height); + + this.ui.preview.extent = + pt(extent.x - this.ui.interactiveGraph.width - this.ui.inspector.width, + this.ui.preview.height); + this.ui.preview.position = + pt(this.ui.interactiveGraph.right + + (this.ui.inspector.left - + this.ui.interactiveGraph.right - + this.ui.preview.width) / 2, 0); + } + async createInteractiveWithNamePrompt () { const name = await $world.prompt( ['New Interactive\n', {}, 'Enter a name for this Interactive:', From 036a5672e43b6038f3e723542a9d21fdcfd086c6 Mon Sep 17 00:00:00 2001 From: Silvan Verhoeven Date: Tue, 1 Jun 2021 09:39:52 +0200 Subject: [PATCH 02/18] Fix bug on startup --- editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor.js b/editor.js index 0c5eac395..ce2657488 100644 --- a/editor.js +++ b/editor.js @@ -98,13 +98,13 @@ export class InteractivesEditor extends QinoqMorph { async initialize () { if ($world.get('lively top bar')) this.customizeTopBar(); connect($world, 'onTopBarLoaded', this, 'customizeTopBar'); - this.initializeLayout(); this.ui.window = this.openInWindow({ title: 'Interactives Editor', name: 'window for interactives editor', acceptsDrops: false }); await this.initializePanels(); + this.initializeLayout(); connect(this.ui.window, 'close', this, 'abandon'); connect(this.ui.window, 'position', this, 'positionChanged'); connect(this.ui.window, 'minimized', this, 'onWindowMinimizedChange'); From eea9235b9700a4d42e228350dbe1005595ce3989 Mon Sep 17 00:00:00 2001 From: Silvan Verhoeven Date: Tue, 1 Jun 2021 10:16:40 +0200 Subject: [PATCH 03/18] Fix layout bug of inspector tabs --- inspector/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/inspector/index.js b/inspector/index.js index dc61785f5..6e67c77d0 100644 --- a/inspector/index.js +++ b/inspector/index.js @@ -86,13 +86,12 @@ export class InteractiveMorphInspector extends QinoqMorph { this.ui.headlinePane.layout = new HorizontalLayout({ spacing: 5, align: 'center' }); this.ui.headlinePane.addMorph(this.ui.targetPicker); this.ui.headlinePane.addMorph(this.ui.headline); - this.addMorph(this.ui.headlinePane); this.ui.tabContainer = await resource('part://tabs/tabs').read(); Object.assign(this.ui.tabContainer, { - position: pt(1, 38), - extent: pt(this.width, this.height - this.ui.headlinePane.height - CONSTANTS.TAB_HEADER_HEIGHT), + position: pt(0, 38), + extent: pt(this.width, this.height - this.ui.headlinePane.height), showNewTabButton: false, tabHeight: 25 }); @@ -107,6 +106,7 @@ export class InteractiveMorphInspector extends QinoqMorph { this.initializeAnimationsInspector(); this.initializeStyleInspector(); + this.ui.animationsInspectorTab.selected = true; this.addMorph(this.ui.tabContainer); this.ui.tabContainer.getSubmorphNamed('tab content container').acceptsDrops = false; From 59ba5c3405caecef3de709e28b997b865dee6bbe Mon Sep 17 00:00:00 2001 From: Silvan Verhoeven Date: Wed, 2 Jun 2021 17:13:14 +0200 Subject: [PATCH 04/18] WIP: Integrate resizeable panel into editor --- editor.js | 55 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/editor.js b/editor.js index ce2657488..d40289702 100644 --- a/editor.js +++ b/editor.js @@ -1,4 +1,5 @@ import { ProportionalLayout, InputLine, config, HorizontalLayout, VerticalLayout, Icon, Label } from 'lively.morphic'; +import { ResizeablePanel } from 'lively.components'; import { connect, disconnectAll, disconnect } from 'lively.bindings'; import { pt, rect } from 'lively.graphics'; import { COLOR_SCHEME } from './colors.js'; @@ -147,8 +148,16 @@ export class InteractivesEditor extends QinoqMorph { }); this.addMorph(this.ui.inspector); + this.ui.subWindow = new SubWindow({ + extent: pt(CONSTANTS.EDITOR_WIDTH, CONSTANTS.SUBWINDOW_HEIGHT), + resizers: { north: true } + }); + connect(this.ui.subWindow, 'onResize', this, 'relayout', { + converter: '() => target.extent' + }); + this.ui.menuBar = new MenuBar({ - position: pt(0, CONSTANTS.SUBWINDOW_HEIGHT), + extent: pt(CONSTANTS.EDITOR_WIDTH, CONSTANTS.MENU_BAR_HEIGHT) _editor: this, borderWidth: { bottom: CONSTANTS.BORDER_WIDTH, @@ -159,7 +168,7 @@ export class InteractivesEditor extends QinoqMorph { } }); this.ui.menuBar.disableUIElements(); - this.addMorph(this.ui.menuBar); + this.ui.subWindow.addMorph(this.ui.menuBar); connect(this, 'onDisplayedTimelineChange', this.ui.menuBar, 'onGlobalTimelineTab', { updater: `($update, displayedTimeline) => { if (displayedTimeline == source.ui.globalTimeline) $update(); @@ -179,8 +188,6 @@ export class InteractivesEditor extends QinoqMorph { this.ui.tabContainer = await resource('part://tabs/tabs').read(); Object.assign(this.ui.tabContainer, { - position: pt(0, CONSTANTS.SUBWINDOW_HEIGHT + CONSTANTS.MENU_BAR_HEIGHT), - extent: pt(CONSTANTS.EDITOR_WIDTH, CONSTANTS.TIMELINE_HEIGHT), showNewTabButton: false, tabHeight: 28, visible: false @@ -199,7 +206,8 @@ export class InteractivesEditor extends QinoqMorph { this.ui.globalTab.closeable = false; this.ui.globalTab.borderColor = COLOR_SCHEME.PRIMARY; - this.addMorph(this.ui.tabContainer); + this.ui.subWindow.addMorph(this.ui.tabContainer); + this.addMorph(this.ui.subWindow); } initializeLayout () { @@ -208,20 +216,23 @@ export class InteractivesEditor extends QinoqMorph { } relayout (extent) { - this.ui.inspector.position = pt(extent.x - this.ui.inspector.width, 0); + if (!this.ui.subWindow.isResizing) { + this.ui.subWindow.position = pt(0, extent.y - this.ui.subWindow.height); + this.ui.subWindow.extent = pt(extent.x, this.ui.subWindow.height); + } + + const topWindowHeight = this.ui.subWindow.top; - this.ui.menuBar.position = - pt(this.ui.interactiveGraph.left, this.ui.interactiveGraph.bottom); - this.ui.menuBar.extent = pt(extent.x, this.ui.menuBar.height); + this.ui.interactiveGraph.extent = pt(this.ui.interactiveGraph.width, + topWindowHeight); - this.ui.tabContainer.position = pt(this.ui.menuBar.left, this.ui.menuBar.bottom); - this.ui.tabContainer.extent = - pt(extent.x, extent.y - this.ui.interactiveGraph.height - - this.ui.menuBar.height); + this.ui.inspector.position = pt(extent.x - this.ui.inspector.width, 0); + this.ui.inspector.extent = pt(this.ui.inspector.width, + topWindowHeight); this.ui.preview.extent = pt(extent.x - this.ui.interactiveGraph.width - this.ui.inspector.width, - this.ui.preview.height); + topWindowHeight); this.ui.preview.position = pt(this.ui.interactiveGraph.right + (this.ui.inspector.left - @@ -1477,6 +1488,22 @@ class MenuBar extends QinoqMorph { } } +class SubWindow extends ResizeablePanel { + relayout () { + super.relayout(); + + const menuBar = this.get('menu bar'); + const tabs = this.get('aTabs'); + + if (!menuBar || !tabs) return; + + menuBar.position = pt(0, 0); + menuBar.extent = pt(this.width, menuBar.height); + tabs.position = pt(0, menuBar.height); + tabs.extent = pt(this.width, this.height - menuBar.height); + } +} + export class Settings extends QinoqMorph { static get properties () { return { From 8e0c99084d72e0d8391229574d5daf48d510c0d7 Mon Sep 17 00:00:00 2001 From: Silvan Verhoeven Date: Wed, 2 Jun 2021 18:55:38 +0200 Subject: [PATCH 05/18] Refresh layout on resize end --- editor.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/editor.js b/editor.js index d40289702..81067a387 100644 --- a/editor.js +++ b/editor.js @@ -155,6 +155,9 @@ export class InteractivesEditor extends QinoqMorph { connect(this.ui.subWindow, 'onResize', this, 'relayout', { converter: '() => target.extent' }); + connect(this.ui.subWindow, 'onResizeEnd', this, 'relayout', { + converter: '() => target.extent' + }); this.ui.menuBar = new MenuBar({ extent: pt(CONSTANTS.EDITOR_WIDTH, CONSTANTS.MENU_BAR_HEIGHT) From 09c90e61eee026ecd4d01c667092ab9377e3eac9 Mon Sep 17 00:00:00 2001 From: Silvan Verhoeven Date: Thu, 3 Jun 2021 10:32:46 +0200 Subject: [PATCH 06/18] Enable proportional panel resizing on window resize --- editor.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/editor.js b/editor.js index 81067a387..ef3664650 100644 --- a/editor.js +++ b/editor.js @@ -92,6 +92,9 @@ export class InteractivesEditor extends QinoqMorph { if (!this._deserializing) this.ui = {}; } }, + _latestSubWindowRatio: { + defaultValue: CONSTANTS.SUBWINDOW_HEIGHT / CONSTANTS.EDITOR_HEIGHT + }, _snappingDisabled: {} }; } @@ -215,13 +218,20 @@ export class InteractivesEditor extends QinoqMorph { initializeLayout () { connect(this, 'extent', this, 'relayout'); + /* this.layout = new ProportionalLayout({ + lastExtent: this.extent + }); */ this.extent = pt(CONSTANTS.EDITOR_WIDTH, CONSTANTS.EDITOR_HEIGHT); } relayout (extent) { if (!this.ui.subWindow.isResizing) { + this.ui.subWindow.extent = pt(extent.x, this.height * this._latestSubWindowRatio); this.ui.subWindow.position = pt(0, extent.y - this.ui.subWindow.height); - this.ui.subWindow.extent = pt(extent.x, this.ui.subWindow.height); + } else { + // remember the ratio between top and sub window + // so we can keep that ratio when resizing the window + this._latestSubWindowRatio = this.ui.subWindow.height / this.height; } const topWindowHeight = this.ui.subWindow.top; From 4908db88fee3c8d508b40f65a978403ac1c26772 Mon Sep 17 00:00:00 2001 From: Silvan Verhoeven Date: Thu, 3 Jun 2021 11:46:12 +0200 Subject: [PATCH 07/18] Remove comment --- editor.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/editor.js b/editor.js index ef3664650..8f0bfafaa 100644 --- a/editor.js +++ b/editor.js @@ -218,9 +218,6 @@ export class InteractivesEditor extends QinoqMorph { initializeLayout () { connect(this, 'extent', this, 'relayout'); - /* this.layout = new ProportionalLayout({ - lastExtent: this.extent - }); */ this.extent = pt(CONSTANTS.EDITOR_WIDTH, CONSTANTS.EDITOR_HEIGHT); } From 536e12fb73723f6bb315e918dbd3b525fdbc10fb Mon Sep 17 00:00:00 2001 From: Silvan Verhoeven Date: Thu, 3 Jun 2021 11:52:53 +0200 Subject: [PATCH 08/18] Make interactive graph responsive to resize --- tree.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tree.js b/tree.js index 1e9af4737..fc59705cb 100644 --- a/tree.js +++ b/tree.js @@ -29,6 +29,12 @@ export class InteractiveGraph extends QinoqMorph { this.build(); } }, + extent: { + set (extent) { + this.setProperty('extent', extent); + if (this.tree) this.tree.extent = extent; + } + }, searchField: { } }; } From 2ef7eba2416cfd7d0569b609fe7a6447d05f6aed Mon Sep 17 00:00:00 2001 From: Silvan Verhoeven Date: Thu, 3 Jun 2021 12:00:33 +0200 Subject: [PATCH 09/18] Make inspector responsive to resize --- inspector/index.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/inspector/index.js b/inspector/index.js index 6e67c77d0..286db6fe8 100644 --- a/inspector/index.js +++ b/inspector/index.js @@ -56,7 +56,10 @@ export class InteractiveMorphInspector extends QinoqMorph { extent: { set (extent) { this.setProperty('extent', extent); - if (!this._deserializing && this.ui && this.ui.tabContainer) this.ui.tabContainer.extent = pt(this.width, this.height - this.ui.headlinePane.height); + if (this.ui && this.ui.tabContainer && this.ui.headlinePane) { + this.ui.tabContainer.extent = + pt(this.ui.tabContainer.width, this.height - this.ui.headlinePane.height); + } } } }; @@ -132,7 +135,8 @@ export class InteractiveMorphInspector extends QinoqMorph { selectMorphThroughHalo (morph) { if (Array.isArray(morph)) morph = morph[0]; // Multi select through halo - if (this.interactive && this.interactive.sequences.includes(Sequence.getSequenceOfMorph(morph))) { + if (this.interactive && + this.interactive.sequences.includes(Sequence.getSequenceOfMorph(morph))) { this.targetMorph = morph; } } From 2d1d588646133650ab6b7fc2510347015a0fc23f Mon Sep 17 00:00:00 2001 From: Linus Hagemann Date: Wed, 9 Jun 2021 15:26:07 +0200 Subject: [PATCH 10/18] Add ResizeablePanel and tests for it Co-authored-by: SilvanVerhoeven --- editor.js | 2 +- tests/resizeable-panel-test.js | 219 +++++++++++++++++++++++++++++++ utilities/resizeable-panel.js | 233 +++++++++++++++++++++++++++++++++ 3 files changed, 453 insertions(+), 1 deletion(-) create mode 100644 tests/resizeable-panel-test.js create mode 100644 utilities/resizeable-panel.js diff --git a/editor.js b/editor.js index 8f0bfafaa..8ab3714ad 100644 --- a/editor.js +++ b/editor.js @@ -1,5 +1,4 @@ import { ProportionalLayout, InputLine, config, HorizontalLayout, VerticalLayout, Icon, Label } from 'lively.morphic'; -import { ResizeablePanel } from 'lively.components'; import { connect, disconnectAll, disconnect } from 'lively.bindings'; import { pt, rect } from 'lively.graphics'; import { COLOR_SCHEME } from './colors.js'; @@ -22,6 +21,7 @@ import { error, success } from './utilities/messages.js'; import { Canvas } from 'lively.components/canvas.js'; import { TIMELINE_CONSTANTS } from './timeline/constants.js'; import { LabeledCheckBox, DropDownSelector } from 'lively.components/widgets.js'; +import { ResizeablePanel } from './utilities/resizeable-panel.js'; const CONSTANTS = { EDITOR_WIDTH: 1000, diff --git a/tests/resizeable-panel-test.js b/tests/resizeable-panel-test.js new file mode 100644 index 000000000..e3f6dde15 --- /dev/null +++ b/tests/resizeable-panel-test.js @@ -0,0 +1,219 @@ +/* global it, describe, before, beforeEach, after, afterEach */ +import { expect } from 'mocha-es6'; +import { pt } from 'lively.graphics'; +import { ResizeablePanel } from '../utilities/resizeable-panel.js'; + +let panel; + +async function setup () { + panel = new ResizeablePanel({ + name: 'panel', + position: pt(140, 140), + extent: pt(20, 20) + }); + panel.resizers = true; + $world.addMorph(panel); + await panel.whenRendered(); +} + +function teardown () { + panel.remove(); +} + +describe('resizeable panel', () => { + beforeEach(setup); + afterEach(teardown); + + it('has no resizers by default', () => { + panel.ui.resizers.north.visible = false; + panel.ui.resizers.south.visible = false; + panel.ui.resizers.east.visible = false; + panel.ui.resizers.west.visible = false; + }); + + describe('has resizers', () => { + it('that can be enabled and disabled together', () => { + panel.resizers = true; + panel.ui.resizers.north.visible = true; + panel.ui.resizers.south.visible = true; + panel.ui.resizers.east.visible = true; + panel.ui.resizers.west.visible = true; + + panel.resizers = false; + panel.ui.resizers.north.visible = false; + panel.ui.resizers.south.visible = false; + panel.ui.resizers.east.visible = false; + panel.ui.resizers.west.visible = false; + }); + + it('that can be enabled and disabled individually', () => { + panel.resizers = { + north: true, + south: true + }; + panel.ui.resizers.north.visible = true; + panel.ui.resizers.south.visible = true; + panel.ui.resizers.east.visible = false; + panel.ui.resizers.west.visible = false; + + panel.resizers = { + north: false, + west: true + }; + panel.ui.resizers.north.visible = false; + panel.ui.resizers.south.visible = true; + panel.ui.resizers.east.visible = false; + panel.ui.resizers.west.visible = true; + + panel.resizers = { + east: false + }; + panel.ui.resizers.north.visible = false; + panel.ui.resizers.south.visible = true; + panel.ui.resizers.east.visible = false; + panel.ui.resizers.west.visible = true; + }); + }); + + describe('can be resized via resizer', () => { + beforeEach(() => { + panel.resizers = true; + }); + + it('in east direction', () => { + expect(panel.ui.resizers.east.position).to.be.equal(pt(16, 0)); + + $world.env.eventDispatcher.simulateDOMEvents({ + type: 'pointerdown', + target: panel.ui.resizers.east, + position: pt(156, 148) + }); + + $world.env.eventDispatcher.simulateDOMEvents({ + type: 'pointermove', + target: panel.ui.resizers.east, + position: pt(190, 148) + }); + + $world.env.eventDispatcher.simulateDOMEvents({ + type: 'pointermove', + target: panel.ui.resizers.east, + position: pt(210, 148) + }); + + $world.env.eventDispatcher.simulateDOMEvents({ + type: 'pointerup', + target: panel.ui.resizers.east, + position: pt(210, 148) + }); + + expect(panel.extent.x).to.be.equal(72); + expect(panel.extent.y).to.be.equal(20); + expect(panel.ui.resizers.north.extent.x).to.be.equal(72); + expect(panel.ui.resizers.south.extent.x).to.be.equal(72); + expect(panel.ui.resizers.east.position).to.be.equal(pt(68, 0)); + }); + + it('in west direction', () => { + expect(panel.ui.resizers.west.position).to.be.equal(pt(0, 0)); + + $world.env.eventDispatcher.simulateDOMEvents({ + type: 'pointerdown', + target: panel.ui.resizers.west, + position: pt(142, 148) + }); + + $world.env.eventDispatcher.simulateDOMEvents({ + type: 'pointermove', + target: panel.ui.resizers.west, + position: pt(110, 148) + }); + + $world.env.eventDispatcher.simulateDOMEvents({ + type: 'pointermove', + target: panel.ui.resizers.west, + position: pt(90, 148) + }); + + $world.env.eventDispatcher.simulateDOMEvents({ + type: 'pointerup', + target: panel.ui.resizers.west, + position: pt(90, 148) + }); + + expect(panel.extent.x).to.be.equal(72); + expect(panel.extent.y).to.be.equal(20); + expect(panel.ui.resizers.north.extent.x).to.be.equal(72); + expect(panel.ui.resizers.south.extent.x).to.be.equal(72); + expect(panel.ui.resizers.west.position).to.be.equal(pt(0, 0)); + }); + + it('in north direction', () => { + expect(panel.ui.resizers.north.position).to.be.equal(pt(0, 0)); + + $world.env.eventDispatcher.simulateDOMEvents({ + type: 'pointerdown', + target: panel.ui.resizers.north, + position: pt(148, 142) + }); + + $world.env.eventDispatcher.simulateDOMEvents({ + type: 'pointermove', + target: panel.ui.resizers.north, + position: pt(148, 130) + }); + + $world.env.eventDispatcher.simulateDOMEvents({ + type: 'pointermove', + target: panel.ui.resizers.north, + position: pt(148, 110) + }); + + $world.env.eventDispatcher.simulateDOMEvents({ + type: 'pointerup', + target: panel.ui.resizers.north, + position: pt(148, 90) + }); + + expect(panel.extent.x).to.be.equal(20); + expect(panel.extent.y).to.be.equal(52); + expect(panel.ui.resizers.west.extent.y).to.be.equal(52); + expect(panel.ui.resizers.east.extent.y).to.be.equal(52); + expect(panel.ui.resizers.north.position).to.be.equal(pt(0, 0)); + }); + + it('in south direction', () => { + expect(panel.ui.resizers.south.position).to.be.equal(pt(0, 16)); + + $world.env.eventDispatcher.simulateDOMEvents({ + type: 'pointerdown', + target: panel.ui.resizers.south, + position: pt(148, 158) + }); + + $world.env.eventDispatcher.simulateDOMEvents({ + type: 'pointermove', + target: panel.ui.resizers.south, + position: pt(148, 170) + }); + + $world.env.eventDispatcher.simulateDOMEvents({ + type: 'pointermove', + target: panel.ui.resizers.south, + position: pt(148, 180) + }); + + $world.env.eventDispatcher.simulateDOMEvents({ + type: 'pointerup', + target: panel.ui.resizers.south, + position: pt(148, 180) + }); + + expect(panel.extent.x).to.be.equal(20); + expect(panel.extent.y).to.be.equal(40); + expect(panel.ui.resizers.west.extent.y).to.be.equal(40); + expect(panel.ui.resizers.east.extent.y).to.be.equal(40); + expect(panel.ui.resizers.south.position).to.be.equal(pt(0, 36)); + }); + }); +}); diff --git a/utilities/resizeable-panel.js b/utilities/resizeable-panel.js new file mode 100644 index 000000000..9cb4efcd9 --- /dev/null +++ b/utilities/resizeable-panel.js @@ -0,0 +1,233 @@ +import { Morph } from 'lively.morphic'; +import { pt, Color } from 'lively.graphics'; +import { connect, disconnectAll } from 'lively.bindings'; + +const CONSTANTS = { + DEFAULT_RESIZER_WIDTH: 4 +}; + +/* +The resizable panel has resizers placed at all four of its borders. +These can be individually enabled programmatically and are layouted correctly when used. +E.g. the north resizer will position itself always at the top of the panels on resizements. +It can be used e.g. as a container when creating applications with resizable sub-windows. +*/ +export class ResizeablePanel extends Morph { + static get properties () { + return { + ui: { + after: ['submorphs', 'extent'], + initialize () { + this.build(); + } + }, + resizers: { + after: ['ui'], + initialize () { + this.resizers = { + north: false, + south: false, + east: false, + west: false + }; + }, + set (resizersOrBoolean) { + const resizers = typeof resizersOrBoolean === 'boolean' + ? { + north: resizersOrBoolean, + south: resizersOrBoolean, + east: resizersOrBoolean, + west: resizersOrBoolean + } + : resizersOrBoolean; + this.setProperty('resizers', resizers); + this.updateResizers(); + } + }, + extent: { + defaultValue: pt(50, 50) + } + }; + } + + defaultResizerPosition (resizer, side) { + switch (side) { + case 'north': + return pt(0, 0); + case 'south': + return pt(0, this.height - resizer.height); + case 'east': + return pt(this.width - resizer.width, 0); + case 'west': + return pt(0, 0); + } + } + + build () { + this._building = true; + + this.ui = {}; + + this.ui.resizers = { + north: this.buildResizer({ + extent: pt(0, CONSTANTS.DEFAULT_RESIZER_WIDTH), + nativeCursor: 'n-resize', + name: 'north resizer' + }), + south: this.buildResizer({ + extent: pt(0, CONSTANTS.DEFAULT_RESIZER_WIDTH), + nativeCursor: 's-resize', + name: 'south resizer' + }), + east: this.buildResizer({ + extent: pt(CONSTANTS.DEFAULT_RESIZER_WIDTH, 0), + nativeCursor: 'e-resize', + name: 'east resizer' + }), + west: this.buildResizer({ + extent: pt(CONSTANTS.DEFAULT_RESIZER_WIDTH, 0), + nativeCursor: 'w-resize', + name: 'west resizer' + }) + }; + + connect(this.ui.resizers.north, 'onDragStart', this, 'onResizeStart'); + connect(this.ui.resizers.south, 'onDragStart', this, 'onResizeStart'); + connect(this.ui.resizers.east, 'onDragStart', this, 'onResizeStart'); + connect(this.ui.resizers.west, 'onDragStart', this, 'onResizeStart'); + connect(this.ui.resizers.north, 'onDrag', this, 'onResizeNorth'); + connect(this.ui.resizers.south, 'onDrag', this, 'onResizeSouth'); + connect(this.ui.resizers.east, 'onDrag', this, 'onResizeEast'); + connect(this.ui.resizers.west, 'onDrag', this, 'onResizeWest'); + connect(this.ui.resizers.north, 'onDragEnd', this, 'onResizeEnd'); + connect(this.ui.resizers.south, 'onDragEnd', this, 'onResizeEnd'); + connect(this.ui.resizers.east, 'onDragEnd', this, 'onResizeEnd'); + connect(this.ui.resizers.west, 'onDragEnd', this, 'onResizeEnd'); + + delete this._building; + + connect(this, 'extent', this, 'relayout').update(this.extent); + } + + buildResizer (props) { + return this.addMorph(new Morph({ + ...props, + draggable: true, + fill: Color.transparent + })); + } + + updateResizers () { + Object.keys(this.resizers).forEach(side => this.enableResizer(side, this.resizers[side])); + } + + enableResizer (side, enable) { + this.ui.resizers[side].reactsToPointer = enable; + this.ui.resizers[side].visible = enable; + } + + onResizeStart (evt) { + evt.state.dragStartPanelExtent = this.extent; + evt.state.dragStartPanelPosition = this.position; + } + + get isResizing () { + return !!this._resizeInProgress; + } + + resizeSetup () { + // setup before resize is conducted + // don't connect to this method to react to resizes, use 'onResize' instead + this._resizeInProgress = true; + } + + onResize () { + // hook to connect to react to resizes in any direction + } + + onResizeNorth (evt) { + this.resizeSetup(); + + const { dragStartPanelPosition, dragStartPanelExtent, absDragDelta } = evt.state; + + const newHeight = dragStartPanelExtent.subPt(absDragDelta).y; + this.extent = pt(this.width, newHeight); + this.position = pt(this.position.x, + this.ui.resizers.north.globalPosition.y + this.ui.resizers.north.height / 2); + + const newPanelPosition = pt( + this.position.x, dragStartPanelPosition.addPt(absDragDelta).y); + this.ui.resizers.north.position = + this.defaultResizerPosition(this.ui.resizers.north, 'north'); + this.position = newPanelPosition; + + this.onResize(); + } + + onResizeSouth () { + this.resizeSetup(); + this.extent = pt(this.width, this.ui.resizers.south.center.y); + this.onResize(); + } + + onResizeEast () { + this.resizeSetup(); + this.extent = pt(this.ui.resizers.east.center.x, this.height); + this.onResize(); + } + + onResizeWest (evt) { + this.resizeSetup(); + const { dragStartPanelPosition, dragStartPanelExtent, absDragDelta } = evt.state; + + const newWidth = dragStartPanelExtent.subPt(absDragDelta).x; + this.extent = pt(newWidth, this.height); + + const newPanelPosition = pt( + dragStartPanelPosition.addPt(absDragDelta).x, this.position.y); + this.ui.resizers.west.position = + this.defaultResizerPosition(this.ui.resizers.west, 'west'); + this.position = newPanelPosition; + + this.onResize(); + } + + onResizeEnd () { + delete this._resizeInProgress; + this.relayout(); + } + + addMorphAt (submorph, index) { + const morph = super.addMorphAt(submorph, index); + const resizers = Object.values(this.ui.resizers || {}); + if (this._building || resizers.includes(morph)) return morph; + resizers.forEach(resizer => resizer.bringToFront()); + return morph; + } + + relayout () { + Object.keys(this.ui.resizers).forEach(side => { + const resizer = this.ui.resizers[side]; + switch (side) { + case 'north': + resizer.extent = pt(this.width, resizer.height); + break; + case 'south': + resizer.extent = pt(this.width, resizer.height); + break; + case 'east': + resizer.extent = pt(resizer.width, this.height); + break; + case 'west': + resizer.extent = pt(resizer.width, this.height); + break; + } + resizer.position = this.defaultResizerPosition(resizer, side); + }); + } + + abandon () { + Object.values(this.ui.resizers).forEach(resizer => disconnectAll(resizer)); + super.abandon(); + } +} From 56ef3192770a22b6898be8c8aec876c3e1cb30a8 Mon Sep 17 00:00:00 2001 From: Linus Hagemann Date: Wed, 9 Jun 2021 15:55:23 +0200 Subject: [PATCH 11/18] Fix layouting of InteractiveGraph --- editor.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/editor.js b/editor.js index 8ab3714ad..fd6bba87f 100644 --- a/editor.js +++ b/editor.js @@ -233,8 +233,12 @@ export class InteractivesEditor extends QinoqMorph { const topWindowHeight = this.ui.subWindow.top; - this.ui.interactiveGraph.extent = pt(this.ui.interactiveGraph.width, - topWindowHeight); + this.ui.interactiveGraph.extent = pt( + this.ui.interactiveGraph.width, + topWindowHeight - this.ui.interactiveGraph.submorphs[0].height - + this.ui.interactiveGraph.scrollbarOffset.x - + 2 * this.ui.interactiveGraph.borderWidth + ); this.ui.inspector.position = pt(extent.x - this.ui.inspector.width, 0); this.ui.inspector.extent = pt(this.ui.inspector.width, From 5fbce4c8ff5d93547dc16c824ec49ff4414b727e Mon Sep 17 00:00:00 2001 From: Linus Hagemann Date: Wed, 9 Jun 2021 18:02:53 +0200 Subject: [PATCH 12/18] WIP fix this for zooming --- editor.js | 1 + 1 file changed, 1 insertion(+) diff --git a/editor.js b/editor.js index fd6bba87f..d4893c458 100644 --- a/editor.js +++ b/editor.js @@ -296,6 +296,7 @@ export class InteractivesEditor extends QinoqMorph { connect(this.interactive, 'remove', this, 'reset'); connect(this.interactive, '_length', this.ui.menuBar.ui.scrollPositionInput, 'max').update(this.interactive.length); + // TODO: let this work with zoom connect(this.ui.preview, 'extent', this.interactive, 'extent'); connect(this.interactive, 'interactiveZoomed', this, 'onInteractiveZoomed'); From 03f88865158094351c8c0b9b067b012835c02332 Mon Sep 17 00:00:00 2001 From: Silvan Verhoeven Date: Mon, 14 Jun 2021 00:04:18 +0200 Subject: [PATCH 13/18] Make editor saveable --- inspector/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/inspector/index.js b/inspector/index.js index 286db6fe8..1723c3004 100644 --- a/inspector/index.js +++ b/inspector/index.js @@ -56,6 +56,7 @@ export class InteractiveMorphInspector extends QinoqMorph { extent: { set (extent) { this.setProperty('extent', extent); + // if (this._deserializing) return; if (this.ui && this.ui.tabContainer && this.ui.headlinePane) { this.ui.tabContainer.extent = pt(this.ui.tabContainer.width, this.height - this.ui.headlinePane.height); From 02b249e454d4cb9192547ce1425d3b02b50a85cd Mon Sep 17 00:00:00 2001 From: Silvan Verhoeven Date: Mon, 14 Jun 2021 00:19:15 +0200 Subject: [PATCH 14/18] Fix inspector layouting bug --- inspector/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/inspector/index.js b/inspector/index.js index 1723c3004..474f021e1 100644 --- a/inspector/index.js +++ b/inspector/index.js @@ -58,6 +58,7 @@ export class InteractiveMorphInspector extends QinoqMorph { this.setProperty('extent', extent); // if (this._deserializing) return; if (this.ui && this.ui.tabContainer && this.ui.headlinePane) { + this.ui.tabContainer.position = pt(0, this.ui.headlinePane.height); this.ui.tabContainer.extent = pt(this.ui.tabContainer.width, this.height - this.ui.headlinePane.height); } From d0215d26ae838c1d65a91d51380c54813c634942 Mon Sep 17 00:00:00 2001 From: T4rikA Date: Mon, 14 Jun 2021 13:50:05 +0200 Subject: [PATCH 15/18] Fix tree height Co-authored-by: T4rikA Co-authored-by: linusha --- editor.js | 8 ++------ tree.js | 15 +++++++++++---- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/editor.js b/editor.js index d4893c458..cb9713ac3 100644 --- a/editor.js +++ b/editor.js @@ -233,12 +233,7 @@ export class InteractivesEditor extends QinoqMorph { const topWindowHeight = this.ui.subWindow.top; - this.ui.interactiveGraph.extent = pt( - this.ui.interactiveGraph.width, - topWindowHeight - this.ui.interactiveGraph.submorphs[0].height - - this.ui.interactiveGraph.scrollbarOffset.x - - 2 * this.ui.interactiveGraph.borderWidth - ); + this.ui.interactiveGraph.extent = pt(this.ui.interactiveGraph.width, topWindowHeight); this.ui.inspector.position = pt(extent.x - this.ui.inspector.width, 0); this.ui.inspector.extent = pt(this.ui.inspector.width, @@ -247,6 +242,7 @@ export class InteractivesEditor extends QinoqMorph { this.ui.preview.extent = pt(extent.x - this.ui.interactiveGraph.width - this.ui.inspector.width, topWindowHeight); + debugger; this.ui.preview.position = pt(this.ui.interactiveGraph.right + (this.ui.inspector.left - diff --git a/tree.js b/tree.js index fc59705cb..1665c2d0f 100644 --- a/tree.js +++ b/tree.js @@ -31,8 +31,13 @@ export class InteractiveGraph extends QinoqMorph { }, extent: { set (extent) { - this.setProperty('extent', extent); - if (this.tree) this.tree.extent = extent; + let adjustedExtent = extent; + if (this.tree) { + adjustedExtent = pt(extent.x, extent.y - this.scrollbarOffset.y - + 2 * this.borderWidth); + this.tree.extent = adjustedExtent; + } + this.setProperty('extent', adjustedExtent); } }, searchField: { } @@ -46,7 +51,8 @@ export class InteractiveGraph extends QinoqMorph { build () { this.removeConnections(); this.layout = new VerticalLayout({ - resizeSubmorphs: true + resizeSubmorphs: true, + autoResize: false }); this.buildSearchField(); this.buildTree(); @@ -70,7 +76,8 @@ export class InteractiveGraph extends QinoqMorph { this.removeTree(); this.tree = new QinoqTree({ treeData: treeData, - extent: pt(this.width, Math.max(CONSTANTS.DEFAULT_HEIGHT, this.height - CONSTANTS.SEARCH_FIELD_HEIGHT)), + extent: pt(this.width, + this.height - CONSTANTS.SEARCH_FIELD_HEIGHT), borderWidth: this.borderWidth, borderColor: this.borderColor, selectionFontColor: COLOR_SCHEME.ON_PRIMARY, From 883d16c7c7e0204751d95b451f543a9bd538cd27 Mon Sep 17 00:00:00 2001 From: T4rikA Date: Mon, 14 Jun 2021 14:16:50 +0200 Subject: [PATCH 16/18] Fix rebasing issue and remove debugger Co-authored-by: frcroth Co-authored-by: Paula-Kli --- editor.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/editor.js b/editor.js index cb9713ac3..25cf9a90a 100644 --- a/editor.js +++ b/editor.js @@ -163,7 +163,7 @@ export class InteractivesEditor extends QinoqMorph { }); this.ui.menuBar = new MenuBar({ - extent: pt(CONSTANTS.EDITOR_WIDTH, CONSTANTS.MENU_BAR_HEIGHT) + extent: pt(CONSTANTS.EDITOR_WIDTH, CONSTANTS.MENU_BAR_HEIGHT), _editor: this, borderWidth: { bottom: CONSTANTS.BORDER_WIDTH, @@ -242,7 +242,6 @@ export class InteractivesEditor extends QinoqMorph { this.ui.preview.extent = pt(extent.x - this.ui.interactiveGraph.width - this.ui.inspector.width, topWindowHeight); - debugger; this.ui.preview.position = pt(this.ui.interactiveGraph.right + (this.ui.inspector.left - From 398383895bcf63fb4ad514b45d65ba09f0782a88 Mon Sep 17 00:00:00 2001 From: T4rikA Date: Mon, 14 Jun 2021 15:04:14 +0200 Subject: [PATCH 17/18] Fix saving issues with resizeable panels Co-authored-by: frcroth Co-authored-by: Paula-Kli --- editor.js | 5 +++-- interactive.js | 1 - utilities/resizeable-panel.js | 9 +++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/editor.js b/editor.js index 25cf9a90a..a60812ca4 100644 --- a/editor.js +++ b/editor.js @@ -38,7 +38,8 @@ const CONSTANTS = { MENU_BAR_WIDGET_WIDTH: 100, MENU_BAR_WIDGET_HEIGHT: 25, FONT_SIZE_TEXT: 18, - FONT_SIZE_HEADINGS: 20 + FONT_SIZE_HEADINGS: 20, + SCROLL_BAR_HEIGHT: 10 }; CONSTANTS.SIDEBAR_WIDTH = (CONSTANTS.EDITOR_WIDTH - CONSTANTS.PREVIEW_WIDTH) / 2; CONSTANTS.TIMELINE_HEIGHT = CONSTANTS.EDITOR_HEIGHT - CONSTANTS.SUBWINDOW_HEIGHT - CONSTANTS.MENU_BAR_HEIGHT; @@ -231,7 +232,7 @@ export class InteractivesEditor extends QinoqMorph { this._latestSubWindowRatio = this.ui.subWindow.height / this.height; } - const topWindowHeight = this.ui.subWindow.top; + const topWindowHeight = this.ui.subWindow.top - CONSTANTS.SCROLL_BAR_HEIGHT; this.ui.interactiveGraph.extent = pt(this.ui.interactiveGraph.width, topWindowHeight); diff --git a/interactive.js b/interactive.js index 02babbe92..6743ab934 100644 --- a/interactive.js +++ b/interactive.js @@ -56,7 +56,6 @@ export class Interactive extends DeserializationAwareMorph { defaultValue: 16 / 9, set (aspectRatio) { this.setProperty('fixedAspectRatio', aspectRatio); - // eslint-disable-next-line no-self-assign this.extent = this.applyAspectRatio(this.extent); } }, diff --git a/utilities/resizeable-panel.js b/utilities/resizeable-panel.js index 9cb4efcd9..e2f481983 100644 --- a/utilities/resizeable-panel.js +++ b/utilities/resizeable-panel.js @@ -1,6 +1,7 @@ -import { Morph } from 'lively.morphic'; import { pt, Color } from 'lively.graphics'; import { connect, disconnectAll } from 'lively.bindings'; +import { DeserializationAwareMorph } from './deserialization-morph.js'; +import { Morph } from 'lively.morphic'; const CONSTANTS = { DEFAULT_RESIZER_WIDTH: 4 @@ -12,7 +13,7 @@ These can be individually enabled programmatically and are layouted correctly wh E.g. the north resizer will position itself always at the top of the panels on resizements. It can be used e.g. as a container when creating applications with resizable sub-windows. */ -export class ResizeablePanel extends Morph { +export class ResizeablePanel extends DeserializationAwareMorph { static get properties () { return { ui: { @@ -41,7 +42,7 @@ export class ResizeablePanel extends Morph { } : resizersOrBoolean; this.setProperty('resizers', resizers); - this.updateResizers(); + if (!this._deserializing) this.updateResizers(); } }, extent: { @@ -206,7 +207,7 @@ export class ResizeablePanel extends Morph { } relayout () { - Object.keys(this.ui.resizers).forEach(side => { + Object.keys(this.ui.resizers).filter(key => key != '_rev').forEach(side => { const resizer = this.ui.resizers[side]; switch (side) { case 'north': From a435a70ee1ff9453e18b3175c3453331824419de Mon Sep 17 00:00:00 2001 From: Silvan Verhoeven Date: Mon, 14 Jun 2021 20:58:26 +0200 Subject: [PATCH 18/18] Decouple sequence extents from preview --- editor.js | 32 +++++++++++++++++++++----------- interactive.js | 7 +++++-- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/editor.js b/editor.js index a60812ca4..f649188a1 100644 --- a/editor.js +++ b/editor.js @@ -304,16 +304,7 @@ export class InteractivesEditor extends QinoqMorph { } onInteractiveZoomed () { - const previewExtent = this.ui.preview.extent; - - // only show scrollbars if they are necessary - if (this.interactive.extent.x >= previewExtent.x) { - this.ui.preview.clipMode = 'scroll'; - } - if (this.interactive.extent.y >= previewExtent.y) { - this.ui.preview.clipMode = 'scroll'; - } - if (!(this.interactive.extent.x > previewExtent.x) && !(this.interactive.extent.y > previewExtent.y)) this.ui.preview.clipMode = 'hidden'; + this.ui.preview.updateScrollbarVisibility(); } // call this to propagate changes to the scrollPosition to the actual interactive @@ -1040,7 +1031,12 @@ class Preview extends QinoqMorph { defaultValue: 'preview' }, extent: { - defaultValue: pt(CONSTANTS.PREVIEW_WIDTH, CONSTANTS.SUBWINDOW_HEIGHT) + defaultValue: pt(CONSTANTS.PREVIEW_WIDTH, CONSTANTS.SUBWINDOW_HEIGHT), + after: ['_editor', 'ui'], + set (extent) { + this.setProperty('extent', extent); + if (!this._deserializing) this.updateScrollbarVisibility(); + } }, borderColor: { defaultValue: COLOR_SCHEME.ON_BACKGROUND_DARKER_VARIANT @@ -1157,6 +1153,20 @@ class Preview extends QinoqMorph { removeAnimationPreview () { this.animationPreview = null; } + + updateScrollbarVisibility () { + if (!this.interactive) return; + + // only show scrollbars if they are necessary + if (this.interactive.width >= this.width || + this.interactive.height >= this.height) { + this.clipMode = 'scroll'; + } + if (!(this.interactive.width > this.width) && + !(this.interactive.height > this.height)) { + this.clipMode = 'hidden'; + } + } } class PositionAnimationPreview extends Canvas { diff --git a/interactive.js b/interactive.js index 6743ab934..d0fb7ab5b 100644 --- a/interactive.js +++ b/interactive.js @@ -68,6 +68,7 @@ export class Interactive extends DeserializationAwareMorph { } this.setProperty('extent', extent); if (!this._deserializing) { + this.updateSequenceExtents(); this.scaleText(previousHeight); } } @@ -99,6 +100,10 @@ export class Interactive extends DeserializationAwareMorph { }; } + updateSequenceExtents () { + this.sequences.forEach(sequence => sequence.extent = this.extent); + } + applyAspectRatio (extent, calculateAspectRatio = false) { let aspectRatio; aspectRatio = calculateAspectRatio ? this.width / this.height : this.fixedAspectRatio; @@ -277,12 +282,10 @@ export class Interactive extends DeserializationAwareMorph { sequence.interactive = this; this.updateInteractiveLength(); signal(this, 'onSequenceAddition', sequence); - connect(this, 'extent', sequence, 'extent'); } removeSequence (sequence) { disconnectAll(sequence); - disconnect(this, 'extent', sequence, 'extent'); arr.remove(this.sequences, sequence); sequence.remove(); signal(this, 'onSequenceRemoval', sequence);