Files
atom/src/workspace-element.js
Antonio Scandurra 8077f46fdf Programmatically detect when mouse approaches the edge of a dock
Previously, we would assign dock elements a minimum width/height of 2
pixels so that we could detect when the mouse approached the edge of a
hidden dock in order to show the chevron buttons. This, however, was
causing confusion for users, who expected that extra space to be
clickable in order to scroll editors located in the center dock.

With this commit we will instead register a global `mousemove` event on
the window right when attaching the workspace element to the DOM (the
event handler is debounced, so this shouldn't have any performance
consequence). Then, when mouse moves, we will programmatically detect
when it is approaching to the edge of a dock and show the chevron button
accordingly. This allows us to remove the `min-width` property from the
dock container element, which eliminates the confusing behavior
described above.
2018-01-12 17:12:55 +01:00

359 lines
13 KiB
JavaScript

'use strict'
const {ipcRenderer} = require('electron')
const path = require('path')
const fs = require('fs-plus')
const {CompositeDisposable, Disposable} = require('event-kit')
const scrollbarStyle = require('scrollbar-style')
const _ = require('underscore-plus')
class WorkspaceElement extends HTMLElement {
attachedCallback () {
this.focus()
this.htmlElement = document.querySelector('html')
this.htmlElement.addEventListener('mouseleave', this.handleCenterLeave)
}
detachedCallback () {
this.subscriptions.dispose()
this.htmlElement.removeEventListener('mouseleave', this.handleCenterLeave)
}
initializeContent () {
this.classList.add('workspace')
this.setAttribute('tabindex', -1)
this.verticalAxis = document.createElement('atom-workspace-axis')
this.verticalAxis.classList.add('vertical')
this.horizontalAxis = document.createElement('atom-workspace-axis')
this.horizontalAxis.classList.add('horizontal')
this.horizontalAxis.appendChild(this.verticalAxis)
this.appendChild(this.horizontalAxis)
}
observeScrollbarStyle () {
this.subscriptions.add(scrollbarStyle.observePreferredScrollbarStyle(style => {
switch (style) {
case 'legacy':
this.classList.remove('scrollbars-visible-when-scrolling')
this.classList.add('scrollbars-visible-always')
break
case 'overlay':
this.classList.remove('scrollbars-visible-always')
this.classList.add('scrollbars-visible-when-scrolling')
break
}
}))
}
observeTextEditorFontConfig () {
this.updateGlobalTextEditorStyleSheet()
this.subscriptions.add(this.config.onDidChange('editor.fontSize', this.updateGlobalTextEditorStyleSheet.bind(this)))
this.subscriptions.add(this.config.onDidChange('editor.fontFamily', this.updateGlobalTextEditorStyleSheet.bind(this)))
this.subscriptions.add(this.config.onDidChange('editor.lineHeight', this.updateGlobalTextEditorStyleSheet.bind(this)))
}
updateGlobalTextEditorStyleSheet () {
const styleSheetSource = `atom-text-editor {
font-size: ${this.config.get('editor.fontSize')}px;
font-family: ${this.config.get('editor.fontFamily')};
line-height: ${this.config.get('editor.lineHeight')};
}`
this.styleManager.addStyleSheet(styleSheetSource, {sourcePath: 'global-text-editor-styles', priority: -1})
}
initialize (model, {config, project, styleManager, viewRegistry}) {
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.viewRegistry = viewRegistry
this.project = project
this.config = config
this.styleManager = styleManager
if (this.viewRegistry == null) { throw new Error('Must pass a viewRegistry parameter when initializing WorkspaceElements') }
if (this.project == null) { throw new Error('Must pass a project parameter when initializing WorkspaceElements') }
if (this.config == null) { throw new Error('Must pass a config parameter when initializing WorkspaceElements') }
if (this.styleManager == null) { throw new Error('Must pass a styleManager parameter when initializing WorkspaceElements') }
this.subscriptions = new CompositeDisposable(
new Disposable(() => {
this.paneContainer.removeEventListener('mouseenter', this.handleCenterEnter)
this.paneContainer.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()
this.paneContainer = this.model.getCenter().paneContainer.getElement()
this.verticalAxis.appendChild(this.paneContainer)
this.addEventListener('focus', this.handleFocus.bind(this))
this.addEventListener('mousewheel', this.handleMousewheel.bind(this), true)
window.addEventListener('dragstart', this.handleDragStart)
window.addEventListener('mousemove', this.handleEdgesMouseMove)
this.panelContainers = {
top: this.model.panelContainers.top.getElement(),
left: this.model.panelContainers.left.getElement(),
right: this.model.panelContainers.right.getElement(),
bottom: this.model.panelContainers.bottom.getElement(),
header: this.model.panelContainers.header.getElement(),
footer: this.model.panelContainers.footer.getElement(),
modal: this.model.panelContainers.modal.getElement()
}
this.horizontalAxis.insertBefore(this.panelContainers.left, this.verticalAxis)
this.horizontalAxis.appendChild(this.panelContainers.right)
this.verticalAxis.insertBefore(this.panelContainers.top, this.paneContainer)
this.verticalAxis.appendChild(this.panelContainers.bottom)
this.insertBefore(this.panelContainers.header, this.horizontalAxis)
this.appendChild(this.panelContainers.footer)
this.appendChild(this.panelContainers.modal)
this.paneContainer.addEventListener('mouseenter', this.handleCenterEnter)
this.paneContainer.addEventListener('mouseleave', this.handleCenterLeave)
return this
}
destroy () {
this.subscriptions.dispose()
}
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('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) {
this.hoveredDock = null
for (let location in this.model.paneContainers) {
if (location !== 'center') {
const dock = this.model.paneContainers[location]
if (!this.hoveredDock && dock.pointWithinHoverArea(mousePosition)) {
this.hoveredDock = dock
dock.setHovered(true)
} else {
dock.setHovered(false)
}
}
}
this.checkCleanupDockHoverEvents()
}
checkCleanupDockHoverEvents () {
if (this.cursorInCenter && !this.hoveredDock) {
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) {
this.model.increaseFontSize()
} else if (event.wheelDeltaY < 0) {
this.model.decreaseFontSize()
}
event.preventDefault()
event.stopPropagation()
}
}
handleFocus (event) {
this.model.getActivePane().activate()
}
focusPaneViewAbove () { this.focusPaneViewInDirection('above') }
focusPaneViewBelow () { this.focusPaneViewInDirection('below') }
focusPaneViewOnLeft () { this.focusPaneViewInDirection('left') }
focusPaneViewOnRight () { this.focusPaneViewInDirection('right') }
focusPaneViewInDirection (direction, pane) {
const activePane = this.model.getActivePane()
const paneToFocus = this.nearestVisiblePaneInDirection(direction, activePane)
paneToFocus && paneToFocus.focus()
}
moveActiveItemToPaneAbove (params) {
this.moveActiveItemToNearestPaneInDirection('above', params)
}
moveActiveItemToPaneBelow (params) {
this.moveActiveItemToNearestPaneInDirection('below', params)
}
moveActiveItemToPaneOnLeft (params) {
this.moveActiveItemToNearestPaneInDirection('left', params)
}
moveActiveItemToPaneOnRight (params) {
this.moveActiveItemToNearestPaneInDirection('right', params)
}
moveActiveItemToNearestPaneInDirection (direction, params) {
const activePane = this.model.getActivePane()
const nearestPaneView = this.nearestVisiblePaneInDirection(direction, activePane)
if (nearestPaneView == null) { return }
if (params && params.keepOriginal) {
activePane.getContainer().copyActiveItemToPane(nearestPaneView.getModel())
} else {
activePane.getContainer().moveActiveItemToPane(nearestPaneView.getModel())
}
nearestPaneView.focus()
}
nearestVisiblePaneInDirection (direction, pane) {
const distance = function (pointA, pointB) {
const x = pointB.x - pointA.x
const y = pointB.y - pointA.y
return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))
}
const paneView = pane.getElement()
const box = this.boundingBoxForPaneView(paneView)
const paneViews = atom.workspace.getVisiblePanes()
.map(otherPane => otherPane.getElement())
.filter(otherPaneView => {
const otherBox = this.boundingBoxForPaneView(otherPaneView)
switch (direction) {
case 'left': return otherBox.right.x <= box.left.x
case 'right': return otherBox.left.x >= box.right.x
case 'above': return otherBox.bottom.y <= box.top.y
case 'below': return otherBox.top.y >= box.bottom.y
}
}).sort((paneViewA, paneViewB) => {
const boxA = this.boundingBoxForPaneView(paneViewA)
const boxB = this.boundingBoxForPaneView(paneViewB)
switch (direction) {
case 'left': return distance(box.left, boxA.right) - distance(box.left, boxB.right)
case 'right': return distance(box.right, boxA.left) - distance(box.right, boxB.left)
case 'above': return distance(box.top, boxA.bottom) - distance(box.top, boxB.bottom)
case 'below': return distance(box.bottom, boxA.top) - distance(box.bottom, boxB.top)
}
})
return paneViews[0]
}
boundingBoxForPaneView (paneView) {
const boundingBox = paneView.getBoundingClientRect()
return {
left: {x: boundingBox.left, y: boundingBox.top},
right: {x: boundingBox.right, y: boundingBox.top},
top: {x: boundingBox.left, y: boundingBox.top},
bottom: {x: boundingBox.left, y: boundingBox.bottom}
}
}
runPackageSpecs () {
const activePaneItem = this.model.getActivePaneItem()
const activePath = activePaneItem && typeof activePaneItem.getPath === 'function' ? activePaneItem.getPath() : null
let projectPath
if (activePath != null) {
[projectPath] = this.project.relativizePath(activePath)
} else {
[projectPath] = this.project.getPaths()
}
if (projectPath) {
let specPath = path.join(projectPath, 'spec')
const testPath = path.join(projectPath, 'test')
if (!fs.existsSync(specPath) && fs.existsSync(testPath)) {
specPath = testPath
}
ipcRenderer.send('run-package-specs', specPath)
}
}
runBenchmarks () {
const activePaneItem = this.model.getActivePaneItem()
const activePath = activePaneItem && typeof activePaneItem.getPath === 'function' ? activePaneItem.getPath() : null
let projectPath
if (activePath) {
[projectPath] = this.project.relativizePath(activePath)
} else {
[projectPath] = this.project.getPaths()
}
if (projectPath) {
ipcRenderer.send('run-benchmarks', path.join(projectPath, 'benchmarks'))
}
}
}
module.exports = document.registerElement('atom-workspace', {prototype: WorkspaceElement.prototype})
function isTab (element) {
let el = element
while (el != null) {
if (el.getAttribute && el.getAttribute('is') === 'tabs-tab') return true
el = el.parentElement
}
return false
}