From 62926e6b5b722670d2543a901f3f2f791e0142b7 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 16 Mar 2017 14:42:10 -0700 Subject: [PATCH 01/37] Don't mutate list during iteration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I saw a situation where this was calling `destroy()` on `undefined`— presumably because destroying one caused the list to be mutated elsewhere and the indexes to shift. --- src/project.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/project.coffee b/src/project.coffee index a02f27dac..f47f7033b 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -30,8 +30,8 @@ class Project extends Model @consumeServices(packageManager) destroyed: -> - buffer.destroy() for buffer in @buffers - repository?.destroy() for repository in @repositories + buffer.destroy() for buffer in @buffers.slice() + repository?.destroy() for repository in @repositories.slice() @rootDirectories = [] @repositories = [] From 7f5ad9a359aebfc6bc76df718f71bd20b34ed9e6 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Fri, 10 Mar 2017 16:42:11 -0800 Subject: [PATCH 02/37] Add workspace center --- src/workspace-center.js | 279 ++++++++++++++++++++++++++++++++++++++++ src/workspace.js | 11 ++ 2 files changed, 290 insertions(+) create mode 100644 src/workspace-center.js diff --git a/src/workspace-center.js b/src/workspace-center.js new file mode 100644 index 000000000..370d37b75 --- /dev/null +++ b/src/workspace-center.js @@ -0,0 +1,279 @@ +'use strict' + +const TextEditor = require('./text-editor') + +module.exports = class WorkspaceCenter { + constructor (paneContainer) { + this.paneContainer = paneContainer + } + + /* + Section: Event Subscription + */ + + // Essential: Invoke the given callback with all current and future text + // editors in the workspace. + // + // * `callback` {Function} to be called with current and future text editors. + // * `editor` An {TextEditor} that is present in {::getTextEditors} at the time + // of subscription or that is added at some later time. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeTextEditors (callback) { + for (let textEditor of this.getTextEditors()) { callback(textEditor) } + return this.onDidAddTextEditor(({textEditor}) => callback(textEditor)) + } + + // Essential: Invoke the given callback with all current and future panes items + // in the workspace. + // + // * `callback` {Function} to be called with current and future pane items. + // * `item` An item that is present in {::getPaneItems} at the time of + // subscription or that is added at some later time. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observePaneItems (callback) { return this.paneContainer.observePaneItems(callback) } + + // Essential: Invoke the given callback when the active pane item changes. + // + // Because observers are invoked synchronously, it's important not to perform + // any expensive operations via this method. Consider + // {::onDidStopChangingActivePaneItem} to delay operations until after changes + // stop occurring. + // + // * `callback` {Function} to be called when the active pane item changes. + // * `item` The active pane item. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeActivePaneItem (callback) { + return this.paneContainer.onDidChangeActivePaneItem(callback) + } + + // Essential: Invoke the given callback when the active pane item stops + // changing. + // + // Observers are called asynchronously 100ms after the last active pane item + // change. Handling changes here rather than in the synchronous + // {::onDidChangeActivePaneItem} prevents unneeded work if the user is quickly + // changing or closing tabs and ensures critical UI feedback, like changing the + // highlighted tab, gets priority over work that can be done asynchronously. + // + // * `callback` {Function} to be called when the active pane item stopts + // changing. + // * `item` The active pane item. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidStopChangingActivePaneItem (callback) { + return this.paneContainer.onDidStopChangingActivePaneItem(callback) + } + + // Essential: Invoke the given callback with the current active pane item and + // with all future active pane items in the workspace. + // + // * `callback` {Function} to be called when the active pane item changes. + // * `item` The current active pane item. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeActivePaneItem (callback) { + return this.paneContainer.observeActivePaneItem(callback) + } + + // Extended: Invoke the given callback when a pane is added to the workspace. + // + // * `callback` {Function} to be called panes are added. + // * `event` {Object} with the following keys: + // * `pane` The added pane. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddPane (callback) { + return this.paneContainer.onDidAddPane(callback) + } + + // Extended: Invoke the given callback before a pane is destroyed in the + // workspace. + // + // * `callback` {Function} to be called before panes are destroyed. + // * `event` {Object} with the following keys: + // * `pane` The pane to be destroyed. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onWillDestroyPane (callback) { + return this.paneContainer.onWillDestroyPane(callback) + } + + // Extended: Invoke the given callback when a pane is destroyed in the + // workspace. + // + // * `callback` {Function} to be called panes are destroyed. + // * `event` {Object} with the following keys: + // * `pane` The destroyed pane. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroyPane (callback) { + return this.paneContainer.onDidDestroyPane(callback) + } + + // Extended: Invoke the given callback with all current and future panes in the + // workspace. + // + // * `callback` {Function} to be called with current and future panes. + // * `pane` A {Pane} that is present in {::getPanes} at the time of + // subscription or that is added at some later time. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observePanes (callback) { + return this.paneContainer.observePanes(callback) + } + + // Extended: Invoke the given callback when the active pane changes. + // + // * `callback` {Function} to be called when the active pane changes. + // * `pane` A {Pane} that is the current return value of {::getActivePane}. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeActivePane (callback) { + return this.paneContainer.onDidChangeActivePane(callback) + } + + // Extended: Invoke the given callback with the current active pane and when + // the active pane changes. + // + // * `callback` {Function} to be called with the current and future active# + // panes. + // * `pane` A {Pane} that is the current return value of {::getActivePane}. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeActivePane (callback) { + return this.paneContainer.observeActivePane(callback) + } + + // Extended: Invoke the given callback when a pane item is added to the + // workspace. + // + // * `callback` {Function} to be called when pane items are added. + // * `event` {Object} with the following keys: + // * `item` The added pane item. + // * `pane` {Pane} containing the added item. + // * `index` {Number} indicating the index of the added item in its pane. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddPaneItem (callback) { + return this.paneContainer.onDidAddPaneItem(callback) + } + + // Extended: Invoke the given callback when a pane item is about to be + // destroyed, before the user is prompted to save it. + // + // * `callback` {Function} to be called before pane items are destroyed. + // * `event` {Object} with the following keys: + // * `item` The item to be destroyed. + // * `pane` {Pane} containing the item to be destroyed. + // * `index` {Number} indicating the index of the item to be destroyed in + // its pane. + // + // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. + onWillDestroyPaneItem (callback) { + return this.paneContainer.onWillDestroyPaneItem(callback) + } + + // Extended: Invoke the given callback when a pane item is destroyed. + // + // * `callback` {Function} to be called when pane items are destroyed. + // * `event` {Object} with the following keys: + // * `item` The destroyed item. + // * `pane` {Pane} containing the destroyed item. + // * `index` {Number} indicating the index of the destroyed item in its + // pane. + // + // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. + onDidDestroyPaneItem (callback) { + return this.paneContainer.onDidDestroyPaneItem(callback) + } + + /* + Section: Pane Items + */ + + // Essential: Get all pane items in the workspace. + // + // Returns an {Array} of items. + getPaneItems () { + return this.paneContainer.getPaneItems() + } + + // Essential: Get the active {Pane}'s active item. + // + // Returns an pane item {Object}. + getActivePaneItem () { + return this.paneContainer.getActivePaneItem() + } + + // Essential: Get all text editors in the workspace. + // + // Returns an {Array} of {TextEditor}s. + getTextEditors () { + return this.getPaneItems().filter(item => item instanceof TextEditor) + } + + // Essential: Get the active item if it is an {TextEditor}. + // + // Returns an {TextEditor} or `undefined` if the current active item is not an + // {TextEditor}. + getActiveTextEditor () { + const activeItem = this.getActivePaneItem() + if (activeItem instanceof TextEditor) { return activeItem } + } + + // Save all pane items. + saveAll () { + this.paneContainer.saveAll() + } + + confirmClose (options) { + return this.paneContainer.confirmClose(options) + } + + /* + Section: Panes + */ + + // Extended: Get all panes in the workspace. + // + // Returns an {Array} of {Pane}s. + getPanes () { + return this.paneContainer.getPanes() + } + + // Extended: Get the active {Pane}. + // + // Returns a {Pane}. + getActivePane () { + return this.paneContainer.getActivePane() + } + + // Extended: Make the next pane active. + activateNextPane () { + return this.paneContainer.activateNextPane() + } + + // Extended: Make the previous pane active. + activatePreviousPane () { + return this.paneContainer.activatePreviousPane() + } + + paneForURI (uri) { + return this.paneContainer.paneForURI(uri) + } + + paneForItem (item) { + return this.paneContainer.paneForItem(item) + } + + // Destroy (close) the active pane. + destroyActivePane () { + const activePane = this.getActivePane() + if (activePane != null) { + activePane.destroy() + } + } +} diff --git a/src/workspace.js b/src/workspace.js index 6c5daba12..04b3f89e5 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -13,6 +13,7 @@ const PaneContainer = require('./pane-container') const Panel = require('./panel') const PanelContainer = require('./panel-container') const Task = require('./task') +const WorkspaceCenter = require('./workspace-center') // Essential: Represents the state of the user interface for the entire window. // An instance of this class is available via the `atom.workspace` global. @@ -57,6 +58,8 @@ module.exports = class Workspace extends Model { this.defaultDirectorySearcher = new DefaultDirectorySearcher() this.consumeServices(this.packageManager) + this.center = new WorkspaceCenter(this.paneContainer) + this.panelContainers = { top: new PanelContainer({location: 'top'}), left: new PanelContainer({location: 'left'}), @@ -1008,6 +1011,14 @@ module.exports = class Workspace extends Model { } } + /* + Section: Pane Locations + */ + + getCenter () { + return this.center + } + /* Section: Panels From bf39947eee2f84d6db8a19740a117d568744fedc Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 9 Mar 2017 11:26:10 -0800 Subject: [PATCH 03/37] Add Dock component --- src/dock.js | 506 +++++++++++++++++++++++++++++++++ src/panel-container-element.js | 6 + src/panel-container.js | 3 +- src/workspace-element.js | 108 ++++++- src/workspace.js | 53 +++- static/atom.less | 1 + static/docks.less | 214 ++++++++++++++ 7 files changed, 882 insertions(+), 9 deletions(-) create mode 100644 src/dock.js create mode 100644 static/docks.less diff --git a/src/dock.js b/src/dock.js new file mode 100644 index 000000000..9bcea6e57 --- /dev/null +++ b/src/dock.js @@ -0,0 +1,506 @@ +'use strict' + +const _ = require('underscore-plus') +const {CompositeDisposable} = require('event-kit') +const PaneContainer = require('./pane-container') +const TextEditor = require('./text-editor') + +const MINIMUM_SIZE = 100 +const DEFAULT_INITIAL_SIZE = 300 +const HANDLE_SIZE = 4 +const SHOULD_ANIMATE_CLASS = 'atom-dock-should-animate' +const OPEN_CLASS = 'atom-dock-open' +const RESIZE_HANDLE_RESIZABLE_CLASS = 'atom-dock-resize-handle-resizable' +const TOGGLE_BUTTON_VISIBLE_CLASS = 'atom-dock-toggle-button-visible' +const CURSOR_OVERLAY_VISIBLE_CLASS = 'atom-dock-cursor-overlay-visible' + +// Extended: A container at the edges of the editor window capable of holding items. +// You should not create a `Dock` directly, instead use {Workspace::open}. +module.exports = class Dock { + constructor (params) { + this.handleResizeHandleDragStart = this.handleResizeHandleDragStart.bind(this) + this.handleMouseMove = this.handleMouseMove.bind(this) + this.handleMouseUp = this.handleMouseUp.bind(this) + this.handleDrag = _.throttle(this.handleDrag.bind(this), 30) + this.handleDragEnd = this.handleDragEnd.bind(this) + + this.location = params.location + this.widthOrHeight = getWidthOrHeight(this.location) + this.config = params.config + this.applicationDelegate = params.applicationDelegate + this.deserializerManager = params.deserializerManager + this.notificationManager = params.notificationManager + this.viewRegistry = params.viewRegistry + + this.paneContainer = new PaneContainer({ + config: this.config, + applicationDelegate: this.applicationDelegate, + deserializerManager: this.deserializerManager, + notificationManager: this.notificationManager, + views: this.viewRegistry + }) + + this.state = { + open: false, + shouldAnimate: false + } + + this.subscriptions = new CompositeDisposable( + this.paneContainer.observePanes(pane => { + pane.onDidAddItem(this.handleDidAddPaneItem.bind(this)) + }), + this.paneContainer.observePanes(pane => { + pane.onDidRemoveItem(this.handleDidRemovePaneItem.bind(this)) + }) + ) + } + + // FIXME(matthewwithanm: This is kinda gross. We need to get a view for the pane container so we + // have to make sure that this is called after its view provider is registered. But we really + // don't have any guarantees about that. + getElement () { + this.render(this.state) + return this.element + } + + getLocation () { + return this.location + } + + destroy () { + this.subscriptions.dispose() + this.paneContainer.destroy() + this.resizeHandle.destroy() + this.toggleButton.destroy() + window.removeEventListener('mousemove', this.handleMouseMove) + window.removeEventListener('mouseup', this.handleMouseUp) + window.removeEventListener('drag', this.handleDrag) + window.removeEventListener('dragend', this.handleDragEnd) + } + + setHovered (hovered) { + if (hovered === this.state.hovered) return + this.setState({hovered}) + } + + setDraggingItem (draggingItem) { + if (draggingItem === this.state.draggingItem) return + this.setState({draggingItem}) + } + + toggle () { + this.setState({open: !this.state.open}) + } + + setState (newState) { + const prevState = this.state + const nextState = Object.assign({}, prevState, newState) + + // Update the `shouldAnimate` state. This needs to be written to the DOM before updating the + // class that changes the animated property. Normally we'd have to defer the class change a + // frame to ensure the property is animated (or not) appropriately, however we luck out in this + // case because the drag start always happens before the item is dragged into the toggle button. + if (nextState.open !== prevState.open) { + // Never animate toggling visiblity... + nextState.shouldAnimate = false + } else if (!nextState.open && nextState.draggingItem && !prevState.draggingItem) { + // ...but do animate if you start dragging while the panel is hidden. + nextState.shouldAnimate = true + } + + this.state = nextState + this.render(this.state) + } + + render (state) { + if (this.element == null) { + this.element = document.createElement('atom-dock') + this.element.classList.add(this.location) + this.innerElement = document.createElement('div') + this.innerElement.classList.add('atom-dock-inner', this.location) + this.maskElement = document.createElement('div') + this.maskElement.classList.add('atom-dock-mask') + this.wrapperElement = document.createElement('div') + this.wrapperElement.classList.add('atom-dock-content-wrapper', this.location) + this.resizeHandle = new DockResizeHandle({ + location: this.location, + onResizeStart: this.handleResizeHandleDragStart, + toggle: this.toggle.bind(this) + }) + this.toggleButton = new DockToggleButton({ + onDragEnter: this.handleToggleButtonDragEnter.bind(this), + location: this.location, + toggle: this.toggle.bind(this) + }) + this.cursorOverlayElement = document.createElement('div') + this.cursorOverlayElement.classList.add('atom-dock-cursor-overlay', this.location) + + // Add the children to the DOM tree + this.element.appendChild(this.innerElement) + this.innerElement.appendChild(this.maskElement) + this.maskElement.appendChild(this.wrapperElement) + this.wrapperElement.appendChild(this.viewRegistry.getView(this.resizeHandle)) + this.wrapperElement.appendChild(this.viewRegistry.getView(this.paneContainer)) + this.wrapperElement.appendChild(this.cursorOverlayElement) + // The toggle button must be rendered outside the mask because (1) it shouldn't be masked and + // (2) if we made the mask larger to avoid masking it, the mask would block mouse events. + this.innerElement.appendChild(this.viewRegistry.getView(this.toggleButton)) + } + + if (state.open) { + this.innerElement.classList.add(OPEN_CLASS) + } else { + this.innerElement.classList.remove(OPEN_CLASS) + } + + if (state.shouldAnimate) { + this.maskElement.classList.add(SHOULD_ANIMATE_CLASS) + } else { + this.maskElement.classList.remove(SHOULD_ANIMATE_CLASS) + } + + if (state.resizing) { + this.cursorOverlayElement.classList.add(CURSOR_OVERLAY_VISIBLE_CLASS) + } else { + this.cursorOverlayElement.classList.remove(CURSOR_OVERLAY_VISIBLE_CLASS) + } + + const shouldBeVisible = state.open || state.showDropTarget + const size = Math.max(MINIMUM_SIZE, state.size == null ? this.getInitialSize() : state.size) + + // We need to change the size of the mask... + this.maskElement.style[this.widthOrHeight] = `${shouldBeVisible ? size : HANDLE_SIZE}px` + // ...but the content needs to maintain a constant size. + this.wrapperElement.style[this.widthOrHeight] = `${size}px` + + this.resizeHandle.update({dockIsOpen: this.state.open}) + this.toggleButton.update({ + open: shouldBeVisible, + visible: state.hovered || (state.draggingItem && !shouldBeVisible) + }) + } + + handleDidAddPaneItem () { + // Show the dock if you drop an item into it. + if (this.paneContainer.getPaneItems().length === 1) { + this.setState({open: true}) + } + } + + handleDidRemovePaneItem () { + // Hide the dock if you remove the last item. + if (this.paneContainer.getPaneItems().length === 0) { + this.setState({open: false}) + } + } + + handleResizeHandleDragStart () { + window.addEventListener('mousemove', this.handleMouseMove) + window.addEventListener('mouseup', this.handleMouseUp) + this.setState({resizing: true}) + } + + handleMouseMove (event) { + if (event.buttons === 0) { // We missed the mouseup event. For some reason it happens on Windows + this.handleMouseUp(event) + return + } + + let size = 0 + switch (this.location) { + case 'left': + size = event.pageX - this.element.getBoundingClientRect().left + break + case 'bottom': + size = this.element.getBoundingClientRect().bottom - event.pageY + break + case 'right': + size = this.element.getBoundingClientRect().right - event.pageX + break + } + this.setState({size}) + } + + handleMouseUp (event) { + window.removeEventListener('mousemove', this.handleMouseMove) + window.removeEventListener('mouseup', this.handleMouseUp) + this.setState({resizing: false}) + } + + handleToggleButtonDragEnter () { + this.setState({showDropTarget: true}) + window.addEventListener('drag', this.handleDrag) + window.addEventListener('dragend', this.handleDragEnd) + } + + handleDrag (event) { + if (!this.pointWithinHoverArea({x: event.pageX, y: event.pageY}, false)) { + this.draggedOut() + } + } + + handleDragEnd () { + this.draggedOut() + } + + draggedOut () { + this.setState({showDropTarget: false}) + window.removeEventListener('drag', this.handleDrag) + window.removeEventListener('dragend', this.handleDragEnd) + } + + // Determine whether the cursor is within the dock hover area. This isn't as simple as just using + // mouseenter/leave because we want to be a little more forgiving. For example, if the cursor is + // over the footer, we want to show the bottom dock's toggle button. + pointWithinHoverArea (point, includeButtonWidth) { + const dockBounds = this.innerElement.getBoundingClientRect() + // Copy the bounds object since we can't mutate it. + const bounds = { + top: dockBounds.top, + right: dockBounds.right, + bottom: dockBounds.bottom, + left: dockBounds.left + } + + // Include all panels that are closer to the edge than the dock in our calculations. + switch (this.location) { + case 'right': + bounds.right = Number.POSITIVE_INFINITY + break + case 'bottom': + bounds.bottom = Number.POSITIVE_INFINITY + break + case 'left': + bounds.left = 0 + break + } + + // The area used when detecting "leave" events is actually larger than when detecting entrances. + if (includeButtonWidth) { + const affordance = 20 + const toggleButtonSize = 50 / 2 // This needs to match the value in the CSS. + switch (this.location) { + case 'right': + bounds.left -= toggleButtonSize + affordance + break + case 'bottom': + bounds.top -= toggleButtonSize + affordance + break + case 'left': + bounds.right += toggleButtonSize + affordance + break + } + } + return rectContainsPoint(bounds, point) + } + + getInitialSize () { + let initialSize + // The item may not have been activated yet. If that's the case, just use the first item. + const activePaneItem = this.paneContainer.getActivePaneItem() || this.paneContainer.getPaneItems()[0] + if (activePaneItem != null) { + initialSize = getPreferredInitialSize(activePaneItem, this.location) + } + return initialSize == null ? DEFAULT_INITIAL_SIZE : initialSize + } + + // PaneContainer-delegating methods + + getPanes () { + return this.paneContainer.getPanes() + } + + observePanes (fn) { + return this.paneContainer.observePanes(fn) + } + + onDidAddPane (fn) { + return this.paneContainer.onDidAddPane(fn) + } + + onWillDestroyPane (fn) { + return this.paneContainer.onWillDestroyPane(fn) + } + + onDidDestroyPane (fn) { + return this.paneContainer.onDidDestroyPane(fn) + } + + paneForURI (uri) { + return this.paneContainer.paneForURI(uri) + } + + paneForItem (item) { + return this.paneContainer.paneForItem(item) + } + + getActivePane () { + return this.paneContainer.getActivePane() + } + + getPaneItems () { + return this.paneContainer.getPaneItems() + } + + getActivePaneItem () { + return this.paneContainer.getActivePaneItem() + } + + getTextEditors () { + return this.paneContainer.getTextEditors() + } + + getActiveTextEditor () { + const activeItem = this.getActivePaneItem() + if (activeItem instanceof TextEditor) { return activeItem } + } + + observePaneItems (fn) { + return this.paneContainer.observePaneItems(fn) + } + + onDidAddPaneItem (fn) { + return this.paneContainer.onDidAddPaneItem(fn) + } + + onWillDestroyPaneItem (fn) { + return this.paneContainer.onWillDestroyPaneItem(fn) + } + + onDidDestroyPaneItem (fn) { + return this.paneContainer.onDidDestroyPaneItem(fn) + } + + saveAll () { + this.paneContainer.saveAll() + } + + confirmClose (options) { + return this.paneContainer.confirmClose(options) + } +} + +class DockResizeHandle { + constructor (props) { + this.handleMouseDown = this.handleMouseDown.bind(this) + this.handleClick = this.handleClick.bind(this) + + this.element = document.createElement('div') + this.element.classList.add('atom-dock-resize-handle', props.location) + this.element.addEventListener('mousedown', this.handleMouseDown) + this.element.addEventListener('click', this.handleClick) + const widthOrHeight = getWidthOrHeight(props.location) + this.element.style[widthOrHeight] = `${HANDLE_SIZE}px` + this.props = props + this.update(props) + } + + update (newProps) { + this.props = Object.assign({}, this.props, newProps) + + if (this.props.dockIsOpen) { + this.element.classList.add(RESIZE_HANDLE_RESIZABLE_CLASS) + } else { + this.element.classList.remove(RESIZE_HANDLE_RESIZABLE_CLASS) + } + } + + destroy () { + this.element.removeEventListener('mousedown', this.handleMouseDown) + this.element.removeEventListener('click', this.handleClick) + } + + handleClick () { + if (!this.props.dockIsOpen) { + this.props.toggle() + } + } + + handleMouseDown () { + if (this.props.dockIsOpen) { + this.props.onResizeStart() + } + } +} + +class DockToggleButton { + constructor (props) { + this.handleClick = this.handleClick.bind(this) + this.handleDragEnter = this.handleDragEnter.bind(this) + + this.element = document.createElement('div') + this.element.classList.add('atom-dock-toggle-button', props.location) + this.element.classList.add(props.location) + this.innerElement = document.createElement('div') + this.innerElement.classList.add('atom-dock-toggle-button-inner', props.location) + this.innerElement.addEventListener('click', this.handleClick) + this.innerElement.addEventListener('dragenter', this.handleDragEnter) + this.iconElement = document.createElement('span') + this.innerElement.appendChild(this.iconElement) + this.element.appendChild(this.innerElement) + + this.props = props + this.update(props) + } + + destroy () { + this.innerElement.removeEventListener('click', this.handleClick) + this.innerElement.removeEventListener('dragenter', this.handleDragEnter) + } + + update (newProps) { + this.props = Object.assign({}, this.props, newProps) + + if (this.props.visible) { + this.element.classList.add(TOGGLE_BUTTON_VISIBLE_CLASS) + } else { + this.element.classList.remove(TOGGLE_BUTTON_VISIBLE_CLASS) + } + + this.iconElement.className = 'icon ' + getIconName(this.props.location, this.props.open) + } + + handleClick () { + this.props.toggle() + } + + handleDragEnter () { + this.props.onDragEnter() + } +} + +function getWidthOrHeight (location) { + return location === 'left' || location === 'right' ? 'width' : 'height' +} + +function getPreferredInitialSize (item, location) { + switch (location) { + case 'left': + case 'right': + return typeof item.getPreferredInitialWidth === 'function' + ? item.getPreferredInitialWidth() + : null + default: + return typeof item.getPreferredInitialHeight === 'function' + ? item.getPreferredInitialHeight() + : null + } +} + +function getIconName (location, open) { + switch (location) { + case 'right': return open ? 'icon-chevron-right' : 'icon-chevron-left' + case 'bottom': return open ? 'icon-chevron-down' : 'icon-chevron-up' + case 'left': return open ? 'icon-chevron-left' : 'icon-chevron-right' + default: throw new Error(`Invalid location: ${location}`) + } +} + +function rectContainsPoint (rect, point) { + return ( + point.x >= rect.left && + point.y >= rect.top && + point.x <= rect.right && + point.y <= rect.bottom + ) +} diff --git a/src/panel-container-element.js b/src/panel-container-element.js index dbc595186..2571d9875 100644 --- a/src/panel-container-element.js +++ b/src/panel-container-element.js @@ -19,6 +19,12 @@ class PanelContainerElement extends HTMLElement { this.subscriptions.add(this.model.onDidAddPanel(this.panelAdded.bind(this))) this.subscriptions.add(this.model.onDidDestroy(this.destroyed.bind(this))) this.classList.add(this.model.getLocation()) + + // Add the dock. + if (this.model.dock != null) { + this.appendChild(this.views.getView(this.model.dock)) + } + return this } diff --git a/src/panel-container.js b/src/panel-container.js index 377b4cd97..943fda4c4 100644 --- a/src/panel-container.js +++ b/src/panel-container.js @@ -3,11 +3,12 @@ const {Emitter, CompositeDisposable} = require('event-kit') module.exports = class PanelContainer { - constructor ({location} = {}) { + constructor ({location, dock} = {}) { this.location = location this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.panels = [] + this.dock = dock } destroy () { diff --git a/src/workspace-element.js b/src/workspace-element.js index 65333e7d3..641e58992 100644 --- a/src/workspace-element.js +++ b/src/workspace-element.js @@ -5,8 +5,9 @@ const {ipcRenderer} = require('electron') const path = require('path') const fs = require('fs-plus') -const {CompositeDisposable} = require('event-kit') +const {CompositeDisposable, Disposable} = require('event-kit') const scrollbarStyle = require('scrollbar-style') +const _ = require('underscore-plus') class WorkspaceElement extends HTMLElement { attachedCallback () { @@ -64,6 +65,14 @@ class WorkspaceElement extends HTMLElement { } initialize (model, {views, workspace, project, config, styles}) { + this.handleCenterEnter = this.handleCenterEnter.bind(this) + this.handleCenterLeave = this.handleCenterLeave.bind(this) + this.handleEdgesMouseMove = _.throttle(this.handleEdgesMouseMove.bind(this), 100) + this.handleDockDragEnd = this.handleDockDragEnd.bind(this) + this.handleDragStart = this.handleDragStart.bind(this) + this.handleDragEnd = this.handleDragEnd.bind(this) + this.handleDrop = this.handleDrop.bind(this) + this.model = model this.views = views this.workspace = workspace @@ -76,7 +85,17 @@ class WorkspaceElement extends HTMLElement { if (this.config == null) { throw new Error('Must pass a config parameter when initializing WorskpaceElements') } if (this.styles == null) { throw new Error('Must pass a styles parameter when initializing WorskpaceElements') } - this.subscriptions = new CompositeDisposable() + this.subscriptions = new CompositeDisposable( + new Disposable(() => { + window.removeEventListener('mouseenter', this.handleCenterEnter) + window.removeEventListener('mouseleave', this.handleCenterLeave) + window.removeEventListener('mousemove', this.handleEdgesMouseMove) + window.removeEventListener('dragend', this.handleDockDragEnd) + window.removeEventListener('dragstart', this.handleDragStart) + window.removeEventListener('dragend', this.handleDragEnd, true) + window.removeEventListener('drop', this.handleDrop, true) + }) + ) this.initializeContent() this.observeScrollbarStyle() this.observeTextEditorFontConfig() @@ -86,6 +105,7 @@ class WorkspaceElement extends HTMLElement { this.addEventListener('focus', this.handleFocus.bind(this)) this.addEventListener('mousewheel', this.handleMousewheel.bind(this), true) + window.addEventListener('dragstart', this.handleDragStart) this.panelContainers = { top: this.views.getView(this.model.panelContainers.top), @@ -108,11 +128,86 @@ class WorkspaceElement extends HTMLElement { this.appendChild(this.panelContainers.modal) + this.paneContainer.addEventListener('mouseenter', this.handleCenterEnter) + this.paneContainer.addEventListener('mouseleave', this.handleCenterLeave) + return this } getModel () { return this.model } + handleDragStart (event) { + if (!isTab(event.target)) return + this.model.setDraggingItem(true) + window.addEventListener('dragend', this.handleDragEnd, true) + window.addEventListener('drop', this.handleDrop, true) + } + + handleDragEnd (event) { + this.dragEnded() + } + + handleDrop (event) { + this.dragEnded() + } + + dragEnded () { + this.model.setDraggingItem(false) + window.removeEventListener('dragend', this.handleDragEnd, true) + window.removeEventListener('drop', this.handleDrop, true) + } + + handleCenterEnter (event) { + // Just re-entering the center isn't enough to hide the dock toggle buttons, since they poke + // into the center and we want to give an affordance. + this.cursorInCenter = true + this.checkCleanupDockHoverEvents() + } + + handleCenterLeave (event) { + // If the cursor leaves the center, we start listening to determine whether one of the docs is + // being hovered. + this.cursorInCenter = false + this.updateHoveredDock({x: event.pageX, y: event.pageY}) + window.addEventListener('mousemove', this.handleEdgesMouseMove) + window.addEventListener('dragend', this.handleDockDragEnd) + } + + handleEdgesMouseMove (event) { + this.updateHoveredDock({x: event.pageX, y: event.pageY}) + } + + handleDockDragEnd (event) { + this.updateHoveredDock({x: event.pageX, y: event.pageY}) + } + + updateHoveredDock (mousePosition) { + // See if we've left the currently hovered dock's area. + if (this.model.hoveredDock) { + const hideToggleButton = !this.model.hoveredDock.pointWithinHoverArea(mousePosition, true) + if (hideToggleButton) { + this.model.setHoveredDock(null) + } + } + // See if we've moved over a dock. + if (this.model.hoveredDock == null) { + const hoveredDock = _.values(this.model.docks).find( + dock => dock.pointWithinHoverArea(mousePosition, false) + ) + if (hoveredDock != null) { + this.model.setHoveredDock(hoveredDock) + } + } + this.checkCleanupDockHoverEvents() + } + + checkCleanupDockHoverEvents () { + if (this.cursorInCenter && !this.model.hoveredDock) { + window.removeEventListener('mousemove', this.handleEdgesMouseMove) + window.removeEventListener('dragend', this.handleDockDragEnd) + } + } + handleMousewheel (event) { if (event.ctrlKey && this.config.get('editor.zoomFontWhenCtrlScrolling') && (event.target.closest('atom-text-editor') != null)) { if (event.wheelDeltaY > 0) { @@ -182,3 +277,12 @@ class WorkspaceElement extends HTMLElement { } module.exports = document.registerElement('atom-workspace', {prototype: WorkspaceElement.prototype}) + +function isTab (element) { + let el = element + while (el != null) { + if (el.getAttribute('is') === 'tabs-tab') { return true } + el = el.parentElement + } + return false +} diff --git a/src/workspace.js b/src/workspace.js index 04b3f89e5..98e945b18 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -7,6 +7,7 @@ const {Emitter, Disposable, CompositeDisposable} = require('event-kit') const fs = require('fs-plus') const {Directory} = require('pathwatcher') const DefaultDirectorySearcher = require('./default-directory-searcher') +const Dock = require('./dock') const Model = require('./model') const TextEditor = require('./text-editor') const PaneContainer = require('./pane-container') @@ -42,6 +43,8 @@ module.exports = class Workspace extends Model { this.assert = params.assert this.deserializerManager = params.deserializerManager this.textEditorRegistry = params.textEditorRegistry + this.hoveredDock = null + this.draggingItem = false this.emitter = new Emitter() this.openers = [] @@ -59,12 +62,17 @@ module.exports = class Workspace extends Model { this.consumeServices(this.packageManager) this.center = new WorkspaceCenter(this.paneContainer) + this.docks = { + left: this.createDock('left'), + right: this.createDock('right'), + bottom: this.createDock('bottom') + } this.panelContainers = { top: new PanelContainer({location: 'top'}), - left: new PanelContainer({location: 'left'}), - right: new PanelContainer({location: 'right'}), - bottom: new PanelContainer({location: 'bottom'}), + left: new PanelContainer({location: 'left', dock: this.docks.left}), + right: new PanelContainer({location: 'right', dock: this.docks.right}), + bottom: new PanelContainer({location: 'bottom', dock: this.docks.bottom}), header: new PanelContainer({location: 'header'}), footer: new PanelContainer({location: 'footer'}), modal: new PanelContainer({location: 'modal'}) @@ -73,6 +81,19 @@ module.exports = class Workspace extends Model { this.subscribeToEvents() } + createDock (location) { + const dock = new Dock({ + location, + config: this.config, + applicationDelegate: this.applicationDelegate, + deserializerManager: this.deserializerManager, + notificationManager: this.notificationManager, + viewRegistry: this.viewRegistry + }) + dock.onDidDestroyPaneItem(this.didDestroyPaneItem) + return dock + } + reset (packageManager) { this.packageManager = packageManager this.emitter.dispose() @@ -89,11 +110,18 @@ module.exports = class Workspace extends Model { }) this.paneContainer.onDidDestroyPaneItem(this.didDestroyPaneItem) + this.center = new WorkspaceCenter(this.paneContainer) + this.docks = { + left: this.createDock('left'), + right: this.createDock('right'), + bottom: this.createDock('bottom') + } + this.panelContainers = { top: new PanelContainer({location: 'top'}), - left: new PanelContainer({location: 'left'}), - right: new PanelContainer({location: 'right'}), - bottom: new PanelContainer({location: 'bottom'}), + left: new PanelContainer({location: 'left', dock: this.docks.left}), + right: new PanelContainer({location: 'right', dock: this.docks.right}), + bottom: new PanelContainer({location: 'bottom', dock: this.docks.bottom}), header: new PanelContainer({location: 'header'}), footer: new PanelContainer({location: 'footer'}), modal: new PanelContainer({location: 'modal'}) @@ -172,6 +200,19 @@ module.exports = class Workspace extends Model { return _.uniq(packageNames) } + setHoveredDock (hoveredDock) { + this.hoveredDock = hoveredDock + _.values(this.docks).forEach(dock => { + dock.setHovered(dock === hoveredDock) + }) + } + + setDraggingItem (draggingItem) { + _.values(this.docks).forEach(dock => { + dock.setDraggingItem(draggingItem) + }) + } + subscribeToActiveItem () { this.updateWindowTitle() this.updateDocumentEdited() diff --git a/static/atom.less b/static/atom.less index 6a8c688e2..caa1e1c6b 100644 --- a/static/atom.less +++ b/static/atom.less @@ -18,6 +18,7 @@ // Core components @import "cursors"; @import "panels"; +@import "docks"; @import "panes"; @import "syntax"; @import "text-editor-light"; diff --git a/static/docks.less b/static/docks.less new file mode 100644 index 000000000..c8761707a --- /dev/null +++ b/static/docks.less @@ -0,0 +1,214 @@ +@import 'ui-variables'; +@import 'syntax-variables'; + +@atom-dock-toggle-button-size: 50px; + +// Dock -------------- + +// The actual dock element is used as a kind of placeholder in the DOM, relative +// to which its children can be positioned. +atom-dock { + display: flex; + position: relative; +} + +.atom-dock-inner { + display: flex; + + &.bottom { width: 100%; } + &.left, &.right { height: 100%; } + + // Make sure to center the toggle buttons + &.bottom { flex-direction: column; } + align-items: center; + + // Position the docks flush with their side of the editor. + &.right { right: 0; } + &.bottom { bottom: 0; } + &.left { left: 0; } + + // Position the docks flush with their side of the editor. + &.right { right: 0; } + &.bottom { bottom: 0; } + &.left { left: 0; } + + &:not(.atom-dock-open) { + // The dock should only take up space when it's active (i.e. it shouldn't + // take up space when you're dragging something into it). + position: absolute; + z-index: 10; // An arbitrary number. Seems high enough. ¯\_(ツ)_/¯ + } +} + +.atom-dock-mask { + position: relative; + background-color: @tool-panel-background-color; + overflow: hidden; // Mask the content. + + // This shouldn't technically be necessary. Apparently, there's a bug in + // Chrome whereby the 100% width (in the bottom dock) and height (in left and + // right docks) won't actually take effect when the docks are given more + // space because another dock is hidden. Unsetting and resetting the width + // will correct the issue, as will changing its "display." However, only this + // seems to fix it without an actual runtime change occurring. + flex: 1; + + // One of these will be overridden by the component with an explicit size. + // Which depends on the position of the dock. + width: 100%; + height: 100%; + + transition: none; + &.atom-dock-should-animate { + transition: width 0.2s ease-out, height 0.2s ease-out; + } +} + +.atom-dock-content-wrapper { + position: absolute; + display: flex; + flex: 1; + align-items: stretch; + width: 100%; + height: 100%; + cursor: default; + -webkit-user-select: none; + white-space: nowrap; + + // The contents of the dock should be "stuck" to the moving edge of the mask, + // so it looks like they're sliding in (instead of being unmasked in place). + &.right { left: 0; } + &.bottom { top: 0; } + &.left { right: 0; } + + // Use flex-direction to put the resize handle in the correct place. + &.left { flex-direction: row-reverse; } + &.bottom { flex-direction: column; } + &.right { flex-direction: row; } +} + +// Toggle button -------------- + +.atom-dock-toggle-button { + position: absolute; + overflow: hidden; // Mask half of the circle. + + // Must be > .scrollbar-content and inactive atom-dock + z-index: 11; + + // Position the toggle button target at the edge of the dock. It's important + // that this is absolutely positioned so that it doesn't expand the area of + // its container (which would block mouse events). + &.right { right: 100%; } + &.bottom { bottom: 100%; } + &.left { left: 100%; } + + width: @atom-dock-toggle-button-size; + height: @atom-dock-toggle-button-size; + &.bottom { height: @atom-dock-toggle-button-size / 2; } + &.left, &.right { width: @atom-dock-toggle-button-size / 2; } + + .atom-dock-toggle-button-inner { + width: @atom-dock-toggle-button-size; + height: @atom-dock-toggle-button-size; + border-radius: @atom-dock-toggle-button-size / 2; + + position: absolute; + display: flex; + text-align: center; + justify-content: center; + align-items: center; + cursor: pointer; + + &.right { left: 0; } + &.bottom { top: 0; } + &.left { right: 0; } + } + + // Hide the button. + &:not(.atom-dock-toggle-button-visible) { + .atom-dock-toggle-button-inner { + &.right { transform: translateX(50%); } + &.bottom { transform: translateY(50%); } + &.left { transform: translateX(-50%); } + } + } + + // Center the icon. + @offset: 8px; + .atom-dock-toggle-button-inner { + &.right .icon { transform: translateX(-@offset); } + &.bottom .icon { transform: translateY(-@offset); } + &.left .icon { transform: translateX(@offset); } + } + + // Animate the icon. + .icon { + transition: opacity 0.1s ease-in 0.1s; // intro + opacity: 1; + + &::before { + // Shrink the icon element to the size of the character. + width: auto; + margin: 0; + } + } + &:not(.atom-dock-toggle-button-visible) .icon { + opacity: 0; + transition: opacity 0.2s ease-out 0s; // outro + } + + .atom-dock-toggle-button-inner { + background-color: @tool-panel-background-color; + border: 1px solid @pane-item-border-color; + transition: transform 0.2s ease-out 0s; // intro + } + + &:not(.atom-dock-toggle-button-visible) { + // Don't contribute to mouseenter/drag events when not visible. + pointer-events: none; + + .atom-dock-toggle-button-inner { + transition: transform 0.2s ease-out 0.1s; // outro + } + } +} + +// Resize handle -------------- + +.atom-dock-resize-handle { + width: auto; + height: auto; + flex: 0 0 auto; + cursor: pointer; + + // Use the resize cursor when the handle's resizable + &.atom-dock-resize-handle-resizable { + &.left, &.right { cursor: col-resize; } + &.bottom { cursor: row-resize; } + } +} + +// Cursor overlay -------------- + +.atom-dock-cursor-overlay { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 4; + + &.left, + &.right { + cursor: col-resize; + } + + &.bottom { + cursor: row-resize; + } + + &:not(.atom-dock-cursor-overlay-visible) { + display: none; + } +} From 3ff830102f4ea620431950ce69ba183f6b794e50 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 9 Mar 2017 11:26:48 -0800 Subject: [PATCH 04/37] Serialize docks --- src/atom-environment.coffee | 2 ++ src/dock.js | 18 ++++++++++++++++++ src/workspace.js | 15 +++++++++++++-- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 7594f5de9..9db5330d3 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -38,6 +38,7 @@ Panel = require './panel' PaneContainer = require './pane-container' PaneAxis = require './pane-axis' Pane = require './pane' +Dock = require './dock' Project = require './project' TextEditor = require './text-editor' TextBuffer = require 'text-buffer' @@ -254,6 +255,7 @@ class AtomEnvironment extends Model @deserializers.add(PaneContainer) @deserializers.add(PaneAxis) @deserializers.add(Pane) + @deserializers.add(Dock) @deserializers.add(Project) @deserializers.add(TextEditor) @deserializers.add(TextBuffer) diff --git a/src/dock.js b/src/dock.js index 9bcea6e57..e72b1415f 100644 --- a/src/dock.js +++ b/src/dock.js @@ -304,6 +304,24 @@ module.exports = class Dock { return initialSize == null ? DEFAULT_INITIAL_SIZE : initialSize } + serialize () { + return { + deserializer: 'Dock', + size: this.state.size, + paneContainer: this.paneContainer.serialize(), + open: this.state.open + } + } + + deserialize (serialized, deserializerManager) { + this.paneContainer.deserialize(serialized.paneContainer, deserializerManager) + this.setState({ + size: serialized.size, + // If no items could be deserialized, we don't want to show the dock (even if it was open last time) + open: serialized.open && (this.paneContainer.getPaneItems().length > 0) + }) + } + // PaneContainer-delegating methods getPanes () { diff --git a/src/workspace.js b/src/workspace.js index 98e945b18..33fec9d67 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -154,7 +154,12 @@ module.exports = class Workspace extends Model { deserializer: 'Workspace', paneContainer: this.paneContainer.serialize(), packagesWithActiveGrammars: this.getPackageNamesWithActiveGrammars(), - destroyedItemURIs: this.destroyedItemURIs.slice() + destroyedItemURIs: this.destroyedItemURIs.slice(), + docks: { + left: this.docks.left.serialize(), + right: this.docks.right.serialize(), + bottom: this.docks.bottom.serialize() + } } } @@ -170,7 +175,13 @@ module.exports = class Workspace extends Model { if (state.destroyedItemURIs != null) { this.destroyedItemURIs = state.destroyedItemURIs } - return this.paneContainer.deserialize(state.paneContainer, deserializerManager) + this.paneContainer.deserialize(state.paneContainer, deserializerManager) + for (let location in this.docks) { + const serialized = state.docks && state.docks[location] + if (serialized) { + this.docks[location].deserialize(serialized, deserializerManager) + } + } } getPackageNamesWithActiveGrammars () { From 939ebb3ddfcd6c35ebf362312e23478e75789eff Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 9 Mar 2017 17:12:16 -0800 Subject: [PATCH 05/37] Add findRightmostSibling and findBottommostSibling methods --- src/pane.coffee | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/pane.coffee b/src/pane.coffee index c55c9f043..467775f45 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -841,17 +841,21 @@ class Pane extends Model else this - # If the parent is a horizontal axis, returns its last child if it is a pane; - # otherwise returns a new pane created by splitting this pane rightward. - findOrCreateRightmostSibling: -> + findRightmostSibling: -> if @parent.orientation is 'horizontal' rightmostSibling = last(@parent.children) if rightmostSibling instanceof PaneAxis - @splitRight() + this else rightmostSibling else - @splitRight() + this + + # If the parent is a horizontal axis, returns its last child if it is a pane; + # otherwise returns a new pane created by splitting this pane rightward. + findOrCreateRightmostSibling: -> + rightmostSibling = @findRightmostSibling() + if rightmostSibling is this then @splitRight() else rightmostSibling # If the parent is a vertical axis, returns its first child if it is a pane; # otherwise returns this pane. @@ -865,17 +869,21 @@ class Pane extends Model else this - # If the parent is a vertical axis, returns its last child if it is a pane; - # otherwise returns a new pane created by splitting this pane bottomward. - findOrCreateBottommostSibling: -> + findBottommostSibling: -> if @parent.orientation is 'vertical' bottommostSibling = last(@parent.children) if bottommostSibling instanceof PaneAxis - @splitDown() + this else bottommostSibling else - @splitDown() + this + + # If the parent is a vertical axis, returns its last child if it is a pane; + # otherwise returns a new pane created by splitting this pane bottomward. + findOrCreateBottommostSibling: -> + bottommostSibling = @findBottommostSibling() + if bottommostSibling is this then @splitDown() else bottommostSibling close: -> @destroy() if @confirmClose() From a6424a795eb3a7aa97971c2ef18ed096f0165989 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 9 Mar 2017 17:13:39 -0800 Subject: [PATCH 06/37] Separate searching panes from creation --- src/workspace.js | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/workspace.js b/src/workspace.js index 33fec9d67..ff8b0d593 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -587,13 +587,13 @@ module.exports = class Workspace extends Model { pane = this.getActivePane().findLeftmostSibling() break case 'right': - pane = this.getActivePane().findOrCreateRightmostSibling() + pane = this.getActivePane().findRightmostSibling() break case 'up': pane = this.getActivePane().findTopmostSibling() break case 'down': - pane = this.getActivePane().findOrCreateBottommostSibling() + pane = this.getActivePane().findBottommostSibling() break default: pane = this.getActivePane() @@ -602,11 +602,12 @@ module.exports = class Workspace extends Model { } let item - if (uri != null) { + if (uri != null && pane != null) { item = pane.itemForURI(uri) } if (item == null) { item = this.createItemForURI(uri, options) + pane = null } return Promise.resolve(item) @@ -712,10 +713,29 @@ module.exports = class Workspace extends Model { } openItem (item, options = {}) { - const {pane} = options + let {pane} = options + const {split} = options if (item == null) return undefined - if (pane.isDestroyed()) return item + if (pane != null && pane.isDestroyed()) return item + + if (pane == null) { + pane = this.getActivePane() + switch (split) { + case 'left': + pane = pane.findLeftmostSibling() + break + case 'right': + pane = pane.findOrCreateRightmostSibling() + break + case 'up': + pane = pane.findTopmostSibling() + break + case 'down': + pane = pane.findOrCreateBottommostSibling() + break + } + } if (!options.pending && (pane.getPendingItem() === item)) { pane.clearPendingItem() From 238ce1d8cd45a577ad07772ecaf965b5241bc84f Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Tue, 14 Mar 2017 11:20:42 -0700 Subject: [PATCH 07/37] Make workspace inspection methods location-aware --- src/workspace.js | 92 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 77 insertions(+), 15 deletions(-) diff --git a/src/workspace.js b/src/workspace.js index ff8b0d593..4f2e1c9be 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -363,7 +363,11 @@ module.exports = class Workspace extends Model { // subscription or that is added at some later time. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observePaneItems (callback) { return this.paneContainer.observePaneItems(callback) } + observePaneItems (callback) { + return new CompositeDisposable( + ...this.getPaneLocations().map(location => location.observePaneItems(callback)) + ) + } // Essential: Invoke the given callback when the active pane item changes. // @@ -430,7 +434,11 @@ module.exports = class Workspace extends Model { // * `pane` The added pane. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddPane (callback) { return this.paneContainer.onDidAddPane(callback) } + onDidAddPane (callback) { + return new CompositeDisposable( + ...this.getPaneLocations().map(location => location.onDidAddPane(callback)) + ) + } // Extended: Invoke the given callback before a pane is destroyed in the // workspace. @@ -440,7 +448,11 @@ module.exports = class Workspace extends Model { // * `pane` The pane to be destroyed. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onWillDestroyPane (callback) { return this.paneContainer.onWillDestroyPane(callback) } + onWillDestroyPane (callback) { + return new CompositeDisposable( + ...this.getPaneLocations().map(location => location.onWillDestroyPane(callback)) + ) + } // Extended: Invoke the given callback when a pane is destroyed in the // workspace. @@ -450,7 +462,11 @@ module.exports = class Workspace extends Model { // * `pane` The destroyed pane. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDestroyPane (callback) { return this.paneContainer.onDidDestroyPane(callback) } + onDidDestroyPane (callback) { + return new CompositeDisposable( + ...this.getPaneLocations().map(location => location.onDidDestroyPane(callback)) + ) + } // Extended: Invoke the given callback with all current and future panes in the // workspace. @@ -460,7 +476,11 @@ module.exports = class Workspace extends Model { // subscription or that is added at some later time. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observePanes (callback) { return this.paneContainer.observePanes(callback) } + observePanes (callback) { + return new CompositeDisposable( + ...this.getPaneLocations().map(location => location.observePanes(callback)) + ) + } // Extended: Invoke the given callback when the active pane changes. // @@ -490,7 +510,11 @@ module.exports = class Workspace extends Model { // * `index` {Number} indicating the index of the added item in its pane. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddPaneItem (callback) { return this.paneContainer.onDidAddPaneItem(callback) } + onDidAddPaneItem (callback) { + return new CompositeDisposable( + ...this.getPaneLocations().map(location => location.onDidAddPaneItem(callback)) + ) + } // Extended: Invoke the given callback when a pane item is about to be // destroyed, before the user is prompted to save it. @@ -503,7 +527,11 @@ module.exports = class Workspace extends Model { // its pane. // // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. - onWillDestroyPaneItem (callback) { return this.paneContainer.onWillDestroyPaneItem(callback) } + onWillDestroyPaneItem (callback) { + return new CompositeDisposable( + ...this.getPaneLocations().map(location => location.onWillDestroyPaneItem(callback)) + ) + } // Extended: Invoke the given callback when a pane item is destroyed. // @@ -515,7 +543,11 @@ module.exports = class Workspace extends Model { // pane. // // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. - onDidDestroyPaneItem (callback) { return this.paneContainer.onDidDestroyPaneItem(callback) } + onDidDestroyPaneItem (callback) { + return new CompositeDisposable( + ...this.getPaneLocations().map(location => location.onDidDestroyPaneItem(callback)) + ) + } // Extended: Invoke the given callback when a text editor is added to the // workspace. @@ -891,7 +923,7 @@ module.exports = class Workspace extends Model { // // Returns an {Array} of items. getPaneItems () { - return this.paneContainer.getPaneItems() + return _.flatten(this.getPaneLocations().map(location => location.getPaneItems())) } // Essential: Get the active {Pane}'s active item. @@ -919,11 +951,15 @@ module.exports = class Workspace extends Model { // Save all pane items. saveAll () { - return this.paneContainer.saveAll() + this.getPaneLocations().forEach(location => { + location.saveAll() + }) } confirmClose (options) { - return this.paneContainer.confirmClose(options) + return this.getPaneLocations() + .map(location => location.confirmClose(options)) + .every(saved => saved) } // Save the active pane item. @@ -961,7 +997,7 @@ module.exports = class Workspace extends Model { // // Returns an {Array} of {Pane}s. getPanes () { - return this.paneContainer.getPanes() + return _.flatten(this.getPaneLocations().map(location => location.getPanes())) } // Extended: Get the active {Pane}. @@ -987,7 +1023,12 @@ module.exports = class Workspace extends Model { // // Returns a {Pane} or `undefined` if no pane exists for the given URI. paneForURI (uri) { - return this.paneContainer.paneForURI(uri) + for (let location of this.getPaneLocations()) { + const pane = location.paneForURI(uri) + if (pane != null) { + return pane + } + } } // Extended: Get the {Pane} containing the given item. @@ -996,7 +1037,12 @@ module.exports = class Workspace extends Model { // // Returns a {Pane} or `undefined` if no pane exists for the given item. paneForItem (item) { - return this.paneContainer.paneForItem(item) + for (let location of this.getPaneLocations()) { + const pane = location.paneForItem(item) + if (pane != null) { + return pane + } + } } // Destroy (close) the active pane. @@ -1012,7 +1058,7 @@ module.exports = class Workspace extends Model { closeActivePaneItemOrEmptyPaneOrWindow () { if (this.getActivePaneItem() != null) { this.destroyActivePaneItem() - } else if (this.getPanes().length > 1) { + } else if (this.getCenter().getPanes().length > 1) { this.destroyActivePane() } else if (this.config.get('core.closeEmptyWindows')) { atom.close() @@ -1091,6 +1137,22 @@ module.exports = class Workspace extends Model { return this.center } + getLeftDock () { + return this.docks.left + } + + getRightDock () { + return this.docks.right + } + + getBottomDock () { + return this.docks.bottom + } + + getPaneLocations () { + return [this.getCenter(), ..._.values(this.docks)] + } + /* Section: Panels From 64e290b57fa17e7472437478a540c678df581c66 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 9 Mar 2017 23:01:50 -0800 Subject: [PATCH 08/37] Make open() location aware --- src/workspace.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/workspace.js b/src/workspace.js index 4f2e1c9be..064a54210 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -745,14 +745,32 @@ module.exports = class Workspace extends Model { } openItem (item, options = {}) { - let {pane} = options - const {split} = options + let {pane, split} = options if (item == null) return undefined if (pane != null && pane.isDestroyed()) return item if (pane == null) { - pane = this.getActivePane() + // If this is a new item, we want to determine where to put it in the following way: + // - If you provided a split, you want to put it in that split of the center location + // (legacy behavior) + // - If the item specifies a default location, use that. + let locationInfo, location + if (split == null) { + if (locationInfo == null && typeof item.getDefaultLocation === 'function') { + locationInfo = item.getDefaultLocation() + } + if (locationInfo != null) { + if (typeof locationInfo === 'string') { + location = locationInfo + } else { + location = locationInfo.location + split = locationInfo.split + } + } + } + + pane = this.docks[location] == null ? this.getActivePane() : this.docks[location].getActivePane() switch (split) { case 'left': pane = pane.findLeftmostSibling() From 5c7bd668967291958b65bf9186d7b5f5c009081c Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 9 Mar 2017 18:28:53 -0800 Subject: [PATCH 09/37] Remember previous item locations --- src/workspace.js | 151 ++++++++++++++++++++++++++--------------------- 1 file changed, 84 insertions(+), 67 deletions(-) diff --git a/src/workspace.js b/src/workspace.js index 064a54210..dbbba1a97 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -9,6 +9,7 @@ const {Directory} = require('pathwatcher') const DefaultDirectorySearcher = require('./default-directory-searcher') const Dock = require('./dock') const Model = require('./model') +const StateStore = require('./state-store') const TextEditor = require('./text-editor') const PaneContainer = require('./pane-container') const Panel = require('./panel') @@ -45,6 +46,7 @@ module.exports = class Workspace extends Model { this.textEditorRegistry = params.textEditorRegistry this.hoveredDock = null this.draggingItem = false + this.previousLocations = new StateStore('AtomPreviousItemLocations', 1) this.emitter = new Emitter() this.openers = [] @@ -137,6 +139,7 @@ module.exports = class Workspace extends Model { this.subscribeToActiveItem() this.subscribeToFontSize() this.subscribeToAddedItems() + this.subscribeToMovedItems() } consumeServices ({serviceHub}) { @@ -282,6 +285,27 @@ module.exports = class Workspace extends Model { }) } + subscribeToMovedItems () { + if (this.movedItemSubscription != null) { + this.movedItemSubscription.dispose() + } + const paneLocations = Object.assign({center: this}, this.docks) + this.movedItemSubscription = new CompositeDisposable( + ..._.map(paneLocations, (host, location) => ( + host.observePanes(pane => { + pane.onDidAddItem(({item}) => { + if (typeof item.getURI === 'function') { + const uri = item.getURI() + if (uri != null) { + this.previousLocations.save(item.getURI(), location) + } + } + }) + }) + )) + ) + } + // Updates the application's title and proxy icon based on whichever file is // open. updateWindowTitle () { @@ -747,78 +771,68 @@ module.exports = class Workspace extends Model { openItem (item, options = {}) { let {pane, split} = options - if (item == null) return undefined - if (pane != null && pane.isDestroyed()) return item + if (item == null) return Promise.resolve() + if (pane != null && pane.isDestroyed()) return Promise.resolve(item) - if (pane == null) { - // If this is a new item, we want to determine where to put it in the following way: - // - If you provided a split, you want to put it in that split of the center location - // (legacy behavior) - // - If the item specifies a default location, use that. - let locationInfo, location - if (split == null) { - if (locationInfo == null && typeof item.getDefaultLocation === 'function') { - locationInfo = item.getDefaultLocation() + const uri = options.uri == null && typeof item.getURI === 'function' ? item.getURI() : options.uri + + let location + // If a split was provided, make sure it goes in the center location (legacy behavior) + if (pane == null && split == null) { + if (uri != null) { + location = this.previousLocations.load(uri) + } + if (location == null && typeof item.getDefaultLocation === 'function') { + location = item.getDefaultLocation() + } + } + + return Promise.resolve(location) + .then(location => { + if (pane != null) return pane + + pane = this.docks[location] == null ? this.getActivePane() : this.docks[location].getActivePane() + switch (split) { + case 'left': return pane.findLeftmostSibling() + case 'right': return pane.findOrCreateRightmostSibling() + case 'up': return pane.findTopmostSibling() + case 'down': return pane.findOrCreateBottommostSibling() + default: return pane } - if (locationInfo != null) { - if (typeof locationInfo === 'string') { - location = locationInfo - } else { - location = locationInfo.location - split = locationInfo.split + }) + .then(pane => { + if (!options.pending && (pane.getPendingItem() === item)) { + pane.clearPendingItem() + } + + const activatePane = options.activatePane != null ? options.activatePane : true + const activateItem = options.activateItem != null ? options.activateItem : true + this.itemOpened(item) + if (activateItem) { + pane.activateItem(item, {pending: options.pending}) + } + if (activatePane) { + pane.activate() + } + + let initialColumn = 0 + let initialLine = 0 + if (!Number.isNaN(options.initialLine)) { + initialLine = options.initialLine + } + if (!Number.isNaN(options.initialColumn)) { + initialColumn = options.initialColumn + } + if ((initialLine >= 0) || (initialColumn >= 0)) { + if (typeof item.setCursorBufferPosition === 'function') { + item.setCursorBufferPosition([initialLine, initialColumn]) } } - } - pane = this.docks[location] == null ? this.getActivePane() : this.docks[location].getActivePane() - switch (split) { - case 'left': - pane = pane.findLeftmostSibling() - break - case 'right': - pane = pane.findOrCreateRightmostSibling() - break - case 'up': - pane = pane.findTopmostSibling() - break - case 'down': - pane = pane.findOrCreateBottommostSibling() - break - } - } - - if (!options.pending && (pane.getPendingItem() === item)) { - pane.clearPendingItem() - } - - const activatePane = options.activatePane != null ? options.activatePane : true - const activateItem = options.activateItem != null ? options.activateItem : true - this.itemOpened(item) - if (activateItem) { - pane.activateItem(item, {pending: options.pending}) - } - if (activatePane) { - pane.activate() - } - - let initialColumn = 0 - let initialLine = 0 - if (!Number.isNaN(options.initialLine)) { - initialLine = options.initialLine - } - if (!Number.isNaN(options.initialColumn)) { - initialColumn = options.initialColumn - } - if ((initialLine >= 0) || (initialColumn >= 0)) { - if (typeof item.setCursorBufferPosition === 'function') { - item.setCursorBufferPosition([initialLine, initialColumn]) - } - } - - const index = pane.getActiveItemIndex() - const uri = options.uri == null && typeof item.getURI === 'function' ? item.getURI() : options.uri - this.emitter.emit('did-open', {uri, pane, item, index}) - return item + const index = pane.getActiveItemIndex() + this.emitter.emit('did-open', {uri, pane, item, index}) + return item + }) } openTextFile (uri, options) { @@ -1145,6 +1159,9 @@ module.exports = class Workspace extends Model { if (this.activeItemSubscriptions != null) { this.activeItemSubscriptions.dispose() } + if (this.movedItemSubscription != null) { + this.movedItemSubscription.dispose() + } } /* From 5b4f4022784397d1b7c669f09e478b824f5c19b2 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 9 Mar 2017 17:12:26 -0800 Subject: [PATCH 10/37] Add toggle commands --- src/register-default-commands.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee index 8196d9237..22f9acb36 100644 --- a/src/register-default-commands.coffee +++ b/src/register-default-commands.coffee @@ -55,6 +55,9 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage 'application:open-license': -> @getModel().openLicense() 'window:run-package-specs': -> @runPackageSpecs() 'window:run-benchmarks': -> @runBenchmarks() + 'window:toggle-left-dock': -> @getModel().getLeftDock().toggle() + 'window:toggle-right-dock': -> @getModel().getRightDock().toggle() + 'window:toggle-bottom-dock': -> @getModel().getBottomDock().toggle() 'window:focus-next-pane': -> @getModel().activateNextPane() 'window:focus-previous-pane': -> @getModel().activatePreviousPane() 'window:focus-pane-above': -> @focusPaneViewAbove() From d854a88dbbe3613ea2d31b83cfffe4a47e6f86f9 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Tue, 14 Mar 2017 16:31:06 -0700 Subject: [PATCH 11/37] Add `workspace.toggle()` method --- src/dock.js | 12 +++++++++ src/workspace-center.js | 2 ++ src/workspace.js | 60 ++++++++++++++++++++++++++++++++++------- 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/src/dock.js b/src/dock.js index e72b1415f..c4d95eecb 100644 --- a/src/dock.js +++ b/src/dock.js @@ -88,10 +88,22 @@ module.exports = class Dock { this.setState({draggingItem}) } + activate () { + this.setState({open: true}) + } + + hide () { + this.setState({open: false}) + } + toggle () { this.setState({open: !this.state.open}) } + isOpen () { + return this.state.open + } + setState (newState) { const prevState = this.state const nextState = Object.assign({}, prevState, newState) diff --git a/src/workspace-center.js b/src/workspace-center.js index 370d37b75..c2c875b74 100644 --- a/src/workspace-center.js +++ b/src/workspace-center.js @@ -7,6 +7,8 @@ module.exports = class WorkspaceCenter { this.paneContainer = paneContainer } + activate () {} + /* Section: Event Subscription */ diff --git a/src/workspace.js b/src/workspace.js index dbbba1a97..786ec85ee 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -776,22 +776,30 @@ module.exports = class Workspace extends Model { const uri = options.uri == null && typeof item.getURI === 'function' ? item.getURI() : options.uri + let paneLocation + if (pane != null) { + paneLocation = this.getPaneLocations().find(location => location.getPanes().includes(pane)) + } + + // Determine which location to use, unless a split was provided. In that case, make sure it goes + // in the center location (legacy behavior) let location - // If a split was provided, make sure it goes in the center location (legacy behavior) - if (pane == null && split == null) { - if (uri != null) { - location = this.previousLocations.load(uri) - } - if (location == null && typeof item.getDefaultLocation === 'function') { - location = item.getDefaultLocation() - } + if (paneLocation == null && pane == null && split == null && uri != null) { + location = this.previousLocations.load(uri) } return Promise.resolve(location) .then(location => { + if (paneLocation == null) { + if (location == null && typeof item.getDefaultLocation === 'function') { + location = item.getDefaultLocation() + } + paneLocation = this.docks[location] || this.getCenter() + } + }) + .then(() => { if (pane != null) return pane - - pane = this.docks[location] == null ? this.getActivePane() : this.docks[location].getActivePane() + pane = paneLocation.getActivePane() switch (split) { case 'left': return pane.findLeftmostSibling() case 'right': return pane.findOrCreateRightmostSibling() @@ -814,6 +822,7 @@ module.exports = class Workspace extends Model { if (activatePane) { pane.activate() } + paneLocation.activate() let initialColumn = 0 let initialLine = 0 @@ -1188,6 +1197,37 @@ module.exports = class Workspace extends Model { return [this.getCenter(), ..._.values(this.docks)] } + toggle (uri) { + let foundItems = false + + // If any visible item has the given URI, hide it + for (const location of this.getPaneLocations()) { + const isCenter = location === this.getCenter() + if (isCenter || location.isOpen()) { + for (const pane of location.getPanes()) { + const activeItem = pane.getActiveItem() + if (activeItem != null && typeof activeItem.getURI === 'function') { + const itemURI = activeItem.getURI() + if (itemURI === uri) { + foundItems = true + // We can't really hide the center so we just destroy the item. + if (isCenter) { + pane.destroyItem(activeItem) + } else { + location.hide() + } + } + } + } + } + } + + // If no visible items had the URI, show it. + if (!foundItems) { + this.open(uri, {searchAllPanes: true}) + } + } + /* Section: Panels From 47cdc74b6a923e433b4f87fc099871197a778d7b Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Wed, 15 Mar 2017 14:46:03 -0700 Subject: [PATCH 12/37] Only count panes in center in workspace test --- spec/workspace-spec.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js index b04d8cbd7..4bc2c3799 100644 --- a/spec/workspace-spec.js +++ b/spec/workspace-spec.js @@ -1985,24 +1985,24 @@ i = /test/; #FIXME\ const pane1 = atom.workspace.getActivePane() const pane2 = pane1.splitRight({copyActiveItem: true}) - expect(atom.workspace.getPanes().length).toBe(2) + expect(atom.workspace.getCenter().getPanes().length).toBe(2) expect(pane2.getItems().length).toBe(1) atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() - expect(atom.workspace.getPanes().length).toBe(2) + expect(atom.workspace.getCenter().getPanes().length).toBe(2) expect(pane2.getItems().length).toBe(0) atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() - expect(atom.workspace.getPanes().length).toBe(1) + expect(atom.workspace.getCenter().getPanes().length).toBe(1) expect(pane1.getItems().length).toBe(1) atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() - expect(atom.workspace.getPanes().length).toBe(1) + expect(atom.workspace.getCenter().getPanes().length).toBe(1) expect(pane1.getItems().length).toBe(0) atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() - expect(atom.workspace.getPanes().length).toBe(1) + expect(atom.workspace.getCenter().getPanes().length).toBe(1) atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() expect(atom.close).toHaveBeenCalled() From 417e9c697985c28812a140d25a9486c4028191e7 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 16 Mar 2017 11:48:38 -0700 Subject: [PATCH 13/37] Add tests for open() and docks --- spec/workspace-spec.js | 134 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js index 4bc2c3799..d5fb333f0 100644 --- a/spec/workspace-spec.js +++ b/spec/workspace-spec.js @@ -24,6 +24,8 @@ describe('Workspace', () => { setDocumentEdited = spyOn(atom.applicationDelegate, 'setWindowDocumentEdited') atom.project.setPaths([atom.project.getDirectories()[0].resolve('dir')]) waits(1) + + waitsForPromise(() => atom.workspace.previousLocations.clear()) }) afterEach(() => temp.cleanupSync()) @@ -116,6 +118,63 @@ describe('Workspace', () => { expect(atom.workspace.getTextEditors().length).toBe(0) }) }) + + describe('where a dock contains an editor', () => { + afterEach(() => { + atom.workspace.getRightDock().paneContainer.destroy() + }) + + it('constructs the view with the same panes', () => { + const getActivePane = () => atom.workspace.getRightDock().getActivePane() + const pane1 = atom.workspace.getRightDock().getActivePane() + const pane2 = pane1.splitRight({copyActiveItem: true}) + const pane3 = pane2.splitRight({copyActiveItem: true}) + let pane4 = null + + waitsForPromise(() => + atom.workspace.open(null, {pane: getActivePane()}).then(editor => editor.setText('An untitled editor.')) + ) + + waitsForPromise(() => + atom.workspace.open('b', {pane: getActivePane()}).then(editor => pane2.activateItem(editor.copy())) + ) + + waitsForPromise(() => + atom.workspace.open('../sample.js', {pane: getActivePane()}).then(editor => pane3.activateItem(editor)) + ) + + runs(() => { + pane3.activeItem.setCursorScreenPosition([2, 4]) + pane4 = pane2.splitDown() + }) + + waitsForPromise(() => + atom.workspace.open('../sample.txt', {pane: getActivePane()}).then(editor => pane4.activateItem(editor)) + ) + + runs(() => { + pane4.getActiveItem().setCursorScreenPosition([0, 2]) + pane2.activate() + + simulateReload() + + expect(atom.workspace.getTextEditors().length).toBe(5) + const [editor1, editor2, untitledEditor, editor3, editor4] = atom.workspace.getTextEditors() + const firstDirectory = atom.project.getDirectories()[0] + expect(firstDirectory).toBeDefined() + expect(editor1.getPath()).toBe(firstDirectory.resolve('b')) + expect(editor2.getPath()).toBe(firstDirectory.resolve('../sample.txt')) + expect(editor2.getCursorScreenPosition()).toEqual([0, 2]) + expect(editor3.getPath()).toBe(firstDirectory.resolve('b')) + expect(editor4.getPath()).toBe(firstDirectory.resolve('../sample.js')) + expect(editor4.getCursorScreenPosition()).toEqual([2, 4]) + expect(untitledEditor.getPath()).toBeUndefined() + expect(untitledEditor.getText()).toBe('An untitled editor.') + + expect(atom.workspace.getRightDock().getActiveTextEditor().getPath()).toBe(editor3.getPath()) + }) + }) + }) }) describe('::open(uri, options)', () => { @@ -201,6 +260,25 @@ describe('Workspace', () => { ]) }) }) + + it('finds items in docks', () => { + const dock = atom.workspace.getRightDock() + const ITEM_URI = 'atom://test' + const item = { + getURI: () => ITEM_URI, + getDefaultLocation: jasmine.createSpy().andReturn('left'), + getElement: () => document.createElement('div') + } + dock.getActivePane().addItem(item) + expect(dock.getPaneItems()).toHaveLength(1) + waitsForPromise(() => atom.workspace.open(ITEM_URI, {searchAllPanes: true})) + runs(() => { + expect(item.getDefaultLocation).not.toHaveBeenCalled() + expect(atom.workspace.getPaneItems()).toHaveLength(1) + expect(dock.getPaneItems()).toHaveLength(1) + expect(dock.getPaneItems()[0]).toBe(item) + }) + }) }) describe('when the active pane does not have an editor for the given uri', () => { @@ -217,6 +295,46 @@ describe('Workspace', () => { expect(workspace.getActivePane().activate).toHaveBeenCalled() }) }) + + it("uses the location specified by the model's `getDefaultLocation()` method", () => { + const item = { + getDefaultLocation: jasmine.createSpy().andReturn('right'), + getElement: () => document.createElement('div') + } + const opener = jasmine.createSpy().andReturn(item) + const dock = atom.workspace.getRightDock() + spyOn(atom.workspace.previousLocations, 'load').andReturn(Promise.resolve()) + spyOn(atom.workspace, 'getOpeners').andReturn([opener]) + expect(dock.getPaneItems()).toHaveLength(0) + waitsForPromise(() => atom.workspace.open('a')) + runs(() => { + expect(dock.getPaneItems()).toHaveLength(1) + expect(opener).toHaveBeenCalled() + expect(item.getDefaultLocation).toHaveBeenCalled() + }) + }) + + it('prefers the last location the user used for that item', () => { + const ITEM_URI = 'atom://test' + const item = { + getURI: () => ITEM_URI, + getDefaultLocation: jasmine.createSpy().andReturn('left'), + getElement: () => document.createElement('div') + } + const opener = uri => uri === ITEM_URI ? item : null + const dock = atom.workspace.getRightDock() + spyOn(atom.workspace.previousLocations, 'load').andCallFake(uri => + uri === 'atom://test' ? Promise.resolve('right') : Promise.resolve() + ) + spyOn(atom.workspace, 'getOpeners').andReturn([opener]) + expect(dock.getPaneItems()).toHaveLength(0) + waitsForPromise(() => atom.workspace.open(ITEM_URI)) + runs(() => { + expect(dock.getPaneItems()).toHaveLength(1) + expect(dock.getPaneItems()[0]).toBe(item) + expect(item.getDefaultLocation).not.toHaveBeenCalled() + }) + }) }) }) }) @@ -248,6 +366,22 @@ describe('Workspace', () => { expect(workspace.getActivePaneItem()).toBe(editor1) }) }) + + it('activates the dock with the matching item', () => { + const dock = atom.workspace.getRightDock() + const ITEM_URI = 'atom://test' + const item = { + getURI: () => ITEM_URI, + getDefaultLocation: jasmine.createSpy().andReturn('left'), + getElement: () => document.createElement('div') + } + dock.getActivePane().addItem(item) + spyOn(dock, 'activate') + waitsForPromise(() => atom.workspace.open(ITEM_URI, {searchAllPanes: true})) + runs(() => { + expect(dock.activate).toHaveBeenCalled() + }) + }) }) describe('when no editor for the given uri is open in any pane', () => { From 6f9893d77da26acac9d5fe741f881192c630a07b Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Mon, 20 Mar 2017 19:50:35 -0700 Subject: [PATCH 14/37] Rename "getActivePane" in tests to clarify intent --- spec/workspace-spec.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js index d5fb333f0..5b6ecc7ba 100644 --- a/spec/workspace-spec.js +++ b/spec/workspace-spec.js @@ -125,22 +125,22 @@ describe('Workspace', () => { }) it('constructs the view with the same panes', () => { - const getActivePane = () => atom.workspace.getRightDock().getActivePane() + const getRightDockActivePane = () => atom.workspace.getRightDock().getActivePane() const pane1 = atom.workspace.getRightDock().getActivePane() const pane2 = pane1.splitRight({copyActiveItem: true}) const pane3 = pane2.splitRight({copyActiveItem: true}) let pane4 = null waitsForPromise(() => - atom.workspace.open(null, {pane: getActivePane()}).then(editor => editor.setText('An untitled editor.')) + atom.workspace.open(null, {pane: getRightDockActivePane()}).then(editor => editor.setText('An untitled editor.')) ) waitsForPromise(() => - atom.workspace.open('b', {pane: getActivePane()}).then(editor => pane2.activateItem(editor.copy())) + atom.workspace.open('b', {pane: getRightDockActivePane()}).then(editor => pane2.activateItem(editor.copy())) ) waitsForPromise(() => - atom.workspace.open('../sample.js', {pane: getActivePane()}).then(editor => pane3.activateItem(editor)) + atom.workspace.open('../sample.js', {pane: getRightDockActivePane()}).then(editor => pane3.activateItem(editor)) ) runs(() => { @@ -149,7 +149,7 @@ describe('Workspace', () => { }) waitsForPromise(() => - atom.workspace.open('../sample.txt', {pane: getActivePane()}).then(editor => pane4.activateItem(editor)) + atom.workspace.open('../sample.txt', {pane: getRightDockActivePane()}).then(editor => pane4.activateItem(editor)) ) runs(() => { From 98e7fcc50511800ffd4211e5edb796606e0178fa Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Mon, 20 Mar 2017 19:52:01 -0700 Subject: [PATCH 15/37] Prefer `getElement()` to view registry for Docks, PaneContainer & Pane --- spec/pane-axis-element-spec.coffee | 5 +++-- spec/pane-container-element-spec.coffee | 16 +++++++++++----- spec/pane-container-spec.coffee | 3 ++- spec/pane-element-spec.coffee | 2 +- src/atom-environment.coffee | 6 ------ src/dock.js | 22 ++++++++++++++-------- src/pane-container.coffee | 8 ++++++-- src/pane.coffee | 11 ++++++++--- src/panel-container-element.js | 2 +- src/workspace.js | 6 ++++-- 10 files changed, 50 insertions(+), 31 deletions(-) diff --git a/spec/pane-axis-element-spec.coffee b/spec/pane-axis-element-spec.coffee index 702e9c5fc..e6aa2ed5f 100644 --- a/spec/pane-axis-element-spec.coffee +++ b/spec/pane-axis-element-spec.coffee @@ -7,12 +7,13 @@ buildPane = -> applicationDelegate: atom.applicationDelegate, config: atom.config, deserializerManager: atom.deserializers, - notificationManager: atom.notifications + notificationManager: atom.notifications, + viewRegistry: atom.views }) describe "PaneAxisElement", -> it "correctly subscribes and unsubscribes to the underlying model events on attach/detach", -> - container = new PaneContainer(config: atom.config, applicationDelegate: atom.applicationDelegate) + container = new PaneContainer(config: atom.config, applicationDelegate: atom.applicationDelegate, viewRegistry: atom.views) axis = new PaneAxis axis.setContainer(container) axisElement = atom.views.getView(axis) diff --git a/spec/pane-container-element-spec.coffee b/spec/pane-container-element-spec.coffee index fe57e89af..e986e5d6c 100644 --- a/spec/pane-container-element-spec.coffee +++ b/spec/pane-container-element-spec.coffee @@ -2,6 +2,12 @@ PaneContainer = require '../src/pane-container' PaneAxisElement = require '../src/pane-axis-element' PaneAxis = require '../src/pane-axis' +params = + config: atom.config + confirm: atom.confirm.bind(atom) + viewRegistry: atom.views + applicationDelegate: atom.applicationDelegate + describe "PaneContainerElement", -> describe "when panes are added or removed", -> it "inserts or removes resize elements", -> @@ -42,7 +48,7 @@ describe "PaneContainerElement", -> ] it "transfers focus to the next pane if a focused pane is removed", -> - container = new PaneContainer(config: atom.config, confirm: atom.confirm.bind(atom)) + container = new PaneContainer(params) containerElement = atom.views.getView(container) leftPane = container.getActivePane() leftPaneElement = atom.views.getView(leftPane) @@ -58,7 +64,7 @@ describe "PaneContainerElement", -> describe "when a pane is split", -> it "builds appropriately-oriented atom-pane-axis elements", -> - container = new PaneContainer(config: atom.config, confirm: atom.confirm.bind(atom)) + container = new PaneContainer(params) containerElement = atom.views.getView(container) pane1 = container.getActivePane() @@ -84,7 +90,7 @@ describe "PaneContainerElement", -> [container, containerElement] = [] beforeEach -> - container = new PaneContainer(config: atom.config, confirm: atom.confirm.bind(atom)) + container = new PaneContainer(params) containerElement = atom.views.getView(container) document.querySelector('#jasmine-content').appendChild(containerElement) @@ -201,7 +207,7 @@ describe "PaneContainerElement", -> [leftPane, rightPane] = [] beforeEach -> - container = new PaneContainer(config: atom.config, confirm: atom.confirm.bind(atom)) + container = new PaneContainer(params) leftPane = container.getActivePane() rightPane = leftPane.splitRight() @@ -258,7 +264,7 @@ describe "PaneContainerElement", -> element.cloneNode(true) element - container = new PaneContainer(config: atom.config, confirm: atom.confirm.bind(atom)) + container = new PaneContainer(params) [item1, item2, item3, item4, item5, item6, item7, item8, item9] = [buildElement('1'), buildElement('2'), buildElement('3'), diff --git a/spec/pane-container-spec.coffee b/spec/pane-container-spec.coffee index 84c6c4fc9..c1c0b11b5 100644 --- a/spec/pane-container-spec.coffee +++ b/spec/pane-container-spec.coffee @@ -9,7 +9,8 @@ describe "PaneContainer", -> params = { config: atom.config, deserializerManager: atom.deserializers - applicationDelegate: atom.applicationDelegate + applicationDelegate: atom.applicationDelegate, + viewRegistry: atom.views } describe "serialization", -> diff --git a/spec/pane-element-spec.coffee b/spec/pane-element-spec.coffee index f5a059c49..f6dc4d535 100644 --- a/spec/pane-element-spec.coffee +++ b/spec/pane-element-spec.coffee @@ -6,7 +6,7 @@ describe "PaneElement", -> beforeEach -> spyOn(atom.applicationDelegate, "open") - container = new PaneContainer(config: atom.config, confirm: atom.confirm.bind(atom)) + container = new PaneContainer(config: atom.config, confirm: atom.confirm.bind(atom), viewRegistry: atom.views, applicationDelegate: atom.applicationDelegate) containerElement = atom.views.getView(container) pane = container.getActivePane() paneElement = atom.views.getView(pane) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 74f90e4f8..cca66f05b 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -49,9 +49,7 @@ AutoUpdateManager = require './auto-update-manager' WorkspaceElement = require './workspace-element' PanelContainerElement = require './panel-container-element' PanelElement = require './panel-element' -PaneContainerElement = require './pane-container-element' PaneAxisElement = require './pane-axis-element' -PaneElement = require './pane-element' {createGutterView} = require './gutter-component-helpers' # Essential: Atom global for dealing with packages, themes, menus, and the window. @@ -270,12 +268,8 @@ class AtomEnvironment extends Model new PanelContainerElement().initialize(model, env) @views.addViewProvider Panel, (model, env) -> new PanelElement().initialize(model, env) - @views.addViewProvider PaneContainer, (model, env) -> - new PaneContainerElement().initialize(model, env) @views.addViewProvider PaneAxis, (model, env) -> new PaneAxisElement().initialize(model, env) - @views.addViewProvider Pane, (model, env) -> - new PaneElement().initialize(model, env) @views.addViewProvider(Gutter, createGutterView) registerDefaultOpeners: -> diff --git a/src/dock.js b/src/dock.js index c4d95eecb..0aeab7336 100644 --- a/src/dock.js +++ b/src/dock.js @@ -37,7 +37,7 @@ module.exports = class Dock { applicationDelegate: this.applicationDelegate, deserializerManager: this.deserializerManager, notificationManager: this.notificationManager, - views: this.viewRegistry + viewRegistry: this.viewRegistry }) this.state = { @@ -53,13 +53,11 @@ module.exports = class Dock { pane.onDidRemoveItem(this.handleDidRemovePaneItem.bind(this)) }) ) + + this.render(this.state) } - // FIXME(matthewwithanm: This is kinda gross. We need to get a view for the pane container so we - // have to make sure that this is called after its view provider is registered. But we really - // don't have any guarantees about that. getElement () { - this.render(this.state) return this.element } @@ -151,12 +149,12 @@ module.exports = class Dock { this.element.appendChild(this.innerElement) this.innerElement.appendChild(this.maskElement) this.maskElement.appendChild(this.wrapperElement) - this.wrapperElement.appendChild(this.viewRegistry.getView(this.resizeHandle)) - this.wrapperElement.appendChild(this.viewRegistry.getView(this.paneContainer)) + this.wrapperElement.appendChild(this.resizeHandle.getElement()) + this.wrapperElement.appendChild(this.paneContainer.getElement()) this.wrapperElement.appendChild(this.cursorOverlayElement) // The toggle button must be rendered outside the mask because (1) it shouldn't be masked and // (2) if we made the mask larger to avoid masking it, the mask would block mouse events. - this.innerElement.appendChild(this.viewRegistry.getView(this.toggleButton)) + this.innerElement.appendChild(this.toggleButton.getElement()) } if (state.open) { @@ -425,6 +423,10 @@ class DockResizeHandle { this.update(props) } + getElement () { + return this.element + } + update (newProps) { this.props = Object.assign({}, this.props, newProps) @@ -473,6 +475,10 @@ class DockToggleButton { this.update(props) } + getElement () { + return this.element + } + destroy () { this.innerElement.removeEventListener('click', this.handleClick) this.innerElement.removeEventListener('dragenter', this.handleDragEnter) diff --git a/src/pane-container.coffee b/src/pane-container.coffee index fc092122e..590f9847d 100644 --- a/src/pane-container.coffee +++ b/src/pane-container.coffee @@ -3,6 +3,7 @@ Model = require './model' Pane = require './pane' ItemRegistry = require './item-registry' +PaneContainerElement = require './pane-container-element' module.exports = class PaneContainer extends Model @@ -14,16 +15,19 @@ class PaneContainer extends Model constructor: (params) -> super - {@config, applicationDelegate, notificationManager, deserializerManager} = params + {@config, applicationDelegate, notificationManager, deserializerManager, @viewRegistry} = params @emitter = new Emitter @subscriptions = new CompositeDisposable @itemRegistry = new ItemRegistry - @setRoot(new Pane({container: this, @config, applicationDelegate, notificationManager, deserializerManager})) + @setRoot(new Pane({container: this, @config, applicationDelegate, notificationManager, deserializerManager, @viewRegistry})) @setActivePane(@getRoot()) @monitorActivePaneItem() @monitorPaneItems() + getElement: -> + @element ?= new PaneContainerElement().initialize(this, {views: @viewRegistry}) + serialize: (params) -> deserializer: 'PaneContainer' version: @serializationVersion diff --git a/src/pane.coffee b/src/pane.coffee index 467775f45..5dfc59374 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -4,6 +4,7 @@ Grim = require 'grim' Model = require './model' PaneAxis = require './pane-axis' TextEditor = require './text-editor' +PaneElement = require './pane-element' # Extended: A container for presenting content in the center of the workspace. # Panes can contain multiple items, one of which is *active* at a given time. @@ -20,7 +21,7 @@ class Pane extends Model activeItem: undefined focused: false - @deserialize: (state, {deserializers, applicationDelegate, config, notifications}) -> + @deserialize: (state, {deserializers, applicationDelegate, config, notifications, views}) -> {items, activeItemIndex, activeItemURI, activeItemUri} = state activeItemURI ?= activeItemUri items = items.map (itemState) -> deserializers.deserialize(itemState) @@ -34,6 +35,7 @@ class Pane extends Model new Pane(extend(state, { deserializerManager: deserializers, notificationManager: notifications, + viewRegistry: views, config, applicationDelegate })) @@ -42,7 +44,7 @@ class Pane extends Model { @activeItem, @focused, @applicationDelegate, @notificationManager, @config, - @deserializerManager + @deserializerManager, @viewRegistry } = params @emitter = new Emitter @@ -55,6 +57,9 @@ class Pane extends Model @addItemsToStack(params?.itemStackIndices ? []) @setFlexScale(params?.flexScale ? 1) + getElement: -> + @element ?= new PaneElement().initialize(this, {views: @viewRegistry, @applicationDelegate}) + serialize: -> itemsToBeSerialized = compact(@items.map((item) -> item if typeof item.serialize is 'function')) itemStackIndices = (itemsToBeSerialized.indexOf(item) for item in @itemStack when typeof item.serialize is 'function') @@ -819,7 +824,7 @@ class Pane extends Model @parent.replaceChild(this, new PaneAxis({@container, orientation, children: [this], @flexScale})) @setFlexScale(1) - newPane = new Pane(extend({@applicationDelegate, @notificationManager, @deserializerManager, @config}, params)) + newPane = new Pane(extend({@applicationDelegate, @notificationManager, @deserializerManager, @config, @viewRegistry}, params)) switch side when 'before' then @parent.insertChildBefore(this, newPane) when 'after' then @parent.insertChildAfter(this, newPane) diff --git a/src/panel-container-element.js b/src/panel-container-element.js index 2571d9875..f78a2e352 100644 --- a/src/panel-container-element.js +++ b/src/panel-container-element.js @@ -22,7 +22,7 @@ class PanelContainerElement extends HTMLElement { // Add the dock. if (this.model.dock != null) { - this.appendChild(this.views.getView(this.model.dock)) + this.appendChild(this.model.dock.getElement()) } return this diff --git a/src/workspace.js b/src/workspace.js index 786ec85ee..9910153d1 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -56,7 +56,8 @@ module.exports = class Workspace extends Model { config: this.config, applicationDelegate: this.applicationDelegate, notificationManager: this.notificationManager, - deserializerManager: this.deserializerManager + deserializerManager: this.deserializerManager, + viewRegistry: this.viewRegistry }) this.paneContainer.onDidDestroyPaneItem(this.didDestroyPaneItem) @@ -108,7 +109,8 @@ module.exports = class Workspace extends Model { config: this.config, applicationDelegate: this.applicationDelegate, notificationManager: this.notificationManager, - deserializerManager: this.deserializerManager + deserializerManager: this.deserializerManager, + viewRegistry: this.viewRegistry }) this.paneContainer.onDidDestroyPaneItem(this.didDestroyPaneItem) From cd62357f0f7ca574a2fd2c945592637d6a1408e6 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Mon, 20 Mar 2017 20:58:58 -0700 Subject: [PATCH 16/37] Mention dock getters in Dock docs --- src/dock.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/dock.js b/src/dock.js index 0aeab7336..a67ad3ab9 100644 --- a/src/dock.js +++ b/src/dock.js @@ -15,7 +15,9 @@ const TOGGLE_BUTTON_VISIBLE_CLASS = 'atom-dock-toggle-button-visible' const CURSOR_OVERLAY_VISIBLE_CLASS = 'atom-dock-cursor-overlay-visible' // Extended: A container at the edges of the editor window capable of holding items. -// You should not create a `Dock` directly, instead use {Workspace::open}. +// You should not create a Dock directly. Instead, access one of the three docks of the workspace +// via {::getLeftDock}, {::getRightDock}, and {::getBottomDock} or add an item to a dock via +// {Workspace::open}. module.exports = class Dock { constructor (params) { this.handleResizeHandleDragStart = this.handleResizeHandleDragStart.bind(this) From 3fcec8b8cda071b3bc0a55c55c70672302492760 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 23 Mar 2017 10:39:02 -0700 Subject: [PATCH 17/37] previousLocations -> itemLocationStore --- spec/workspace-spec.js | 6 +++--- src/workspace.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js index 5b6ecc7ba..a4dbf65df 100644 --- a/spec/workspace-spec.js +++ b/spec/workspace-spec.js @@ -25,7 +25,7 @@ describe('Workspace', () => { atom.project.setPaths([atom.project.getDirectories()[0].resolve('dir')]) waits(1) - waitsForPromise(() => atom.workspace.previousLocations.clear()) + waitsForPromise(() => atom.workspace.itemLocationStore.clear()) }) afterEach(() => temp.cleanupSync()) @@ -303,7 +303,7 @@ describe('Workspace', () => { } const opener = jasmine.createSpy().andReturn(item) const dock = atom.workspace.getRightDock() - spyOn(atom.workspace.previousLocations, 'load').andReturn(Promise.resolve()) + spyOn(atom.workspace.itemLocationStore, 'load').andReturn(Promise.resolve()) spyOn(atom.workspace, 'getOpeners').andReturn([opener]) expect(dock.getPaneItems()).toHaveLength(0) waitsForPromise(() => atom.workspace.open('a')) @@ -323,7 +323,7 @@ describe('Workspace', () => { } const opener = uri => uri === ITEM_URI ? item : null const dock = atom.workspace.getRightDock() - spyOn(atom.workspace.previousLocations, 'load').andCallFake(uri => + spyOn(atom.workspace.itemLocationStore, 'load').andCallFake(uri => uri === 'atom://test' ? Promise.resolve('right') : Promise.resolve() ) spyOn(atom.workspace, 'getOpeners').andReturn([opener]) diff --git a/src/workspace.js b/src/workspace.js index 9910153d1..106d20526 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -46,7 +46,7 @@ module.exports = class Workspace extends Model { this.textEditorRegistry = params.textEditorRegistry this.hoveredDock = null this.draggingItem = false - this.previousLocations = new StateStore('AtomPreviousItemLocations', 1) + this.itemLocationStore = new StateStore('AtomPreviousItemLocations', 1) this.emitter = new Emitter() this.openers = [] @@ -299,7 +299,7 @@ module.exports = class Workspace extends Model { if (typeof item.getURI === 'function') { const uri = item.getURI() if (uri != null) { - this.previousLocations.save(item.getURI(), location) + this.itemLocationStore.save(item.getURI(), location) } } }) @@ -787,7 +787,7 @@ module.exports = class Workspace extends Model { // in the center location (legacy behavior) let location if (paneLocation == null && pane == null && split == null && uri != null) { - location = this.previousLocations.load(uri) + location = this.itemLocationStore.load(uri) } return Promise.resolve(location) From d307c791c4eb9a10e87593086292fcf1a2d22ce8 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 23 Mar 2017 10:54:24 -0700 Subject: [PATCH 18/37] Be consistent about what "location" refers to --- src/workspace.js | 54 ++++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/workspace.js b/src/workspace.js index 106d20526..6008f60f1 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -291,9 +291,9 @@ module.exports = class Workspace extends Model { if (this.movedItemSubscription != null) { this.movedItemSubscription.dispose() } - const paneLocations = Object.assign({center: this}, this.docks) + const paneContainers = Object.assign({center: this}, this.docks) this.movedItemSubscription = new CompositeDisposable( - ..._.map(paneLocations, (host, location) => ( + ..._.map(paneContainers, (host, location) => ( host.observePanes(pane => { pane.onDidAddItem(({item}) => { if (typeof item.getURI === 'function') { @@ -391,7 +391,7 @@ module.exports = class Workspace extends Model { // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observePaneItems (callback) { return new CompositeDisposable( - ...this.getPaneLocations().map(location => location.observePaneItems(callback)) + ...this.getPaneContainers().map(container => container.observePaneItems(callback)) ) } @@ -462,7 +462,7 @@ module.exports = class Workspace extends Model { // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidAddPane (callback) { return new CompositeDisposable( - ...this.getPaneLocations().map(location => location.onDidAddPane(callback)) + ...this.getPaneContainers().map(container => container.onDidAddPane(callback)) ) } @@ -476,7 +476,7 @@ module.exports = class Workspace extends Model { // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onWillDestroyPane (callback) { return new CompositeDisposable( - ...this.getPaneLocations().map(location => location.onWillDestroyPane(callback)) + ...this.getPaneContainers().map(container => container.onWillDestroyPane(callback)) ) } @@ -490,7 +490,7 @@ module.exports = class Workspace extends Model { // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidDestroyPane (callback) { return new CompositeDisposable( - ...this.getPaneLocations().map(location => location.onDidDestroyPane(callback)) + ...this.getPaneContainers().map(container => container.onDidDestroyPane(callback)) ) } @@ -504,7 +504,7 @@ module.exports = class Workspace extends Model { // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observePanes (callback) { return new CompositeDisposable( - ...this.getPaneLocations().map(location => location.observePanes(callback)) + ...this.getPaneContainers().map(container => container.observePanes(callback)) ) } @@ -538,7 +538,7 @@ module.exports = class Workspace extends Model { // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidAddPaneItem (callback) { return new CompositeDisposable( - ...this.getPaneLocations().map(location => location.onDidAddPaneItem(callback)) + ...this.getPaneContainers().map(container => container.onDidAddPaneItem(callback)) ) } @@ -555,7 +555,7 @@ module.exports = class Workspace extends Model { // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. onWillDestroyPaneItem (callback) { return new CompositeDisposable( - ...this.getPaneLocations().map(location => location.onWillDestroyPaneItem(callback)) + ...this.getPaneContainers().map(container => container.onWillDestroyPaneItem(callback)) ) } @@ -571,7 +571,7 @@ module.exports = class Workspace extends Model { // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. onDidDestroyPaneItem (callback) { return new CompositeDisposable( - ...this.getPaneLocations().map(location => location.onDidDestroyPaneItem(callback)) + ...this.getPaneContainers().map(container => container.onDidDestroyPaneItem(callback)) ) } @@ -778,30 +778,30 @@ module.exports = class Workspace extends Model { const uri = options.uri == null && typeof item.getURI === 'function' ? item.getURI() : options.uri - let paneLocation + let paneContainer if (pane != null) { - paneLocation = this.getPaneLocations().find(location => location.getPanes().includes(pane)) + paneContainer = this.getPaneContainers().find(container => container.getPanes().includes(pane)) } // Determine which location to use, unless a split was provided. In that case, make sure it goes // in the center location (legacy behavior) let location - if (paneLocation == null && pane == null && split == null && uri != null) { + if (paneContainer == null && pane == null && split == null && uri != null) { location = this.itemLocationStore.load(uri) } return Promise.resolve(location) .then(location => { - if (paneLocation == null) { + if (paneContainer == null) { if (location == null && typeof item.getDefaultLocation === 'function') { location = item.getDefaultLocation() } - paneLocation = this.docks[location] || this.getCenter() + paneContainer = this.docks[location] || this.getCenter() } }) .then(() => { if (pane != null) return pane - pane = paneLocation.getActivePane() + pane = paneContainer.getActivePane() switch (split) { case 'left': return pane.findLeftmostSibling() case 'right': return pane.findOrCreateRightmostSibling() @@ -824,7 +824,7 @@ module.exports = class Workspace extends Model { if (activatePane) { pane.activate() } - paneLocation.activate() + paneContainer.activate() let initialColumn = 0 let initialLine = 0 @@ -966,7 +966,7 @@ module.exports = class Workspace extends Model { // // Returns an {Array} of items. getPaneItems () { - return _.flatten(this.getPaneLocations().map(location => location.getPaneItems())) + return _.flatten(this.getPaneContainers().map(container => container.getPaneItems())) } // Essential: Get the active {Pane}'s active item. @@ -994,14 +994,14 @@ module.exports = class Workspace extends Model { // Save all pane items. saveAll () { - this.getPaneLocations().forEach(location => { - location.saveAll() + this.getPaneContainers().forEach(container => { + container.saveAll() }) } confirmClose (options) { - return this.getPaneLocations() - .map(location => location.confirmClose(options)) + return this.getPaneContainers() + .map(container => container.confirmClose(options)) .every(saved => saved) } @@ -1040,7 +1040,7 @@ module.exports = class Workspace extends Model { // // Returns an {Array} of {Pane}s. getPanes () { - return _.flatten(this.getPaneLocations().map(location => location.getPanes())) + return _.flatten(this.getPaneContainers().map(container => container.getPanes())) } // Extended: Get the active {Pane}. @@ -1066,7 +1066,7 @@ module.exports = class Workspace extends Model { // // Returns a {Pane} or `undefined` if no pane exists for the given URI. paneForURI (uri) { - for (let location of this.getPaneLocations()) { + for (let location of this.getPaneContainers()) { const pane = location.paneForURI(uri) if (pane != null) { return pane @@ -1080,7 +1080,7 @@ module.exports = class Workspace extends Model { // // Returns a {Pane} or `undefined` if no pane exists for the given item. paneForItem (item) { - for (let location of this.getPaneLocations()) { + for (let location of this.getPaneContainers()) { const pane = location.paneForItem(item) if (pane != null) { return pane @@ -1195,7 +1195,7 @@ module.exports = class Workspace extends Model { return this.docks.bottom } - getPaneLocations () { + getPaneContainers () { return [this.getCenter(), ..._.values(this.docks)] } @@ -1203,7 +1203,7 @@ module.exports = class Workspace extends Model { let foundItems = false // If any visible item has the given URI, hide it - for (const location of this.getPaneLocations()) { + for (const location of this.getPaneContainers()) { const isCenter = location === this.getCenter() if (isCenter || location.isOpen()) { for (const pane of location.getPanes()) { From e01bc40a78c1352a7883306ad9e054540cb0df26 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 23 Mar 2017 10:55:31 -0700 Subject: [PATCH 19/37] "affordance" -> "hoverMargin" --- src/dock.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/dock.js b/src/dock.js index a67ad3ab9..0908d8311 100644 --- a/src/dock.js +++ b/src/dock.js @@ -289,17 +289,17 @@ module.exports = class Dock { // The area used when detecting "leave" events is actually larger than when detecting entrances. if (includeButtonWidth) { - const affordance = 20 + const hoverMargin = 20 const toggleButtonSize = 50 / 2 // This needs to match the value in the CSS. switch (this.location) { case 'right': - bounds.left -= toggleButtonSize + affordance + bounds.left -= toggleButtonSize + hoverMargin break case 'bottom': - bounds.top -= toggleButtonSize + affordance + bounds.top -= toggleButtonSize + hoverMargin break case 'left': - bounds.right += toggleButtonSize + affordance + bounds.right += toggleButtonSize + hoverMargin break } } From 37a3c9b59c8e3d73bc4a53afed58877eb5efc245 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 23 Mar 2017 11:04:52 -0700 Subject: [PATCH 20/37] Measure toggle button size instead of hardcoding it --- src/dock.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/dock.js b/src/dock.js index 0908d8311..7ddc55883 100644 --- a/src/dock.js +++ b/src/dock.js @@ -290,16 +290,16 @@ module.exports = class Dock { // The area used when detecting "leave" events is actually larger than when detecting entrances. if (includeButtonWidth) { const hoverMargin = 20 - const toggleButtonSize = 50 / 2 // This needs to match the value in the CSS. + const {width, height} = this.toggleButton.getSize() switch (this.location) { case 'right': - bounds.left -= toggleButtonSize + hoverMargin + bounds.left -= width + hoverMargin break case 'bottom': - bounds.top -= toggleButtonSize + hoverMargin + bounds.top -= height + hoverMargin break case 'left': - bounds.right += toggleButtonSize + hoverMargin + bounds.right += width + hoverMargin break } } @@ -481,6 +481,13 @@ class DockToggleButton { return this.element } + getSize () { + if (this.size == null) { + this.size = this.element.getBoundingClientRect() + } + return this.size + } + destroy () { this.innerElement.removeEventListener('click', this.handleClick) this.innerElement.removeEventListener('dragenter', this.handleDragEnter) From 791457d9a710a59f21c8a40dc347aa86badb160f Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 23 Mar 2017 11:19:54 -0700 Subject: [PATCH 21/37] Add remaining pane container methods and documentation to docks --- src/dock.js | 303 ++++++++++++++++++++++++++++++++-------- src/workspace-center.js | 23 +-- 2 files changed, 258 insertions(+), 68 deletions(-) diff --git a/src/dock.js b/src/dock.js index 7ddc55883..585b2441d 100644 --- a/src/dock.js +++ b/src/dock.js @@ -336,24 +336,250 @@ module.exports = class Dock { // PaneContainer-delegating methods + /* + Section: Event Subscription + */ + + // Essential: Invoke the given callback with all current and future text + // editors in the dock. + // + // * `callback` {Function} to be called with current and future text editors. + // * `editor` An {TextEditor} that is present in {::getTextEditors} at the time + // of subscription or that is added at some later time. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeTextEditors (callback) { + for (const textEditor of this.getTextEditors()) { + callback(textEditor) + } + return this.onDidAddTextEditor(({textEditor}) => callback(textEditor)) + } + + // Essential: Invoke the given callback with all current and future panes items + // in the dock. + // + // * `callback` {Function} to be called with current and future pane items. + // * `item` An item that is present in {::getPaneItems} at the time of + // subscription or that is added at some later time. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observePaneItems (callback) { + return this.paneContainer.observePaneItems(callback) + } + + // Essential: Invoke the given callback when the active pane item changes. + // + // Because observers are invoked synchronously, it's important not to perform + // any expensive operations via this method. Consider + // {::onDidStopChangingActivePaneItem} to delay operations until after changes + // stop occurring. + // + // * `callback` {Function} to be called when the active pane item changes. + // * `item` The active pane item. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeActivePaneItem (callback) { + return this.paneContainer.onDidChangeActivePaneItem(callback) + } + + // Essential: Invoke the given callback when the active pane item stops + // changing. + // + // Observers are called asynchronously 100ms after the last active pane item + // change. Handling changes here rather than in the synchronous + // {::onDidChangeActivePaneItem} prevents unneeded work if the user is quickly + // changing or closing tabs and ensures critical UI feedback, like changing the + // highlighted tab, gets priority over work that can be done asynchronously. + // + // * `callback` {Function} to be called when the active pane item stopts + // changing. + // * `item` The active pane item. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidStopChangingActivePaneItem (callback) { + return this.paneContainer.onDidStopChangingActivePaneItem(callback) + } + + // Essential: Invoke the given callback with the current active pane item and + // with all future active pane items in the dock. + // + // * `callback` {Function} to be called when the active pane item changes. + // * `item` The current active pane item. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeActivePaneItem (callback) { + return this.paneContainer.observeActivePaneItem(callback) + } + + // Extended: Invoke the given callback when a pane is added to the dock. + // + // * `callback` {Function} to be called panes are added. + // * `event` {Object} with the following keys: + // * `pane` The added pane. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddPane (callback) { + return this.paneContainer.onDidAddPane(callback) + } + + // Extended: Invoke the given callback before a pane is destroyed in the + // dock. + // + // * `callback` {Function} to be called before panes are destroyed. + // * `event` {Object} with the following keys: + // * `pane` The pane to be destroyed. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onWillDestroyPane (callback) { + return this.paneContainer.onWillDestroyPane(callback) + } + + // Extended: Invoke the given callback when a pane is destroyed in the dock. + // + // * `callback` {Function} to be called panes are destroyed. + // * `event` {Object} with the following keys: + // * `pane` The destroyed pane. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroyPane (callback) { + return this.paneContainer.onDidDestroyPane(callback) + } + + // Extended: Invoke the given callback with all current and future panes in the + // dock. + // + // * `callback` {Function} to be called with current and future panes. + // * `pane` A {Pane} that is present in {::getPanes} at the time of + // subscription or that is added at some later time. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observePanes (callback) { + return this.paneContainer.observePanes(callback) + } + + // Extended: Invoke the given callback when the active pane changes. + // + // * `callback` {Function} to be called when the active pane changes. + // * `pane` A {Pane} that is the current return value of {::getActivePane}. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeActivePane (callback) { + return this.paneContainer.onDidChangeActivePane(callback) + } + + // Extended: Invoke the given callback with the current active pane and when + // the active pane changes. + // + // * `callback` {Function} to be called with the current and future active# + // panes. + // * `pane` A {Pane} that is the current return value of {::getActivePane}. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeActivePane (callback) { + return this.paneContainer.observeActivePane(callback) + } + + // Extended: Invoke the given callback when a pane item is added to the dock. + // + // * `callback` {Function} to be called when pane items are added. + // * `event` {Object} with the following keys: + // * `item` The added pane item. + // * `pane` {Pane} containing the added item. + // * `index` {Number} indicating the index of the added item in its pane. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddPaneItem (callback) { + return this.paneContainer.onDidAddPaneItem(callback) + } + + // Extended: Invoke the given callback when a pane item is about to be + // destroyed, before the user is prompted to save it. + // + // * `callback` {Function} to be called before pane items are destroyed. + // * `event` {Object} with the following keys: + // * `item` The item to be destroyed. + // * `pane` {Pane} containing the item to be destroyed. + // * `index` {Number} indicating the index of the item to be destroyed in + // its pane. + // + // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. + onWillDestroyPaneItem (callback) { + return this.paneContainer.onWillDestroyPaneItem(callback) + } + + // Extended: Invoke the given callback when a pane item is destroyed. + // + // * `callback` {Function} to be called when pane items are destroyed. + // * `event` {Object} with the following keys: + // * `item` The destroyed item. + // * `pane` {Pane} containing the destroyed item. + // * `index` {Number} indicating the index of the destroyed item in its + // pane. + // + // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. + onDidDestroyPaneItem (callback) { + return this.paneContainer.onDidDestroyPaneItem(callback) + } + + /* + Section: Pane Items + */ + + // Essential: Get all pane items in the dock. + // + // Returns an {Array} of items. + getPaneItems () { + return this.paneContainer.getPaneItems() + } + + // Essential: Get the active {Pane}'s active item. + // + // Returns an pane item {Object}. + getActivePaneItem () { + return this.paneContainer.getActivePaneItem() + } + + // Essential: Get all text editors in the dock. + // + // Returns an {Array} of {TextEditor}s. + getTextEditors () { + return this.paneContainer.getTextEditors() + } + + // Essential: Get the active item if it is an {TextEditor}. + // + // Returns an {TextEditor} or `undefined` if the current active item is not an + // {TextEditor}. + getActiveTextEditor () { + const activeItem = this.getActivePaneItem() + if (activeItem instanceof TextEditor) { return activeItem } + } + + // Save all pane items. + saveAll () { + this.paneContainer.saveAll() + } + + confirmClose (options) { + return this.paneContainer.confirmClose(options) + } + + /* + Section: Panes + */ + + // Extended: Get all panes in the dock. + // + // Returns an {Array} of {Pane}s. getPanes () { return this.paneContainer.getPanes() } - observePanes (fn) { - return this.paneContainer.observePanes(fn) - } - - onDidAddPane (fn) { - return this.paneContainer.onDidAddPane(fn) - } - - onWillDestroyPane (fn) { - return this.paneContainer.onWillDestroyPane(fn) - } - - onDidDestroyPane (fn) { - return this.paneContainer.onDidDestroyPane(fn) + // Extended: Get the active {Pane}. + // + // Returns a {Pane}. + getActivePane () { + return this.paneContainer.getActivePane() } paneForURI (uri) { @@ -364,49 +590,12 @@ module.exports = class Dock { return this.paneContainer.paneForItem(item) } - getActivePane () { - return this.paneContainer.getActivePane() - } - - getPaneItems () { - return this.paneContainer.getPaneItems() - } - - getActivePaneItem () { - return this.paneContainer.getActivePaneItem() - } - - getTextEditors () { - return this.paneContainer.getTextEditors() - } - - getActiveTextEditor () { - const activeItem = this.getActivePaneItem() - if (activeItem instanceof TextEditor) { return activeItem } - } - - observePaneItems (fn) { - return this.paneContainer.observePaneItems(fn) - } - - onDidAddPaneItem (fn) { - return this.paneContainer.onDidAddPaneItem(fn) - } - - onWillDestroyPaneItem (fn) { - return this.paneContainer.onWillDestroyPaneItem(fn) - } - - onDidDestroyPaneItem (fn) { - return this.paneContainer.onDidDestroyPaneItem(fn) - } - - saveAll () { - this.paneContainer.saveAll() - } - - confirmClose (options) { - return this.paneContainer.confirmClose(options) + // Destroy (close) the active pane. + destroyActivePane () { + const activePane = this.getActivePane() + if (activePane != null) { + activePane.destroy() + } } } diff --git a/src/workspace-center.js b/src/workspace-center.js index c2c875b74..4979f4a6f 100644 --- a/src/workspace-center.js +++ b/src/workspace-center.js @@ -14,7 +14,7 @@ module.exports = class WorkspaceCenter { */ // Essential: Invoke the given callback with all current and future text - // editors in the workspace. + // editors in the workspace center. // // * `callback` {Function} to be called with current and future text editors. // * `editor` An {TextEditor} that is present in {::getTextEditors} at the time @@ -27,7 +27,7 @@ module.exports = class WorkspaceCenter { } // Essential: Invoke the given callback with all current and future panes items - // in the workspace. + // in the workspace center. // // * `callback` {Function} to be called with current and future pane items. // * `item` An item that is present in {::getPaneItems} at the time of @@ -70,7 +70,7 @@ module.exports = class WorkspaceCenter { } // Essential: Invoke the given callback with the current active pane item and - // with all future active pane items in the workspace. + // with all future active pane items in the workspace center. // // * `callback` {Function} to be called when the active pane item changes. // * `item` The current active pane item. @@ -80,7 +80,8 @@ module.exports = class WorkspaceCenter { return this.paneContainer.observeActivePaneItem(callback) } - // Extended: Invoke the given callback when a pane is added to the workspace. + // Extended: Invoke the given callback when a pane is added to the workspace + // center. // // * `callback` {Function} to be called panes are added. // * `event` {Object} with the following keys: @@ -92,7 +93,7 @@ module.exports = class WorkspaceCenter { } // Extended: Invoke the given callback before a pane is destroyed in the - // workspace. + // workspace center. // // * `callback` {Function} to be called before panes are destroyed. // * `event` {Object} with the following keys: @@ -104,7 +105,7 @@ module.exports = class WorkspaceCenter { } // Extended: Invoke the given callback when a pane is destroyed in the - // workspace. + // workspace center. // // * `callback` {Function} to be called panes are destroyed. // * `event` {Object} with the following keys: @@ -116,7 +117,7 @@ module.exports = class WorkspaceCenter { } // Extended: Invoke the given callback with all current and future panes in the - // workspace. + // workspace center. // // * `callback` {Function} to be called with current and future panes. // * `pane` A {Pane} that is present in {::getPanes} at the time of @@ -150,7 +151,7 @@ module.exports = class WorkspaceCenter { } // Extended: Invoke the given callback when a pane item is added to the - // workspace. + // workspace center. // // * `callback` {Function} to be called when pane items are added. // * `event` {Object} with the following keys: @@ -196,7 +197,7 @@ module.exports = class WorkspaceCenter { Section: Pane Items */ - // Essential: Get all pane items in the workspace. + // Essential: Get all pane items in the workspace center. // // Returns an {Array} of items. getPaneItems () { @@ -210,7 +211,7 @@ module.exports = class WorkspaceCenter { return this.paneContainer.getActivePaneItem() } - // Essential: Get all text editors in the workspace. + // Essential: Get all text editors in the workspace center. // // Returns an {Array} of {TextEditor}s. getTextEditors () { @@ -239,7 +240,7 @@ module.exports = class WorkspaceCenter { Section: Panes */ - // Extended: Get all panes in the workspace. + // Extended: Get all panes in the workspace center. // // Returns an {Array} of {Pane}s. getPanes () { From 3e826591fda6b51096fd42d1b3d7f08f4d9325e3 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 23 Mar 2017 11:27:37 -0700 Subject: [PATCH 22/37] Clean up storage of most recent location --- src/workspace-center.js | 4 ++++ src/workspace.js | 29 +++++++++-------------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/workspace-center.js b/src/workspace-center.js index 4979f4a6f..efaf0654c 100644 --- a/src/workspace-center.js +++ b/src/workspace-center.js @@ -9,6 +9,10 @@ module.exports = class WorkspaceCenter { activate () {} + getLocation () { + return 'center' + } + /* Section: Event Subscription */ diff --git a/src/workspace.js b/src/workspace.js index 6008f60f1..9a6f40204 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -288,24 +288,16 @@ module.exports = class Workspace extends Model { } subscribeToMovedItems () { - if (this.movedItemSubscription != null) { - this.movedItemSubscription.dispose() + for (const paneContainer of this.getPaneContainers()) { + paneContainer.onDidAddPaneItem(({item}) => { + if (typeof item.getURI === 'function') { + const uri = item.getURI() + if (uri != null) { + this.itemLocationStore.save(item.getURI(), paneContainer.getLocation()) + } + } + }) } - const paneContainers = Object.assign({center: this}, this.docks) - this.movedItemSubscription = new CompositeDisposable( - ..._.map(paneContainers, (host, location) => ( - host.observePanes(pane => { - pane.onDidAddItem(({item}) => { - if (typeof item.getURI === 'function') { - const uri = item.getURI() - if (uri != null) { - this.itemLocationStore.save(item.getURI(), location) - } - } - }) - }) - )) - ) } // Updates the application's title and proxy icon based on whichever file is @@ -1170,9 +1162,6 @@ module.exports = class Workspace extends Model { if (this.activeItemSubscriptions != null) { this.activeItemSubscriptions.dispose() } - if (this.movedItemSubscription != null) { - this.movedItemSubscription.dispose() - } } /* From 77ea97e623cdef220de6e933b56ef30539e2c94b Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 23 Mar 2017 11:43:06 -0700 Subject: [PATCH 23/37] Use async/await in `openItem()` --- src/workspace.js | 115 ++++++++++++++++++++++++----------------------- 1 file changed, 59 insertions(+), 56 deletions(-) diff --git a/src/workspace.js b/src/workspace.js index 9a6f40204..49f69ff5e 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -1,4 +1,4 @@ -'use strict' +'use babel' const _ = require('underscore-plus') const url = require('url') @@ -762,11 +762,11 @@ module.exports = class Workspace extends Model { } } - openItem (item, options = {}) { + async openItem (item, options = {}) { let {pane, split} = options - if (item == null) return Promise.resolve() - if (pane != null && pane.isDestroyed()) return Promise.resolve(item) + if (item == null) return undefined + if (pane != null && pane.isDestroyed()) return item const uri = options.uri == null && typeof item.getURI === 'function' ? item.getURI() : options.uri @@ -779,63 +779,66 @@ module.exports = class Workspace extends Model { // in the center location (legacy behavior) let location if (paneContainer == null && pane == null && split == null && uri != null) { - location = this.itemLocationStore.load(uri) + location = await this.itemLocationStore.load(uri) } - return Promise.resolve(location) - .then(location => { - if (paneContainer == null) { - if (location == null && typeof item.getDefaultLocation === 'function') { - location = item.getDefaultLocation() - } - paneContainer = this.docks[location] || this.getCenter() - } - }) - .then(() => { - if (pane != null) return pane - pane = paneContainer.getActivePane() - switch (split) { - case 'left': return pane.findLeftmostSibling() - case 'right': return pane.findOrCreateRightmostSibling() - case 'up': return pane.findTopmostSibling() - case 'down': return pane.findOrCreateBottommostSibling() - default: return pane - } - }) - .then(pane => { - if (!options.pending && (pane.getPendingItem() === item)) { - pane.clearPendingItem() - } + if (paneContainer == null) { + if (location == null && typeof item.getDefaultLocation === 'function') { + location = item.getDefaultLocation() + } + paneContainer = this.docks[location] || this.getCenter() + } - const activatePane = options.activatePane != null ? options.activatePane : true - const activateItem = options.activateItem != null ? options.activateItem : true - this.itemOpened(item) - if (activateItem) { - pane.activateItem(item, {pending: options.pending}) - } - if (activatePane) { - pane.activate() - } - paneContainer.activate() + if (pane == null) { + pane = paneContainer.getActivePane() + switch (split) { + case 'left': + pane = pane.findLeftmostSibling() + break + case 'right': + pane = pane.findOrCreateRightmostSibling() + break + case 'up': + pane = pane.findTopmostSibling() + break + case 'down': + pane = pane.findOrCreateBottommostSibling() + break + } + } - let initialColumn = 0 - let initialLine = 0 - if (!Number.isNaN(options.initialLine)) { - initialLine = options.initialLine - } - if (!Number.isNaN(options.initialColumn)) { - initialColumn = options.initialColumn - } - if ((initialLine >= 0) || (initialColumn >= 0)) { - if (typeof item.setCursorBufferPosition === 'function') { - item.setCursorBufferPosition([initialLine, initialColumn]) - } - } + if (!options.pending && (pane.getPendingItem() === item)) { + pane.clearPendingItem() + } - const index = pane.getActiveItemIndex() - this.emitter.emit('did-open', {uri, pane, item, index}) - return item - }) + const activatePane = options.activatePane != null ? options.activatePane : true + const activateItem = options.activateItem != null ? options.activateItem : true + this.itemOpened(item) + if (activateItem) { + pane.activateItem(item, {pending: options.pending}) + } + if (activatePane) { + pane.activate() + } + paneContainer.activate() + + let initialColumn = 0 + let initialLine = 0 + if (!Number.isNaN(options.initialLine)) { + initialLine = options.initialLine + } + if (!Number.isNaN(options.initialColumn)) { + initialColumn = options.initialColumn + } + if ((initialLine >= 0) || (initialColumn >= 0)) { + if (typeof item.setCursorBufferPosition === 'function') { + item.setCursorBufferPosition([initialLine, initialColumn]) + } + } + + const index = pane.getActiveItemIndex() + this.emitter.emit('did-open', {uri, pane, item, index}) + return item } openTextFile (uri, options) { From 52606171bfa837071c2c18ceeae9bc316b110475 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 23 Mar 2017 18:28:12 -0700 Subject: [PATCH 24/37] Add "location" param to `open()` --- spec/workspace-spec.js | 9 ++++----- src/workspace.js | 20 ++++++++++++-------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js index 5c2cfb1b9..ae692087f 100644 --- a/spec/workspace-spec.js +++ b/spec/workspace-spec.js @@ -126,22 +126,21 @@ describe('Workspace', () => { }) it('constructs the view with the same panes', () => { - const getRightDockActivePane = () => atom.workspace.getRightDock().getActivePane() const pane1 = atom.workspace.getRightDock().getActivePane() const pane2 = pane1.splitRight({copyActiveItem: true}) const pane3 = pane2.splitRight({copyActiveItem: true}) let pane4 = null waitsForPromise(() => - atom.workspace.open(null, {pane: getRightDockActivePane()}).then(editor => editor.setText('An untitled editor.')) + atom.workspace.open(null, {location: 'right'}).then(editor => editor.setText('An untitled editor.')) ) waitsForPromise(() => - atom.workspace.open('b', {pane: getRightDockActivePane()}).then(editor => pane2.activateItem(editor.copy())) + atom.workspace.open('b', {location: 'right'}).then(editor => pane2.activateItem(editor.copy())) ) waitsForPromise(() => - atom.workspace.open('../sample.js', {pane: getRightDockActivePane()}).then(editor => pane3.activateItem(editor)) + atom.workspace.open('../sample.js', {location: 'right'}).then(editor => pane3.activateItem(editor)) ) runs(() => { @@ -150,7 +149,7 @@ describe('Workspace', () => { }) waitsForPromise(() => - atom.workspace.open('../sample.txt', {pane: getRightDockActivePane()}).then(editor => pane4.activateItem(editor)) + atom.workspace.open('../sample.txt', {location: 'right'}).then(editor => pane4.activateItem(editor)) ) runs(() => { diff --git a/src/workspace.js b/src/workspace.js index ef92e0da4..75cb0dd42 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -617,6 +617,12 @@ module.exports = class Workspace extends Model { // activate an existing item for the given URI on any pane. // If `false`, only the active pane will be searched for // an existing item for the same URI. Defaults to `false`. + // * `location` (optional) A {String} containing the name of the location + // in which this item should be opened (one of "left", "right", "bottom", + // or "center"). If omitted, Atom will fall back to the last location in + // which a user has placed an item with the same URI or, if this is a new + // URI, the default location specified by the item. NOTE: This option + // should almost always be omitted to honor user preference. // // Returns a {Promise} that resolves to the {TextEditor} for the file URI. open (uri_, options = {}) { @@ -767,7 +773,7 @@ module.exports = class Workspace extends Model { } async openItem (item, options = {}) { - let {pane, split} = options + let {pane, split, location} = options if (item == null) return undefined if (pane != null && pane.isDestroyed()) return item @@ -779,14 +785,12 @@ module.exports = class Workspace extends Model { paneContainer = this.getPaneContainers().find(container => container.getPanes().includes(pane)) } - // Determine which location to use, unless a split was provided. In that case, make sure it goes - // in the center location (legacy behavior) - let location - if (paneContainer == null && pane == null && split == null && uri != null) { - location = await this.itemLocationStore.load(uri) - } - if (paneContainer == null) { + // Determine which location to use, unless a split was provided. In that case, make sure it goes + // in the center location (legacy behavior) + if (location == null && pane == null && split == null && uri != null) { + location = await this.itemLocationStore.load(uri) + } if (location == null && typeof item.getDefaultLocation === 'function') { location = item.getDefaultLocation() } From 51b40edebd4e4808e14cce1d838aa98e07ae593f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 24 Mar 2017 12:02:49 -0700 Subject: [PATCH 25/37] :arrow_up: packages to fix test failures --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 302b5023d..090b6b190 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "solarized-dark-syntax": "1.1.2", "solarized-light-syntax": "1.1.2", "about": "1.7.5", - "archive-view": "0.63.1", + "archive-view": "0.63.2", "autocomplete-atom-api": "0.10.1", "autocomplete-css": "0.16.1", "autocomplete-html": "0.7.3", @@ -123,7 +123,7 @@ "status-bar": "1.8.5", "styleguide": "0.49.4", "symbols-view": "0.115.3", - "tabs": "0.104.4", + "tabs": "0.104.5", "timecop": "0.36.0", "tree-view": "0.215.3", "update-package-dependencies": "0.11.0", From 93ba6109fa1f8f970d9bd23ebebb2a6246e75e76 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 24 Mar 2017 15:29:46 -0700 Subject: [PATCH 26/37] Create Dock element lazily to be compatible w/ snapshotting --- src/dock.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/dock.js b/src/dock.js index 585b2441d..a171c5d66 100644 --- a/src/dock.js +++ b/src/dock.js @@ -55,11 +55,10 @@ module.exports = class Dock { pane.onDidRemoveItem(this.handleDidRemovePaneItem.bind(this)) }) ) - - this.render(this.state) } getElement () { + if (!this.element) this.render(this.state); return this.element } From 4082b67fb0f68351620fa79f7f839dc483e76610 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 24 Mar 2017 15:35:52 -0700 Subject: [PATCH 27/37] Refactor Workspace.open Signed-off-by: Nathan Sobo --- spec/workspace-spec.js | 10 +- src/workspace.js | 217 +++++++++++++++++++---------------------- 2 files changed, 106 insertions(+), 121 deletions(-) diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js index ae692087f..2237dc33e 100644 --- a/spec/workspace-spec.js +++ b/spec/workspace-spec.js @@ -818,9 +818,13 @@ describe('Workspace', () => { }) ) - it('creates a notification', () => { - const open = () => workspace.open('file1', workspace.getActivePane()) - expect(open).toThrow() + it('rejects the promise', () => { + waitsFor((done) => { + workspace.open('file1').catch(error => { + expect(error.message).toBe('I dont even know what is happening right now!!') + done() + }) + }) }) }) }) diff --git a/src/workspace.js b/src/workspace.js index 75cb0dd42..105820094 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -625,9 +625,8 @@ module.exports = class Workspace extends Model { // should almost always be omitted to honor user preference. // // Returns a {Promise} that resolves to the {TextEditor} for the file URI. - open (uri_, options = {}) { + async open (uri_, options = {}) { const uri = this.project.resolvePath(uri_) - const {searchAllPanes, split} = options if (!atom.config.get('core.allowPendingPaneItems')) { options.pending = false @@ -635,43 +634,110 @@ module.exports = class Workspace extends Model { // Avoid adding URLs as recent documents to work-around this Spotlight crash: // https://github.com/atom/atom/issues/10071 - if ((uri != null) && ((url.parse(uri).protocol == null) || (process.platform === 'win32'))) { + if (uri && (!url.parse(uri).protocol || process.platform === 'win32')) { this.applicationDelegate.addRecentDocument(uri) } - let pane - if (searchAllPanes) { pane = this.paneForURI(uri) } - if (pane == null) { - switch (split) { - case 'left': - pane = this.getActivePane().findLeftmostSibling() - break - case 'right': - pane = this.getActivePane().findRightmostSibling() - break - case 'up': - pane = this.getActivePane().findTopmostSibling() - break - case 'down': - pane = this.getActivePane().findBottommostSibling() - break - default: - pane = this.getActivePane() - break + let pane, item + + // Try to find an existing item with the given URI. + if (uri) { + if (options.pane) { + pane = options.pane + } else if (options.searchAllPanes) { + pane = this.paneForURI(uri) + } else { + + // The `split` option affects where we search for the item. + pane = this.getActivePane() + switch (options.split) { + case 'left': + pane = pane.findLeftmostSibling() + break + case 'right': + pane = pane.findRightmostSibling() + break + case 'up': + pane = pane.findTopmostSibling() + break + case 'down': + pane = pane.findBottommostSibling() + break + } + } + + if (pane) item = pane.itemForURI(uri) + } + + // Create an item if one was not found. + if (!item) { + item = await this.createItemForURI(uri, options) + if (!item) return + + if (options.pane) { + pane = options.pane + } else { + let location = options.location + if (!location && !options.split && uri) { + location = await this.itemLocationStore.load(uri) + } + if (!location && typeof item.getDefaultLocation === 'function') { + location = item.getDefaultLocation() + } + + const container = this.docks[location] || this.getCenter() + pane = container.getActivePane() + switch (options.split) { + case 'left': + pane = pane.findLeftmostSibling() + break + case 'right': + pane = pane.findOrCreateRightmostSibling() + break + case 'up': + pane = pane.findTopmostSibling() + break + case 'down': + pane = pane.findOrCreateBottommostSibling() + break + } } } - let item - if (uri != null && pane != null) { - item = pane.itemForURI(uri) - } - if (item == null) { - item = this.createItemForURI(uri, options) - pane = null + if (!options.pending && (pane.getPendingItem() === item)) { + pane.clearPendingItem() } - return Promise.resolve(item) - .then(item => this.openItem(item, Object.assign({pane, uri}, options))) + pane.addItem(item, options) + this.itemOpened(item) + + if (options.activateItem !== false) { + pane.activateItem(item, {pending: options.pending}) + } + + if (options.activatePane !== false) { + pane.activate() + const container = this.getPaneContainers().find(container => container.getPanes().includes(pane)) + container.activate() + } + + let initialColumn = 0 + let initialLine = 0 + if (!Number.isNaN(options.initialLine)) { + initialLine = options.initialLine + } + if (!Number.isNaN(options.initialColumn)) { + initialColumn = options.initialColumn + } + if (initialLine >= 0 || initialColumn >= 0) { + if (typeof item.setCursorBufferPosition === 'function') { + item.setCursorBufferPosition([initialLine, initialColumn]) + } + } + + const index = pane.getActiveItemIndex() + this.emitter.emit('did-open', {uri, pane, item, index}) + return item } // Open Atom's license in the active pane. @@ -720,16 +786,8 @@ module.exports = class Workspace extends Model { return item } - openURIInPane (uri, pane, options = {}) { - let item - if (uri != null) { - item = pane.itemForURI(uri) - } - if (item == null) { - item = this.createItemForURI(uri, options) - } - return Promise.resolve(item) - .then(item => this.openItem(item, Object.assign({pane, uri}, options))) + openURIInPane (uri, pane) { + return this.open(uri, {pane}) } // Returns a {Promise} that resolves to the {TextEditor} (or other item) for the given URI. @@ -772,83 +830,6 @@ module.exports = class Workspace extends Model { } } - async openItem (item, options = {}) { - let {pane, split, location} = options - - if (item == null) return undefined - if (pane != null && pane.isDestroyed()) return item - - const uri = options.uri == null && typeof item.getURI === 'function' ? item.getURI() : options.uri - - let paneContainer - if (pane != null) { - paneContainer = this.getPaneContainers().find(container => container.getPanes().includes(pane)) - } - - if (paneContainer == null) { - // Determine which location to use, unless a split was provided. In that case, make sure it goes - // in the center location (legacy behavior) - if (location == null && pane == null && split == null && uri != null) { - location = await this.itemLocationStore.load(uri) - } - if (location == null && typeof item.getDefaultLocation === 'function') { - location = item.getDefaultLocation() - } - paneContainer = this.docks[location] || this.getCenter() - } - - if (pane == null) { - pane = paneContainer.getActivePane() - switch (split) { - case 'left': - pane = pane.findLeftmostSibling() - break - case 'right': - pane = pane.findOrCreateRightmostSibling() - break - case 'up': - pane = pane.findTopmostSibling() - break - case 'down': - pane = pane.findOrCreateBottommostSibling() - break - } - } - - if (!options.pending && (pane.getPendingItem() === item)) { - pane.clearPendingItem() - } - - const activatePane = options.activatePane != null ? options.activatePane : true - const activateItem = options.activateItem != null ? options.activateItem : true - this.itemOpened(item) - if (activateItem) { - pane.activateItem(item, {pending: options.pending}) - } - if (activatePane) { - pane.activate() - } - paneContainer.activate() - - let initialColumn = 0 - let initialLine = 0 - if (!Number.isNaN(options.initialLine)) { - initialLine = options.initialLine - } - if (!Number.isNaN(options.initialColumn)) { - initialColumn = options.initialColumn - } - if ((initialLine >= 0) || (initialColumn >= 0)) { - if (typeof item.setCursorBufferPosition === 'function') { - item.setCursorBufferPosition([initialLine, initialColumn]) - } - } - - const index = pane.getActiveItemIndex() - this.emitter.emit('did-open', {uri, pane, item, index}) - return item - } - openTextFile (uri, options) { const filePath = this.project.resolvePath(uri) From bc872143cc28a2b7b99be6999647496dead17d7d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 24 Mar 2017 15:49:35 -0700 Subject: [PATCH 28/37] Avoid duplicate search for pane container in Workspace.open --- src/dock.js | 2 +- src/workspace.js | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/dock.js b/src/dock.js index a171c5d66..9d0fe8247 100644 --- a/src/dock.js +++ b/src/dock.js @@ -58,7 +58,7 @@ module.exports = class Dock { } getElement () { - if (!this.element) this.render(this.state); + if (!this.element) this.render(this.state) return this.element } diff --git a/src/workspace.js b/src/workspace.js index 105820094..24df8ec1f 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -638,7 +638,7 @@ module.exports = class Workspace extends Model { this.applicationDelegate.addRecentDocument(uri) } - let pane, item + let container, pane, item // Try to find an existing item with the given URI. if (uri) { @@ -647,7 +647,6 @@ module.exports = class Workspace extends Model { } else if (options.searchAllPanes) { pane = this.paneForURI(uri) } else { - // The `split` option affects where we search for the item. pane = this.getActivePane() switch (options.split) { @@ -685,7 +684,7 @@ module.exports = class Workspace extends Model { location = item.getDefaultLocation() } - const container = this.docks[location] || this.getCenter() + container = this.docks[location] || this.getCenter() pane = container.getActivePane() switch (options.split) { case 'left': @@ -717,7 +716,9 @@ module.exports = class Workspace extends Model { if (options.activatePane !== false) { pane.activate() - const container = this.getPaneContainers().find(container => container.getPanes().includes(pane)) + if (!container) { + container = this.getPaneContainers().find(container => container.getPanes().includes(pane)) + } container.activate() } From d691c3e5aaf7cd9009dc109b806b2c804e06d200 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Mon, 27 Mar 2017 10:30:55 -0700 Subject: [PATCH 29/37] Docks: Don't change inherited presentation styles --- static/docks.less | 3 --- 1 file changed, 3 deletions(-) diff --git a/static/docks.less b/static/docks.less index c8761707a..04b548e31 100644 --- a/static/docks.less +++ b/static/docks.less @@ -71,9 +71,6 @@ atom-dock { align-items: stretch; width: 100%; height: 100%; - cursor: default; - -webkit-user-select: none; - white-space: nowrap; // The contents of the dock should be "stuck" to the moving edge of the mask, // so it looks like they're sliding in (instead of being unmasked in place). From 13f0c8a977125e413e36f31d89b5863d791a516a Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Mon, 27 Mar 2017 12:04:30 -0700 Subject: [PATCH 30/37] Docks: define handle size in CSS; measure in JS --- src/dock.js | 28 +++++++++++++++++++--------- src/panel-container-element.js | 6 ++++++ static/docks.less | 4 ++++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/dock.js b/src/dock.js index 9d0fe8247..1a409adf7 100644 --- a/src/dock.js +++ b/src/dock.js @@ -7,7 +7,6 @@ const TextEditor = require('./text-editor') const MINIMUM_SIZE = 100 const DEFAULT_INITIAL_SIZE = 300 -const HANDLE_SIZE = 4 const SHOULD_ANIMATE_CLASS = 'atom-dock-should-animate' const OPEN_CLASS = 'atom-dock-open' const RESIZE_HANDLE_RESIZABLE_CLASS = 'atom-dock-resize-handle-resizable' @@ -57,6 +56,12 @@ module.exports = class Dock { ) } + // This method is called explicitly by the object which adds the Dock to the document. + elementAttached () { + // Re-render when the dock is attached to make sure we remeasure sizes defined in CSS. + this.render(this.state) + } + getElement () { if (!this.element) this.render(this.state) return this.element @@ -180,7 +185,7 @@ module.exports = class Dock { const size = Math.max(MINIMUM_SIZE, state.size == null ? this.getInitialSize() : state.size) // We need to change the size of the mask... - this.maskElement.style[this.widthOrHeight] = `${shouldBeVisible ? size : HANDLE_SIZE}px` + this.maskElement.style[this.widthOrHeight] = `${shouldBeVisible ? size : this.resizeHandle.getSize()}px` // ...but the content needs to maintain a constant size. this.wrapperElement.style[this.widthOrHeight] = `${size}px` @@ -289,7 +294,7 @@ module.exports = class Dock { // The area used when detecting "leave" events is actually larger than when detecting entrances. if (includeButtonWidth) { const hoverMargin = 20 - const {width, height} = this.toggleButton.getSize() + const {width, height} = this.toggleButton.getBounds() switch (this.location) { case 'right': bounds.left -= width + hoverMargin @@ -607,8 +612,6 @@ class DockResizeHandle { this.element.classList.add('atom-dock-resize-handle', props.location) this.element.addEventListener('mousedown', this.handleMouseDown) this.element.addEventListener('click', this.handleClick) - const widthOrHeight = getWidthOrHeight(props.location) - this.element.style[widthOrHeight] = `${HANDLE_SIZE}px` this.props = props this.update(props) } @@ -617,6 +620,13 @@ class DockResizeHandle { return this.element } + getSize () { + if (!this.size) { + this.size = this.element.getBoundingClientRect()[getWidthOrHeight(this.props.location)] + } + return this.size + } + update (newProps) { this.props = Object.assign({}, this.props, newProps) @@ -669,11 +679,11 @@ class DockToggleButton { return this.element } - getSize () { - if (this.size == null) { - this.size = this.element.getBoundingClientRect() + getBounds () { + if (this.bounds == null) { + this.bounds = this.element.getBoundingClientRect() } - return this.size + return this.bounds } destroy () { diff --git a/src/panel-container-element.js b/src/panel-container-element.js index f78a2e352..c51321181 100644 --- a/src/panel-container-element.js +++ b/src/panel-container-element.js @@ -9,6 +9,12 @@ class PanelContainerElement extends HTMLElement { this.subscriptions = new CompositeDisposable() } + attachedCallback () { + if (this.model.dock) { + this.model.dock.elementAttached() + } + } + initialize (model, {views}) { this.model = model this.views = views diff --git a/static/docks.less b/static/docks.less index 04b548e31..583409726 100644 --- a/static/docks.less +++ b/static/docks.less @@ -2,6 +2,7 @@ @import 'syntax-variables'; @atom-dock-toggle-button-size: 50px; +@atom-dock-resize-handle-size: 4px; // Dock -------------- @@ -184,6 +185,9 @@ atom-dock { &.left, &.right { cursor: col-resize; } &.bottom { cursor: row-resize; } } + + &.left, &.right { width: @atom-dock-resize-handle-size; } + &.bottom { height: @atom-dock-resize-handle-size; } } // Cursor overlay -------------- From f3c3917825e08739f77ca8122072b3fd6a1a80f9 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Mon, 27 Mar 2017 14:20:47 -0700 Subject: [PATCH 31/37] Don't show the dock toggle button if it's closed and empty --- src/dock.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/dock.js b/src/dock.js index 1a409adf7..19e3528b2 100644 --- a/src/dock.js +++ b/src/dock.js @@ -192,7 +192,9 @@ module.exports = class Dock { this.resizeHandle.update({dockIsOpen: this.state.open}) this.toggleButton.update({ open: shouldBeVisible, - visible: state.hovered || (state.draggingItem && !shouldBeVisible) + // Don't show the toggle button if the dock is closed and empty. + visible: (state.hovered && (this.state.open || this.getPaneItems().length > 0)) || + (state.draggingItem && !shouldBeVisible) }) } @@ -206,7 +208,7 @@ module.exports = class Dock { handleDidRemovePaneItem () { // Hide the dock if you remove the last item. if (this.paneContainer.getPaneItems().length === 0) { - this.setState({open: false}) + this.setState({open: false, hovered: false}) } } From 41953ae7d6fc3d97a8fa0a83b2eba3cb0f1246a8 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Mon, 27 Mar 2017 15:09:32 -0700 Subject: [PATCH 32/37] Only show dock toggle buttons when dragging if item is allowed --- src/dock.js | 14 +++++++++++--- src/workspace-element.js | 6 ++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/dock.js b/src/dock.js index 19e3528b2..707aba2d4 100644 --- a/src/dock.js +++ b/src/dock.js @@ -192,9 +192,11 @@ module.exports = class Dock { this.resizeHandle.update({dockIsOpen: this.state.open}) this.toggleButton.update({ open: shouldBeVisible, - // Don't show the toggle button if the dock is closed and empty. - visible: (state.hovered && (this.state.open || this.getPaneItems().length > 0)) || - (state.draggingItem && !shouldBeVisible) + visible: + // Don't show the toggle button if the dock is closed and empty... + (state.hovered && (this.state.open || this.getPaneItems().length > 0)) || + // ...or if the item can't be dropped in that dock. + (!shouldBeVisible && state.draggingItem && isItemAllowed(state.draggingItem, this.location)) }) } @@ -749,3 +751,9 @@ function rectContainsPoint (rect, point) { point.y <= rect.bottom ) } + +// Is the item allowed in the given location? +function isItemAllowed (item, location) { + if (typeof item.getAllowedLocations !== 'function') return true + return item.getAllowedLocations().includes(location) +} diff --git a/src/workspace-element.js b/src/workspace-element.js index d18708ae4..a2fe39d5a 100644 --- a/src/workspace-element.js +++ b/src/workspace-element.js @@ -138,7 +138,9 @@ class WorkspaceElement extends HTMLElement { handleDragStart (event) { if (!isTab(event.target)) return - this.model.setDraggingItem(true) + const {item} = event.target + if (!item) return + this.model.setDraggingItem(item) window.addEventListener('dragend', this.handleDragEnd, true) window.addEventListener('drop', this.handleDrop, true) } @@ -152,7 +154,7 @@ class WorkspaceElement extends HTMLElement { } dragEnded () { - this.model.setDraggingItem(false) + this.model.setDraggingItem(null) window.removeEventListener('dragend', this.handleDragEnd, true) window.removeEventListener('drop', this.handleDrop, true) } From 401a549bf53b22e0cb13db39fc1abd7429d1558a Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Mon, 27 Mar 2017 15:21:22 -0700 Subject: [PATCH 33/37] Don't open items in disallowed locations --- src/workspace.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/workspace.js b/src/workspace.js index 24df8ec1f..3e94eccaf 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -684,6 +684,9 @@ module.exports = class Workspace extends Model { location = item.getDefaultLocation() } + const allowedLocations = typeof item.getAllowedLocations === 'function' ? item.getAllowedLocations() : ALL_LOCATIONS + location = allowedLocations.includes(location) ? location : allowedLocations[0] + container = this.docks[location] || this.getCenter() pane = container.getActivePane() switch (options.split) { @@ -1620,3 +1623,5 @@ module.exports = class Workspace extends Model { } } } + +const ALL_LOCATIONS = ['center', 'left', 'right', 'bottom'] From 3b23ab44bc0f1e3912e36193204ea322e1c1b815 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Mon, 27 Mar 2017 17:40:35 -0700 Subject: [PATCH 34/37] Add `getLocation()` to PaneContainer class This allows the location to be inspected without having to jump to the DOM and searching for a dock element. --- spec/pane-container-element-spec.coffee | 1 + spec/pane-container-spec.coffee | 1 + spec/pane-element-spec.coffee | 7 ++++++- spec/pane-spec.coffee | 5 ++++- src/dock.js | 1 + src/pane-container.coffee | 4 +++- src/workspace.js | 2 ++ 7 files changed, 18 insertions(+), 3 deletions(-) diff --git a/spec/pane-container-element-spec.coffee b/spec/pane-container-element-spec.coffee index e986e5d6c..55692c8e2 100644 --- a/spec/pane-container-element-spec.coffee +++ b/spec/pane-container-element-spec.coffee @@ -3,6 +3,7 @@ PaneAxisElement = require '../src/pane-axis-element' PaneAxis = require '../src/pane-axis' params = + location: 'center' config: atom.config confirm: atom.confirm.bind(atom) viewRegistry: atom.views diff --git a/spec/pane-container-spec.coffee b/spec/pane-container-spec.coffee index 939df49e5..153ef2bda 100644 --- a/spec/pane-container-spec.coffee +++ b/spec/pane-container-spec.coffee @@ -7,6 +7,7 @@ describe "PaneContainer", -> beforeEach -> confirm = spyOn(atom.applicationDelegate, 'confirm').andReturn(0) params = { + location: 'center', config: atom.config, deserializerManager: atom.deserializers applicationDelegate: atom.applicationDelegate, diff --git a/spec/pane-element-spec.coffee b/spec/pane-element-spec.coffee index f6dc4d535..d1725d11a 100644 --- a/spec/pane-element-spec.coffee +++ b/spec/pane-element-spec.coffee @@ -6,7 +6,12 @@ describe "PaneElement", -> beforeEach -> spyOn(atom.applicationDelegate, "open") - container = new PaneContainer(config: atom.config, confirm: atom.confirm.bind(atom), viewRegistry: atom.views, applicationDelegate: atom.applicationDelegate) + container = new PaneContainer + location: 'center' + config: atom.config + confirm: atom.confirm.bind(atom) + viewRegistry: atom.views + applicationDelegate: atom.applicationDelegate containerElement = atom.views.getView(container) pane = container.getActivePane() paneElement = atom.views.getView(pane) diff --git a/spec/pane-spec.coffee b/spec/pane-spec.coffee index 596b1ecea..53bccbf13 100644 --- a/spec/pane-spec.coffee +++ b/spec/pane-spec.coffee @@ -51,7 +51,10 @@ describe "Pane", -> [container, pane1, pane2] = [] beforeEach -> - container = new PaneContainer(config: atom.config, applicationDelegate: atom.applicationDelegate) + container = new PaneContainer + location: 'center' + config: atom.config + applicationDelegate: atom.applicationDelegate container.getActivePane().splitRight() [pane1, pane2] = container.getPanes() diff --git a/src/dock.js b/src/dock.js index 707aba2d4..47ad9b715 100644 --- a/src/dock.js +++ b/src/dock.js @@ -34,6 +34,7 @@ module.exports = class Dock { this.viewRegistry = params.viewRegistry this.paneContainer = new PaneContainer({ + location: this.location, config: this.config, applicationDelegate: this.applicationDelegate, deserializerManager: this.deserializerManager, diff --git a/src/pane-container.coffee b/src/pane-container.coffee index 20087c564..54d1d6cbe 100644 --- a/src/pane-container.coffee +++ b/src/pane-container.coffee @@ -15,7 +15,7 @@ class PaneContainer extends Model constructor: (params) -> super - {@config, applicationDelegate, notificationManager, deserializerManager, @viewRegistry} = params + {@config, applicationDelegate, notificationManager, deserializerManager, @viewRegistry, @location} = params @emitter = new Emitter @subscriptions = new CompositeDisposable @itemRegistry = new ItemRegistry @@ -27,6 +27,8 @@ class PaneContainer extends Model initialize: -> @monitorActivePaneItem() + getLocation: -> @location + getElement: -> @element ?= new PaneContainerElement().initialize(this, {views: @viewRegistry}) diff --git a/src/workspace.js b/src/workspace.js index 3e94eccaf..1a1786bbc 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -54,6 +54,7 @@ module.exports = class Workspace extends Model { this.destroyedItemURIs = [] this.paneContainer = new PaneContainer({ + location: 'center', config: this.config, applicationDelegate: this.applicationDelegate, notificationManager: this.notificationManager, @@ -111,6 +112,7 @@ module.exports = class Workspace extends Model { _.values(this.panelContainers).forEach(panelContainer => { panelContainer.destroy() }) this.paneContainer = new PaneContainer({ + location: 'center', config: this.config, applicationDelegate: this.applicationDelegate, notificationManager: this.notificationManager, From 08e8975a103f616d50715ab82912367abb82f109 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Mon, 27 Mar 2017 18:17:02 -0700 Subject: [PATCH 35/37] Always show the dock when an item is dropped into it Previously, we were only showing it when going from 0 -> 1 items (which is a bug). --- src/dock.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/dock.js b/src/dock.js index 47ad9b715..c04bbcb50 100644 --- a/src/dock.js +++ b/src/dock.js @@ -203,9 +203,7 @@ module.exports = class Dock { handleDidAddPaneItem () { // Show the dock if you drop an item into it. - if (this.paneContainer.getPaneItems().length === 1) { - this.setState({open: true}) - } + this.setState({open: true}) } handleDidRemovePaneItem () { From 45bd466384930dbc9a126bfd91f655456629afce Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 28 Mar 2017 15:00:13 -0600 Subject: [PATCH 36/37] =?UTF-8?q?Don=E2=80=99t=20add=20item=20in=20Workspa?= =?UTF-8?q?ce.open=20if=20activateItem=20is=20false?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We thought it was a bug that activateItem: false caused the item not to be added, but it turned out there were package tests that depended on this behavior. Ideally, we should have an addItem option that exhibits this behavior instead. Signed-off-by: Max Brunsfeld --- src/workspace.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/workspace.js b/src/workspace.js index 1a1786bbc..8d346e9bd 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -712,7 +712,6 @@ module.exports = class Workspace extends Model { pane.clearPendingItem() } - pane.addItem(item, options) this.itemOpened(item) if (options.activateItem !== false) { From c7a47558084b200d1db3dbb79dba4ebca0bbe10e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 29 Mar 2017 09:27:21 -0600 Subject: [PATCH 37/37] If Workspace.open finds existing item, yield event loop This ensures that the function always behaves asynchronously regardless of the state of the workspace. /cc @maxbrunsfeld Signed-off-by: Antonio Scandurra --- src/workspace.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/workspace.js b/src/workspace.js index 8d346e9bd..6c9621b28 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -670,8 +670,12 @@ module.exports = class Workspace extends Model { if (pane) item = pane.itemForURI(uri) } - // Create an item if one was not found. - if (!item) { + // If an item is already present, yield the event loop to ensure this method + // is consistently asynchronous regardless of the workspace state. If no + // item is present, create one. + if (item) { + await Promise.resolve() + } else { item = await this.createItemForURI(uri, options) if (!item) return