mirror of
https://github.com/atom/atom.git
synced 2026-01-23 22:08:08 -05:00
@@ -86,7 +86,7 @@
|
||||
"solarized-dark-syntax": "1.1.2",
|
||||
"solarized-light-syntax": "1.1.2",
|
||||
"about": "1.7.6",
|
||||
"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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2,6 +2,13 @@ PaneContainer = require '../src/pane-container'
|
||||
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
|
||||
applicationDelegate: atom.applicationDelegate
|
||||
|
||||
describe "PaneContainerElement", ->
|
||||
describe "when panes are added or removed", ->
|
||||
it "inserts or removes resize elements", ->
|
||||
@@ -42,7 +49,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 +65,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 +91,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 +208,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 +265,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'),
|
||||
|
||||
@@ -7,9 +7,11 @@ describe "PaneContainer", ->
|
||||
beforeEach ->
|
||||
confirm = spyOn(atom.applicationDelegate, 'confirm').andReturn(0)
|
||||
params = {
|
||||
location: 'center',
|
||||
config: atom.config,
|
||||
deserializerManager: atom.deserializers
|
||||
applicationDelegate: atom.applicationDelegate
|
||||
applicationDelegate: atom.applicationDelegate,
|
||||
viewRegistry: atom.views
|
||||
}
|
||||
|
||||
describe "serialization", ->
|
||||
|
||||
@@ -6,7 +6,12 @@ describe "PaneElement", ->
|
||||
beforeEach ->
|
||||
spyOn(atom.applicationDelegate, "open")
|
||||
|
||||
container = new PaneContainer(config: atom.config, confirm: atom.confirm.bind(atom))
|
||||
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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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.itemLocationStore.clear())
|
||||
})
|
||||
|
||||
afterEach(() => temp.cleanupSync())
|
||||
@@ -117,6 +119,62 @@ 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 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, {location: 'right'}).then(editor => editor.setText('An untitled editor.'))
|
||||
)
|
||||
|
||||
waitsForPromise(() =>
|
||||
atom.workspace.open('b', {location: 'right'}).then(editor => pane2.activateItem(editor.copy()))
|
||||
)
|
||||
|
||||
waitsForPromise(() =>
|
||||
atom.workspace.open('../sample.js', {location: 'right'}).then(editor => pane3.activateItem(editor))
|
||||
)
|
||||
|
||||
runs(() => {
|
||||
pane3.activeItem.setCursorScreenPosition([2, 4])
|
||||
pane4 = pane2.splitDown()
|
||||
})
|
||||
|
||||
waitsForPromise(() =>
|
||||
atom.workspace.open('../sample.txt', {location: 'right'}).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)', () => {
|
||||
@@ -202,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', () => {
|
||||
@@ -218,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.itemLocationStore, '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.itemLocationStore, '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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -249,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', () => {
|
||||
@@ -685,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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1986,24 +2123,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()
|
||||
|
||||
@@ -39,6 +39,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'
|
||||
@@ -49,9 +50,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.
|
||||
@@ -274,6 +273,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)
|
||||
@@ -288,12 +288,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: ->
|
||||
|
||||
758
src/dock.js
Normal file
758
src/dock.js
Normal file
@@ -0,0 +1,758 @@
|
||||
'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 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, 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)
|
||||
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({
|
||||
location: this.location,
|
||||
config: this.config,
|
||||
applicationDelegate: this.applicationDelegate,
|
||||
deserializerManager: this.deserializerManager,
|
||||
notificationManager: this.notificationManager,
|
||||
viewRegistry: 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))
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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})
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// 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.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.toggleButton.getElement())
|
||||
}
|
||||
|
||||
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 : this.resizeHandle.getSize()}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:
|
||||
// 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))
|
||||
})
|
||||
}
|
||||
|
||||
handleDidAddPaneItem () {
|
||||
// Show the dock if you drop an item into it.
|
||||
this.setState({open: true})
|
||||
}
|
||||
|
||||
handleDidRemovePaneItem () {
|
||||
// Hide the dock if you remove the last item.
|
||||
if (this.paneContainer.getPaneItems().length === 0) {
|
||||
this.setState({open: false, hovered: 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 hoverMargin = 20
|
||||
const {width, height} = this.toggleButton.getBounds()
|
||||
switch (this.location) {
|
||||
case 'right':
|
||||
bounds.left -= width + hoverMargin
|
||||
break
|
||||
case 'bottom':
|
||||
bounds.top -= height + hoverMargin
|
||||
break
|
||||
case 'left':
|
||||
bounds.right += width + hoverMargin
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
/*
|
||||
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()
|
||||
}
|
||||
|
||||
// Extended: Get the active {Pane}.
|
||||
//
|
||||
// Returns a {Pane}.
|
||||
getActivePane () {
|
||||
return this.paneContainer.getActivePane()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
this.props = props
|
||||
this.update(props)
|
||||
}
|
||||
|
||||
getElement () {
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
getElement () {
|
||||
return this.element
|
||||
}
|
||||
|
||||
getBounds () {
|
||||
if (this.bounds == null) {
|
||||
this.bounds = this.element.getBoundingClientRect()
|
||||
}
|
||||
return this.bounds
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
// Is the item allowed in the given location?
|
||||
function isItemAllowed (item, location) {
|
||||
if (typeof item.getAllowedLocations !== 'function') return true
|
||||
return item.getAllowedLocations().includes(location)
|
||||
}
|
||||
@@ -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,18 +15,23 @@ class PaneContainer extends Model
|
||||
constructor: (params) ->
|
||||
super
|
||||
|
||||
{@config, applicationDelegate, notificationManager, deserializerManager} = params
|
||||
{@config, applicationDelegate, notificationManager, deserializerManager, @viewRegistry, @location} = 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())
|
||||
@monitorPaneItems()
|
||||
|
||||
initialize: ->
|
||||
@monitorActivePaneItem()
|
||||
|
||||
getLocation: -> @location
|
||||
|
||||
getElement: ->
|
||||
@element ?= new PaneContainerElement().initialize(this, {views: @viewRegistry})
|
||||
|
||||
serialize: (params) ->
|
||||
deserializer: 'PaneContainer'
|
||||
version: @serializationVersion
|
||||
|
||||
@@ -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)
|
||||
@@ -841,17 +846,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 +874,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()
|
||||
|
||||
@@ -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
|
||||
@@ -19,6 +25,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.model.dock.getElement())
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
|
||||
@@ -57,6 +57,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()
|
||||
|
||||
286
src/workspace-center.js
Normal file
286
src/workspace-center.js
Normal file
@@ -0,0 +1,286 @@
|
||||
'use strict'
|
||||
|
||||
const TextEditor = require('./text-editor')
|
||||
|
||||
module.exports = class WorkspaceCenter {
|
||||
constructor (paneContainer) {
|
||||
this.paneContainer = paneContainer
|
||||
}
|
||||
|
||||
activate () {}
|
||||
|
||||
getLocation () {
|
||||
return 'center'
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Event Subscription
|
||||
*/
|
||||
|
||||
// Essential: Invoke the given callback with all current and future text
|
||||
// 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
|
||||
// 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 center.
|
||||
//
|
||||
// * `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 center.
|
||||
//
|
||||
// * `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
|
||||
// center.
|
||||
//
|
||||
// * `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 center.
|
||||
//
|
||||
// * `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 center.
|
||||
//
|
||||
// * `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 center.
|
||||
//
|
||||
// * `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 center.
|
||||
//
|
||||
// * `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 center.
|
||||
//
|
||||
// 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 center.
|
||||
//
|
||||
// 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 center.
|
||||
//
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,88 @@ 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
|
||||
const {item} = event.target
|
||||
if (!item) return
|
||||
this.model.setDraggingItem(item)
|
||||
window.addEventListener('dragend', this.handleDragEnd, true)
|
||||
window.addEventListener('drop', this.handleDrop, true)
|
||||
}
|
||||
|
||||
handleDragEnd (event) {
|
||||
this.dragEnded()
|
||||
}
|
||||
|
||||
handleDrop (event) {
|
||||
this.dragEnded()
|
||||
}
|
||||
|
||||
dragEnded () {
|
||||
this.model.setDraggingItem(null)
|
||||
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 +279,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
|
||||
}
|
||||
|
||||
423
src/workspace.js
423
src/workspace.js
@@ -1,4 +1,4 @@
|
||||
'use strict'
|
||||
'use babel'
|
||||
|
||||
const _ = require('underscore-plus')
|
||||
const url = require('url')
|
||||
@@ -7,12 +7,15 @@ 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 StateStore = require('./state-store')
|
||||
const TextEditor = require('./text-editor')
|
||||
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.
|
||||
@@ -42,27 +45,39 @@ 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.itemLocationStore = new StateStore('AtomPreviousItemLocations', 1)
|
||||
|
||||
this.emitter = new Emitter()
|
||||
this.openers = []
|
||||
this.destroyedItemURIs = []
|
||||
|
||||
this.paneContainer = new PaneContainer({
|
||||
location: 'center',
|
||||
config: this.config,
|
||||
applicationDelegate: this.applicationDelegate,
|
||||
notificationManager: this.notificationManager,
|
||||
deserializerManager: this.deserializerManager
|
||||
deserializerManager: this.deserializerManager,
|
||||
viewRegistry: this.viewRegistry
|
||||
})
|
||||
this.paneContainer.onDidDestroyPaneItem(this.didDestroyPaneItem)
|
||||
|
||||
this.defaultDirectorySearcher = new DefaultDirectorySearcher()
|
||||
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,7 +88,19 @@ module.exports = class Workspace extends Model {
|
||||
|
||||
initialize () {
|
||||
this.paneContainer.initialize()
|
||||
this.didChangeActivePaneItem()
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -85,18 +112,27 @@ 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,
|
||||
deserializerManager: this.deserializerManager
|
||||
deserializerManager: this.deserializerManager,
|
||||
viewRegistry: this.viewRegistry
|
||||
})
|
||||
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'})
|
||||
@@ -113,6 +149,7 @@ module.exports = class Workspace extends Model {
|
||||
this.subscribeToActiveItem()
|
||||
this.subscribeToFontSize()
|
||||
this.subscribeToAddedItems()
|
||||
this.subscribeToMovedItems()
|
||||
}
|
||||
|
||||
consumeServices ({serviceHub}) {
|
||||
@@ -130,7 +167,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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +188,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 () {
|
||||
@@ -176,6 +224,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.project.onDidChangePaths(this.updateWindowTitle)
|
||||
this.onDidChangeActivePaneItem(this.didChangeActivePaneItem)
|
||||
@@ -232,6 +293,19 @@ module.exports = class Workspace extends Model {
|
||||
})
|
||||
}
|
||||
|
||||
subscribeToMovedItems () {
|
||||
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())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Updates the application's title and proxy icon based on whichever file is
|
||||
// open.
|
||||
updateWindowTitle () {
|
||||
@@ -313,7 +387,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.getPaneContainers().map(container => container.observePaneItems(callback))
|
||||
)
|
||||
}
|
||||
|
||||
// Essential: Invoke the given callback when the active pane item changes.
|
||||
//
|
||||
@@ -380,7 +458,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.getPaneContainers().map(container => container.onDidAddPane(callback))
|
||||
)
|
||||
}
|
||||
|
||||
// Extended: Invoke the given callback before a pane is destroyed in the
|
||||
// workspace.
|
||||
@@ -390,7 +472,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.getPaneContainers().map(container => container.onWillDestroyPane(callback))
|
||||
)
|
||||
}
|
||||
|
||||
// Extended: Invoke the given callback when a pane is destroyed in the
|
||||
// workspace.
|
||||
@@ -400,7 +486,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.getPaneContainers().map(container => container.onDidDestroyPane(callback))
|
||||
)
|
||||
}
|
||||
|
||||
// Extended: Invoke the given callback with all current and future panes in the
|
||||
// workspace.
|
||||
@@ -410,7 +500,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.getPaneContainers().map(container => container.observePanes(callback))
|
||||
)
|
||||
}
|
||||
|
||||
// Extended: Invoke the given callback when the active pane changes.
|
||||
//
|
||||
@@ -440,7 +534,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.getPaneContainers().map(container => container.onDidAddPaneItem(callback))
|
||||
)
|
||||
}
|
||||
|
||||
// Extended: Invoke the given callback when a pane item is about to be
|
||||
// destroyed, before the user is prompted to save it.
|
||||
@@ -453,7 +551,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.getPaneContainers().map(container => container.onWillDestroyPaneItem(callback))
|
||||
)
|
||||
}
|
||||
|
||||
// Extended: Invoke the given callback when a pane item is destroyed.
|
||||
//
|
||||
@@ -465,7 +567,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.getPaneContainers().map(container => container.onDidDestroyPaneItem(callback))
|
||||
)
|
||||
}
|
||||
|
||||
// Extended: Invoke the given callback when a text editor is added to the
|
||||
// workspace.
|
||||
@@ -513,11 +619,16 @@ 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 = {}) {
|
||||
async open (uri_, options = {}) {
|
||||
const uri = this.project.resolvePath(uri_)
|
||||
const {searchAllPanes, split} = options
|
||||
|
||||
if (!atom.config.get('core.allowPendingPaneItems')) {
|
||||
options.pending = false
|
||||
@@ -525,42 +636,117 @@ 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().findOrCreateRightmostSibling()
|
||||
break
|
||||
case 'up':
|
||||
pane = this.getActivePane().findTopmostSibling()
|
||||
break
|
||||
case 'down':
|
||||
pane = this.getActivePane().findOrCreateBottommostSibling()
|
||||
break
|
||||
default:
|
||||
pane = this.getActivePane()
|
||||
break
|
||||
let container, 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
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 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) {
|
||||
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) {
|
||||
item = pane.itemForURI(uri)
|
||||
}
|
||||
if (item == null) {
|
||||
item = this.createItemForURI(uri, options)
|
||||
if (!options.pending && (pane.getPendingItem() === item)) {
|
||||
pane.clearPendingItem()
|
||||
}
|
||||
|
||||
return Promise.resolve(item)
|
||||
.then(item => this.openItem(item, Object.assign({pane, uri}, options)))
|
||||
this.itemOpened(item)
|
||||
|
||||
if (options.activateItem !== false) {
|
||||
pane.activateItem(item, {pending: options.pending})
|
||||
}
|
||||
|
||||
if (options.activatePane !== false) {
|
||||
pane.activate()
|
||||
if (!container) {
|
||||
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.
|
||||
@@ -609,16 +795,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.
|
||||
@@ -661,46 +839,6 @@ module.exports = class Workspace extends Model {
|
||||
}
|
||||
}
|
||||
|
||||
openItem (item, options = {}) {
|
||||
const {pane} = options
|
||||
|
||||
if (item == null) return undefined
|
||||
if (pane.isDestroyed()) return item
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
openTextFile (uri, options) {
|
||||
const filePath = this.project.resolvePath(uri)
|
||||
|
||||
@@ -821,7 +959,7 @@ module.exports = class Workspace extends Model {
|
||||
//
|
||||
// Returns an {Array} of items.
|
||||
getPaneItems () {
|
||||
return this.paneContainer.getPaneItems()
|
||||
return _.flatten(this.getPaneContainers().map(container => container.getPaneItems()))
|
||||
}
|
||||
|
||||
// Essential: Get the active {Pane}'s active item.
|
||||
@@ -849,11 +987,15 @@ module.exports = class Workspace extends Model {
|
||||
|
||||
// Save all pane items.
|
||||
saveAll () {
|
||||
return this.paneContainer.saveAll()
|
||||
this.getPaneContainers().forEach(container => {
|
||||
container.saveAll()
|
||||
})
|
||||
}
|
||||
|
||||
confirmClose (options) {
|
||||
return this.paneContainer.confirmClose(options)
|
||||
return this.getPaneContainers()
|
||||
.map(container => container.confirmClose(options))
|
||||
.every(saved => saved)
|
||||
}
|
||||
|
||||
// Save the active pane item.
|
||||
@@ -891,7 +1033,7 @@ module.exports = class Workspace extends Model {
|
||||
//
|
||||
// Returns an {Array} of {Pane}s.
|
||||
getPanes () {
|
||||
return this.paneContainer.getPanes()
|
||||
return _.flatten(this.getPaneContainers().map(container => container.getPanes()))
|
||||
}
|
||||
|
||||
// Extended: Get the active {Pane}.
|
||||
@@ -917,7 +1059,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.getPaneContainers()) {
|
||||
const pane = location.paneForURI(uri)
|
||||
if (pane != null) {
|
||||
return pane
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extended: Get the {Pane} containing the given item.
|
||||
@@ -926,7 +1073,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.getPaneContainers()) {
|
||||
const pane = location.paneForItem(item)
|
||||
if (pane != null) {
|
||||
return pane
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Destroy (close) the active pane.
|
||||
@@ -942,7 +1094,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()
|
||||
@@ -1013,6 +1165,61 @@ module.exports = class Workspace extends Model {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Pane Locations
|
||||
*/
|
||||
|
||||
getCenter () {
|
||||
return this.center
|
||||
}
|
||||
|
||||
getLeftDock () {
|
||||
return this.docks.left
|
||||
}
|
||||
|
||||
getRightDock () {
|
||||
return this.docks.right
|
||||
}
|
||||
|
||||
getBottomDock () {
|
||||
return this.docks.bottom
|
||||
}
|
||||
|
||||
getPaneContainers () {
|
||||
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.getPaneContainers()) {
|
||||
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
|
||||
|
||||
@@ -1421,3 +1628,5 @@ module.exports = class Workspace extends Model {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ALL_LOCATIONS = ['center', 'left', 'right', 'bottom']
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
// Core components
|
||||
@import "cursors";
|
||||
@import "panels";
|
||||
@import "docks";
|
||||
@import "panes";
|
||||
@import "syntax";
|
||||
@import "text-editor-light";
|
||||
|
||||
215
static/docks.less
Normal file
215
static/docks.less
Normal file
@@ -0,0 +1,215 @@
|
||||
@import 'ui-variables';
|
||||
@import 'syntax-variables';
|
||||
|
||||
@atom-dock-toggle-button-size: 50px;
|
||||
@atom-dock-resize-handle-size: 4px;
|
||||
|
||||
// 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%;
|
||||
|
||||
// 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; }
|
||||
}
|
||||
|
||||
&.left, &.right { width: @atom-dock-resize-handle-size; }
|
||||
&.bottom { height: @atom-dock-resize-handle-size; }
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user