浏览代码

Merge pull request #424 from danielballan/dragable-output-area

Dragging OutputAreaWidget produces new OutputAreaWidget mirroring same model
Steven Silvester 8 年之前
父节点
当前提交
aebb7501fb
共有 3 个文件被更改,包括 216 次插入1 次删除
  1. 1 0
      src/notebook/cells/model.ts
  2. 10 0
      src/notebook/output-area/model.ts
  3. 205 1
      src/notebook/output-area/widget.ts

+ 1 - 0
src/notebook/cells/model.ts

@@ -410,6 +410,7 @@ class CodeCellModel extends CellModel implements ICodeCellModel {
       return;
     }
     this._outputs.clear(false);
+    this._outputs.dispose();
     this._outputs = null;
     super.dispose();
   }

+ 10 - 0
src/notebook/output-area/model.ts

@@ -42,6 +42,13 @@ class OutputAreaModel implements IDisposable {
     return Private.changedSignal.bind(this);
   }
 
+  /**
+   * A signal emitted when the model is disposed.
+   */
+  get disposed(): ISignal<OutputAreaModel, void> {
+    return Private.disposedSignal.bind(this);
+  }
+
   /**
    * Get the length of the items in the model.
    *
@@ -69,6 +76,7 @@ class OutputAreaModel implements IDisposable {
     if (this.isDisposed) {
       return;
     }
+    this.disposed.emit(void 0);
     this._list.clear();
     this._list = null;
     clearSignalData(this);
@@ -199,4 +207,6 @@ namespace Private {
    */
   export
   const changedSignal = new Signal<OutputAreaModel, IListChangedArgs<nbformat.IOutput>>();
+  export
+  const disposedSignal = new Signal<OutputAreaModel, void>();
 }

+ 205 - 1
src/notebook/output-area/widget.ts

@@ -25,6 +25,10 @@ import {
   Widget
 } from 'phosphor-widget';
 
+import {
+  Drag, DropAction, DropActions, IDragEvent, MimeData
+} from 'phosphor-dragdrop';
+
 import {
   nbformat
 } from '../notebook/nbformat';
@@ -38,11 +42,26 @@ import {
 } from '../../sanitizer';
 
 
+/**
+ * The threshold in pixels to start a drag event.
+ */
+const DRAG_THRESHOLD = 5;
+
+/**
+ * The factory MIME type supported by phosphor dock panels.
+ */
+const FACTORY_MIME = 'application/x-phosphor-widget-factory';
+
 /**
  * The class name added to an output area widget.
  */
 const OUTPUT_AREA_CLASS = 'jp-OutputArea';
 
+/**
+ * The class name added to a "mirrored" output area widget created by a drag.
+ */
+const MIRRORED_OUTPUT_AREA_CLASS = 'jp-MirroredOutputArea';
+
 /**
  * The class name added to an output widget.
  */
@@ -128,6 +147,18 @@ class OutputAreaWidget extends Widget {
     this.layout = new PanelLayout();
   }
 
+  get mirror(): OutputAreaWidget {
+    let rendermime = this._rendermime;
+    let renderer = this._renderer;
+    let widget = new OutputAreaWidget({ rendermime, renderer });
+    widget.model = this._model;
+    widget.trusted = this._trusted;
+    widget.title.text = 'Mirrored Output';
+    widget.title.closable = true;
+    widget.addClass(MIRRORED_OUTPUT_AREA_CLASS);
+    return widget;
+  }
+
   /**
    * A signal emitted when the widget's model changes.
    */
@@ -135,6 +166,13 @@ class OutputAreaWidget extends Widget {
      return Private.modelChangedSignal.bind(this);
   }
 
+  /**
+   * A signal emitted when the widget's model is disposed.
+   */
+  get modelDisposed(): ISignal<OutputAreaWidget, void> {
+     return Private.modelDisposedSignal.bind(this);
+  }
+
   /**
    * The model for the widget.
    */
