workspace-element.js
1 'use strict'; 2 3 const { ipcRenderer } = require('electron'); 4 const path = require('path'); 5 const fs = require('fs-plus'); 6 const { CompositeDisposable, Disposable } = require('event-kit'); 7 const scrollbarStyle = require('scrollbar-style'); 8 const _ = require('underscore-plus'); 9 10 class WorkspaceElement extends HTMLElement { 11 attachedCallback() { 12 this.focus(); 13 this.htmlElement = document.querySelector('html'); 14 this.htmlElement.addEventListener('mouseleave', this.handleCenterLeave); 15 } 16 17 detachedCallback() { 18 this.subscriptions.dispose(); 19 this.htmlElement.removeEventListener('mouseleave', this.handleCenterLeave); 20 } 21 22 initializeContent() { 23 this.classList.add('workspace'); 24 this.setAttribute('tabindex', -1); 25 26 this.verticalAxis = document.createElement('atom-workspace-axis'); 27 this.verticalAxis.classList.add('vertical'); 28 29 this.horizontalAxis = document.createElement('atom-workspace-axis'); 30 this.horizontalAxis.classList.add('horizontal'); 31 this.horizontalAxis.appendChild(this.verticalAxis); 32 33 this.appendChild(this.horizontalAxis); 34 } 35 36 observeScrollbarStyle() { 37 this.subscriptions.add( 38 scrollbarStyle.observePreferredScrollbarStyle(style => { 39 switch (style) { 40 case 'legacy': 41 this.classList.remove('scrollbars-visible-when-scrolling'); 42 this.classList.add('scrollbars-visible-always'); 43 break; 44 case 'overlay': 45 this.classList.remove('scrollbars-visible-always'); 46 this.classList.add('scrollbars-visible-when-scrolling'); 47 break; 48 } 49 }) 50 ); 51 } 52 53 observeTextEditorFontConfig() { 54 this.updateGlobalTextEditorStyleSheet(); 55 this.subscriptions.add( 56 this.config.onDidChange( 57 'editor.fontSize', 58 this.updateGlobalTextEditorStyleSheet.bind(this) 59 ) 60 ); 61 this.subscriptions.add( 62 this.config.onDidChange( 63 'editor.fontFamily', 64 this.updateGlobalTextEditorStyleSheet.bind(this) 65 ) 66 ); 67 this.subscriptions.add( 68 this.config.onDidChange( 69 'editor.lineHeight', 70 this.updateGlobalTextEditorStyleSheet.bind(this) 71 ) 72 ); 73 } 74 75 updateGlobalTextEditorStyleSheet() { 76 const styleSheetSource = `atom-workspace { 77 --editor-font-size: ${this.config.get('editor.fontSize')}px; 78 --editor-font-family: ${this.config.get('editor.fontFamily')}; 79 --editor-line-height: ${this.config.get('editor.lineHeight')}; 80 }`; 81 this.styleManager.addStyleSheet(styleSheetSource, { 82 sourcePath: 'global-text-editor-styles', 83 priority: -1 84 }); 85 } 86 87 initialize(model, { config, project, styleManager, viewRegistry }) { 88 this.handleCenterEnter = this.handleCenterEnter.bind(this); 89 this.handleCenterLeave = this.handleCenterLeave.bind(this); 90 this.handleEdgesMouseMove = _.throttle( 91 this.handleEdgesMouseMove.bind(this), 92 100 93 ); 94 this.handleDockDragEnd = this.handleDockDragEnd.bind(this); 95 this.handleDragStart = this.handleDragStart.bind(this); 96 this.handleDragEnd = this.handleDragEnd.bind(this); 97 this.handleDrop = this.handleDrop.bind(this); 98 99 this.model = model; 100 this.viewRegistry = viewRegistry; 101 this.project = project; 102 this.config = config; 103 this.styleManager = styleManager; 104 if (this.viewRegistry == null) { 105 throw new Error( 106 'Must pass a viewRegistry parameter when initializing WorkspaceElements' 107 ); 108 } 109 if (this.project == null) { 110 throw new Error( 111 'Must pass a project parameter when initializing WorkspaceElements' 112 ); 113 } 114 if (this.config == null) { 115 throw new Error( 116 'Must pass a config parameter when initializing WorkspaceElements' 117 ); 118 } 119 if (this.styleManager == null) { 120 throw new Error( 121 'Must pass a styleManager parameter when initializing WorkspaceElements' 122 ); 123 } 124 125 this.subscriptions = new CompositeDisposable( 126 new Disposable(() => { 127 this.paneContainer.removeEventListener( 128 'mouseenter', 129 this.handleCenterEnter 130 ); 131 this.paneContainer.removeEventListener( 132 'mouseleave', 133 this.handleCenterLeave 134 ); 135 window.removeEventListener('mousemove', this.handleEdgesMouseMove); 136 window.removeEventListener('dragend', this.handleDockDragEnd); 137 window.removeEventListener('dragstart', this.handleDragStart); 138 window.removeEventListener('dragend', this.handleDragEnd, true); 139 window.removeEventListener('drop', this.handleDrop, true); 140 }), 141 ...[ 142 this.model.getLeftDock(), 143 this.model.getRightDock(), 144 this.model.getBottomDock() 145 ].map(dock => 146 dock.onDidChangeHovered(hovered => { 147 if (hovered) this.hoveredDock = dock; 148 else if (dock === this.hoveredDock) this.hoveredDock = null; 149 this.checkCleanupDockHoverEvents(); 150 }) 151 ) 152 ); 153 this.initializeContent(); 154 this.observeScrollbarStyle(); 155 this.observeTextEditorFontConfig(); 156 157 this.paneContainer = this.model.getCenter().paneContainer.getElement(); 158 this.verticalAxis.appendChild(this.paneContainer); 159 this.addEventListener('focus', this.handleFocus.bind(this)); 160 161 this.addEventListener('mousewheel', this.handleMousewheel.bind(this), true); 162 window.addEventListener('dragstart', this.handleDragStart); 163 window.addEventListener('mousemove', this.handleEdgesMouseMove); 164 165 this.panelContainers = { 166 top: this.model.panelContainers.top.getElement(), 167 left: this.model.panelContainers.left.getElement(), 168 right: this.model.panelContainers.right.getElement(), 169 bottom: this.model.panelContainers.bottom.getElement(), 170 header: this.model.panelContainers.header.getElement(), 171 footer: this.model.panelContainers.footer.getElement(), 172 modal: this.model.panelContainers.modal.getElement() 173 }; 174 175 this.horizontalAxis.insertBefore( 176 this.panelContainers.left, 177 this.verticalAxis 178 ); 179 this.horizontalAxis.appendChild(this.panelContainers.right); 180 181 this.verticalAxis.insertBefore( 182 this.panelContainers.top, 183 this.paneContainer 184 ); 185 this.verticalAxis.appendChild(this.panelContainers.bottom); 186 187 this.insertBefore(this.panelContainers.header, this.horizontalAxis); 188 this.appendChild(this.panelContainers.footer); 189 190 this.appendChild(this.panelContainers.modal); 191 192 this.paneContainer.addEventListener('mouseenter', this.handleCenterEnter); 193 this.paneContainer.addEventListener('mouseleave', this.handleCenterLeave); 194 195 return this; 196 } 197 198 destroy() { 199 this.subscriptions.dispose(); 200 } 201 202 getModel() { 203 return this.model; 204 } 205 206 handleDragStart(event) { 207 if (!isTab(event.target)) return; 208 const { item } = event.target; 209 if (!item) return; 210 this.model.setDraggingItem(item); 211 window.addEventListener('dragend', this.handleDragEnd, true); 212 window.addEventListener('drop', this.handleDrop, true); 213 } 214 215 handleDragEnd(event) { 216 this.dragEnded(); 217 } 218 219 handleDrop(event) { 220 this.dragEnded(); 221 } 222 223 dragEnded() { 224 this.model.setDraggingItem(null); 225 window.removeEventListener('dragend', this.handleDragEnd, true); 226 window.removeEventListener('drop', this.handleDrop, true); 227 } 228 229 handleCenterEnter(event) { 230 // Just re-entering the center isn't enough to hide the dock toggle buttons, since they poke 231 // into the center and we want to give an affordance. 232 this.cursorInCenter = true; 233 this.checkCleanupDockHoverEvents(); 234 } 235 236 handleCenterLeave(event) { 237 // If the cursor leaves the center, we start listening to determine whether one of the docs is 238 // being hovered. 239 this.cursorInCenter = false; 240 this.updateHoveredDock({ x: event.pageX, y: event.pageY }); 241 window.addEventListener('dragend', this.handleDockDragEnd); 242 } 243 244 handleEdgesMouseMove(event) { 245 this.updateHoveredDock({ x: event.pageX, y: event.pageY }); 246 } 247 248 handleDockDragEnd(event) { 249 this.updateHoveredDock({ x: event.pageX, y: event.pageY }); 250 } 251 252 updateHoveredDock(mousePosition) { 253 // If we haven't left the currently hovered dock, don't change anything. 254 if ( 255 this.hoveredDock && 256 this.hoveredDock.pointWithinHoverArea(mousePosition, true) 257 ) 258 return; 259 260 const docks = [ 261 this.model.getLeftDock(), 262 this.model.getRightDock(), 263 this.model.getBottomDock() 264 ]; 265 const nextHoveredDock = docks.find( 266 dock => 267 dock !== this.hoveredDock && dock.pointWithinHoverArea(mousePosition) 268 ); 269 docks.forEach(dock => { 270 dock.setHovered(dock === nextHoveredDock); 271 }); 272 } 273 274 checkCleanupDockHoverEvents() { 275 if (this.cursorInCenter && !this.hoveredDock) { 276 window.removeEventListener('dragend', this.handleDockDragEnd); 277 } 278 } 279 280 handleMousewheel(event) { 281 if ( 282 event.ctrlKey && 283 this.config.get('editor.zoomFontWhenCtrlScrolling') && 284 event.target.closest('atom-text-editor') != null 285 ) { 286 if (event.wheelDeltaY > 0) { 287 this.model.increaseFontSize(); 288 } else if (event.wheelDeltaY < 0) { 289 this.model.decreaseFontSize(); 290 } 291 event.preventDefault(); 292 event.stopPropagation(); 293 } 294 } 295 296 handleFocus(event) { 297 this.model.getActivePane().activate(); 298 } 299 300 focusPaneViewAbove() { 301 this.focusPaneViewInDirection('above'); 302 } 303 304 focusPaneViewBelow() { 305 this.focusPaneViewInDirection('below'); 306 } 307 308 focusPaneViewOnLeft() { 309 this.focusPaneViewInDirection('left'); 310 } 311 312 focusPaneViewOnRight() { 313 this.focusPaneViewInDirection('right'); 314 } 315 316 focusPaneViewInDirection(direction, pane) { 317 const activePane = this.model.getActivePane(); 318 const paneToFocus = this.nearestVisiblePaneInDirection( 319 direction, 320 activePane 321 ); 322 paneToFocus && paneToFocus.focus(); 323 } 324 325 moveActiveItemToPaneAbove(params) { 326 this.moveActiveItemToNearestPaneInDirection('above', params); 327 } 328 329 moveActiveItemToPaneBelow(params) { 330 this.moveActiveItemToNearestPaneInDirection('below', params); 331 } 332 333 moveActiveItemToPaneOnLeft(params) { 334 this.moveActiveItemToNearestPaneInDirection('left', params); 335 } 336 337 moveActiveItemToPaneOnRight(params) { 338 this.moveActiveItemToNearestPaneInDirection('right', params); 339 } 340 341 moveActiveItemToNearestPaneInDirection(direction, params) { 342 const activePane = this.model.getActivePane(); 343 const nearestPaneView = this.nearestVisiblePaneInDirection( 344 direction, 345 activePane 346 ); 347 if (nearestPaneView == null) { 348 return; 349 } 350 if (params && params.keepOriginal) { 351 activePane 352 .getContainer() 353 .copyActiveItemToPane(nearestPaneView.getModel()); 354 } else { 355 activePane 356 .getContainer() 357 .moveActiveItemToPane(nearestPaneView.getModel()); 358 } 359 nearestPaneView.focus(); 360 } 361 362 nearestVisiblePaneInDirection(direction, pane) { 363 const distance = function(pointA, pointB) { 364 const x = pointB.x - pointA.x; 365 const y = pointB.y - pointA.y; 366 return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); 367 }; 368 369 const paneView = pane.getElement(); 370 const box = this.boundingBoxForPaneView(paneView); 371 372 const paneViews = atom.workspace 373 .getVisiblePanes() 374 .map(otherPane => otherPane.getElement()) 375 .filter(otherPaneView => { 376 const otherBox = this.boundingBoxForPaneView(otherPaneView); 377 switch (direction) { 378 case 'left': 379 return otherBox.right.x <= box.left.x; 380 case 'right': 381 return otherBox.left.x >= box.right.x; 382 case 'above': 383 return otherBox.bottom.y <= box.top.y; 384 case 'below': 385 return otherBox.top.y >= box.bottom.y; 386 } 387 }) 388 .sort((paneViewA, paneViewB) => { 389 const boxA = this.boundingBoxForPaneView(paneViewA); 390 const boxB = this.boundingBoxForPaneView(paneViewB); 391 switch (direction) { 392 case 'left': 393 return ( 394 distance(box.left, boxA.right) - distance(box.left, boxB.right) 395 ); 396 case 'right': 397 return ( 398 distance(box.right, boxA.left) - distance(box.right, boxB.left) 399 ); 400 case 'above': 401 return ( 402 distance(box.top, boxA.bottom) - distance(box.top, boxB.bottom) 403 ); 404 case 'below': 405 return ( 406 distance(box.bottom, boxA.top) - distance(box.bottom, boxB.top) 407 ); 408 } 409 }); 410 411 return paneViews[0]; 412 } 413 414 boundingBoxForPaneView(paneView) { 415 const boundingBox = paneView.getBoundingClientRect(); 416 417 return { 418 left: { x: boundingBox.left, y: boundingBox.top }, 419 right: { x: boundingBox.right, y: boundingBox.top }, 420 top: { x: boundingBox.left, y: boundingBox.top }, 421 bottom: { x: boundingBox.left, y: boundingBox.bottom } 422 }; 423 } 424 425 runPackageSpecs(options = {}) { 426 const activePaneItem = this.model.getActivePaneItem(); 427 const activePath = 428 activePaneItem && typeof activePaneItem.getPath === 'function' 429 ? activePaneItem.getPath() 430 : null; 431 let projectPath; 432 if (activePath != null) { 433 [projectPath] = this.project.relativizePath(activePath); 434 } else { 435 [projectPath] = this.project.getPaths(); 436 } 437 if (projectPath) { 438 let specPath = path.join(projectPath, 'spec'); 439 const testPath = path.join(projectPath, 'test'); 440 if (!fs.existsSync(specPath) && fs.existsSync(testPath)) { 441 specPath = testPath; 442 } 443 444 ipcRenderer.send('run-package-specs', specPath, options); 445 } 446 } 447 448 runBenchmarks() { 449 const activePaneItem = this.model.getActivePaneItem(); 450 const activePath = 451 activePaneItem && typeof activePaneItem.getPath === 'function' 452 ? activePaneItem.getPath() 453 : null; 454 let projectPath; 455 if (activePath) { 456 [projectPath] = this.project.relativizePath(activePath); 457 } else { 458 [projectPath] = this.project.getPaths(); 459 } 460 461 if (projectPath) { 462 ipcRenderer.send('run-benchmarks', path.join(projectPath, 'benchmarks')); 463 } 464 } 465 } 466 467 module.exports = document.registerElement('atom-workspace', { 468 prototype: WorkspaceElement.prototype 469 }); 470 471 function isTab(element) { 472 let el = element; 473 while (el != null) { 474 if (el.getAttribute && el.getAttribute('is') === 'tabs-tab') return true; 475 el = el.parentElement; 476 } 477 return false; 478 }