/ src / dock.js
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  }