浏览代码

Merge pull request #196 from blink1073/terminal-tracker

Create a widget tracker service
Brian E. Granger 8 年之前
父节点
当前提交
5128cef151

+ 1 - 1
jupyterlab/index.js

@@ -28,7 +28,7 @@ var app = new phosphide.Application({
   providers: [
     require('jupyterlab/lib/clipboard/plugin').clipboardProvider,
     require('jupyterlab/lib/docregistry/plugin').docRegistryProvider,
-    require('jupyterlab/lib/notebook/plugin').activeNotebookProvider,
+    require('jupyterlab/lib/notebook/plugin').notebookTrackerProvider,
     require('jupyterlab/lib/rendermime/plugin').renderMimeProvider,
     require('jupyterlab/lib/services/plugin').servicesProvider,
   ]

+ 18 - 2
src/docregistry/default.ts

@@ -257,7 +257,14 @@ class Base64ModelFactory extends TextModelFactory {
  * The default implemetation of a widget factory.
  */
 export
-abstract class ABCWidgetFactory implements IWidgetFactory<Widget, IDocumentModel> {
+abstract class ABCWidgetFactory<T extends Widget, U extends IDocumentModel> implements IWidgetFactory<T, U> {
+  /**
+   * A signal emitted when a widget is created.
+   */
+  get widgetCreated(): ISignal<IWidgetFactory<T, U>, T> {
+    return Private.widgetCreatedSignal.bind(this);
+  }
+
   /**
    * Get whether the model factory has been disposed.
    */
@@ -274,8 +281,11 @@ abstract class ABCWidgetFactory implements IWidgetFactory<Widget, IDocumentModel
 
   /**
    * Create a new widget given a document model and a context.
+   *
+   * #### Notes
+   * It should emit the [widgetCreated] signal with the new widget.
    */
-  abstract createNew(context: IDocumentContext<IDocumentModel>, kernel?: IKernel.IModel): Widget;
+  abstract createNew(context: IDocumentContext<U>, kernel?: IKernel.IModel): T;
 
   private _isDisposed = false;
 }
@@ -291,6 +301,12 @@ namespace Private {
   export
   const contentChangedSignal = new Signal<IDocumentModel, void>();
 
+  /**
+   * A signal emitted when a widget is created.
+   */
+  export
+  const widgetCreatedSignal = new Signal<IWidgetFactory<Widget, IDocumentModel>, Widget>();
+
   /**
    * A signal emitted when a document dirty state changes.
    */

+ 8 - 0
src/docregistry/interfaces.ts

@@ -268,8 +268,16 @@ interface IWidgetFactoryOptions {
  */
 export
 interface IWidgetFactory<T extends Widget, U extends IDocumentModel> extends IDisposable {
+  /**
+   * A signal emitted when a widget is created.
+   */
+  widgetCreated: ISignal<IWidgetFactory<T, U>, T>;
+
   /**
    * Create a new widget.
+   *
+   * #### Notes
+   * It should emit the [widgetCreated] signal with the new widget.
    */
   createNew(context: IDocumentContext<U>, kernel?: IKernel.IModel): T;
 }

+ 4 - 2
src/editorwidget/widget.ts

@@ -85,7 +85,7 @@ class EditorWidget extends CodeMirrorWidget {
  * A widget factory for editors.
  */
 export
-class EditorWidgetFactory extends ABCWidgetFactory implements IWidgetFactory<EditorWidget, IDocumentModel> {
+class EditorWidgetFactory extends ABCWidgetFactory<EditorWidget, IDocumentModel> {
   /**
    * Create a new widget given a context.
    */
@@ -93,6 +93,8 @@ class EditorWidgetFactory extends ABCWidgetFactory implements IWidgetFactory<Edi
     if (kernel) {
       context.changeKernel(kernel);
     }
-    return new EditorWidget(context);
+    let widget = new EditorWidget(context);
+    this.widgetCreated.emit(widget);
+    return widget;
   }
 }

+ 30 - 52
src/filebrowser/plugin.ts

@@ -25,10 +25,6 @@ import {
   Menu, MenuItem
 } from 'phosphor-menus';
 
-import {
-  TabPanel
-} from 'phosphor-tabs';
-
 import {
   Widget
 } from 'phosphor-widget';
@@ -37,6 +33,10 @@ import {
   JupyterServices
 } from '../services/plugin';
 
+import {
+  WidgetTracker
+} from '../widgettracker';
+
 
 /**
  * The default file browser extension.
@@ -55,10 +55,14 @@ const fileBrowserExtension = {
 function activateFileBrowser(app: Application, provider: JupyterServices, registry: DocumentRegistry): Promise<void> {
   let contents = provider.contentsManager;
   let sessions = provider.sessionManager;
-  let widgets: Widget[] = [];
-  let activeWidget: Widget;
   let id = 0;
 
+  let tracker = new WidgetTracker<Widget>();
+  let activeWidget: Widget;
+  tracker.activeWidgetChanged.connect((sender, widget) => {
+    activeWidget = widget;
+  });
+
   let opener: IWidgetOpener = {
     open: (widget) => {
       if (!widget.id) {
@@ -66,35 +70,11 @@ function activateFileBrowser(app: Application, provider: JupyterServices, regist
       }
       if (!widget.isAttached) {
         app.shell.addToMainArea(widget);
+        tracker.addWidget(widget);
       }
-      // TODO: Move this logic to the shell.
-      let stack = widget.parent;
-      if (!stack) {
-        return;
-      }
-      let tabs = stack.parent;
-      if (tabs instanceof TabPanel) {
-        tabs.currentWidget = widget;
-      }
-      activeWidget = widget;
-      widget.disposed.connect(() => {
-        let index = widgets.indexOf(widget);
-        widgets.splice(index, 1);
-      });
     }
   };
 
-  // TODO: Move focus tracking to the shell.
-  document.addEventListener('focus', event => {
-    for (let i = 0; i < widgets.length; i++) {
-      let widget = widgets[i];
-      if (widget.node.contains(event.target as HTMLElement)) {
-        activeWidget = widget;
-        break;
-      }
-    }
-  });
-
   let docManager = new DocumentManager({
     registry,
     contentsManager: contents,
@@ -102,20 +82,20 @@ function activateFileBrowser(app: Application, provider: JupyterServices, regist
     kernelspecs: provider.kernelspecs,
     opener
   });
-  let model = new FileBrowserModel({
+  let fbModel = new FileBrowserModel({
     contentsManager: contents,
     sessionManager: sessions,
     kernelspecs: provider.kernelspecs
   });
-  let widget = new FileBrowserWidget({
-    model,
+  let fbWidget = new FileBrowserWidget({
+    model: fbModel,
     manager: docManager,
     opener
   });
-  let menu = createMenu(widget);
+  let menu = createMenu(fbWidget);
 
   // Add a context menu to the dir listing.
-  let node = widget.node.getElementsByClassName('jp-DirListing-content')[0];
+  let node = fbWidget.node.getElementsByClassName('jp-DirListing-content')[0];
   node.addEventListener('contextmenu', (event: MouseEvent) => {
     event.preventDefault();
     let x = event.clientX;
@@ -129,7 +109,7 @@ function activateFileBrowser(app: Application, provider: JupyterServices, regist
   app.commands.add([
     {
       id: newTextFileId,
-      handler: () => widget.createNew('file')
+      handler: () => fbWidget.createNew('file')
     }
   ]);
 
@@ -138,7 +118,7 @@ function activateFileBrowser(app: Application, provider: JupyterServices, regist
   app.commands.add([
   {
     id: newNotebookId,
-    handler: () => widget.createNew('notebook')
+    handler: () => fbWidget.createNew('notebook')
   }]);
 
 
@@ -149,11 +129,10 @@ function activateFileBrowser(app: Application, provider: JupyterServices, regist
     {
       id: saveDocumentId,
       handler: () => {
-        if (!activeWidget) {
-          return;
+        if (activeWidget) {
+          let context = docManager.contextForWidget(activeWidget);
+          context.save();
         }
-        let context = docManager.contextForWidget(activeWidget);
-        context.save();
       }
     }
   ]);
@@ -173,11 +152,10 @@ function activateFileBrowser(app: Application, provider: JupyterServices, regist
     {
       id: revertDocumentId,
       handler: () => {
-        if (!activeWidget) {
-          return;
+        if (activeWidget) {
+          let context = docManager.contextForWidget(activeWidget);
+          context.revert();
         }
-        let context = docManager.contextForWidget(activeWidget);
-        context.revert();
       }
     }
   ]);
@@ -262,24 +240,24 @@ function activateFileBrowser(app: Application, provider: JupyterServices, regist
     }
   ]);
 
-  widget.title.text = 'Files';
-  widget.id = 'file-browser';
-  app.shell.addToLeftArea(widget, { rank: 40 });
+  fbWidget.title.text = 'Files';
+  fbWidget.id = 'file-browser';
+  app.shell.addToLeftArea(fbWidget, { rank: 40 });
   showBrowser();
   return Promise.resolve(void 0);
 
   function showBrowser(): void {
-    app.shell.activateLeft(widget.id);
+    app.shell.activateLeft(fbWidget.id);
   }
 
   function hideBrowser(): void {
-    if (!widget.isHidden) {
+    if (!fbWidget.isHidden) {
       app.shell.collapseLeft();
     }
   }
 
   function toggleBrowser(): void {
-    if (widget.isHidden) {
+    if (fbWidget.isHidden) {
       showBrowser();
     } else {
       hideBrowser();

+ 4 - 2
src/imagewidget/widget.ts

@@ -86,11 +86,13 @@ class ImageWidget extends Widget {
  * A widget factory for images.
  */
 export
-class ImageWidgetFactory extends ABCWidgetFactory implements IWidgetFactory<ImageWidget, IDocumentModel> {
+class ImageWidgetFactory extends ABCWidgetFactory<ImageWidget, IDocumentModel> {
   /**
    * Create a new widget given a context.
    */
   createNew(context: IDocumentContext<IDocumentModel>, kernel?: IKernel.IModel): ImageWidget {
-    return new ImageWidget(context);
+    let widget = new ImageWidget(context);
+    this.widgetCreated.emit(widget);
+    return widget;
   }
 }

+ 8 - 25
src/notebook/notebook/widgetfactory.ts

@@ -6,7 +6,7 @@ import {
 } from 'jupyter-js-services';
 
 import {
-  IWidgetFactory, IDocumentContext, findKernel
+  ABCWidgetFactory, IDocumentContext, findKernel
 } from '../../docregistry';
 
 import {
@@ -38,7 +38,7 @@ import {
  * A widget factory for notebook panels.
  */
 export
-class NotebookWidgetFactory implements IWidgetFactory<NotebookPanel, INotebookModel> {
+class NotebookWidgetFactory extends ABCWidgetFactory<NotebookPanel, INotebookModel> {
   /**
    * Construct a new notebook widget factory.
    *
@@ -47,26 +47,21 @@ class NotebookWidgetFactory implements IWidgetFactory<NotebookPanel, INotebookMo
    * @param clipboard - The application clipboard.
    */
   constructor(rendermime: RenderMime<Widget>, clipboard: IClipboard) {
+    super();
     this._rendermime = rendermime;
     this._clipboard = clipboard;
   }
 
-  /**
-   * Get whether the factory has been disposed.
-   *
-   * #### Notes
-   * This is a read-only property.
-   */
-  get isDisposed(): boolean {
-    return this._rendermime === null;
-  }
-
   /**
    * Dispose of the resources used by the factory.
    */
   dispose(): void {
+    if (this.isDisposed) {
+      return;
+    }
     this._rendermime = null;
     this._clipboard = null;
+    super.dispose();
   }
 
   /**
@@ -88,22 +83,10 @@ class NotebookWidgetFactory implements IWidgetFactory<NotebookPanel, INotebookMo
     let panel = new NotebookPanel({ rendermime, clipboard: this._clipboard });
     panel.context = context;
     ToolbarItems.populateDefaults(panel);
+    this.widgetCreated.emit(panel);
     return panel;
   }
 
-  /**
-   * Take an action on a widget before closing it.
-   *
-   * @returns A promise that resolves to true if the document should close
-   *   and false otherwise.
-   *
-   * ### The default implementation is a no-op.
-   */
-  beforeClose(widget: NotebookPanel, context: IDocumentContext<INotebookModel>): Promise<boolean> {
-    // No special action required.
-    return Promise.resolve(true);
-  }
-
   private _rendermime: RenderMime<Widget> = null;
   private _clipboard: IClipboard = null;
 }

+ 102 - 176
src/notebook/plugin.ts

@@ -2,16 +2,20 @@
 // Distributed under the terms of the Modified BSD License.
 
 import {
-  NotebookPanel, NotebookModelFactory, INotebookModel,
-  NotebookWidgetFactory, NotebookActions
-} from './index';
+  Application
+} from 'phosphide/lib/core/application';
+
+import {
+  MimeData as IClipboard
+} from 'phosphor-dragdrop';
 
 import {
-  IKernel
-} from 'jupyter-js-services';
+  Widget
+} from 'phosphor-widget';
 
 import {
-  IDocumentContext, DocumentRegistry, restartKernel, selectKernelForContext
+  DocumentRegistry, restartKernel, selectKernelForContext,
+  IWidgetExtension
 } from '../docregistry';
 
 import {
@@ -19,24 +23,16 @@ import {
 } from '../rendermime';
 
 import {
-  Application
-} from 'phosphide/lib/core/application';
-
-import {
-  MimeData as IClipboard
-} from 'phosphor-dragdrop';
-
-import {
-  ISignal, Signal
-} from 'phosphor-signaling';
+  JupyterServices
+} from '../services/plugin';
 
 import {
-  Widget
-} from 'phosphor-widget';
+  WidgetTracker
+} from '../widgettracker';
 
 import {
-  JupyterServices
-} from '../services/plugin';
+  NotebookPanel, NotebookModelFactory, NotebookWidgetFactory, NotebookActions
+} from './index';
 
 
 /**
@@ -80,7 +76,7 @@ const cmdIds = {
 
 
 /**
- * The notebook file handler provider.
+ * The notebook file handler extension.
  */
 export
 const notebookHandlerExtension = {
@@ -91,75 +87,12 @@ const notebookHandlerExtension = {
 
 
 /**
- * An interface exposing the current active notebook.
- */
-export
-class ActiveNotebook {
-  /**
-   * Construct a new active notebook tracker.
-   */
-  constructor() {
-    // Temporary notebook focus follower.
-    document.body.addEventListener('focus', event => {
-      for (let widget of this._widgets) {
-        let target = event.target as HTMLElement;
-        if (widget.isAttached && widget.isVisible) {
-          if (widget.node.contains(target)) {
-            this.activeNotebook = widget;
-            return;
-          }
-        }
-      }
-    }, true);
-  }
-
-  /**
-   * A signal emitted when the active notebook changes.
-   */
-  get activeNotebookChanged(): ISignal<ActiveNotebook, NotebookPanel> {
-    return Private.activeNotebookChangedSignal.bind(this);
-  }
-
-  /**
-   * The current active notebook.
-   */
-  get activeNotebook(): NotebookPanel {
-    return this._activeWidget;
-  }
-  set activeNotebook(widget: NotebookPanel) {
-    if (this._activeWidget === widget) {
-      return;
-    }
-    if (this._widgets.indexOf(widget) !== -1) {
-      this._activeWidget = widget;
-      this.activeNotebookChanged.emit(widget);
-      return;
-    }
-    if (widget === null) {
-      return;
-    }
-    this._widgets.push(widget);
-    widget.disposed.connect(() => {
-      let index = this._widgets.indexOf(widget);
-      this._widgets.splice(index, 1);
-      if (this._activeWidget === widget) {
-        this.activeNotebook = null;
-      }
-    });
-  }
-
-  private _activeWidget: NotebookPanel = null;
-  private _widgets: NotebookPanel[] = [];
-}
-
-
-/**
- * A service tracking the active notebook widget.
+ * The notebook widget tracker provider.
  */
 export
-const activeNotebookProvider = {
-  id: 'jupyter.services.activeNotebook',
-  provides: ActiveNotebook,
+const notebookTrackerProvider = {
+  id: 'jupyter.plugins.notebookTracker',
+  provides: NotebookTracker,
   resolve: () => {
     return Private.notebookTracker;
   }
@@ -167,26 +100,18 @@ const activeNotebookProvider = {
 
 
 /**
- * A version of the notebook widget factory that uses the notebook tracker.
+ * A class that tracks notebook widgets.
  */
-class TrackingNotebookWidgetFactory extends NotebookWidgetFactory {
-  /**
-   * Create a new widget.
-   */
-  createNew(context: IDocumentContext<INotebookModel>, kernel?: IKernel.IModel): NotebookPanel {
-    let widget = super.createNew(context, kernel);
-    Private.notebookTracker.activeNotebook = widget;
-    return widget;
-  }
-}
+export
+class NotebookTracker extends WidgetTracker<NotebookPanel> { }
 
 
 /**
  * Activate the notebook handler extension.
  */
-function activateNotebookHandler(app: Application, registry: DocumentRegistry, services: JupyterServices, rendermime: RenderMime<Widget>, clipboard: IClipboard): Promise<void> {
+function activateNotebookHandler(app: Application, registry: DocumentRegistry, services: JupyterServices, rendermime: RenderMime<Widget>, clipboard: IClipboard): void {
 
-  let widgetFactory = new TrackingNotebookWidgetFactory(rendermime, clipboard);
+  let widgetFactory = new NotebookWidgetFactory(rendermime, clipboard);
   registry.addModelFactory(new NotebookModelFactory());
   registry.addWidgetFactory(widgetFactory,
   {
@@ -218,13 +143,22 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
     });
   }
 
+  // Track the current active notebook.
+  let activeNotebook: NotebookPanel;
   let tracker = Private.notebookTracker;
+  widgetFactory.widgetCreated.connect((sender, widget) => {
+    tracker.addWidget(widget);
+  });
+  tracker.activeWidgetChanged.connect((sender, widget) => {
+    activeNotebook = widget;
+  });
+
   app.commands.add([
   {
     id: cmdIds['runAndAdvance'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         NotebookActions.runAndAdvance(nbWidget.content, nbWidget.context.kernel);
       }
     }
@@ -232,8 +166,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['run'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         NotebookActions.run(nbWidget.content, nbWidget.context.kernel);
       }
     }
@@ -241,8 +175,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['runAndInsert'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         NotebookActions.runAndInsert(nbWidget.content, nbWidget.context.kernel);
       }
     }
@@ -250,8 +184,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['runAll'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         NotebookActions.runAll(nbWidget.content, nbWidget.context.kernel);
       }
     }
@@ -259,8 +193,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['restart'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         restartKernel(nbWidget.kernel, nbWidget.node);
       }
     }
@@ -268,8 +202,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['restartClear'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         let promise = restartKernel(nbWidget.kernel, nbWidget.node);
         promise.then(result => {
           if (result) {
@@ -282,8 +216,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['restartRunAll'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         let promise = restartKernel(nbWidget.kernel, nbWidget.node);
         promise.then(result => {
           NotebookActions.runAll(nbWidget.content, nbWidget.context.kernel);
@@ -294,8 +228,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['clearAllOutputs'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         NotebookActions.clearAllOutputs(nbWidget.content);
       }
     }
@@ -303,8 +237,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['clearOutputs'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         NotebookActions.clearOutputs(nbWidget.content);
       }
     }
@@ -312,8 +246,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['interrupt'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let kernel = tracker.activeNotebook.context.kernel;
+      if (activeNotebook) {
+        let kernel = activeNotebook.context.kernel;
         if (kernel) {
           kernel.interrupt();
         }
@@ -323,8 +257,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['toCode'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         NotebookActions.changeCellType(nbWidget.content, 'code');
       }
     }
@@ -332,8 +266,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['toMarkdown'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         NotebookActions.changeCellType(nbWidget.content, 'markdown');
       }
     }
@@ -341,8 +275,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['toRaw'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         NotebookActions.changeCellType(nbWidget.content, 'raw');
       }
     }
@@ -350,8 +284,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['cut'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         NotebookActions.cut(nbWidget.content, nbWidget.clipboard);
       }
     }
@@ -359,8 +293,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['copy'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         NotebookActions.copy(nbWidget.content, nbWidget.clipboard);
       }
     }
@@ -368,8 +302,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['paste'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         NotebookActions.paste(nbWidget.content, nbWidget.clipboard);
       }
     }
@@ -377,8 +311,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['deleteCell'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         NotebookActions.deleteCells(nbWidget.content);
       }
     }
@@ -386,8 +320,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['split'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         NotebookActions.splitCell(nbWidget.content);
       }
     }
@@ -395,8 +329,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['merge'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         NotebookActions.mergeCells(nbWidget.content);
       }
     }
@@ -404,8 +338,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['insertAbove'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         NotebookActions.insertAbove(nbWidget.content);
       }
     }
@@ -413,8 +347,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['insertBelow'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         NotebookActions.insertBelow(nbWidget.content);
       }
     }
@@ -422,8 +356,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['selectAbove'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         NotebookActions.selectAbove(nbWidget.content);
       }
     }
@@ -431,8 +365,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['selectBelow'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         NotebookActions.selectBelow(nbWidget.content);
       }
     }
@@ -440,8 +374,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['extendAbove'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         NotebookActions.extendSelectionAbove(nbWidget.content);
       }
     }
@@ -449,8 +383,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['extendBelow'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         NotebookActions.extendSelectionBelow(nbWidget.content);
       }
     }
@@ -458,8 +392,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['toggleLines'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         NotebookActions.toggleLineNumbers(nbWidget.content);
       }
     }
@@ -467,8 +401,8 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['toggleAllLines'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        let nbWidget = tracker.activeNotebook;
+      if (activeNotebook) {
+        let nbWidget = activeNotebook;
         NotebookActions.toggleAllLineNumbers(nbWidget.content);
       }
     }
@@ -476,40 +410,40 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
   {
     id: cmdIds['commandMode'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        tracker.activeNotebook.content.mode = 'command';
+      if (activeNotebook) {
+        activeNotebook.content.mode = 'command';
       }
     }
   },
   {
     id: cmdIds['editMode'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        tracker.activeNotebook.content.mode = 'edit';
+      if (activeNotebook) {
+        activeNotebook.content.mode = 'edit';
       }
     }
   },
   {
     id: cmdIds['undo'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        NotebookActions.undo(tracker.activeNotebook.content);
+      if (activeNotebook) {
+        NotebookActions.undo(activeNotebook.content);
       }
     }
   },
   {
     id: cmdIds['redo'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        NotebookActions.redo(tracker.activeNotebook.content);
+      if (activeNotebook) {
+        NotebookActions.redo(activeNotebook.content);
       }
     }
   },
   {
     id: cmdIds['switchKernel'],
     handler: () => {
-      if (tracker.activeNotebook) {
-        selectKernelForContext(tracker.activeNotebook.context, tracker.activeNotebook.node);
+      if (activeNotebook) {
+        selectKernelForContext(activeNotebook.context, activeNotebook.node);
       }
     }
   }
@@ -676,24 +610,16 @@ function activateNotebookHandler(app: Application, registry: DocumentRegistry, s
     text: 'Redo Cell Operation'
   }
   ]);
-
-  return Promise.resolve(void 0);
 }
 
 
 /**
- * A namespace for notebook plugin private data.
+ * A namespace for private data.
  */
 namespace Private {
   /**
-   * A signal emitted when the active notebook changes.
-   */
-  export
-  const activeNotebookChangedSignal = new Signal<ActiveNotebook, NotebookPanel>();
-
-  /**
-   * A singleton notebook tracker instance.
+   * A singleton instance of a notebook tracker.
    */
   export
-  const notebookTracker = new ActiveNotebook();
+  const notebookTracker = new NotebookTracker();
 }

