Browse Source

Merge pull request #54 from blink1073/more-shortcuts

Add save/revert/close commands
Dave Willmer 9 years ago
parent
commit
dfb92a79e5

+ 0 - 5
examples/lab/webpack.conf.js

@@ -28,10 +28,5 @@ module.exports = {
       "base/js/namespace": "base/js/namespace",
       "notebook/js/outputarea": "notebook/js/outputarea",
       "services/kernels/comm": "services/kernels/comm"
-  },
-  resolve: {
-    alias: {
-      'requirejs': 'requirejs/require'
-    }
   }
 }

+ 4 - 4
package.json

@@ -8,11 +8,11 @@
     "codemirror": "^5.10.0",
     "jquery": "^2.2.0",
     "jquery-ui": "^1.10.5",
-    "jupyter-js-filebrowser": "^0.7.1",
+    "jupyter-js-filebrowser": "^0.7.2",
     "jupyter-js-notebook": "^0.5.0",
-    "jupyter-js-services": "^0.4.2",
-    "jupyter-js-terminal": "^0.1.12",
-    "jupyter-js-utils": "^0.2.17",
+    "jupyter-js-services": "^0.5.0",
+    "jupyter-js-terminal": "^0.1.15",
+    "jupyter-js-utils": "^0.3.0",
     "jupyter-js-widgets": "^0.0.6",
     "phosphide": "^0.4.0",
     "phosphor-codemirror": "^0.0.1",

+ 5 - 0
src/default-theme/index.css

@@ -23,6 +23,11 @@ body {
 }
 
 
