mirror of
https://github.com/atom/atom.git
synced 2026-04-06 03:02:13 -04:00
Add Dock component
This commit is contained in:
506
src/dock.js
Normal file
506
src/dock.js
Normal file
@@ -0,0 +1,506 @@
|
||||
'use strict'
|
||||
|
||||
const _ = require('underscore-plus')
|
||||
const {CompositeDisposable} = require('event-kit')
|
||||
const PaneContainer = require('./pane-container')
|
||||
const TextEditor = require('./text-editor')
|
||||
|
||||
const MINIMUM_SIZE = 100
|
||||
const DEFAULT_INITIAL_SIZE = 300
|
||||
const HANDLE_SIZE = 4
|
||||
const SHOULD_ANIMATE_CLASS = 'atom-dock-should-animate'
|
||||
const OPEN_CLASS = 'atom-dock-open'
|
||||
const RESIZE_HANDLE_RESIZABLE_CLASS = 'atom-dock-resize-handle-resizable'
|
||||
const TOGGLE_BUTTON_VISIBLE_CLASS = 'atom-dock-toggle-button-visible'
|
||||
const CURSOR_OVERLAY_VISIBLE_CLASS = 'atom-dock-cursor-overlay-visible'
|
||||
|
||||
// Extended: A container at the edges of the editor window capable of holding items.
|
||||
// You should not create a `Dock` directly, instead use {Workspace::open}.
|
||||
module.exports = class Dock {
|
||||
constructor (params) {
|
||||
this.handleResizeHandleDragStart = this.handleResizeHandleDragStart.bind(this)
|
||||
this.handleMouseMove = this.handleMouseMove.bind(this)
|
||||
this.handleMouseUp = this.handleMouseUp.bind(this)
|
||||
this.handleDrag = _.throttle(this.handleDrag.bind(this), 30)
|
||||
this.handleDragEnd = this.handleDragEnd.bind(this)
|
||||
|
||||
this.location = params.location
|
||||
this.widthOrHeight = getWidthOrHeight(this.location)
|
||||
this.config = params.config
|
||||
this.applicationDelegate = params.applicationDelegate
|
||||
this.deserializerManager = params.deserializerManager
|
||||
this.notificationManager = params.notificationManager
|
||||
this.viewRegistry = params.viewRegistry
|
||||
|
||||
this.paneContainer = new PaneContainer({
|
||||
config: this.config,
|
||||
applicationDelegate: this.applicationDelegate,
|
||||
deserializerManager: this.deserializerManager,
|
||||
notificationManager: this.notificationManager,
|
||||
views: this.viewRegistry
|
||||
})
|
||||
|
||||
this.state = {
|
||||
open: false,
|
||||
shouldAnimate: false
|
||||
}
|
||||
|
||||
this.subscriptions = new CompositeDisposable(
|
||||
this.paneContainer.observePanes(pane => {
|
||||
pane.onDidAddItem(this.handleDidAddPaneItem.bind(this))
|
||||
}),
|
||||
this.paneContainer.observePanes(pane => {
|
||||
pane.onDidRemoveItem(this.handleDidRemovePaneItem.bind(this))
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// FIXME(matthewwithanm: This is kinda gross. We need to get a view for the pane container so we
|
||||
// have to make sure that this is called after its view provider is registered. But we really
|
||||
// don't have any guarantees about that.
|
||||
getElement () {
|
||||
this.render(this.state)
|
||||
return this.element
|
||||
}
|
||||
|
||||
getLocation () {
|
||||
return this.location
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.subscriptions.dispose()
|
||||
this.paneContainer.destroy()
|
||||
this.resizeHandle.destroy()
|
||||
this.toggleButton.destroy()
|
||||
window.removeEventListener('mousemove', this.handleMouseMove)
|
||||
window.removeEventListener('mouseup', this.handleMouseUp)
|
||||
window.removeEventListener('drag', this.handleDrag)
|
||||
window.removeEventListener('dragend', this.handleDragEnd)
|
||||
}
|
||||
|
||||
setHovered (hovered) {
|
||||
if (hovered === this.state.hovered) return
|
||||
this.setState({hovered})
|
||||
}
|
||||
|
||||
setDraggingItem (draggingItem) {
|
||||
if (draggingItem === this.state.draggingItem) return
|
||||
this.setState({draggingItem})
|
||||
}
|
||||
|
||||
toggle () {
|
||||
this.setState({open: !this.state.open})
|
||||
}
|
||||
|
||||
setState (newState) {
|
||||
const prevState = this.state
|
||||
const nextState = Object.assign({}, prevState, newState)
|
||||
|
||||
// Update the `shouldAnimate` state. This needs to be written to the DOM before updating the
|
||||
// class that changes the animated property. Normally we'd have to defer the class change a
|
||||
// frame to ensure the property is animated (or not) appropriately, however we luck out in this
|
||||
// case because the drag start always happens before the item is dragged into the toggle button.
|
||||
if (nextState.open !== prevState.open) {
|
||||
// Never animate toggling visiblity...
|
||||
nextState.shouldAnimate = false
|
||||
} else if (!nextState.open && nextState.draggingItem && !prevState.draggingItem) {
|
||||
// ...but do animate if you start dragging while the panel is hidden.
|
||||
nextState.shouldAnimate = true
|
||||
}
|
||||
|
||||
this.state = nextState
|
||||
this.render(this.state)
|
||||
}
|
||||
|
||||
render (state) {
|
||||
if (this.element == null) {
|
||||
this.element = document.createElement('atom-dock')
|
||||
this.element.classList.add(this.location)
|
||||
this.innerElement = document.createElement('div')
|
||||
this.innerElement.classList.add('atom-dock-inner', this.location)
|
||||
this.maskElement = document.createElement('div')
|
||||
this.maskElement.classList.add('atom-dock-mask')
|
||||
this.wrapperElement = document.createElement('div')
|
||||
this.wrapperElement.classList.add('atom-dock-content-wrapper', this.location)
|
||||
this.resizeHandle = new DockResizeHandle({
|
||||
location: this.location,
|
||||
onResizeStart: this.handleResizeHandleDragStart,
|
||||
toggle: this.toggle.bind(this)
|
||||
})
|
||||
this.toggleButton = new DockToggleButton({
|
||||
onDragEnter: this.handleToggleButtonDragEnter.bind(this),
|
||||
location: this.location,
|
||||
toggle: this.toggle.bind(this)
|
||||
})
|
||||
this.cursorOverlayElement = document.createElement('div')
|
||||
this.cursorOverlayElement.classList.add('atom-dock-cursor-overlay', this.location)
|
||||
|
||||
// Add the children to the DOM tree
|
||||
this.element.appendChild(this.innerElement)
|
||||
this.innerElement.appendChild(this.maskElement)
|
||||
this.maskElement.appendChild(this.wrapperElement)
|
||||
this.wrapperElement.appendChild(this.viewRegistry.getView(this.resizeHandle))
|
||||
this.wrapperElement.appendChild(this.viewRegistry.getView(this.paneContainer))
|
||||
this.wrapperElement.appendChild(this.cursorOverlayElement)
|
||||
// The toggle button must be rendered outside the mask because (1) it shouldn't be masked and
|
||||
// (2) if we made the mask larger to avoid masking it, the mask would block mouse events.
|
||||
this.innerElement.appendChild(this.viewRegistry.getView(this.toggleButton))
|
||||
}
|
||||
|
||||
if (state.open) {
|
||||
this.innerElement.classList.add(OPEN_CLASS)
|
||||
} else {
|
||||
this.innerElement.classList.remove(OPEN_CLASS)
|
||||
}
|
||||
|
||||
if (state.shouldAnimate) {
|
||||
this.maskElement.classList.add(SHOULD_ANIMATE_CLASS)
|
||||
} else {
|
||||
this.maskElement.classList.remove(SHOULD_ANIMATE_CLASS)
|
||||
}
|
||||
|
||||
if (state.resizing) {
|
||||
this.cursorOverlayElement.classList.add(CURSOR_OVERLAY_VISIBLE_CLASS)
|
||||
} else {
|
||||
this.cursorOverlayElement.classList.remove(CURSOR_OVERLAY_VISIBLE_CLASS)
|
||||
}
|
||||
|
||||
const shouldBeVisible = state.open || state.showDropTarget
|
||||
const size = Math.max(MINIMUM_SIZE, state.size == null ? this.getInitialSize() : state.size)
|
||||
|
||||
// We need to change the size of the mask...
|
||||
this.maskElement.style[this.widthOrHeight] = `${shouldBeVisible ? size : HANDLE_SIZE}px`
|
||||
// ...but the content needs to maintain a constant size.
|
||||
this.wrapperElement.style[this.widthOrHeight] = `${size}px`
|
||||
|
||||
this.resizeHandle.update({dockIsOpen: this.state.open})
|
||||
this.toggleButton.update({
|
||||
open: shouldBeVisible,
|
||||
visible: state.hovered || (state.draggingItem && !shouldBeVisible)
|
||||
})
|
||||
}
|
||||
|
||||
handleDidAddPaneItem () {
|
||||
// Show the dock if you drop an item into it.
|
||||
if (this.paneContainer.getPaneItems().length === 1) {
|
||||
this.setState({open: true})
|
||||
}
|
||||
}
|
||||
|
||||
handleDidRemovePaneItem () {
|
||||
// Hide the dock if you remove the last item.
|
||||
if (this.paneContainer.getPaneItems().length === 0) {
|
||||
this.setState({open: false})
|
||||
}
|
||||
}
|
||||
|
||||
handleResizeHandleDragStart () {
|
||||
window.addEventListener('mousemove', this.handleMouseMove)
|
||||
window.addEventListener('mouseup', this.handleMouseUp)
|
||||
this.setState({resizing: true})
|
||||
}
|
||||
|
||||
handleMouseMove (event) {
|
||||
if (event.buttons === 0) { // We missed the mouseup event. For some reason it happens on Windows
|
||||
this.handleMouseUp(event)
|
||||
return
|
||||
}
|
||||
|
||||
let size = 0
|
||||
switch (this.location) {
|
||||
case 'left':
|
||||
size = event.pageX - this.element.getBoundingClientRect().left
|
||||
break
|
||||
case 'bottom':
|
||||
size = this.element.getBoundingClientRect().bottom - event.pageY
|
||||
break
|
||||
case 'right':
|
||||
size = this.element.getBoundingClientRect().right - event.pageX
|
||||
break
|
||||
}
|
||||
this.setState({size})
|
||||
}
|
||||
|
||||
handleMouseUp (event) {
|
||||
window.removeEventListener('mousemove', this.handleMouseMove)
|
||||
window.removeEventListener('mouseup', this.handleMouseUp)
|
||||
this.setState({resizing: false})
|
||||
}
|
||||
|
||||
handleToggleButtonDragEnter () {
|
||||
this.setState({showDropTarget: true})
|
||||
window.addEventListener('drag', this.handleDrag)
|
||||
window.addEventListener('dragend', this.handleDragEnd)
|
||||
}
|
||||
|
||||
handleDrag (event) {
|
||||
if (!this.pointWithinHoverArea({x: event.pageX, y: event.pageY}, false)) {
|
||||
this.draggedOut()
|
||||
}
|
||||
}
|
||||
|
||||
handleDragEnd () {
|
||||
this.draggedOut()
|
||||
}
|
||||
|
||||
draggedOut () {
|
||||
this.setState({showDropTarget: false})
|
||||
window.removeEventListener('drag', this.handleDrag)
|
||||
window.removeEventListener('dragend', this.handleDragEnd)
|
||||
}
|
||||
|
||||
// Determine whether the cursor is within the dock hover area. This isn't as simple as just using
|
||||
// mouseenter/leave because we want to be a little more forgiving. For example, if the cursor is
|
||||
// over the footer, we want to show the bottom dock's toggle button.
|
||||
pointWithinHoverArea (point, includeButtonWidth) {
|
||||
const dockBounds = this.innerElement.getBoundingClientRect()
|
||||
// Copy the bounds object since we can't mutate it.
|
||||
const bounds = {
|
||||
top: dockBounds.top,
|
||||
right: dockBounds.right,
|
||||
bottom: dockBounds.bottom,
|
||||
left: dockBounds.left
|
||||
}
|
||||
|
||||
// Include all panels that are closer to the edge than the dock in our calculations.
|
||||
switch (this.location) {
|
||||
case 'right':
|
||||
bounds.right = Number.POSITIVE_INFINITY
|
||||
break
|
||||
case 'bottom':
|
||||
bounds.bottom = Number.POSITIVE_INFINITY
|
||||
break
|
||||
case 'left':
|
||||
bounds.left = 0
|
||||
break
|
||||
}
|
||||
|
||||
// The area used when detecting "leave" events is actually larger than when detecting entrances.
|
||||
if (includeButtonWidth) {
|
||||
const affordance = 20
|
||||
const toggleButtonSize = 50 / 2 // This needs to match the value in the CSS.
|
||||
switch (this.location) {
|
||||
case 'right':
|
||||
bounds.left -= toggleButtonSize + affordance
|
||||
break
|
||||
case 'bottom':
|
||||
bounds.top -= toggleButtonSize + affordance
|
||||
break
|
||||
case 'left':
|
||||
bounds.right += toggleButtonSize + affordance
|
||||
break
|
||||
}
|
||||
}
|
||||
return rectContainsPoint(bounds, point)
|
||||
}
|
||||
|
||||
getInitialSize () {
|
||||
let initialSize
|
||||
// The item may not have been activated yet. If that's the case, just use the first item.
|
||||
const activePaneItem = this.paneContainer.getActivePaneItem() || this.paneContainer.getPaneItems()[0]
|
||||
if (activePaneItem != null) {
|
||||
initialSize = getPreferredInitialSize(activePaneItem, this.location)
|
||||
}
|
||||
return initialSize == null ? DEFAULT_INITIAL_SIZE : initialSize
|
||||
}
|
||||
|
||||
// PaneContainer-delegating methods
|
||||
|
||||
getPanes () {
|
||||
return this.paneContainer.getPanes()
|
||||
}
|
||||
|
||||
observePanes (fn) {
|
||||
return this.paneContainer.observePanes(fn)
|
||||
}
|
||||
|
||||
onDidAddPane (fn) {
|
||||
return this.paneContainer.onDidAddPane(fn)
|
||||
}
|
||||
|
||||
onWillDestroyPane (fn) {
|
||||
return this.paneContainer.onWillDestroyPane(fn)
|
||||
}
|
||||
|
||||
onDidDestroyPane (fn) {
|
||||
return this.paneContainer.onDidDestroyPane(fn)
|
||||
}
|
||||
|
||||
paneForURI (uri) {
|
||||
return this.paneContainer.paneForURI(uri)
|
||||
}
|
||||
|
||||
paneForItem (item) {
|
||||
return this.paneContainer.paneForItem(item)
|
||||
}
|
||||
|
||||
getActivePane () {
|
||||
return this.paneContainer.getActivePane()
|
||||
}
|
||||
|
||||
getPaneItems () {
|
||||
return this.paneContainer.getPaneItems()
|
||||
}
|
||||
|
||||
getActivePaneItem () {
|
||||
return this.paneContainer.getActivePaneItem()
|
||||
}
|
||||
|
||||
getTextEditors () {
|
||||
return this.paneContainer.getTextEditors()
|
||||
}
|
||||
|
||||
getActiveTextEditor () {
|
||||
const activeItem = this.getActivePaneItem()
|
||||
if (activeItem instanceof TextEditor) { return activeItem }
|
||||
}
|
||||
|
||||
observePaneItems (fn) {
|
||||
return this.paneContainer.observePaneItems(fn)
|
||||
}
|
||||
|
||||
onDidAddPaneItem (fn) {
|
||||
return this.paneContainer.onDidAddPaneItem(fn)
|
||||
}
|
||||
|
||||
onWillDestroyPaneItem (fn) {
|
||||
return this.paneContainer.onWillDestroyPaneItem(fn)
|
||||
}
|
||||
|
||||
onDidDestroyPaneItem (fn) {
|
||||
return this.paneContainer.onDidDestroyPaneItem(fn)
|
||||
}
|
||||
|
||||
saveAll () {
|
||||
this.paneContainer.saveAll()
|
||||
}
|
||||
|
||||
confirmClose (options) {
|
||||
return this.paneContainer.confirmClose(options)
|
||||
}
|
||||
}
|
||||
|
||||
class DockResizeHandle {
|
||||
constructor (props) {
|
||||
this.handleMouseDown = this.handleMouseDown.bind(this)
|
||||
this.handleClick = this.handleClick.bind(this)
|
||||
|
||||
this.element = document.createElement('div')
|
||||
this.element.classList.add('atom-dock-resize-handle', props.location)
|
||||
this.element.addEventListener('mousedown', this.handleMouseDown)
|
||||
this.element.addEventListener('click', this.handleClick)
|
||||
const widthOrHeight = getWidthOrHeight(props.location)
|
||||
this.element.style[widthOrHeight] = `${HANDLE_SIZE}px`
|
||||
this.props = props
|
||||
this.update(props)
|
||||
}
|
||||
|
||||
update (newProps) {
|
||||
this.props = Object.assign({}, this.props, newProps)
|
||||
|
||||
if (this.props.dockIsOpen) {
|
||||
this.element.classList.add(RESIZE_HANDLE_RESIZABLE_CLASS)
|
||||
} else {
|
||||
this.element.classList.remove(RESIZE_HANDLE_RESIZABLE_CLASS)
|
||||
}
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.element.removeEventListener('mousedown', this.handleMouseDown)
|
||||
this.element.removeEventListener('click', this.handleClick)
|
||||
}
|
||||
|
||||
handleClick () {
|
||||
if (!this.props.dockIsOpen) {
|
||||
this.props.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseDown () {
|
||||
if (this.props.dockIsOpen) {
|
||||
this.props.onResizeStart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DockToggleButton {
|
||||
constructor (props) {
|
||||
this.handleClick = this.handleClick.bind(this)
|
||||
this.handleDragEnter = this.handleDragEnter.bind(this)
|
||||
|
||||
this.element = document.createElement('div')
|
||||
this.element.classList.add('atom-dock-toggle-button', props.location)
|
||||
this.element.classList.add(props.location)
|
||||
this.innerElement = document.createElement('div')
|
||||
this.innerElement.classList.add('atom-dock-toggle-button-inner', props.location)
|
||||
this.innerElement.addEventListener('click', this.handleClick)
|
||||
this.innerElement.addEventListener('dragenter', this.handleDragEnter)
|
||||
this.iconElement = document.createElement('span')
|
||||
this.innerElement.appendChild(this.iconElement)
|
||||
this.element.appendChild(this.innerElement)
|
||||
|
||||
this.props = props
|
||||
this.update(props)
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.innerElement.removeEventListener('click', this.handleClick)
|
||||
this.innerElement.removeEventListener('dragenter', this.handleDragEnter)
|
||||
}
|
||||
|
||||
update (newProps) {
|
||||
this.props = Object.assign({}, this.props, newProps)
|
||||
|
||||
if (this.props.visible) {
|
||||
this.element.classList.add(TOGGLE_BUTTON_VISIBLE_CLASS)
|
||||
} else {
|
||||
this.element.classList.remove(TOGGLE_BUTTON_VISIBLE_CLASS)
|
||||
}
|
||||
|
||||
this.iconElement.className = 'icon ' + getIconName(this.props.location, this.props.open)
|
||||
}
|
||||
|
||||
handleClick () {
|
||||
this.props.toggle()
|
||||
}
|
||||
|
||||
handleDragEnter () {
|
||||
this.props.onDragEnter()
|
||||
}
|
||||
}
|
||||
|
||||
function getWidthOrHeight (location) {
|
||||
return location === 'left' || location === 'right' ? 'width' : 'height'
|
||||
}
|
||||
|
||||
function getPreferredInitialSize (item, location) {
|
||||
switch (location) {
|
||||
case 'left':
|
||||
case 'right':
|
||||
return typeof item.getPreferredInitialWidth === 'function'
|
||||
? item.getPreferredInitialWidth()
|
||||
: null
|
||||
default:
|
||||
return typeof item.getPreferredInitialHeight === 'function'
|
||||
? item.getPreferredInitialHeight()
|
||||
: null
|
||||
}
|
||||
}
|
||||
|
||||
function getIconName (location, open) {
|
||||
switch (location) {
|
||||
case 'right': return open ? 'icon-chevron-right' : 'icon-chevron-left'
|
||||
case 'bottom': return open ? 'icon-chevron-down' : 'icon-chevron-up'
|
||||
case 'left': return open ? 'icon-chevron-left' : 'icon-chevron-right'
|
||||
default: throw new Error(`Invalid location: ${location}`)
|
||||
}
|
||||
}
|
||||
|
||||
function rectContainsPoint (rect, point) {
|
||||
return (
|
||||
point.x >= rect.left &&
|
||||
point.y >= rect.top &&
|
||||
point.x <= rect.right &&
|
||||
point.y <= rect.bottom
|
||||
)
|
||||
}
|
||||
@@ -19,6 +19,12 @@ class PanelContainerElement extends HTMLElement {
|
||||
this.subscriptions.add(this.model.onDidAddPanel(this.panelAdded.bind(this)))
|
||||
this.subscriptions.add(this.model.onDidDestroy(this.destroyed.bind(this)))
|
||||
this.classList.add(this.model.getLocation())
|
||||
|
||||
// Add the dock.
|
||||
if (this.model.dock != null) {
|
||||
this.appendChild(this.views.getView(this.model.dock))
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
const {ipcRenderer} = require('electron')
|
||||
const path = require('path')
|
||||
const fs = require('fs-plus')
|
||||
const {CompositeDisposable} = require('event-kit')
|
||||
const {CompositeDisposable, Disposable} = require('event-kit')
|
||||
const scrollbarStyle = require('scrollbar-style')
|
||||
const _ = require('underscore-plus')
|
||||
|
||||
class WorkspaceElement extends HTMLElement {
|
||||
attachedCallback () {
|
||||
@@ -64,6 +65,14 @@ class WorkspaceElement extends HTMLElement {
|
||||
}
|
||||
|
||||
initialize (model, {views, workspace, project, config, styles}) {
|
||||
this.handleCenterEnter = this.handleCenterEnter.bind(this)
|
||||
this.handleCenterLeave = this.handleCenterLeave.bind(this)
|
||||
this.handleEdgesMouseMove = _.throttle(this.handleEdgesMouseMove.bind(this), 100)
|
||||
this.handleDockDragEnd = this.handleDockDragEnd.bind(this)
|
||||
this.handleDragStart = this.handleDragStart.bind(this)
|
||||
this.handleDragEnd = this.handleDragEnd.bind(this)
|
||||
this.handleDrop = this.handleDrop.bind(this)
|
||||
|
||||
this.model = model
|
||||
this.views = views
|
||||
this.workspace = workspace
|
||||
@@ -76,7 +85,17 @@ class WorkspaceElement extends HTMLElement {
|
||||
if (this.config == null) { throw new Error('Must pass a config parameter when initializing WorskpaceElements') }
|
||||
if (this.styles == null) { throw new Error('Must pass a styles parameter when initializing WorskpaceElements') }
|
||||
|
||||
this.subscriptions = new CompositeDisposable()
|
||||
this.subscriptions = new CompositeDisposable(
|
||||
new Disposable(() => {
|
||||
window.removeEventListener('mouseenter', this.handleCenterEnter)
|
||||
window.removeEventListener('mouseleave', this.handleCenterLeave)
|
||||
window.removeEventListener('mousemove', this.handleEdgesMouseMove)
|
||||
window.removeEventListener('dragend', this.handleDockDragEnd)
|
||||
window.removeEventListener('dragstart', this.handleDragStart)
|
||||
window.removeEventListener('dragend', this.handleDragEnd, true)
|
||||
window.removeEventListener('drop', this.handleDrop, true)
|
||||
})
|
||||
)
|
||||
this.initializeContent()
|
||||
this.observeScrollbarStyle()
|
||||
this.observeTextEditorFontConfig()
|
||||
@@ -86,6 +105,7 @@ class WorkspaceElement extends HTMLElement {
|
||||
this.addEventListener('focus', this.handleFocus.bind(this))
|
||||
|
||||
this.addEventListener('mousewheel', this.handleMousewheel.bind(this), true)
|
||||
window.addEventListener('dragstart', this.handleDragStart)
|
||||
|
||||
this.panelContainers = {
|
||||
top: this.views.getView(this.model.panelContainers.top),
|
||||
@@ -108,11 +128,86 @@ class WorkspaceElement extends HTMLElement {
|
||||
|
||||
this.appendChild(this.panelContainers.modal)
|
||||
|
||||
this.paneContainer.addEventListener('mouseenter', this.handleCenterEnter)
|
||||
this.paneContainer.addEventListener('mouseleave', this.handleCenterLeave)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
getModel () { return this.model }
|
||||
|
||||
handleDragStart (event) {
|
||||
if (!isTab(event.target)) return
|
||||
this.model.setDraggingItem(true)
|
||||
window.addEventListener('dragend', this.handleDragEnd, true)
|
||||
window.addEventListener('drop', this.handleDrop, true)
|
||||
}
|
||||
|
||||
handleDragEnd (event) {
|
||||
this.dragEnded()
|
||||
}
|
||||
|
||||
handleDrop (event) {
|
||||
this.dragEnded()
|
||||
}
|
||||
|
||||
dragEnded () {
|
||||
this.model.setDraggingItem(false)
|
||||
window.removeEventListener('dragend', this.handleDragEnd, true)
|
||||
window.removeEventListener('drop', this.handleDrop, true)
|
||||
}
|
||||
|
||||
handleCenterEnter (event) {
|
||||
// Just re-entering the center isn't enough to hide the dock toggle buttons, since they poke
|
||||
// into the center and we want to give an affordance.
|
||||
this.cursorInCenter = true
|
||||
this.checkCleanupDockHoverEvents()
|
||||
}
|
||||
|
||||
handleCenterLeave (event) {
|
||||
// If the cursor leaves the center, we start listening to determine whether one of the docs is
|
||||
// being hovered.
|
||||
this.cursorInCenter = false
|
||||
this.updateHoveredDock({x: event.pageX, y: event.pageY})
|
||||
window.addEventListener('mousemove', this.handleEdgesMouseMove)
|
||||
window.addEventListener('dragend', this.handleDockDragEnd)
|
||||
}
|
||||
|
||||
handleEdgesMouseMove (event) {
|
||||
this.updateHoveredDock({x: event.pageX, y: event.pageY})
|
||||
}
|
||||
|
||||
handleDockDragEnd (event) {
|
||||
this.updateHoveredDock({x: event.pageX, y: event.pageY})
|
||||
}
|
||||
|
||||
updateHoveredDock (mousePosition) {
|
||||
// See if we've left the currently hovered dock's area.
|
||||
if (this.model.hoveredDock) {
|
||||
const hideToggleButton = !this.model.hoveredDock.pointWithinHoverArea(mousePosition, true)
|
||||
if (hideToggleButton) {
|
||||
this.model.setHoveredDock(null)
|
||||
}
|
||||
}
|
||||
// See if we've moved over a dock.
|
||||
if (this.model.hoveredDock == null) {
|
||||
const hoveredDock = _.values(this.model.docks).find(
|
||||
dock => dock.pointWithinHoverArea(mousePosition, false)
|
||||
)
|
||||
if (hoveredDock != null) {
|
||||
this.model.setHoveredDock(hoveredDock)
|
||||
}
|
||||
}
|
||||
this.checkCleanupDockHoverEvents()
|
||||
}
|
||||
|
||||
checkCleanupDockHoverEvents () {
|
||||
if (this.cursorInCenter && !this.model.hoveredDock) {
|
||||
window.removeEventListener('mousemove', this.handleEdgesMouseMove)
|
||||
window.removeEventListener('dragend', this.handleDockDragEnd)
|
||||
}
|
||||
}
|
||||
|
||||
handleMousewheel (event) {
|
||||
if (event.ctrlKey && this.config.get('editor.zoomFontWhenCtrlScrolling') && (event.target.closest('atom-text-editor') != null)) {
|
||||
if (event.wheelDeltaY > 0) {
|
||||
@@ -182,3 +277,12 @@ class WorkspaceElement extends HTMLElement {
|
||||
}
|
||||
|
||||
module.exports = document.registerElement('atom-workspace', {prototype: WorkspaceElement.prototype})
|
||||
|
||||
function isTab (element) {
|
||||
let el = element
|
||||
while (el != null) {
|
||||
if (el.getAttribute('is') === 'tabs-tab') { return true }
|
||||
el = el.parentElement
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ const {Emitter, Disposable, CompositeDisposable} = require('event-kit')
|
||||
const fs = require('fs-plus')
|
||||
const {Directory} = require('pathwatcher')
|
||||
const DefaultDirectorySearcher = require('./default-directory-searcher')
|
||||
const Dock = require('./dock')
|
||||
const Model = require('./model')
|
||||
const TextEditor = require('./text-editor')
|
||||
const PaneContainer = require('./pane-container')
|
||||
@@ -42,6 +43,8 @@ module.exports = class Workspace extends Model {
|
||||
this.assert = params.assert
|
||||
this.deserializerManager = params.deserializerManager
|
||||
this.textEditorRegistry = params.textEditorRegistry
|
||||
this.hoveredDock = null
|
||||
this.draggingItem = false
|
||||
|
||||
this.emitter = new Emitter()
|
||||
this.openers = []
|
||||
@@ -59,12 +62,17 @@ module.exports = class Workspace extends Model {
|
||||
this.consumeServices(this.packageManager)
|
||||
|
||||
this.center = new WorkspaceCenter(this.paneContainer)
|
||||
this.docks = {
|
||||
left: this.createDock('left'),
|
||||
right: this.createDock('right'),
|
||||
bottom: this.createDock('bottom')
|
||||
}
|
||||
|
||||
this.panelContainers = {
|
||||
top: new PanelContainer({location: 'top'}),
|
||||
left: new PanelContainer({location: 'left'}),
|
||||
right: new PanelContainer({location: 'right'}),
|
||||
bottom: new PanelContainer({location: 'bottom'}),
|
||||
left: new PanelContainer({location: 'left', dock: this.docks.left}),
|
||||
right: new PanelContainer({location: 'right', dock: this.docks.right}),
|
||||
bottom: new PanelContainer({location: 'bottom', dock: this.docks.bottom}),
|
||||
header: new PanelContainer({location: 'header'}),
|
||||
footer: new PanelContainer({location: 'footer'}),
|
||||
modal: new PanelContainer({location: 'modal'})
|
||||
@@ -73,6 +81,19 @@ module.exports = class Workspace extends Model {
|
||||
this.subscribeToEvents()
|
||||
}
|
||||
|
||||
createDock (location) {
|
||||
const dock = new Dock({
|
||||
location,
|
||||
config: this.config,
|
||||
applicationDelegate: this.applicationDelegate,
|
||||
deserializerManager: this.deserializerManager,
|
||||
notificationManager: this.notificationManager,
|
||||
viewRegistry: this.viewRegistry
|
||||
})
|
||||
dock.onDidDestroyPaneItem(this.didDestroyPaneItem)
|
||||
return dock
|
||||
}
|
||||
|
||||
reset (packageManager) {
|
||||
this.packageManager = packageManager
|
||||
this.emitter.dispose()
|
||||
@@ -89,11 +110,18 @@ module.exports = class Workspace extends Model {
|
||||
})
|
||||
this.paneContainer.onDidDestroyPaneItem(this.didDestroyPaneItem)
|
||||
|
||||
this.center = new WorkspaceCenter(this.paneContainer)
|
||||
this.docks = {
|
||||
left: this.createDock('left'),
|
||||
right: this.createDock('right'),
|
||||
bottom: this.createDock('bottom')
|
||||
}
|
||||
|
||||
this.panelContainers = {
|
||||
top: new PanelContainer({location: 'top'}),
|
||||
left: new PanelContainer({location: 'left'}),
|
||||
right: new PanelContainer({location: 'right'}),
|
||||
bottom: new PanelContainer({location: 'bottom'}),
|
||||
left: new PanelContainer({location: 'left', dock: this.docks.left}),
|
||||
right: new PanelContainer({location: 'right', dock: this.docks.right}),
|
||||
bottom: new PanelContainer({location: 'bottom', dock: this.docks.bottom}),
|
||||
header: new PanelContainer({location: 'header'}),
|
||||
footer: new PanelContainer({location: 'footer'}),
|
||||
modal: new PanelContainer({location: 'modal'})
|
||||
@@ -172,6 +200,19 @@ module.exports = class Workspace extends Model {
|
||||
return _.uniq(packageNames)
|
||||
}
|
||||
|
||||
setHoveredDock (hoveredDock) {
|
||||
this.hoveredDock = hoveredDock
|
||||
_.values(this.docks).forEach(dock => {
|
||||
dock.setHovered(dock === hoveredDock)
|
||||
})
|
||||
}
|
||||
|
||||
setDraggingItem (draggingItem) {
|
||||
_.values(this.docks).forEach(dock => {
|
||||
dock.setDraggingItem(draggingItem)
|
||||
})
|
||||
}
|
||||
|
||||
subscribeToActiveItem () {
|
||||
this.updateWindowTitle()
|
||||
this.updateDocumentEdited()
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
// Core components
|
||||
@import "cursors";
|
||||
@import "panels";
|
||||
@import "docks";
|
||||
@import "panes";
|
||||
@import "syntax";
|
||||
@import "text-editor-light";
|
||||
|
||||
214
static/docks.less
Normal file
214
static/docks.less
Normal file
@@ -0,0 +1,214 @@
|
||||
@import 'ui-variables';
|
||||
@import 'syntax-variables';
|
||||
|
||||
@atom-dock-toggle-button-size: 50px;
|
||||
|
||||
// Dock --------------
|
||||
|
||||
// The actual dock element is used as a kind of placeholder in the DOM, relative
|
||||
// to which its children can be positioned.
|
||||
atom-dock {
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.atom-dock-inner {
|
||||
display: flex;
|
||||
|
||||
&.bottom { width: 100%; }
|
||||
&.left, &.right { height: 100%; }
|
||||
|
||||
// Make sure to center the toggle buttons
|
||||
&.bottom { flex-direction: column; }
|
||||
align-items: center;
|
||||
|
||||
// Position the docks flush with their side of the editor.
|
||||
&.right { right: 0; }
|
||||
&.bottom { bottom: 0; }
|
||||
&.left { left: 0; }
|
||||
|
||||
// Position the docks flush with their side of the editor.
|
||||
&.right { right: 0; }
|
||||
&.bottom { bottom: 0; }
|
||||
&.left { left: 0; }
|
||||
|
||||
&:not(.atom-dock-open) {
|
||||
// The dock should only take up space when it's active (i.e. it shouldn't
|
||||
// take up space when you're dragging something into it).
|
||||
position: absolute;
|
||||
z-index: 10; // An arbitrary number. Seems high enough. ¯\_(ツ)_/¯
|
||||
}
|
||||
}
|
||||
|
||||
.atom-dock-mask {
|
||||
position: relative;
|
||||
background-color: @tool-panel-background-color;
|
||||
overflow: hidden; // Mask the content.
|
||||
|
||||
// This shouldn't technically be necessary. Apparently, there's a bug in
|
||||
// Chrome whereby the 100% width (in the bottom dock) and height (in left and
|
||||
// right docks) won't actually take effect when the docks are given more
|
||||
// space because another dock is hidden. Unsetting and resetting the width
|
||||
// will correct the issue, as will changing its "display." However, only this
|
||||
// seems to fix it without an actual runtime change occurring.
|
||||
flex: 1;
|
||||
|
||||
// One of these will be overridden by the component with an explicit size.
|
||||
// Which depends on the position of the dock.
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
transition: none;
|
||||
&.atom-dock-should-animate {
|
||||
transition: width 0.2s ease-out, height 0.2s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
.atom-dock-content-wrapper {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: default;
|
||||
-webkit-user-select: none;
|
||||
white-space: nowrap;
|
||||
|
||||
// The contents of the dock should be "stuck" to the moving edge of the mask,
|
||||
// so it looks like they're sliding in (instead of being unmasked in place).
|
||||
&.right { left: 0; }
|
||||
&.bottom { top: 0; }
|
||||
&.left { right: 0; }
|
||||
|
||||
// Use flex-direction to put the resize handle in the correct place.
|
||||
&.left { flex-direction: row-reverse; }
|
||||
&.bottom { flex-direction: column; }
|
||||
&.right { flex-direction: row; }
|
||||
}
|
||||
|
||||
// Toggle button --------------
|
||||
|
||||
.atom-dock-toggle-button {
|
||||
position: absolute;
|
||||
overflow: hidden; // Mask half of the circle.
|
||||
|
||||
// Must be > .scrollbar-content and inactive atom-dock
|
||||
z-index: 11;
|
||||
|
||||
// Position the toggle button target at the edge of the dock. It's important
|
||||
// that this is absolutely positioned so that it doesn't expand the area of
|
||||
// its container (which would block mouse events).
|
||||
&.right { right: 100%; }
|
||||
&.bottom { bottom: 100%; }
|
||||
&.left { left: 100%; }
|
||||
|
||||
width: @atom-dock-toggle-button-size;
|
||||
height: @atom-dock-toggle-button-size;
|
||||
&.bottom { height: @atom-dock-toggle-button-size / 2; }
|
||||
&.left, &.right { width: @atom-dock-toggle-button-size / 2; }
|
||||
|
||||
.atom-dock-toggle-button-inner {
|
||||
width: @atom-dock-toggle-button-size;
|
||||
height: @atom-dock-toggle-button-size;
|
||||
border-radius: @atom-dock-toggle-button-size / 2;
|
||||
|
||||
position: absolute;
|
||||
display: flex;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
&.right { left: 0; }
|
||||
&.bottom { top: 0; }
|
||||
&.left { right: 0; }
|
||||
}
|
||||
|
||||
// Hide the button.
|
||||
&:not(.atom-dock-toggle-button-visible) {
|
||||
.atom-dock-toggle-button-inner {
|
||||
&.right { transform: translateX(50%); }
|
||||
&.bottom { transform: translateY(50%); }
|
||||
&.left { transform: translateX(-50%); }
|
||||
}
|
||||
}
|
||||
|
||||
// Center the icon.
|
||||
@offset: 8px;
|
||||
.atom-dock-toggle-button-inner {
|
||||
&.right .icon { transform: translateX(-@offset); }
|
||||
&.bottom .icon { transform: translateY(-@offset); }
|
||||
&.left .icon { transform: translateX(@offset); }
|
||||
}
|
||||
|
||||
// Animate the icon.
|
||||
.icon {
|
||||
transition: opacity 0.1s ease-in 0.1s; // intro
|
||||
opacity: 1;
|
||||
|
||||
&::before {
|
||||
// Shrink the icon element to the size of the character.
|
||||
width: auto;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
&:not(.atom-dock-toggle-button-visible) .icon {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-out 0s; // outro
|
||||
}
|
||||
|
||||
.atom-dock-toggle-button-inner {
|
||||
background-color: @tool-panel-background-color;
|
||||
border: 1px solid @pane-item-border-color;
|
||||
transition: transform 0.2s ease-out 0s; // intro
|
||||
}
|
||||
|
||||
&:not(.atom-dock-toggle-button-visible) {
|
||||
// Don't contribute to mouseenter/drag events when not visible.
|
||||
pointer-events: none;
|
||||
|
||||
.atom-dock-toggle-button-inner {
|
||||
transition: transform 0.2s ease-out 0.1s; // outro
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resize handle --------------
|
||||
|
||||
.atom-dock-resize-handle {
|
||||
width: auto;
|
||||
height: auto;
|
||||
flex: 0 0 auto;
|
||||
cursor: pointer;
|
||||
|
||||
// Use the resize cursor when the handle's resizable
|
||||
&.atom-dock-resize-handle-resizable {
|
||||
&.left, &.right { cursor: col-resize; }
|
||||
&.bottom { cursor: row-resize; }
|
||||
}
|
||||
}
|
||||
|
||||
// Cursor overlay --------------
|
||||
|
||||
.atom-dock-cursor-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 4;
|
||||
|
||||
&.left,
|
||||
&.right {
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
&:not(.atom-dock-cursor-overlay-visible) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user