+ 15 - 17
src/terminal/plugin.ts

@@ -1,17 +1,17 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import {
-  TerminalWidget
-} from './index';
-
 import {
   Application
 } from 'phosphide/lib/core/application';
 
 import {
-  TabPanel
-} from 'phosphor-tabs';
+  WidgetTracker
+} from '../widgettracker';
+
+import {
+  TerminalWidget
+} from './index';
 
 
 /**
@@ -24,24 +24,24 @@ const terminalExtension = {
 };
 
 
-function activateTerminal(app: Application): Promise<void> {
+function activateTerminal(app: Application): void {
 
   let newTerminalId = 'terminal:create-new';
 
+  // Track the current active terminal.
+  let tracker = new WidgetTracker<TerminalWidget>();
+  let activeTerm: TerminalWidget;
+  tracker.activeWidgetChanged.connect((sender, widget) => {
+    activeTerm = widget;
+  });
+
   app.commands.add([{
     id: newTerminalId,
     handler: () => {
       let term = new TerminalWidget();
       term.title.closable = true;
       app.shell.addToMainArea(term);
-      let stack = term.parent;
-      if (!stack) {
-        return;
-      }
-      let tabs = stack.parent;
-      if (tabs instanceof TabPanel) {
-        tabs.currentWidget = term;
-      }
+      tracker.addWidget(term);
     }
   }]);
   app.palette.add([
@@ -52,6 +52,4 @@ function activateTerminal(app: Application): Promise<void> {
       caption: 'Start a new terminal session'
     }
   ]);
-
-  return Promise.resolve(void 0);
 }

+ 172 - 0
src/widgettracker/index.ts

@@ -0,0 +1,172 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import {
+  IDisposable, DisposableDelegate
+} from 'phosphor-disposable';
+
+import {
+  ISignal, Signal
+} from 'phosphor-signaling';
+
+import {
+  TabPanel
+} from 'phosphor-tabs';
+
+import {
+  Widget
+} from 'phosphor-widget';
+
+
+/**
+ * The class name added to the currently active widget's title.
+ */
+const SEMANTIC_FOCUS_CLASS = 'jp-mod-semanticFocus';
+
+
+/**
+ * An object that tracks the active widget in an application.
+ */
+export
+class WidgetTracker<T extends Widget> implements IDisposable {
+  /**
+   * Construct a new widget tracker.
+   */
+  constructor() {
+    // TODO: Replace this with message filters on semantic-focus
+    // events when available.
+    document.body.addEventListener('focus', this, true);
+  }
+
+  /**
+   * A signal emitted when the active widget changes.
+   */
+  get activeWidgetChanged(): ISignal<WidgetTracker<T>, T> {
+    return activeWidgetChangedSignal.bind(this);
+  }
+
+  /**
+   * Test whether the widget tracker has been disposed.
+   *
+   * #### Notes
+   * This is a read-only property.
+   */
+  get isDisposed(): boolean {
+    return this._widgets = null;
+  }
+
+  /**
+   * The read-only list of tracked widgets.
+   *
+   * #### Notes
+   * This is a read-only property.
+   */
+  get widgets(): T[] {
+    return this._widgets.slice();
+  }
+
+  /**
+   * The currently active widget.
+   *
+   * #### Notes
+   * This is set automatically due to user events but can
+   * also be set programatically.
+   * The widget must be one of the current widgets.
+   * The widget will be activated in the application shell.
+   * The [[activeWidgetChanged]] signal will be emitted.
+   */
+  get activeWidget(): T {
+    return this._activeWidget;
+  }
+  set activeWidget(widget: T) {
+    if (this._activeWidget === widget) {
+      return;
+    }
+    if (this._widgets.indexOf(widget) === -1) {
+      return;
+    }
+    // Activate the widget in the dock panel.
+    // TODO: Use an API for this for this when available.
+    let stack = widget.parent;
+    if (!stack) {
+      return;
+    }
+    let tabs = stack.parent;
+    if (tabs instanceof TabPanel) {
+      tabs.currentWidget = widget;
+    }
+    // Toggle the active class in the widget titles.
+    if (this._activeWidget) {
+      let className =  this._activeWidget.title.className;
+      className = className.replace(SEMANTIC_FOCUS_CLASS, '');
+      this._activeWidget.title.className = className;
+    }
+    this._activeWidget = widget;
+    if (widget) {
+      widget.title.className += ` ${SEMANTIC_FOCUS_CLASS}`;
+    }
+    this.activeWidgetChanged.emit(widget);
+  }
+
+  /**
+   * Dispose of the resources used by the tracker.
+   */
+  dispose(): void {
+    if (this.isDisposed) {
+      return;
+    }
+    this._widgets = null;
+    this._activeWidget = null;
+    document.body.removeEventListener('focus', this, true);
+  }
+
+  /**
+   * Add a widget to the widget tracker.
+   *
+   * @param widget - The widget to add to the tracker.
+   *
+   * @returns A disposable that can be used to remove the widget from
+   *   the tracker.
+   *
+   * #### Notes
+   * The new widget will be set as the active widget.
+   */
+  addWidget(widget: T): IDisposable {
+    this._widgets.push(widget);
+    this.activeWidget = widget;
+    let disposal = () => {
+      let index = this._widgets.indexOf(widget);
+      this._widgets.splice(index, 1);
+    };
+    widget.disposed.connect(() => { disposal(); });
+    return new DisposableDelegate(() => { disposal(); });
+  }
+
+  /**
+   * Handle the DOM events for the widget tracker.
+   *
+   * @param event - The DOM event sent to the widget.
+   */
+  handleEvent(event: Event): void {
+    if (event.type === 'focus') {
+      for (let widget of this._widgets) {
+        let target = event.target as HTMLElement;
+        if (widget.isAttached && widget.isVisible) {
+          if (widget.node.contains(target)) {
+            this.activeWidget = widget;
+            return;
+          }
+        }
+      }
+    }
+  }
+
+  private _widgets: T[] = [];
+  private _activeWidget: T = null;
+}
+
+
+/**
+ * A signal emitted when the active widget changes.
+ */
+ const activeWidgetChangedSignal = new Signal<WidgetTracker<Widget>, Widget>();

+ 1 - 1
test/src/docregistry/default.spec.ts

@@ -21,7 +21,7 @@ import {
 } from '../docmanager/mockcontext';
 
 
-class WidgetFactory extends ABCWidgetFactory {
+class WidgetFactory extends ABCWidgetFactory<Widget, IDocumentModel> {
 
   createNew(context: IDocumentContext<IDocumentModel>, kernel?: IKernel.IModel): Widget {
     return new Widget();

+ 0 - 14
test/src/notebook/notebook/widgetfactory.spec.ts

@@ -135,20 +135,6 @@ describe('notebook/notebook/widgetfactory', () => {
 
     });
 
-    describe('#beforeClose()', () => {
-
-      it('should be a no-op', (done) => {
-        let model = new NotebookModel();
-        let context = new MockContext<NotebookModel>(model);
-        let factory = new NotebookWidgetFactory(rendermime, clipboard);
-        let panel = factory.createNew(context);
-        factory.beforeClose(panel, context).then(() => {
-          done();
-        });
-      });
-
-    });
-
   });
 
 });