UNPKG

windowmanager

Version:

A framework to manage multiple dockable, HTML windows

619 lines (509 loc) 19.7 kB
/* global fin */ import windowmanager from './global'; import readySync from '../ready'; import { EventHandler, getUniqueWindowName, SyncCallback } from '../../utils/index'; import { BoundingBox, Position, Size, Vector } from '../../geometry/index'; const defaultConfig = { defaultWidth: 600, defaultHeight: 600, frame: false, resizable: true, saveWindowState: false, autoShow: true, icon: location.href + 'favicon.ico' }; const configMap = { title: 'name', left: 'defaultLeft', top: 'defaultTop', width: 'defaultWidth', height: 'defaultHeight' }; const acceptedEventHandlers = [ 'ready', 'drag-start', 'drag-before', 'drag-stop', 'dock-before', 'move', 'move-before', 'resize-before', 'close', 'show', 'hide', 'restore', 'minimize', 'maximize', 'focus', 'blur']; let currentWin; function _setupDOM(config) { let thisWindow = this; // TODO: Rewrite to remove setTimeout for the following: function setWindows() { if (thisWindow._window.contentWindow.windowmanager) { thisWindow._window.contentWindow.windowmanager._launcher = windowmanager._launcher; thisWindow._window.contentWindow.windowmanager._windows = windowmanager._windows; thisWindow._window.contentWindow.windowmanager._internalBus = windowmanager._internalBus; } else { setTimeout(setWindows, 5); } } setWindows(); this._window.getBounds(function (bounds) { bounds.right = bounds.left + bounds.width; bounds.bottom = bounds.top + bounds.height; thisWindow._bounds.set(new BoundingBox(bounds)); }); // Setup _window event listeners: // TODO: look into moving these elsewhere, might not work if currentWin is closed, and thisWindow is not. function onBoundsChange(event) { event.right = event.left + event.width; event.bottom = event.top + event.height; thisWindow._bounds.set(new BoundingBox(event)); if (event.changeType !== 0) { thisWindow.undock(); // Undock on resize. TODO: Allow resize with docking } if (event.changeType !== 1) { thisWindow.emit('move'); // TODO: Pass what position it is at. } } this._window.addEventListener('bounds-changing', onBoundsChange); this._window.addEventListener('bounds-changed', onBoundsChange); function onClose() { // TODO: Is it possible that onClose might not be called when the window is closed? // What if this event is set up on a window that has closed already, and then this window closes? thisWindow._isClosed = true; windowmanager._windows.delete(thisWindow._id); // Undock: thisWindow.undock(); // Move children to parent: const parent = thisWindow.getParent(); for (const child of thisWindow.getChildren()) { // We use getChildren to have a copy of the list, so child.setParent doesn't modify this loop's list! // TODO: Optimize this loop, by not making a copy of children, and not executing splice in each setParent! child.setParent(parent); } thisWindow.setParent(undefined); // Remove from parent thisWindow.emit('close'); windowmanager._internalBus.emit('window-close', thisWindow); thisWindow._window = undefined; // TODO: Clean up ALL listeners } this._window.addEventListener('closed', onClose); // Setup event listeners: this._window.addEventListener('shown', () => { thisWindow.emit('show'); }); this._window.addEventListener('hidden', () => { thisWindow.emit('hide'); }); this._window.addEventListener('restored', () => { thisWindow.emit('restore'); }); this._window.addEventListener('minimized', () => { thisWindow.emit('minimize'); }); this._window.addEventListener('maximized', () => { thisWindow.emit('maximize'); }); this._window.addEventListener('focused', () => { thisWindow.emit('focus'); }); this._window.addEventListener('blurred', () => { thisWindow.emit('blur'); }); // Setup title element: this._titleEl = this._window.contentWindow.document.createElement('title'); this._titleEl.innerText = this._title; this._window.contentWindow.document.head.appendChild(this._titleEl); this._isFramed = config.frame; this._ready = true; this.emit('ready'); windowmanager._internalBus.emit('window-create', this); }; class Window extends EventHandler { constructor(config) { // Call the parent constructor: super(acceptedEventHandlers); config = config || {}; // If no arguments are passed, assume we are creating a default blank window const isArgConfig = (config.app_uuid === undefined); // Setup private variables: this._bounds = new BoundingBox(); this._ready = false; // TODO: Identify current states. this._isClosed = false; this._isHidden = false; this._isMinimized = false; this._isMaximized = false; this._dockedGroup = [this]; this._children = []; this._parent = undefined; this._title = undefined; if (isArgConfig) { for (const prop in config) { if (config.hasOwnProperty(prop) && configMap[prop] !== undefined) { config[configMap[prop]] = config[prop]; delete config[prop]; } } for (const prop in defaultConfig) { if (defaultConfig.hasOwnProperty(prop)) { config[prop] = (config[prop] != null ? config[prop] : defaultConfig[prop]); } } this._id = getUniqueWindowName(); this._title = config.name == null ? this._id : config.name; config.name = this._id; // Need name to be unique if (config.parent) { config.parent._children.push(this); this._parent = config.parent; // TODO: Emit event 'child-added' on parent delete config.parent; } windowmanager._windows.set(this._id, this); this._window = new fin.desktop.Window(config, _setupDOM.bind(this, config), function (err) { console.error(err, config); }); } else { this._id = config._id || config.name; this._title = this._id; this._window = config; windowmanager._windows.set(this._id, this); this._window.getOptions(_setupDOM.bind(this), function (err) { console.error(err); }); } // TODO: Ensure docking system } isReady() { return this._ready; } onReady(callback) { if (this.isClosed()) { throw new Error('onReady can\'t be called on a closed window'); } if (this.isReady()) { return callback.call(this); } this.once('ready', callback); } isClosed() { return this._isClosed; } getPosition() { return this._bounds.getPosition(); } getWidth() { return this._bounds.getWidth(); } getHeight() { return this._bounds.getHeight(); } getSize() { return this._bounds.getSize(); } getBounds() { return this._bounds.clone(); } getParent() { return this._parent; } setParent(parent) { // TODO: Execute appropriate checks (if not closed, and is this new parent a window) if (parent === this._parent) { return; } if (this._parent) { const index = this._parent._children.indexOf(this); if (index >= 0) { this._parent._children.splice(index, 1); } // TODO: Emit event 'child-removed' on current parent. } if (parent) { this._parent = parent; this._parent._children.push(this); // TODO: Emit event 'child-added on parent'. } } getChildren() { return this._children.slice(); } addChild(child) { child.setParent(this); } getTitle() { return this._title; } setTitle(newTitle) { if (!newTitle) { throw new Error('setTitle requires one argument of type String'); } this._titleEl.innerText = this._title = newTitle; } isHidden() { return this._isHidden; } isShown() { return !this._isHidden; } isMinimized() { return this._isMinimized; } isMaximized() { return this._isMaximized; } isRestored() { return this.isShown() && !this.isMinimized() && !this.isMaximized(); } close(callback) { if (this.isClosed()) { return callback && callback(); } this._window.close(callback); } minimize(callback) { if (!this._ready) { throw new Error('minimize can\'t be called on an unready window'); } callback = new SyncCallback(callback); for (let window of this._dockedGroup) { window._isMinimized = true; window._window.minimize(callback.ref()); } } maximize(callback) { if (!this._ready) { throw new Error('maximize can\'t be called on an unready window'); } this._isMaximized = true; this._window.maximize(callback); } show(callback) { if (!this._ready) { throw new Error('show can\'t be called on an unready window'); } callback = new SyncCallback(callback); for (let window of this._dockedGroup) { window._isHidden = false; window._window.show(callback.ref()); } } hide(callback) { if (!this._ready) { throw new Error('hide can\'t be called on an unready window'); } callback = new SyncCallback(callback); for (let window of this._dockedGroup) { window._isHidden = true; window._window.hide(callback.ref()); } } restore(callback) { if (!this._ready) { throw new Error('restore can\'t be called on an unready window'); } callback = new SyncCallback(callback); for (let window of this._dockedGroup) { window._isHidden = false; window._isMinimized = false; window._isMaximized = false; window._window.restore(callback.ref()); } } resizable(resizable, callback) { if (!this._ready) { throw new Error('restore can\'t be called on an unready window'); } this._window.updateOptions({ resizable: resizable }, callback); } bringToFront(callback) { if (!this._ready) { throw new Error('bringToFront can\'t be called on an unready window'); } let thisWindow = this; let beforeCallback = new SyncCallback(function () { thisWindow._window.bringToFront(callback); }); for (let window of this._dockedGroup) { if (window !== this) { window._window.bringToFront(beforeCallback.ref()); } } } focus(callback) { if (!this._ready) { throw new Error('focus can\'t be called on an unready window'); } let thisWindow = this; let beforeCallback = new SyncCallback(function () { thisWindow._window.focus(callback); }); for (let window of this._dockedGroup) { if (window !== this) { window._window.focus(beforeCallback.ref()); } } } resizeTo(width, height, callback) { if (!this._ready) { throw new Error('resizeTo can\'t be called on an unready window'); } if (!this.emit('resize-before')) { return; } // Allow preventing resize let size = new Position(width, height); this._window.resizeTo(size.left, size.top, 'top-left', callback); } moveTo(left, top, callback) { if (!this._ready) { throw new Error('moveTo can\'t be called on an unready window'); } if (!this.emit('move-before')) { return; } // Allow preventing move let deltaPos = (new Position(left, top)).subtract(this.getPosition()); callback = new SyncCallback(callback); for (let window of this._dockedGroup) { let pos = window.getPosition().add(deltaPos); window._bounds.moveTo(pos); window._window.moveTo(pos.left, pos.top, callback.ref()); } } moveBy(deltaLeft, deltaTop, callback) { if (!this._ready) { throw new Error('moveBy can\'t be called on an unready window'); } if (!this.emit('move-before')) { return; } // Allow preventing move let deltaPos = new Position(deltaLeft, deltaTop); callback = new SyncCallback(callback); for (let window of this._dockedGroup) { let pos = window.getPosition().add(deltaPos); window._bounds.moveTo(pos); window._window.moveTo(pos.left, pos.top, callback.ref()); } } setSize(width, height, callback) { if (!this._ready) { throw new Error('setSize can\'t be called on an unready window'); } const size = new Size(width, height); this._window.resizeTo(size.left, size.top, 'top-left', callback); } setBounds(left, top, right, bottom, callback) { if (!this._ready) { throw new Error('resizeTo can\'t be called on an unready window'); } let bounds = new BoundingBox(left, top, right, bottom); this._window.setBounds(bounds.left, bounds.top, bounds.right, bounds.bottom, callback); } dock(other) { if (!this.emit('dock-before')) { return; } // Allow preventing dock if (other == null) { return; } // Failed to find other. TODO: Return error if (this._isFramed || other._isFramed) return; // If window is framed, don't support dock system. // If other is already in the group, return: if (this._dockedGroup.indexOf(other) >= 0) { return; } // Loop through all windows in otherGroup and add them to this's group: for (let otherWin of other._dockedGroup) { this._dockedGroup.push(otherWin); // Sharing the array between window objects makes it easier to manage: otherWin._dockedGroup = this._dockedGroup; } // TODO: Check if otherGroup is touching } undock(other) { // Check to see if window is already undocked: if (this._dockedGroup.length === 1) { return; } // Undock this: this._dockedGroup.splice(this._dockedGroup.indexOf(this), 1); this._dockedGroup = [this]; // TODO: Redock those still touching, EXCEPT 'this'. } _dragStart() { if (!this.emit('drag-start')) { return; } // Allow preventing drag for (let window of this._dockedGroup) { window._dragStartPos = window.getPosition(); } } _dragBy(deltaLeft, deltaTop) { if (!this.emit('drag-before')) { return; } // Allow preventing drag // Perform Snap: const thisBounds = this.getBounds().moveTo(this._dragStartPos.left + deltaLeft, this._dragStartPos.top + deltaTop); let snapDelta = new Vector(NaN, NaN); if (!this._isFramed) { // If window is framed, don't support snap system. for (const other of windowmanager._windows.values()) { if (!other._isFramed && other._dockedGroup !== this._dockedGroup) { snapDelta.setMin(thisBounds.getSnapDelta(other.getBounds())); } } } deltaLeft += snapDelta.left || 0; deltaTop += snapDelta.top || 0; for (let other of this._dockedGroup) { let pos = other._dragStartPos; // If other doesn't have a drag position, start it: if (pos === undefined) { pos = other._dragStartPos = other.getPosition(); pos.left -= deltaLeft; pos.top -= deltaTop; } other._window.moveTo(pos.left + deltaLeft, pos.top + deltaTop); } } _dragStop() { // Dock to those it snapped to: const thisBounds = this.getBounds(); if (!this._isFramed) { // If window is framed, don't support dock system. for (const other of windowmanager._windows.values()) { if (!other._isFramed && thisBounds.isTouching(other.getBounds())) { this.dock(other); } } } for (let window of this._dockedGroup) { delete window._dragStartPos; } this.emit('drag-stop'); } static getAll() { return Array.from(windowmanager._windows.values()); } static getByID(id) { return windowmanager._windows.get(id); } static getMain(id) { return windowmanager._windows.get(windowmanager._launcher.name); } static getCurrent() { return Window.current; } } function setupCurrentWindow() { Window.current = windowmanager._windows.get(currentWin.name) || new Window(currentWin); // Setup handlers on this window: let wX = 0; let wY = 0; let dragging = false; window.addEventListener('focus', function () { Window.current.bringToFront(); }); window.addEventListener('mousedown', function onDragStart(event) { if (event.target.classList && event.target.classList.contains('window-drag')) { dragging = true; wX = event.screenX; wY = event.screenY; Window.current._dragStart(); } }); window.addEventListener('touchstart', function (event) { if (event.target.classList && event.target.classList.contains('window-drag')) { event.preventDefault(); dragging = true; wX = event.touches[0].screenX; wY = event.touches[0].screenY; Window.current._dragStart(); } }); window.addEventListener('mousemove', function (event) { if (dragging) { // Stop text selection: window.getSelection().removeAllRanges(); // Drag: Window.current._dragBy(event.screenX - wX, event.screenY - wY); } }); window.addEventListener('touchmove', function (event) { if (dragging) { event.preventDefault(); // Stop text selection: window.getSelection().removeAllRanges(); // Drag: Window.current._dragBy(event.touches[0].screenX - wX, event.touches[0].screenY - wY); } }); window.addEventListener('mouseup', function (event) { if (dragging) { dragging = false; Window.current._dragStop(); } }); window.addEventListener('touchend', function (event) { if (dragging) { event.preventDefault(); dragging = false; Window.current._dragStop(); } }); } // Handle current window in this context: // TODO: Rewrite to remove setTimeout for the following: fin.desktop.main(readySync.ref(function () { currentWin = fin.desktop.Window.getCurrent(); let currentReady = readySync.ref(setupCurrentWindow); function getCurrent() { if (windowmanager._windows) { currentReady(); } else { setTimeout(getCurrent, 5); } } getCurrent(); })); windowmanager.Window = Window; export default Window;