dock.js
1 const etch = require('etch'); 2 const _ = require('underscore-plus'); 3 const { CompositeDisposable, Emitter } = require('event-kit'); 4 const PaneContainer = require('./pane-container'); 5 const TextEditor = require('./text-editor'); 6 const Grim = require('grim'); 7 8 const $ = etch.dom; 9 const MINIMUM_SIZE = 100; 10 const DEFAULT_INITIAL_SIZE = 300; 11 const SHOULD_ANIMATE_CLASS = 'atom-dock-should-animate'; 12 const VISIBLE_CLASS = 'atom-dock-open'; 13 const RESIZE_HANDLE_RESIZABLE_CLASS = 'atom-dock-resize-handle-resizable'; 14 const TOGGLE_BUTTON_VISIBLE_CLASS = 'atom-dock-toggle-button-visible'; 15 const CURSOR_OVERLAY_VISIBLE_CLASS = 'atom-dock-cursor-overlay-visible'; 16 17 // Extended: A container at the edges of the editor window capable of holding items. 18 // You should not create a Dock directly. Instead, access one of the three docks of the workspace 19 // via {Workspace::getLeftDock}, {Workspace::getRightDock}, and {Workspace::getBottomDock} 20 // or add an item to a dock via {Workspace::open}. 21 module.exports = class Dock { 22 constructor(params) { 23 this.handleResizeHandleDragStart = this.handleResizeHandleDragStart.bind( 24 this 25 ); 26 this.handleResizeToFit = this.handleResizeToFit.bind(this); 27 this.handleMouseMove = this.handleMouseMove.bind(this); 28 this.handleMouseUp = this.handleMouseUp.bind(this); 29 this.handleDrag = _.throttle(this.handleDrag.bind(this), 30); 30 this.handleDragEnd = this.handleDragEnd.bind(this); 31 this.handleToggleButtonDragEnter = this.handleToggleButtonDragEnter.bind( 32 this 33 ); 34 this.toggle = this.toggle.bind(this); 35 36 this.location = params.location; 37 this.widthOrHeight = getWidthOrHeight(this.location); 38 this.config = params.config; 39 this.applicationDelegate = params.applicationDelegate; 40 this.deserializerManager = params.deserializerManager; 41 this.notificationManager = params.notificationManager; 42 this.viewRegistry = params.viewRegistry; 43 this.didActivate = params.didActivate; 44 45 this.emitter = new Emitter(); 46 47 this.paneContainer = new PaneContainer({ 48 location: this.location, 49 config: this.config, 50 applicationDelegate: this.applicationDelegate, 51 deserializerManager: this.deserializerManager, 52 notificationManager: this.notificationManager, 53 viewRegistry: this.viewRegistry 54 }); 55 56 this.state = { 57 size: null, 58 visible: false, 59 shouldAnimate: false 60 }; 61 62 this.subscriptions = new CompositeDisposable( 63 this.emitter, 64 this.paneContainer.onDidActivatePane(() => { 65 this.show(); 66 this.didActivate(this); 67 }), 68 this.paneContainer.observePanes(pane => { 69 pane.onDidAddItem(this.handleDidAddPaneItem.bind(this)); 70 pane.onDidRemoveItem(this.handleDidRemovePaneItem.bind(this)); 71 }), 72 this.paneContainer.onDidChangeActivePane(item => 73 params.didChangeActivePane(this, item) 74 ), 75 this.paneContainer.onDidChangeActivePaneItem(item => 76 params.didChangeActivePaneItem(this, item) 77 ), 78 this.paneContainer.onDidDestroyPaneItem(item => 79 params.didDestroyPaneItem(item) 80 ) 81 ); 82 } 83 84 // This method is called explicitly by the object which adds the Dock to the document. 85 elementAttached() { 86 // Re-render when the dock is attached to make sure we remeasure sizes defined in CSS. 87 etch.updateSync(this); 88 } 89 90 getElement() { 91 // Because this code is included in the snapshot, we have to make sure we don't touch the DOM 92 // during initialization. Therefore, we defer initialization of the component (which creates a 93 // DOM element) until somebody asks for the element. 94 if (this.element == null) { 95 etch.initialize(this); 96 } 97 return this.element; 98 } 99 100 getLocation() { 101 return this.location; 102 } 103 104 destroy() { 105 this.subscriptions.dispose(); 106 this.paneContainer.destroy(); 107 window.removeEventListener('mousemove', this.handleMouseMove); 108 window.removeEventListener('mouseup', this.handleMouseUp); 109 window.removeEventListener('drag', this.handleDrag); 110 window.removeEventListener('dragend', this.handleDragEnd); 111 } 112 113 setHovered(hovered) { 114 if (hovered === this.state.hovered) return; 115 this.setState({ hovered }); 116 } 117 118 setDraggingItem(draggingItem) { 119 if (draggingItem === this.state.draggingItem) return; 120 this.setState({ draggingItem }); 121 } 122 123 // Extended: Show the dock and focus its active {Pane}. 124 activate() { 125 this.getActivePane().activate(); 126 } 127 128 // Extended: Show the dock without focusing it. 129 show() { 130 this.setState({ visible: true }); 131 } 132 133 // Extended: Hide the dock and activate the {WorkspaceCenter} if the dock was 134 // was previously focused. 135 hide() { 136 this.setState({ visible: false }); 137 } 138 139 // Extended: Toggle the dock's visibility without changing the {Workspace}'s 140 // active pane container. 141 toggle() { 142 const state = { visible: !this.state.visible }; 143 if (!state.visible) state.hovered = false; 144 this.setState(state); 145 } 146 147 // Extended: Check if the dock is visible. 148 // 149 // Returns a {Boolean}. 150 isVisible() { 151 return this.state.visible; 152 } 153 154 setState(newState) { 155 const prevState = this.state; 156 const nextState = Object.assign({}, prevState, newState); 157 158 // Update the `shouldAnimate` state. This needs to be written to the DOM before updating the 159 // class that changes the animated property. Normally we'd have to defer the class change a 160 // frame to ensure the property is animated (or not) appropriately, however we luck out in this 161 // case because the drag start always happens before the item is dragged into the toggle button. 162 if (nextState.visible !== prevState.visible) { 163 // Never animate toggling visibility... 164 nextState.shouldAnimate = false; 165 } else if ( 166 !nextState.visible && 167 nextState.draggingItem && 168 !prevState.draggingItem 169 ) { 170 // ...but do animate if you start dragging while the panel is hidden. 171 nextState.shouldAnimate = true; 172 } 173 174 this.state = nextState; 175 176 const { hovered, visible } = this.state; 177 178 // Render immediately if the dock becomes visible or the size changes in case people are 179 // measuring after opening, for example. 180 if (this.element != null) { 181 if ((visible && !prevState.visible) || this.state.size !== prevState.size) 182 etch.updateSync(this); 183 else etch.update(this); 184 } 185 186 if (hovered !== prevState.hovered) { 187 this.emitter.emit('did-change-hovered', hovered); 188 } 189 if (visible !== prevState.visible) { 190 this.emitter.emit('did-change-visible', visible); 191 } 192 } 193 194 render() { 195 const innerElementClassList = ['atom-dock-inner', this.location]; 196 if (this.state.visible) innerElementClassList.push(VISIBLE_CLASS); 197 198 const maskElementClassList = ['atom-dock-mask']; 199 if (this.state.shouldAnimate) 200 maskElementClassList.push(SHOULD_ANIMATE_CLASS); 201 202 const cursorOverlayElementClassList = [ 203 'atom-dock-cursor-overlay', 204 this.location 205 ]; 206 if (this.state.resizing) 207 cursorOverlayElementClassList.push(CURSOR_OVERLAY_VISIBLE_CLASS); 208 209 const shouldBeVisible = this.state.visible || this.state.showDropTarget; 210 const size = Math.max( 211 MINIMUM_SIZE, 212 this.state.size || 213 (this.state.draggingItem && 214 getPreferredSize(this.state.draggingItem, this.location)) || 215 DEFAULT_INITIAL_SIZE 216 ); 217 218 // We need to change the size of the mask... 219 const maskStyle = { 220 [this.widthOrHeight]: `${shouldBeVisible ? size : 0}px` 221 }; 222 // ...but the content needs to maintain a constant size. 223 const wrapperStyle = { [this.widthOrHeight]: `${size}px` }; 224 225 return $( 226 'atom-dock', 227 { className: this.location }, 228 $.div( 229 { ref: 'innerElement', className: innerElementClassList.join(' ') }, 230 $.div( 231 { 232 className: maskElementClassList.join(' '), 233 style: maskStyle 234 }, 235 $.div( 236 { 237 ref: 'wrapperElement', 238 className: `atom-dock-content-wrapper ${this.location}`, 239 style: wrapperStyle 240 }, 241 $(DockResizeHandle, { 242 location: this.location, 243 onResizeStart: this.handleResizeHandleDragStart, 244 onResizeToFit: this.handleResizeToFit, 245 dockIsVisible: this.state.visible 246 }), 247 $(ElementComponent, { element: this.paneContainer.getElement() }), 248 $.div({ className: cursorOverlayElementClassList.join(' ') }) 249 ) 250 ), 251 $(DockToggleButton, { 252 ref: 'toggleButton', 253 onDragEnter: this.state.draggingItem 254 ? this.handleToggleButtonDragEnter 255 : null, 256 location: this.location, 257 toggle: this.toggle, 258 dockIsVisible: shouldBeVisible, 259 visible: 260 // Don't show the toggle button if the dock is closed and empty... 261 (this.state.hovered && 262 (this.state.visible || this.getPaneItems().length > 0)) || 263 // ...or if the item can't be dropped in that dock. 264 (!shouldBeVisible && 265 this.state.draggingItem && 266 isItemAllowed(this.state.draggingItem, this.location)) 267 }) 268 ) 269 ); 270 } 271 272 update(props) { 273 // Since we're interopping with non-etch stuff, this method's actually never called. 274 return etch.update(this); 275 } 276 277 handleDidAddPaneItem() { 278 if (this.state.size == null) { 279 this.setState({ size: this.getInitialSize() }); 280 } 281 } 282 283 handleDidRemovePaneItem() { 284 // Hide the dock if you remove the last item. 285 if (this.paneContainer.getPaneItems().length === 0) { 286 this.setState({ visible: false, hovered: false, size: null }); 287 } 288 } 289 290 handleResizeHandleDragStart() { 291 window.addEventListener('mousemove', this.handleMouseMove); 292 window.addEventListener('mouseup', this.handleMouseUp); 293 this.setState({ resizing: true }); 294 } 295 296 handleResizeToFit() { 297 const item = this.getActivePaneItem(); 298 if (item) { 299 const size = getPreferredSize(item, this.getLocation()); 300 if (size != null) this.setState({ size }); 301 } 302 } 303 304 handleMouseMove(event) { 305 if (event.buttons === 0) { 306 // We missed the mouseup event. For some reason it happens on Windows 307 this.handleMouseUp(event); 308 return; 309 } 310 311 let size = 0; 312 switch (this.location) { 313 case 'left': 314 size = event.pageX - this.element.getBoundingClientRect().left; 315 break; 316 case 'bottom': 317 size = this.element.getBoundingClientRect().bottom - event.pageY; 318 break; 319 case 'right': 320 size = this.element.getBoundingClientRect().right - event.pageX; 321 break; 322 } 323 this.setState({ size }); 324 } 325 326 handleMouseUp(event) { 327 window.removeEventListener('mousemove', this.handleMouseMove); 328 window.removeEventListener('mouseup', this.handleMouseUp); 329 this.setState({ resizing: false }); 330 } 331 332 handleToggleButtonDragEnter() { 333 this.setState({ showDropTarget: true }); 334 window.addEventListener('drag', this.handleDrag); 335 window.addEventListener('dragend', this.handleDragEnd); 336 } 337 338 handleDrag(event) { 339 if (!this.pointWithinHoverArea({ x: event.pageX, y: event.pageY }, true)) { 340 this.draggedOut(); 341 } 342 } 343 344 handleDragEnd() { 345 this.draggedOut(); 346 } 347 348 draggedOut() { 349 this.setState({ showDropTarget: false }); 350 window.removeEventListener('drag', this.handleDrag); 351 window.removeEventListener('dragend', this.handleDragEnd); 352 } 353 354 // Determine whether the cursor is within the dock hover area. This isn't as simple as just using 355 // mouseenter/leave because we want to be a little more forgiving. For example, if the cursor is 356 // over the footer, we want to show the bottom dock's toggle button. Also note that our criteria 357 // for detecting entry are different than detecting exit but, in order for us to avoid jitter, the 358 // area considered when detecting exit MUST fully encompass the area considered when detecting 359 // entry. 360 pointWithinHoverArea(point, detectingExit) { 361 const dockBounds = this.refs.innerElement.getBoundingClientRect(); 362 363 // Copy the bounds object since we can't mutate it. 364 const bounds = { 365 top: dockBounds.top, 366 right: dockBounds.right, 367 bottom: dockBounds.bottom, 368 left: dockBounds.left 369 }; 370 371 // To provide a minimum target, expand the area toward the center a bit. 372 switch (this.location) { 373 case 'right': 374 bounds.left = Math.min(bounds.left, bounds.right - 2); 375 break; 376 case 'bottom': 377 bounds.top = Math.min(bounds.top, bounds.bottom - 1); 378 break; 379 case 'left': 380 bounds.right = Math.max(bounds.right, bounds.left + 2); 381 break; 382 } 383 384 // Further expand the area to include all panels that are closer to the edge than the dock. 385 switch (this.location) { 386 case 'right': 387 bounds.right = Number.POSITIVE_INFINITY; 388 break; 389 case 'bottom': 390 bounds.bottom = Number.POSITIVE_INFINITY; 391 break; 392 case 'left': 393 bounds.left = Number.NEGATIVE_INFINITY; 394 break; 395 } 396 397 // If we're in this area, we know we're within the hover area without having to take further 398 // measurements. 399 if (rectContainsPoint(bounds, point)) return true; 400 401 // If we're within the toggle button, we're definitely in the hover area. Unfortunately, we 402 // can't do this measurement conditionally (e.g. only if the toggle button is visible) because 403 // our knowledge of the toggle's button is incomplete due to CSS animations. (We may think the 404 // toggle button isn't visible when in actuality it is, but is animating to its hidden state.) 405 // 406 // Since `point` is always the current mouse position, one possible optimization would be to 407 // remove it as an argument and determine whether we're inside the toggle button using 408 // mouseenter/leave events on it. This class would still need to keep track of the mouse 409 // position (via a mousemove listener) for the other measurements, though. 410 const toggleButtonBounds = this.refs.toggleButton.getBounds(); 411 if (rectContainsPoint(toggleButtonBounds, point)) return true; 412 413 // The area used when detecting exit is actually larger than when detecting entrances. Expand 414 // our bounds and recheck them. 415 if (detectingExit) { 416 const hoverMargin = 20; 417 switch (this.location) { 418 case 'right': 419 bounds.left = 420 Math.min(bounds.left, toggleButtonBounds.left) - hoverMargin; 421 break; 422 case 'bottom': 423 bounds.top = 424 Math.min(bounds.top, toggleButtonBounds.top) - hoverMargin; 425 break; 426 case 'left': 427 bounds.right = 428 Math.max(bounds.right, toggleButtonBounds.right) + hoverMargin; 429 break; 430 } 431 if (rectContainsPoint(bounds, point)) return true; 432 } 433 434 return false; 435 } 436 437 getInitialSize() { 438 // The item may not have been activated yet. If that's the case, just use the first item. 439 const activePaneItem = 440 this.paneContainer.getActivePaneItem() || 441 this.paneContainer.getPaneItems()[0]; 442 // If there are items, we should have an explicit width; if not, we shouldn't. 443 return activePaneItem 444 ? getPreferredSize(activePaneItem, this.location) || DEFAULT_INITIAL_SIZE 445 : null; 446 } 447 448 serialize() { 449 return { 450 deserializer: 'Dock', 451 size: this.state.size, 452 paneContainer: this.paneContainer.serialize(), 453 visible: this.state.visible 454 }; 455 } 456 457 deserialize(serialized, deserializerManager) { 458 this.paneContainer.deserialize( 459 serialized.paneContainer, 460 deserializerManager 461 ); 462 this.setState({ 463 size: serialized.size || this.getInitialSize(), 464 // If no items could be deserialized, we don't want to show the dock (even if it was visible last time) 465 visible: 466 serialized.visible && this.paneContainer.getPaneItems().length > 0 467 }); 468 } 469 470 /* 471 Section: Event Subscription 472 */ 473 474 // Essential: Invoke the given callback when the visibility of the dock changes. 475 // 476 // * `callback` {Function} to be called when the visibility changes. 477 // * `visible` {Boolean} Is the dock now visible? 478 // 479 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 480 onDidChangeVisible(callback) { 481 return this.emitter.on('did-change-visible', callback); 482 } 483 484 // Essential: Invoke the given callback with the current and all future visibilities of the dock. 485 // 486 // * `callback` {Function} to be called when the visibility changes. 487 // * `visible` {Boolean} Is the dock now visible? 488 // 489 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 490 observeVisible(callback) { 491 callback(this.isVisible()); 492 return this.onDidChangeVisible(callback); 493 } 494 495 // Essential: Invoke the given callback with all current and future panes items 496 // in the dock. 497 // 498 // * `callback` {Function} to be called with current and future pane items. 499 // * `item` An item that is present in {::getPaneItems} at the time of 500 // subscription or that is added at some later time. 501 // 502 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 503 observePaneItems(callback) { 504 return this.paneContainer.observePaneItems(callback); 505 } 506 507 // Essential: Invoke the given callback when the active pane item changes. 508 // 509 // Because observers are invoked synchronously, it's important not to perform 510 // any expensive operations via this method. Consider 511 // {::onDidStopChangingActivePaneItem} to delay operations until after changes 512 // stop occurring. 513 // 514 // * `callback` {Function} to be called when the active pane item changes. 515 // * `item` The active pane item. 516 // 517 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 518 onDidChangeActivePaneItem(callback) { 519 return this.paneContainer.onDidChangeActivePaneItem(callback); 520 } 521 522 // Essential: Invoke the given callback when the active pane item stops 523 // changing. 524 // 525 // Observers are called asynchronously 100ms after the last active pane item 526 // change. Handling changes here rather than in the synchronous 527 // {::onDidChangeActivePaneItem} prevents unneeded work if the user is quickly 528 // changing or closing tabs and ensures critical UI feedback, like changing the 529 // highlighted tab, gets priority over work that can be done asynchronously. 530 // 531 // * `callback` {Function} to be called when the active pane item stopts 532 // changing. 533 // * `item` The active pane item. 534 // 535 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 536 onDidStopChangingActivePaneItem(callback) { 537 return this.paneContainer.onDidStopChangingActivePaneItem(callback); 538 } 539 540 // Essential: Invoke the given callback with the current active pane item and 541 // with all future active pane items in the dock. 542 // 543 // * `callback` {Function} to be called when the active pane item changes. 544 // * `item` The current active pane item. 545 // 546 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 547 observeActivePaneItem(callback) { 548 return this.paneContainer.observeActivePaneItem(callback); 549 } 550 551 // Extended: Invoke the given callback when a pane is added to the dock. 552 // 553 // * `callback` {Function} to be called panes are added. 554 // * `event` {Object} with the following keys: 555 // * `pane` The added pane. 556 // 557 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 558 onDidAddPane(callback) { 559 return this.paneContainer.onDidAddPane(callback); 560 } 561 562 // Extended: Invoke the given callback before a pane is destroyed in the 563 // dock. 564 // 565 // * `callback` {Function} to be called before panes are destroyed. 566 // * `event` {Object} with the following keys: 567 // * `pane` The pane to be destroyed. 568 // 569 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 570 onWillDestroyPane(callback) { 571 return this.paneContainer.onWillDestroyPane(callback); 572 } 573 574 // Extended: Invoke the given callback when a pane is destroyed in the dock. 575 // 576 // * `callback` {Function} to be called panes are destroyed. 577 // * `event` {Object} with the following keys: 578 // * `pane` The destroyed pane. 579 // 580 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 581 onDidDestroyPane(callback) { 582 return this.paneContainer.onDidDestroyPane(callback); 583 } 584 585 // Extended: Invoke the given callback with all current and future panes in the 586 // dock. 587 // 588 // * `callback` {Function} to be called with current and future panes. 589 // * `pane` A {Pane} that is present in {::getPanes} at the time of 590 // subscription or that is added at some later time. 591 // 592 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 593 observePanes(callback) { 594 return this.paneContainer.observePanes(callback); 595 } 596 597 // Extended: Invoke the given callback when the active pane changes. 598 // 599 // * `callback` {Function} to be called when the active pane changes. 600 // * `pane` A {Pane} that is the current return value of {::getActivePane}. 601 // 602 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 603 onDidChangeActivePane(callback) { 604 return this.paneContainer.onDidChangeActivePane(callback); 605 } 606 607 // Extended: Invoke the given callback with the current active pane and when 608 // the active pane changes. 609 // 610 // * `callback` {Function} to be called with the current and future active# 611 // panes. 612 // * `pane` A {Pane} that is the current return value of {::getActivePane}. 613 // 614 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 615 observeActivePane(callback) { 616 return this.paneContainer.observeActivePane(callback); 617 } 618 619 // Extended: Invoke the given callback when a pane item is added to the dock. 620 // 621 // * `callback` {Function} to be called when pane items are added. 622 // * `event` {Object} with the following keys: 623 // * `item` The added pane item. 624 // * `pane` {Pane} containing the added item. 625 // * `index` {Number} indicating the index of the added item in its pane. 626 // 627 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 628 onDidAddPaneItem(callback) { 629 return this.paneContainer.onDidAddPaneItem(callback); 630 } 631 632 // Extended: Invoke the given callback when a pane item is about to be 633 // destroyed, before the user is prompted to save it. 634 // 635 // * `callback` {Function} to be called before pane items are destroyed. 636 // * `event` {Object} with the following keys: 637 // * `item` The item to be destroyed. 638 // * `pane` {Pane} containing the item to be destroyed. 639 // * `index` {Number} indicating the index of the item to be destroyed in 640 // its pane. 641 // 642 // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. 643 onWillDestroyPaneItem(callback) { 644 return this.paneContainer.onWillDestroyPaneItem(callback); 645 } 646 647 // Extended: Invoke the given callback when a pane item is destroyed. 648 // 649 // * `callback` {Function} to be called when pane items are destroyed. 650 // * `event` {Object} with the following keys: 651 // * `item` The destroyed item. 652 // * `pane` {Pane} containing the destroyed item. 653 // * `index` {Number} indicating the index of the destroyed item in its 654 // pane. 655 // 656 // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. 657 onDidDestroyPaneItem(callback) { 658 return this.paneContainer.onDidDestroyPaneItem(callback); 659 } 660 661 // Extended: Invoke the given callback when the hovered state of the dock changes. 662 // 663 // * `callback` {Function} to be called when the hovered state changes. 664 // * `hovered` {Boolean} Is the dock now hovered? 665 // 666 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 667 onDidChangeHovered(callback) { 668 return this.emitter.on('did-change-hovered', callback); 669 } 670 671 /* 672 Section: Pane Items 673 */ 674 675 // Essential: Get all pane items in the dock. 676 // 677 // Returns an {Array} of items. 678 getPaneItems() { 679 return this.paneContainer.getPaneItems(); 680 } 681 682 // Essential: Get the active {Pane}'s active item. 683 // 684 // Returns an pane item {Object}. 685 getActivePaneItem() { 686 return this.paneContainer.getActivePaneItem(); 687 } 688 689 // Deprecated: Get the active item if it is a {TextEditor}. 690 // 691 // Returns a {TextEditor} or `undefined` if the current active item is not a 692 // {TextEditor}. 693 getActiveTextEditor() { 694 Grim.deprecate( 695 'Text editors are not allowed in docks. Use atom.workspace.getActiveTextEditor() instead.' 696 ); 697 698 const activeItem = this.getActivePaneItem(); 699 if (activeItem instanceof TextEditor) { 700 return activeItem; 701 } 702 } 703 704 // Save all pane items. 705 saveAll() { 706 this.paneContainer.saveAll(); 707 } 708 709 confirmClose(options) { 710 return this.paneContainer.confirmClose(options); 711 } 712 713 /* 714 Section: Panes 715 */ 716 717 // Extended: Get all panes in the dock. 718 // 719 // Returns an {Array} of {Pane}s. 720 getPanes() { 721 return this.paneContainer.getPanes(); 722 } 723 724 // Extended: Get the active {Pane}. 725 // 726 // Returns a {Pane}. 727 getActivePane() { 728 return this.paneContainer.getActivePane(); 729 } 730 731 // Extended: Make the next pane active. 732 activateNextPane() { 733 return this.paneContainer.activateNextPane(); 734 } 735 736 // Extended: Make the previous pane active. 737 activatePreviousPane() { 738 return this.paneContainer.activatePreviousPane(); 739 } 740 741 paneForURI(uri) { 742 return this.paneContainer.paneForURI(uri); 743 } 744 745 paneForItem(item) { 746 return this.paneContainer.paneForItem(item); 747 } 748 749 // Destroy (close) the active pane. 750 destroyActivePane() { 751 const activePane = this.getActivePane(); 752 if (activePane != null) { 753 activePane.destroy(); 754 } 755 } 756 }; 757 758 class DockResizeHandle { 759 constructor(props) { 760 this.props = props; 761 etch.initialize(this); 762 } 763 764 render() { 765 const classList = ['atom-dock-resize-handle', this.props.location]; 766 if (this.props.dockIsVisible) classList.push(RESIZE_HANDLE_RESIZABLE_CLASS); 767 768 return $.div({ 769 className: classList.join(' '), 770 on: { mousedown: this.handleMouseDown } 771 }); 772 } 773 774 getElement() { 775 return this.element; 776 } 777 778 getSize() { 779 if (!this.size) { 780 this.size = this.element.getBoundingClientRect()[ 781 getWidthOrHeight(this.props.location) 782 ]; 783 } 784 return this.size; 785 } 786 787 update(newProps) { 788 this.props = Object.assign({}, this.props, newProps); 789 return etch.update(this); 790 } 791 792 handleMouseDown(event) { 793 if (event.detail === 2) { 794 this.props.onResizeToFit(); 795 } else if (this.props.dockIsVisible) { 796 this.props.onResizeStart(); 797 } 798 } 799 } 800 801 class DockToggleButton { 802 constructor(props) { 803 this.props = props; 804 etch.initialize(this); 805 } 806 807 render() { 808 const classList = ['atom-dock-toggle-button', this.props.location]; 809 if (this.props.visible) classList.push(TOGGLE_BUTTON_VISIBLE_CLASS); 810 811 return $.div( 812 { className: classList.join(' ') }, 813 $.div( 814 { 815 ref: 'innerElement', 816 className: `atom-dock-toggle-button-inner ${this.props.location}`, 817 on: { 818 click: this.handleClick, 819 dragenter: this.props.onDragEnter 820 } 821 }, 822 $.span({ 823 ref: 'iconElement', 824 className: `icon ${getIconName( 825 this.props.location, 826 this.props.dockIsVisible 827 )}` 828 }) 829 ) 830 ); 831 } 832 833 getElement() { 834 return this.element; 835 } 836 837 getBounds() { 838 return this.refs.innerElement.getBoundingClientRect(); 839 } 840 841 update(newProps) { 842 this.props = Object.assign({}, this.props, newProps); 843 return etch.update(this); 844 } 845 846 handleClick() { 847 this.props.toggle(); 848 } 849 } 850 851 // An etch component that doesn't use etch, this component provides a gateway from JSX back into 852 // the mutable DOM world. 853 class ElementComponent { 854 constructor(props) { 855 this.element = props.element; 856 } 857 858 update(props) { 859 this.element = props.element; 860 } 861 } 862 863 function getWidthOrHeight(location) { 864 return location === 'left' || location === 'right' ? 'width' : 'height'; 865 } 866 867 function getPreferredSize(item, location) { 868 switch (location) { 869 case 'left': 870 case 'right': 871 return typeof item.getPreferredWidth === 'function' 872 ? item.getPreferredWidth() 873 : null; 874 default: 875 return typeof item.getPreferredHeight === 'function' 876 ? item.getPreferredHeight() 877 : null; 878 } 879 } 880 881 function getIconName(location, visible) { 882 switch (location) { 883 case 'right': 884 return visible ? 'icon-chevron-right' : 'icon-chevron-left'; 885 case 'bottom': 886 return visible ? 'icon-chevron-down' : 'icon-chevron-up'; 887 case 'left': 888 return visible ? 'icon-chevron-left' : 'icon-chevron-right'; 889 default: 890 throw new Error(`Invalid location: ${location}`); 891 } 892 } 893 894 function rectContainsPoint(rect, point) { 895 return ( 896 point.x >= rect.left && 897 point.y >= rect.top && 898 point.x <= rect.right && 899 point.y <= rect.bottom 900 ); 901 } 902 903 // Is the item allowed in the given location? 904 function isItemAllowed(item, location) { 905 if (typeof item.getAllowedLocations !== 'function') return true; 906 return item.getAllowedLocations().includes(location); 907 }