Browse Source

Merge pull request #151 from blink1073/multiple-selection

Implement multi-cell selection
Jason Grout 9 years ago
parent
commit
8f69c0f6de

+ 3 - 2
example/package.json

@@ -4,8 +4,9 @@
   "dependencies": {
     "jupyter-js-notebook": "file:..",
     "phosphor-commandpalette": "^0.2.0",
-    "phosphor-keymap": "^0.7.0",
-    "phosphor-splitpanel": "^1.0.0-rc.1"
+    "phosphor-keymap": "^0.8.0",
+    "phosphor-splitpanel": "^1.0.0-rc.1",
+    "phosphor-widget": "^1.0.0-rc.1"
   },
   "scripts": {
     "build": "tsc --project src && webpack --config webpack.conf.js",

+ 47 - 88
example/src/index.ts

@@ -209,6 +209,18 @@ function main(): void {
     shortcut: 'B',
     handler: () => { nbManager.insertBelow() ; }
   },
+  {
+    category: 'Notebook Cell',
+    text: 'Extend Selection Above',
+    shortcut: 'Shift J',
+    handler: () => { nbManager.extendSelectionAbove() ; }
+  },
+  {
+    category: 'Notebook Cell',
+    text: 'Extend Selection Below',
+    shortcut: 'Shift K',
+    handler: () => { nbManager.extendSelectionBelow() ; }
+  },
   {
     category: 'Notebook Cell',
     text: 'Merge Selected',
@@ -235,15 +247,15 @@ function main(): void {
   },
   {
     category: 'Notebook Cell',
-    text: 'Select Previous',
+    text: 'Select Above',
     shortcut: 'ArrowUp',
-    handler: () => { nbModel.activeCellIndex -= 1; }
+    handler: () => { nbManager.selectAbove(); }
   },
   {
     category: 'Notebook Cell',
-    text: 'Select Next',
+    text: 'Select Below',
     shortcut: 'ArrowDown',
-    handler: () => { nbModel.activeCellIndex += 1; }
+    handler: () => { nbManager.selectBelow(); }
   },
   ];
   pModel.addItems(items);
@@ -252,170 +264,117 @@ function main(): void {
   {
     selector: '.jp-Notebook',
     sequence: ['Shift Enter'],
-    handler: () => {
-      nbManager.runAndAdvance();
-      return true;
-    }
+    handler: () => { nbManager.runAndAdvance(); }
   },
   {
     selector: '.jp-Notebook',
     sequence: ['Accel S'],
-    handler: () => {
-      nbManager.save();
-      return true;
-    }
+    handler: () => { nbManager.save(); }
   },
   {
     selector: '.jp-Notebook.jp-mod-commandMode',
     sequence: ['I', 'I'],
-    handler: () => {
-      nbManager.interrupt();
-      return true;
-    }
+    handler: () => { nbManager.interrupt(); }
   },
   {
     selector: '.jp-Notebook.jp-mod-commandMode',
     sequence: ['0', '0'],
-    handler: () => {
-      nbManager.restart();
-      return true;
-    }
+    handler: () => { nbManager.restart(); }
   },
   {
     selector: '.jp-Notebook.jp-mod-commandMode',
     sequence: ['Enter'],
-    handler: () => {
-      nbModel.mode = 'edit';
-      return true;
-    }
+    handler: () => { nbModel.mode = 'edit'; }
   },
   {
     selector: '.jp-Notebook.jp-mod-editMode',
     sequence: ['Escape'],
-    handler: () => {
-      nbModel.mode = 'command';
-      return true;
-    }
+    handler: () => { nbModel.mode = 'command'; }
   },
   {
     selector: '.jp-Notebook.jp-mod-commandMode',
     sequence: ['Y'],
-    handler: () => {
-      nbManager.changeCellType('code');
-      return true;
-    }
+    handler: () => { nbManager.changeCellType('code'); }
   },
   {
     selector: '.jp-Notebook.jp-mod-commandMode',
     sequence: ['M'],
-    handler: () => {
-      nbManager.changeCellType('markdown');
-      return true;
-    }
+    handler: () => { nbManager.changeCellType('markdown'); }
   },
   {
     selector: '.jp-Notebook.jp-mod-commandMode',
     sequence: ['R'],
-    handler: () => {
-      nbManager.changeCellType('raw');
-      return true;
-    }
+    handler: () => { nbManager.changeCellType('raw'); }
   },
   {
     selector: '.jp-Notebook.jp-mod-commandMode',
     sequence: ['X'],
-    handler: () => {
-      nbManager.cut();
-      return true;
-    }
+    handler: () => { nbManager.cut(); }
   },
   {
     selector: '.jp-Notebook.jp-mod-commandMode',
     sequence: ['C'],
-    handler: () => {
-      nbManager.copy();
-      return true;
-    }
+    handler: () => { nbManager.copy(); }
   },
   {
     selector: '.jp-Notebook.jp-mod-commandMode',
     sequence: ['V'],
-    handler: () => {
-      nbManager.paste();
-      return true;
-    }
+    handler: () => { nbManager.paste(); }
   },
   {
     selector: '.jp-Notebook.jp-mod-commandMode',
     sequence: ['D', 'D'],
-    handler: () => {
-      nbManager.delete();
-      return true;
-    }
+    handler: () => { nbManager.delete(); }
   },
   {
     selector: '.jp-Notebook.jp-mod-commandMode',
     sequence: ['Z'],
-    handler: () => {
-      nbManager.undelete();
-      return true;
-    }
+    handler: () => { nbManager.undelete(); }
   },
   {
     selector: '.jp-Notebook.jp-mod-commandMode',
     sequence: ['Shift M'],
-    handler: () => {
-      nbManager.merge();
-      return true;
-    }
+    handler: () => { nbManager.merge(); }
   },
   {
     selector: '.jp-Notebook.jp-mod-commandMode',
     sequence: ['A'],
-    handler: () => {
-      nbManager.insertAbove();
-      return true;
-    }
+    handler: () => { nbManager.insertAbove(); }
   },
   {
     selector: '.jp-Notebook.jp-mod-commandMode',
     sequence: ['B'],
-    handler: () => {
-      nbManager.insertBelow();
-      return true;
-    }
+    handler: () => { nbManager.insertBelow(); }
   },
   {
     selector: '.jp-Notebook.jp-mod-commandMode',
     sequence: ['J'],
-    handler: () => {
-      nbModel.activeCellIndex += 1;
-      return true;
-    }
+    handler: () => { nbManager.selectBelow(); }
   },
   {
     selector: '.jp-Notebook.jp-mod-commandMode',
     sequence: ['ArrowDown'],
-    handler: () => {
-      nbModel.activeCellIndex += 1;
-      return true;
-    }
+    handler: () => { nbManager.selectBelow(); }
   },
   {
     selector: '.jp-Notebook.jp-mod-commandMode',
     sequence: ['K'],
-    handler: () => {
-      nbModel.activeCellIndex -= 1;
-      return true;
-    }
+    handler: () => { nbManager.selectAbove(); }
   },
   {
     selector: '.jp-Notebook.jp-mod-commandMode',
     sequence: ['ArrowUp'],
-    handler: () => {
-      nbModel.activeCellIndex -= 1;
-      return true;
-    }
+    handler: () => { nbManager.selectAbove(); }
+  },
+  {
+    selector: '.jp-Notebook.jp-mod-commandMode',
+    sequence: ['Shift K'],
+    handler: () => { nbManager.extendSelectionAbove(); }
+  },
+  {
+    selector: '.jp-Notebook.jp-mod-commandMode',
+    sequence: ['Shift J'],
+    handler: () => { nbManager.extendSelectionBelow(); }
   }
   ];
   keymap.add(bindings);

BIN
src/notebook/fonts/icomoon.eot


+ 23 - 0
src/notebook/fonts/icomoon.svg

@@ -0,0 +1,23 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata>Generated by IcoMoon</metadata>
+<defs>
+<font id="icomoon" horiz-adv-x="1024">
+<font-face units-per-em="1024" ascent="960" descent="-64" />
+<missing-glyph horiz-adv-x="1024" />
+<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
+<glyph unicode="&#xe900;" glyph-name="upload" d="M306.531 176.941h410.937v361.412h273.958l-479.427 421.647-479.427-421.647h273.958v-361.412zM32.573 56.471h958.853v-120.471h-958.853v120.471z" />
+<glyph unicode="&#xe901;" glyph-name="new" d="M585.143 382.906v-446.906h-146.286v446.906h-438.857v130.187h438.857v446.906h146.286v-446.906h438.857v-130.187h-438.857z" />
+<glyph unicode="&#xe902;" glyph-name="home" d="M409.6-0.6v316.659h204.8v-316.659h256v422.212h153.6l-512 474.988-512-474.988h153.6v-422.212h256z" />
+<glyph unicode="&#xe903;" glyph-name="close" d="M1024 856.869l-103.131 103.131-408.869-408.869-408.869 408.869-103.131-103.131 408.869-408.869-408.869-408.869 103.131-103.131 408.869 408.869 408.869-408.869 103.131 103.131-408.869 408.869 408.869 408.869z" />
+<glyph unicode="&#xe904;" glyph-name="restart" d="M512 755.2v204.8l-251.429-256 251.429-256v204.8c166.446 0 301.714-137.728 301.714-307.2s-135.269-307.2-301.714-307.2c-166.446 0-301.714 137.728-301.714 307.2h-100.571c0-226.304 180.023-409.6 402.286-409.6s402.286 183.296 402.286 409.6c0 226.304-180.023 409.6-402.286 409.6v0z" />
+<glyph unicode="&#xe905;" glyph-name="stop" d="M512 963.615c-282.624 0-512-230.996-512-515.615s229.376-515.615 512-515.615c282.624 0 512 230.996 512 515.615s-229.376 515.615-512 515.615zM512 35.508c-225.792 0-409.6 185.106-409.6 412.492s183.808 412.492 409.6 412.492c225.792 0 409.6-185.106 409.6-412.492s-183.808-412.492-409.6-412.492zM341.216 622.399h338.824v-341.216h-338.824v341.216z" />
+<glyph unicode="&#xe906;" glyph-name="run" d="M409.6 215.973l307.2 232.027-307.2 232.027v-464.054zM512 963.615c-282.624 0-512-230.996-512-515.615s229.376-515.615 512-515.615c282.624 0 512 230.996 512 515.615s-229.376 515.615-512 515.615v0zM102.4 448c0 227.386 183.808 412.492 409.6 412.492s409.6-185.106 409.6-412.492c0-227.386-183.808-412.492-409.6-412.492s-409.6 185.106-409.6 412.492z" />
+<glyph unicode="&#xe907;" glyph-name="paste" d="M824.889 866.909h-186.839c-18.773 53.993-67.942 93.091-126.050 93.091s-107.276-39.098-126.050-93.091h-186.839c-49.168 0-89.397-41.891-89.397-93.091v-744.727c0-51.2 40.229-93.091 89.397-93.091h625.778c49.168 0 89.397 41.891 89.397 93.091v744.727c0 51.2-40.229 93.091-89.397 93.091v0zM512 866.909c24.584 0 44.698-20.945 44.698-46.545s-20.114-46.545-44.698-46.545c-24.584 0-44.698 20.945-44.698 46.545s20.114 46.545 44.698 46.545v0zM824.889 29.091h-625.778v744.727h89.397v-139.636h446.984v139.636h89.397v-744.727z" />
+<glyph unicode="&#xe908;" glyph-name="copy" d="M728.104 960h-576.277c-52.825 0-96.046-41.891-96.046-93.091v-651.636h96.046v651.636h576.277v93.091zM872.173 773.818h-528.254c-52.825 0-96.046-41.891-96.046-93.091v-651.636c0-51.2 43.221-93.091 96.046-93.091h528.254c52.825 0 96.046 41.891 96.046 93.091v651.636c0 51.2-43.221 93.091-96.046 93.091v0zM872.173 29.091h-528.254v651.636h528.254v-651.636z" />
+<glyph unicode="&#xe909;" glyph-name="cut" d="M391.168 671.232c11.776 25.6 18.432 53.76 18.432 83.968 0 113.152-91.648 204.8-204.8 204.8s-204.8-91.648-204.8-204.8c0-113.152 91.648-204.8 204.8-204.8 30.208 0 58.368 6.656 83.968 18.432l120.832-120.832-120.832-120.832c-25.6 11.776-53.76 18.432-83.968 18.432-113.152 0-204.8-91.648-204.8-204.8s91.648-204.8 204.8-204.8c113.152 0 204.8 91.648 204.8 204.8 0 30.208-6.656 58.368-18.432 83.968l120.832 120.832 358.4-358.4h153.6v51.2l-632.832 632.832zM204.8 652.8c-56.32 0-102.4 45.568-102.4 102.4s46.080 102.4 102.4 102.4c56.32 0 102.4-45.568 102.4-102.4s-46.080-102.4-102.4-102.4v0zM204.8 38.4c-56.32 0-102.4 45.568-102.4 102.4s46.080 102.4 102.4 102.4c56.32 0 102.4-45.568 102.4-102.4s-46.080-102.4-102.4-102.4v0zM512 422.4c-14.336 0-25.6 11.264-25.6 25.6s11.264 25.6 25.6 25.6c14.336 0 25.6-11.264 25.6-25.6s-11.264-25.6-25.6-25.6v0zM870.4 908.8l-307.2-307.2 102.4-102.4 358.4 358.4v51.2h-153.6z" />
+<glyph unicode="&#xe90a;" glyph-name="newcell" d="M1024 374.857h-438.857v-438.857h-146.286v438.857h-438.857v146.286h438.857v438.857h146.286v-438.857h438.857v-146.286z" />
+<glyph unicode="&#xe90b;" glyph-name="save" d="M796.444 963.615h-682.667c-62.862 0-113.778-51.275-113.778-114.581v-802.068c0-63.306 50.916-114.581 113.778-114.581h796.444c62.862 0 113.778 51.275 113.778 114.581v687.487l-227.556 229.162zM512 46.966c-94.151 0-170.667 77.056-170.667 171.872s76.516 171.872 170.667 171.872c94.151 0 170.667-77.056 170.667-171.872s-76.516-171.872-170.667-171.872v0zM682.667 619.872h-568.889v229.162h568.889v-229.162z" />
+<glyph unicode="&#xe90c;" glyph-name="RestartNotebook" d="M512 780.8v179.2l-220-224 220-224v179.2c145.64 0 264-120.512 264-268.8s-118.36-268.8-264-268.8c-145.64 0-264 120.512-264 268.8h-88c0-198.016 157.52-358.4 352-358.4s352 160.384 352 358.4c0 198.016-157.52 358.4-352 358.4v0z" />
+</font></defs></svg>

BIN
src/notebook/fonts/icomoon.ttf


BIN
src/notebook/fonts/icomoon.woff


+ 110 - 8
src/notebook/notebook/manager.ts

@@ -56,7 +56,7 @@ class NotebookManager {
     let model = this.model;
     for (let i = 0; i < model.cells.length; i++) {
       let cell = model.cells.get(i);
-      if (i === model.activeCellIndex || model.isSelected(cell)) {
+      if (model.isSelected(cell)) {
         undelete.push(this.cloneCell(cell));
         model.cells.remove(cell);
       }
@@ -67,6 +67,7 @@ class NotebookManager {
     if (this._undeleteStack.length > DELETE_STACK_SIZE) {
       this._undeleteStack.shift();
     }
+    this.deselectCells();
   }
 
   /**
@@ -83,6 +84,7 @@ class NotebookManager {
     for (let cell of undelete.reverse()) {
       model.cells.insert(index, cell);
     }
+    this.deselectCells();
   }
 
   /**
@@ -95,7 +97,7 @@ class NotebookManager {
     let model = this.model;
     for (let i = 0; i < model.cells.length; i++) {
       let cell = model.cells.get(i);
-      if (i === model.activeCellIndex || model.isSelected(cell)) {
+      if (model.isSelected(cell)) {
         toMerge.push(cell.input.textEditor.text);
       }
       if (i === model.activeCellIndex) {
@@ -104,6 +106,7 @@ class NotebookManager {
         toDelete.push(cell);
       }
     }
+    this.deselectCells();
     // Make sure there are cells to merge.
     if (toMerge.length < 2 || !activeCell) {
       return;
@@ -131,6 +134,7 @@ class NotebookManager {
   insertAbove(): void {
     let cell = this.model.createCodeCell();
     this.model.cells.insert(this.model.activeCellIndex, cell);
+    this.deselectCells();
   }
 
   /**
@@ -139,6 +143,7 @@ class NotebookManager {
   insertBelow(): void {
     let cell = this.model.createCodeCell();
     this.model.cells.insert(this.model.activeCellIndex + 1, cell);
+    this.deselectCells();
   }
 
   /**
@@ -150,10 +155,11 @@ class NotebookManager {
     let model = this.model;
     for (let i = 0; i < model.cells.length; i++) {
       let cell = model.cells.get(i);
-      if (i === model.activeCellIndex || model.isSelected(cell)) {
+      if (model.isSelected(cell)) {
         this._copied.push(this.cloneCell(cell));
       }
     }
+    this.deselectCells();
   }
 
   /**
@@ -165,11 +171,12 @@ class NotebookManager {
     let model = this.model;
     for (let i = 0; i < model.cells.length; i++) {
       let cell = model.cells.get(i);
-      if (i === model.activeCellIndex || model.isSelected(cell)) {
+      if (model.isSelected(cell)) {
         this._cut.push(this.cloneCell(cell));
         model.cells.remove(cell);
       }
     }
+    this.deselectCells();
   }
 
   /**
@@ -193,6 +200,7 @@ class NotebookManager {
     }
     this._copied = [];
     this._cut = [];
+    this.deselectCells();
   }
 
   /**
@@ -202,7 +210,7 @@ class NotebookManager {
     let model = this.model;
     for (let i = 0; i < model.cells.length; i++) {
       let cell = model.cells.get(i);
-      if (i !== model.activeCellIndex && !model.isSelected(cell)) {
+      if (!model.isSelected(cell)) {
         continue;
       }
       let newCell: ICellModel;
@@ -221,6 +229,7 @@ class NotebookManager {
       model.cells.remove(cell);
       model.cells.insert(i, newCell);
     }
+    this.deselectCells();
   }
 
   /**
@@ -232,13 +241,13 @@ class NotebookManager {
     let selected: ICellModel[] = [];
     for (let i = 0; i < cells.length; i++) {
       let cell = cells.get(i);
-      if (i === model.activeCellIndex || model.isSelected(cell)) {
+      if (model.isSelected(cell)) {
         selected.push(cell);
       }
     }
     for (let cell of selected) {
-       model.activeCellIndex = cells.indexOf(cell);
-       model.runActiveCell();
+      model.activeCellIndex = cells.indexOf(cell);
+      model.runActiveCell();
     }
   }
 
@@ -258,6 +267,7 @@ class NotebookManager {
       model.mode = 'edit';
     }
     model.activeCellIndex += 1;
+    this.deselectCells();
   }
 
   /**
@@ -269,6 +279,7 @@ class NotebookManager {
     let cell = model.createCodeCell();
     model.cells.insert(model.activeCellIndex, cell);
     model.mode = 'edit';
+    this.deselectCells();
   }
 
   /**
@@ -296,6 +307,97 @@ class NotebookManager {
     .then(() => { model.dirty = false; });
   }
 
+  /**
+   * Select the cell below the active cell.
+   */
+  selectBelow(): void {
+    if (this.model.activeCellIndex === this.model.cells.length - 1) {
+      return;
+    }
+    this.model.activeCellIndex += 1;
+    this.deselectCells();
+  }
+
+  /**
+   * Select the above the active cell.
+   */
+  selectAbove(): void {
+    if (this.model.activeCellIndex === 0) {
+      return;
+    }
+    this.model.activeCellIndex -= 1;
+    this.deselectCells();
+  }
+
+  /**
+   * Extend the selection to the cell above.
+   */
+  extendSelectionAbove(): void {
+    let model = this.model;
+    let cells = model.cells;
+    // Do not wrap around.
+    if (model.activeCellIndex === 0) {
+      return;
+    }
+    let current = cells.get(model.activeCellIndex);
+    let prev = cells.get(model.activeCellIndex - 1);
+    if (model.isSelected(prev)) {
+      model.deselect(current);
+      if (model.activeCellIndex >= 1) {
+        let prevPrev = cells.get(model.activeCellIndex - 1);
+        if (!model.isSelected(prevPrev)) {
+          model.deselect(prev);
+        }
+      } else {
+        model.deselect(prev);
+      }
+    } else {
+      model.select(current);
+    }
+    model.activeCellIndex -= 1;
+  }
+
+  /**
+   * Extend the selection to the cell below.
+   */
+  extendSelectionBelow(): void {
+    let model = this.model;
+    let cells = model.cells;
+    // Do not wrap around.
+    if (model.activeCellIndex === cells.length - 1) {
+      return;
+    }
+    let current = cells.get(model.activeCellIndex);
+    let next = cells.get(model.activeCellIndex + 1);
+    if (model.isSelected(next)) {
+      model.deselect(current);
+      if (model.activeCellIndex < cells.length - 1) {
+        let nextNext = cells.get(model.activeCellIndex + 1);
+        if (!model.isSelected(nextNext)) {
+          model.deselect(next);
+        }
+      } else {
+        model.deselect(next);
+      }
+    } else {
+      model.select(current);
+    }
+    model.activeCellIndex += 1;
+  }
+
+  /**
+   * Deselect all of the cells.
+   */
+  protected deselectCells(): void {
+    let cells = this.model.cells;
+    for (let i = 0; i < cells.length; i++) {
+      let cell = cells.get(i);
+      if (this.model.isSelected(cell)) {
+        this.model.deselect(cell);
+      }
+    }
+  }
+
   /**
    * Clone a cell model.
    */

+ 42 - 8
src/notebook/notebook/model.ts

@@ -87,6 +87,16 @@ interface INotebookModel extends IDisposable {
    */
   stateChanged: ISignal<INotebookModel, IChangedArgs<any>>;
 
+  /**
+   * A signal emitted when a user metadata state changes.
+   */
+  metadataChanged: ISignal<INotebookModel, string>;
+
+  /**
+   * A signal emitted when the selection changes.
+   */
+  selectionChanged: ISignal<INotebookModel, void>;
+
   /**
    * The default mime type for new code cells in the notebook.
    *
@@ -154,7 +164,7 @@ interface INotebookModel extends IDisposable {
   deselect(cell: ICellModel): void;
 
   /**
-   * Weheter a cell is selected.
+   * Whether a cell is selected.
    */
   isSelected(cell: ICellModel): boolean;
 
@@ -268,6 +278,13 @@ class NotebookModel implements INotebookModel {
     return NotebookModelPrivate.metadataChangedSignal.bind(this);
   }
 
+  /**
+   * A signal emitted when the selection changes.
+   */
+  get selectionChanged(): ISignal<INotebookModel, void> {
+    return NotebookModelPrivate.selectionChangedSignal.bind(this);
+  }
+
   /**
    * Get the observable list of notebook cells.
    *
@@ -383,8 +400,7 @@ class NotebookModel implements INotebookModel {
    * The index of the active cell.
    *
    * #### Notes
-   * The value will be clamped.  When setting this, all other cells
-   * will be marked as inactive.
+   * The value will be clamped.  The active cell is considered to be selected.
    */
   get activeCellIndex(): number {
     return this._activeCellIndex;
@@ -414,6 +430,13 @@ class NotebookModel implements INotebookModel {
     let oldValue = this._mode;
     this._mode = newValue;
     NotebookModelPrivate.modeChanged(this, newValue);
+    // Edit mode deselects all cells.
+    if (newValue === 'edit') {
+      for (let i = 0; i < this.cells.length; i++) {
+        let cell = this.cells.get(i);
+        this.deselect(cell);
+      }
+    }
     let name = 'mode';
     this.stateChanged.emit({ name, oldValue, newValue });
   }
@@ -467,20 +490,26 @@ class NotebookModel implements INotebookModel {
    */
   select(cell: ICellModel): void {
     NotebookModelPrivate.selectedProperty.set(cell, true);
+    this.selectionChanged.emit(void 0);
   }
 
   /**
    * Deselect a cell.
+   *
+   * #### Notes
+   * This has no effect on the "active" cell.
    */
   deselect(cell: ICellModel): void {
     NotebookModelPrivate.selectedProperty.set(cell, false);
+    this.selectionChanged.emit(void 0);
   }
 
   /**
-   * Weheter a cell is selected.
+   * Whether a cell is selected.
    */
   isSelected(cell: ICellModel): boolean {
-    return NotebookModelPrivate.selectedProperty.get(cell);
+    return (NotebookModelPrivate.selectedProperty.get(cell) ||
+            this.cells.indexOf(cell) === this.activeCellIndex);
   }
 
   /**
@@ -627,13 +656,12 @@ class NotebookModel implements INotebookModel {
     }
     this.dirty = true;
     let text = cell.input.textEditor.text.trim();
+    cell.executionCount = null;
     if (!text) {
-      cell.input.prompt = 'In [ ]:';
       return;
     }
     let session = this.session;
     if (!session || !session.kernel) {
-      cell.input.prompt = 'In [ ]:';
       return;
     }
     cell.input.prompt = 'In [*]:';
@@ -728,6 +756,12 @@ namespace NotebookModelPrivate {
   export
   const metadataChangedSignal = new Signal<INotebookModel, string>();
 
+  /**
+   * A signal emitted when a the selection state changes.
+   */
+  export
+  const selectionChangedSignal = new Signal<INotebookModel, void>();
+
   /**
    * An attached property for the selected state of a cell.
    */
@@ -746,7 +780,7 @@ namespace NotebookModelPrivate {
       let cell = cells.get(i);
       if (i === model.activeCellIndex || model.isSelected(cell)) {
         if (isMarkdownCellModel(cell)) {
-          cell.rendered = mode === 'edit';
+          cell.rendered = mode !== 'edit';
         }
       }
     }

+ 21 - 1
src/notebook/notebook/widget.ts

@@ -180,6 +180,11 @@ const ACTIVE_CLASS = 'jp-mod-active';
  */
 const SELECTED_CLASS = 'jp-mod-selected';
 
+/**
+ * The class name added to an active cell when there are other selected cells.
+ */
+const OTHER_SELECTED_CLASS = 'jp-mod-multiSelected';
+
 
 /**
  * A panel which contains a toolbar and a notebook.
@@ -306,6 +311,7 @@ class NotebookWidget extends Widget {
     }
     model.cells.changed.connect(this.onCellsChanged, this);
     model.stateChanged.connect(this.onModelChanged, this);
+    model.selectionChanged.connect(this.onSelectionChanged, this);
   }
 
   /**
@@ -436,18 +442,25 @@ class NotebookWidget extends Widget {
     if (widget) {
       widget.addClass(ACTIVE_CLASS);
     }
+    let count = 0;
     for (let i = 0; i < layout.childCount(); i++) {
       let cell = model.cells.get(i);
       widget = layout.childAt(i) as BaseCellWidget;
       if (i !== model.activeCellIndex) {
         widget.removeClass(ACTIVE_CLASS);
       }
-      if (i === model.activeCellIndex || model.isSelected(cell)) {
+      widget.removeClass(OTHER_SELECTED_CLASS);
+      if (model.isSelected(cell)) {
         widget.addClass(SELECTED_CLASS);
+        count++;
       } else {
         widget.removeClass(SELECTED_CLASS);
       }
     }
+    if (count > 1) {
+      widget = layout.childAt(model.activeCellIndex) as BaseCellWidget;
+      widget.addClass(OTHER_SELECTED_CLASS);
+    }
   }
 
   /**
@@ -520,6 +533,13 @@ class NotebookWidget extends Widget {
     this.update();
   }
 
+  /**
+   * Handle a change in the model selection.
+   */
+  protected onSelectionChanged(model: INotebookModel): void {
+    this.update();
+  }
+
   /**
    * Handle `click` events for the widget.
    */

+ 11 - 1
src/notebook/theme.css

@@ -105,13 +105,23 @@
 }
 
 
-.jp-Notebook.jp-mod-commandMode .jp-Notebook-cell.jp-mod-active {
+.jp-Notebook.jp-mod-commandMode .jp-Notebook-cell.jp-mod-active.jp-mod-selected {
   border-color: #ABABAB;
   border-left-width: 1px;
   background: linear-gradient(to right, #42A5F5 -40px, #42A5F5 5px, transparent 5px, transparent 100%);
 }
 
 
+.jp-Notebook.jp-mod-commandMode .jp-Notebook-cell.jp-mod-otherSelected.jp-mod-active {
+  background: linear-gradient(to right, #42A5F5 -40px, #42A5F5 7px, #E3F2FD 7px, #E3F2FD 100%);
+}
+
+
+.jp-Notebook.jp-mod-commandMode .jp-Notebook-cell.jp-mod-selected {
+  background: #E3F2FD;
+}
+
+
 .jp-Notebook.jp-mod-editMode .jp-Notebook-cell.jp-mod-active {
   border-color: #66BB6A;
   border-left-width: 1px;