+.jp-Document {
+  outline: none;
+}
+
+
 .jp-Dialog {
   padding-left: 3px;
   background: rgba(245, 245, 245, .6);

+ 18 - 31
src/documentmanager/index.ts

@@ -2,6 +2,10 @@
 // Distributed under the terms of the Modified BSD License.
 'use strict';
 
+import {
+  AbstractFileHandler
+} from 'jupyter-js-filebrowser';
+
 import {
   IContentsModel
 } from 'jupyter-js-services';
@@ -20,63 +24,46 @@ import {
 
 
 /**
- * An interface for a file handler
+ * An interface for a document manager.
  */
 export
-interface IFileHandler {
-  /**
-   * A signal emitted when the widget is finished populating.
-   */
-  finished: ISignal<IFileHandler, IContentsModel>;
-
-  /**
-   * The list of file extensions supported by the handler.
-   */
-  fileExtensions: string[];
-
+interface IDocumentManager {
   /**
-   * The list of mime types explicitly supported by the handler.
+   * Open the file and add the widget to the application shell.
    */
-  mimeTypes: string[];
+  open(model: IContentsModel): Widget;
 
   /**
-   * The current set of widgets managed by the handler.
+   * Save the current document.
    */
-  widgets: Widget[];
+  save(): void;
 
   /**
-   * Open the file and return a populated widget.
+   * Revert the current document.
    */
-  open(model: IContentsModel): Widget;
+  revert(): void;
 
   /**
-   * Close the file widget.
+   * Close the current document.
    */
-  close(widget: Widget): boolean;
-}
+  close(): void;
 
-
-/**
- * An interface for a document manager.
- */
-export
-interface IDocumentManager {
   /**
-   * Open the file and add the widget to the application shell.
+   * Close all documents.
    */
-  open(model: IContentsModel): Widget;
+  closeAll(): void;
 
   /**
    * Register a file handler.
    *
    * @param handler - The file handler to register.
    */
-  register(handler: IFileHandler): void;
+  register(handler: AbstractFileHandler): void;
 
   /**
    * Register a default file handler.
    */
-  registerDefault(handler: IFileHandler): void;
+  registerDefault(handler: AbstractFileHandler): void;
 }
 
 

+ 187 - 13
src/documentmanager/plugin.ts

@@ -3,7 +3,7 @@
 'use strict';
 
 import {
-  FileBrowserWidget, FileHandler
+  FileBrowserWidget, AbstractFileHandler
 } from 'jupyter-js-filebrowser';
 
 import {
@@ -42,10 +42,16 @@ import {
 } from '../index';
 
 import {
-  IDocumentManager, IFileHandler
+  IDocumentManager
 } from './index';
 
 
+/**
+ * The class name added to document widgets.
+ */
+export
+const DOCUMENT_CLASS = 'jp-Document';
+
 /**
  * The class name added to focused widgets.
  */
@@ -131,6 +137,134 @@ function resolve(container: Container): Promise<void> {
         }
       ]);
 
+      // Add the command for saving a document.
+      let saveDocumentId = 'file-operations:save';
+      let saveDocumentCommand = new SimpleCommand({
+        category: 'File Operations',
+        text: 'Save Document',
+        caption: 'Save the current document',
+        handler: () => {
+          manager.save();
+          return true;
+        }
+      });
+
+      registry.add([
+        {
+          id: saveDocumentId,
+          command: saveDocumentCommand
+        }
+      ]);
+      shortcuts.add([
+        {
+          sequence: ['Accel S'],
+          selector: `.${DOCUMENT_CLASS}.${FOCUS_CLASS}`,
+          command: saveDocumentId
+        }
+      ]);
+      palette.add([
+        {
+          id: saveDocumentId,
+          args: void 0
+        }
+      ]);
+
+      // Add the command for reverting a document.
+      let revertDocumentId = 'file-operations:revert';
+      let revertDocumentCommand = new SimpleCommand({
+        category: 'File Operations',
+        text: 'Revert Document',
+        caption: 'Revert the current document',
+        handler: () => {
+          manager.revert();
+          return true;
+        }
+      });
+
+      registry.add([
+        {
+          id: revertDocumentId,
+          command: revertDocumentCommand
+        }
+      ]);
+      shortcuts.add([
+        {
+          sequence: ['Accel R'],
+          selector: `.${DOCUMENT_CLASS}.${FOCUS_CLASS}`,
+          command: revertDocumentId
+        }
+      ]);
+      palette.add([
+        {
+          id: revertDocumentId,
+          args: void 0
+        }
+      ]);
+
+      // Add the command for closing a document.
+      let closeDocumentId = 'file-operations:close';
+      let closeDocumentCommand = new SimpleCommand({
+        category: 'File Operations',
+        text: 'Close Document',
+        caption: 'Close the current document',
+        handler: () => {
+          manager.close();
+          return true;
+        }
+      });
+
+      registry.add([
+        {
+          id: closeDocumentId,
+          command: closeDocumentCommand
+        }
+      ]);
+      shortcuts.add([
+        {
+          sequence: ['Ctrl Q'],
+          selector: `.${DOCUMENT_CLASS}.${FOCUS_CLASS}`,
+          command: closeDocumentId
+        }
+      ]);
+      palette.add([
+        {
+          id: closeDocumentId,
+          args: void 0
+        }
+      ]);
+
+      // Add the command for closing all documents.
+      let closeAllId = 'file-operations:close-all';
+      let closeAllCommand = new SimpleCommand({
+        category: 'File Operations',
+        text: 'Close All',
+        caption: 'Close all open documents',
+        handler: () => {
+          manager.closeAll();
+          return true;
+        }
+      });
+
+      registry.add([
+        {
+          id: closeAllId,
+          command: closeAllCommand
+        }
+      ]);
+      shortcuts.add([
+        {
+          sequence: ['Ctrl Shift Q'],
+          selector: `.${DOCUMENT_CLASS}`,
+          command: closeAllId
+        }
+      ]);
+      palette.add([
+        {
+          id: closeAllId,
+          args: void 0
+        }
+      ]);
+
       browser.widgetFactory = model => {
         return manager.open(model);
       }
@@ -178,14 +312,14 @@ class DocumentManager implements IDocumentManager {
   /**
    * Register a file handler.
    */
-  register(handler: IFileHandler): void {
+  register(handler: AbstractFileHandler): void {
     this._handlers.push(handler);
   }
 
   /**
    * Register a default file handler.
    */
-  registerDefault(handler: IFileHandler): void {
+  registerDefault(handler: AbstractFileHandler): void {
     if (this._defaultHandler !== null) {
       throw Error('Default handler already registered');
     }
@@ -202,20 +336,20 @@ class DocumentManager implements IDocumentManager {
     }
     let path = model.path;
     let ext = '.' + path.split('.').pop();
-    let handlers: IFileHandler[] = [];
+    let handlers: AbstractFileHandler[] = [];
     // Look for matching file extensions.
     for (let h of this._handlers) {
       if (h.fileExtensions.indexOf(ext) !== -1) handlers.push(h);
     }
-
+    let widget: Widget;
     // If there was only one match, use it.
     if (handlers.length === 1) {
-      return this._open(handlers[0], model);
+      widget = this._open(handlers[0], model);
 
     // If there were no matches, use default handler.
     } else if (handlers.length === 0) {
       if (this._defaultHandler !== null) {
-        return this._open(this._defaultHandler, model);
+        widget = this._open(this._defaultHandler, model);
       } else {
         throw new Error(`Could not open file '${path}'`);
       }
@@ -223,8 +357,46 @@ class DocumentManager implements IDocumentManager {
     // There are more than one possible handlers.
     } else {
       // TODO: Ask the user to choose one.
-      return this._open(handlers[0], model);
+      widget = this._open(handlers[0], model);
+    }
+    widget.addClass(DOCUMENT_CLASS);
+    return widget;
+  }
+
+  /**
+   * Save the current document.
+   */
+  save(): void {
+    if (this._currentHandler) this._currentHandler.save(this._currentWidget);
+  }
+
+  /**
+   * Revert the current document.
+   */
+  revert(): void {
+    if (this._currentHandler) this._currentHandler.revert(this._currentWidget);
+  }
+
+  /**
+   * Close the current document.
+   */
+  close(): void {
+    if (this._currentHandler) this._currentHandler.close(this._currentWidget);
+    this._currentWidget = null;
+    this._currentHandler = null;
+  }
+
+  /**
+   * Close all documents.
+   */
+  closeAll(): void {
+    for (let h of this._handlers) {
+      for (let w of h.widgets) {
+        w.close();
+      }
     }
+    this._currentWidget = null;
+    this._currentHandler = null;
   }
 
   /**
@@ -237,7 +409,7 @@ class DocumentManager implements IDocumentManager {
   /**
    * Open a file and add it to the application shell and give it focus.
    */
-  private _open(handler: IFileHandler, model: IContentsModel): Widget {
+  private _open(handler: AbstractFileHandler, model: IContentsModel): Widget {
     let widget = handler.open(model);
     if (!widget.isAttached) {
       this._appShell.addToMainArea(widget);
@@ -260,20 +432,22 @@ class DocumentManager implements IDocumentManager {
     for (let h of this._handlers) {
       // If the widget belongs to the handler, update the focused widget.
       let widget = arrays.find(h.widgets,
-        w => { return w.node.contains(event.target as HTMLElement); });
+        w => { return w.isVisible && w.node.contains(event.target as HTMLElement); });
       if (widget === this._currentWidget) {
         return;
       } else if (widget) {
         if (this._currentWidget) this._currentWidget.removeClass(FOCUS_CLASS);
         this._currentWidget = widget;
+        this._currentHandler = h;
         widget.addClass(FOCUS_CLASS);
         return;
       }
     }
   }
 
-  private _handlers: IFileHandler[] = [];
+  private _handlers: AbstractFileHandler[] = [];
   private _appShell: IAppShell = null;
-  private _defaultHandler: IFileHandler = null;
+  private _defaultHandler: AbstractFileHandler = null;
   private _currentWidget: Widget = null;
+  private _currentHandler: AbstractFileHandler = null;
 }

+ 3 - 3
src/filehandler/plugin.ts

@@ -3,7 +3,7 @@
 'use strict';
 
 import {
-  FileBrowserWidget, FileHandler
+  FileBrowserWidget, FileHandler, AbstractFileHandler
 } from 'jupyter-js-filebrowser';
 
 import {
@@ -15,7 +15,7 @@ import {
 } from 'phosphor-widget';
 
 import {
-  IServicesProvider, IDocumentManager, IFileHandler
+  IServicesProvider, IDocumentManager
 } from '../index';
 
 
@@ -28,7 +28,7 @@ import {
  * This is called automatically when the plugin is loaded.
  */
 export
-function resolve(container: Container): Promise<IFileHandler> {
+function resolve(container: Container): Promise<AbstractFileHandler> {
   return container.resolve({
     requires: [IServicesProvider, IDocumentManager],
     create: (services: IServicesProvider, manager: IDocumentManager) => {

+ 3 - 2
src/imagehandler/plugin.ts

@@ -19,7 +19,7 @@ import {
 } from 'phosphor-widget';
 
 import {
-  IServicesProvider, IDocumentManager, IFileHandler
+  IServicesProvider, IDocumentManager
 } from '../index';
 
 
@@ -32,7 +32,7 @@ import {
  * This is called automatically when the plugin is loaded.
  */
 export
-function resolve(container: Container): Promise<IFileHandler> {
+function resolve(container: Container): Promise<AbstractFileHandler> {
   return container.resolve({
     requires: [IServicesProvider, IDocumentManager],
     create: (services: IServicesProvider, manager: IDocumentManager) => {
@@ -67,6 +67,7 @@ class ImageHandler extends AbstractFileHandler {
   protected createWidget(model: IContentsModel): Widget {
     let ext = model.path.split('.').pop();
     var widget = new Widget();
+    widget.node.tabIndex = 0;
     let image = document.createElement('img');
     widget.node.appendChild(image);
     widget.node.style.overflowX = 'auto';

+ 4 - 10
src/notebook/plugin.ts

@@ -41,7 +41,7 @@ import {
 } from 'phosphor-widget';
 
 import {
-  IServicesProvider, IDocumentManager, IFileHandler
+  IServicesProvider, IDocumentManager
 } from '../index';
 
 import {
@@ -227,8 +227,8 @@ class NotebookFileHandler extends AbstractFileHandler {
     this.session.startNew({notebookPath: contents.path}).then(s => {
       // TODO: it's probably better to make *one* shortcut that executes whatever
       // the current notebook's selected cell is, rather than registering a
-      // a new shortcut for every open notebook.  
-      // One way to do this is to have the active notebook have a 
+      // a new shortcut for every open notebook.
+      // One way to do this is to have the active notebook have a
       // specific `.jp-active-document` class, for example. Then the keyboard shortcut
       // selects on that. The application state would also have a handle on this active
       // document (model or widget), and so we could execute the current active cell.
@@ -255,17 +255,11 @@ class NotebookFileHandler extends AbstractFileHandler {
         args: {model: model}
       }])
 
-      s.kernel.commOpened.connect((kernel, msg) => {
-        let content = msg.content;
-        if (content.target_name !== 'jupyter.widget') {
-          return;
-        }
-        let comm = kernel.connectToComm('jupyter.widget', content.comm_id);
+      s.kernel.registerCommTarget('jupyter.widget', (comm, msg) => {
         console.log('comm message', msg);
 
         let modelPromise = manager.handle_comm_open(comm, msg);
 
-
         comm.onMsg = (msg) => {
           manager.handle_comm_open(comm, msg)
           // create the widget model and (if needed) the view