@@ -282,8 +320,10 @@ class OutputAreaWidget extends Widget {
     let layout = this.layout as PanelLayout;
     if (oldValue) {
       oldValue.changed.disconnect(this._onModelStateChanged, this);
+      oldValue.disposed.disconnect(this._onModelDisposed, this);
     }
     newValue.changed.connect(this._onModelStateChanged, this);
+    newValue.disposed.connect(this._onModelDisposed, this);
     let start = newValue ? newValue.length : 0;
     // Clear unnecessary child widgets.
     for (let i = start; i < layout.childCount(); i++) {
@@ -302,6 +342,16 @@ class OutputAreaWidget extends Widget {
     }
   }
 
+  /**
+   * Handle a model disposal.
+   */
+  protected onModelDisposed(oldValue: OutputAreaModel, newValue: OutputAreaModel): void { }
+
+  private _onModelDisposed(): void {
+    this.modelDisposed.emit(void 0);
+    this.dispose();
+  }
+
   /**
    * Add a child to the layout.
    */
@@ -434,6 +484,140 @@ namespace OutputAreaWidget {
   const defaultRenderer = new Renderer();
 }
 
+/**
+ * The gutter on the left side of the OutputWidget
+ */
+export
+class OutputGutter extends Widget {
+  /**
+   * Handle the DOM events for the output gutter widget.
+   *
+   * @param event - The DOM event sent to the widget.
+   *
+   * #### Notes
+   * This method implements the DOM `EventListener` interface and is
+   * called in response to events on the panel's DOM node. It should
+   * not be called directly by user code.
+   */
+  handleEvent(event: Event): void {
+    switch (event.type) {
+    case 'mousedown':
+      this._evtMousedown(event as MouseEvent);
+      break;
+    case 'mouseup':
+      this._evtMouseup(event as MouseEvent);
+      break;
+    case 'mousemove':
+      this._evtMousemove(event as MouseEvent);
+      break;
+    }
+  }
+
+  /**
+   * A message handler invoked on an `'after-attach'` message.
+   */
+  protected onAfterAttach(msg: Message): void {
+    super.onAfterAttach(msg);
+    this.node.addEventListener('mousedown', this);
+  }
+
+  /**
+   * A message handler invoked on a `'before-detach'` message.
+   */
+  protected onBeforeDetach(msg: Message): void {
+    super.onBeforeDetach(msg);
+    let node = this.node;
+    node.removeEventListener('mousedown', this);
+  }
+
+  /**
+   * Handle the `'mousedown'` event for the widget.
+   */
+  private _evtMousedown(event: MouseEvent): void {
+    // Left mouse press for drag start.
+    if (event.button === 0) {
+      this._dragData = { pressX: event.clientX, pressY: event.clientY };
+      document.addEventListener('mouseup', this, true);
+      document.addEventListener('mousemove', this, true);
+    }
+  }
+
+  /**
+   * Handle the `'mouseup'` event for the widget.
+   */
+  private _evtMouseup(event: MouseEvent): void {
+    if (event.button !== 0 || !this._drag) {
+      document.removeEventListener('mousemove', this, true);
+      document.removeEventListener('mouseup', this, true);
+      return;
+    }
+    event.preventDefault();
+    event.stopPropagation();
+  }
+
+  /**
+   * Handle the `'mousemove'` event for the widget.
+   */
+  private _evtMousemove(event: MouseEvent): void {
+    event.preventDefault();
+    event.stopPropagation();
+
+    // Bail if we are the one dragging.
+    if (this._drag) {
+      return;
+    }
+
+    // Check for a drag initialization.
+    let data = this._dragData;
+    let dx = Math.abs(event.clientX - data.pressX);
+    let dy = Math.abs(event.clientY - data.pressY);
+    if (dx < DRAG_THRESHOLD && dy < DRAG_THRESHOLD) {
+      return;
+    }
+
+    this._startDrag(event.clientX, event.clientY);
+  }
+
+  /**
+   * Start a drag event.
+   */
+  private _startDrag(clientX: number, clientY: number): void {
+    // Set up the drag event.
+    this._drag = new Drag({
+      mimeData: new MimeData(),
+      supportedActions: DropActions.Copy,
+      proposedAction: DropAction.Copy
+    });
+
+    this._drag.mimeData.setData(FACTORY_MIME, () => {
+      let output_area = this.parent.parent as OutputAreaWidget;
+      return output_area.mirror;
+    });
+
+    // Start the drag and remove the mousemove listener.
+    this._drag.start(clientX, clientY).then(action => {
+      this._drag = null;
+    });
+    document.removeEventListener('mousemove', this, true);
+  }
+
+  /**
+   * Dispose of the resources held by the widget.
+   */
+  dispose() {
+    // Do nothing if already disposed.
+    if (this.isDisposed) {
+      return;
+    }
+    this._dragData = null;
+    this._drag = null;
+    super.dispose();
+  }
+
+  private _drag: Drag = null;
+  private _dragData: { pressX: number, pressY: number } = null;
+}
+
 
 /**
  * An output widget.
@@ -447,7 +631,7 @@ class OutputWidget extends Widget {
     super();
     let layout = new PanelLayout();
     this.layout = layout;
-    let prompt = new Widget();
+    let prompt = new OutputGutter();
     this._placeholder = new Widget();
     this.addClass(OUTPUT_CLASS);
     prompt.addClass(PROMPT_CLASS);
@@ -667,6 +851,24 @@ class OutputWidget extends Widget {
 }
 
 
+/**
+ * A namespace for OutputGutter statics.
+ */
+export
+namespace OutputGutter {
+  /**
+   * The options to pass to an `OutputGutter`.
+   */
+  export
+  interface IOptions {
+    /**
+     * The rendermime instance used by the widget.
+     */
+    rendermime: RenderMime<Widget>;
+  }
+}
+
+
 
 /**
  * A namespace for OutputArea statics.
@@ -695,4 +897,6 @@ namespace Private {
    */
   export
   const modelChangedSignal = new Signal<OutputAreaWidget, void>();
+  export
+  const modelDisposedSignal = new Signal<OutputAreaWidget, void>();
 }