From f05dfae0f3b2368bead6e374779ac21f7948e245 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 5 Nov 2015 15:05:34 -0800 Subject: [PATCH] Rework multiple selections to be continuous It use the notion of Head/Anchor moving the selection normally move both the head and the anchor. It is possible to mot move the anchor, for example by pressing Shift. Everything in between the head and the anchor (head and anchor included) is in a "soft-selection" marked with the 'jupyter-soft-selection' class in CSS. This then remove completely the notion of marked cell, and thus disallow discontinuous selection. Unlike previous implementation the used a thick left border, this use background gradient with 2 stops at the same position to archived the same visual effect without the gap in between left border in between contiguous cells. For compatibility with previous versions, a few choices have been made. An extra multi-selected class is added to the body when multiple cells are selected. This allow to toggle the background to a soft blue indicating selection. And the Head of the selection is still called the "selected" cell. The on_focus event have been reworked as a click-let-click-release-outside of codemirror was putting the notebook in undefined state with multiple selected cells, and the head in edit mode. Finally Travis now test on edge group, to get faster, which can be removed in a few weeks as the all infrastructure of Travis will move on Google Cloud. --- .travis.yml | 1 + notebook/static/base/js/keyboard.js | 2 +- notebook/static/notebook/js/actions.js | 69 ++--- notebook/static/notebook/js/cell.js | 82 +++--- notebook/static/notebook/js/codecell.js | 8 +- .../static/notebook/js/keyboardmanager.js | 10 +- notebook/static/notebook/js/menubar.js | 2 +- notebook/static/notebook/js/notebook.js | 244 +++++++----------- notebook/static/notebook/js/textcell.js | 2 +- notebook/static/notebook/less/cell.less | 54 ++-- notebook/static/notebook/less/variables.less | 17 +- notebook/tests/notebook/dualmode_merge.js | 2 +- ...ked_cells.js => execute_selected_cells.js} | 69 ++--- notebook/tests/notebook/marks.js | 73 ------ notebook/tests/notebook/multiselect.js | 49 ++++ notebook/tests/notebook/undelete.js | 14 +- notebook/tests/util.js | 28 +- 17 files changed, 302 insertions(+), 424 deletions(-) rename notebook/tests/notebook/{execute_marked_cells.js => execute_selected_cells.js} (61%) delete mode 100644 notebook/tests/notebook/marks.js create mode 100644 notebook/tests/notebook/multiselect.js diff --git a/.travis.yml b/.travis.yml index 99ae90699..7463d827e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ # http://travis-ci.org/#!/ipython/ipython language: python +group: edge cache: directories: - ~/.cache/bower diff --git a/notebook/static/base/js/keyboard.js b/notebook/static/base/js/keyboard.js index 2259cd126..628ff99e4 100644 --- a/notebook/static/base/js/keyboard.js +++ b/notebook/static/base/js/keyboard.js @@ -362,7 +362,7 @@ define([ **/ var action_name = this.actions.get_name(data); if (! action_name){ - throw new Error('does not know how to deal with', data); + throw new Error('does not know how to deal with : ' + data); } shortcut = normalize_shortcut(shortcut); this.set_shortcut(shortcut, action_name); diff --git a/notebook/static/notebook/js/actions.js b/notebook/static/notebook/js/actions.js index 2b05f7603..b39a68902 100644 --- a/notebook/static/notebook/js/actions.js +++ b/notebook/static/notebook/js/actions.js @@ -107,10 +107,10 @@ define(function(require){ } }, 'run-cell':{ - help : 'run marked cells', + help : 'run selected cells', help_index : 'bb', handler : function (env) { - env.notebook.execute_marked_cells(); + env.notebook.execute_selected_cells(); } }, 'run-cell-and-insert-below':{ @@ -163,7 +163,7 @@ define(function(require){ handler : function (env) { var index = env.notebook.get_selected_index(); if (index !== 0 && index !== null) { - env.notebook.select_prev(); + env.notebook.select_prev(true); env.notebook.focus_cell(); } } @@ -174,23 +174,23 @@ define(function(require){ handler : function (env) { var index = env.notebook.get_selected_index(); if (index !== (env.notebook.ncells()-1) && index !== null) { - env.notebook.select_next(); + env.notebook.select_next(true); env.notebook.focus_cell(); } } }, - 'extend-marked-cells-above' : { - help: 'extend marked cells above', + 'extend-selection-above' : { + help: 'extend selected cells above', help_index : 'dc', handler : function (env) { - env.notebook.extend_marked(-1); + env.notebook.extend_selection_by(-1) } }, - 'extend-marked-cells-below' : { - help: 'extend marked cells below', + 'extend-selection-below' : { + help: 'extend selected cells below', help_index : 'dd', handler : function (env) { - env.notebook.extend_marked(1); + env.notebook.extend_selection_by(1) } }, 'cut-cell' : { @@ -229,7 +229,7 @@ define(function(require){ help_index : 'ec', handler : function (env) { env.notebook.insert_cell_above(); - env.notebook.select_prev(); + env.notebook.select_prev(true); env.notebook.focus_cell(); } }, @@ -239,7 +239,7 @@ define(function(require){ help_index : 'ed', handler : function (env) { env.notebook.insert_cell_below(); - env.notebook.select_next(); + env.notebook.select_next(true); env.notebook.focus_cell(); } }, @@ -380,16 +380,10 @@ define(function(require){ } }, 'merge-cells' : { - help : 'merge marked cells', + help : 'merge selected cells', help_index: 'el', handler: function(env) { - env.notebook.merge_marked_cells(); - } - }, - 'close-pager' : { - help_index : 'gd', - handler : function (env) { - env.pager.collapse(); + env.notebook.merge_selected_cells(); } }, 'show-command-palette': { @@ -400,29 +394,6 @@ define(function(require){ env.notebook.show_command_palette(); } }, - 'toggle-cell-marked': { - help_index : 'cj', - help: 'toggle marks', - icon: 'fa-check', - handler : function(env){ - // Use bitwise logic to toggle the marked state. - env.notebook.get_selected_cell().marked ^= true; - } - }, - 'unmark-all-cells': { - help_index : 'ck', - help : 'unmark all cells', - handler : function(env) { - env.notebook.unmark_all_cells(); - } - }, - 'mark-all-cells': { - help_index : 'cl', - help : 'mark all cells', - handler : function(env) { - env.notebook.mark_all_cells(); - } - }, 'toggle-toolbar':{ help: 'hide/show the toolbar', handler : function(env){ @@ -438,14 +409,12 @@ define(function(require){ events.trigger('resize-header.Page'); } }, - 'close-pager-or-unmark-all-cells': { - help : 'close the pager or unmark all cells', + 'close-pager': { + help : 'close the pager', handler : function(env) { - // Collapse the page if it is open, otherwise unmark all. + // Collapse the page if it is open if (env.pager && env.pager.expanded) { env.pager.collapse(); - } else { - env.notebook.unmark_all_cells(); } } }, @@ -481,7 +450,7 @@ define(function(require){ event.preventDefault(); } env.notebook.command_mode(); - env.notebook.select_prev(); + env.notebook.select_prev(true); env.notebook.edit_mode(); cm = env.notebook.get_selected_cell().code_mirror; cm.setCursor(cm.lastLine(), 0); @@ -498,7 +467,7 @@ define(function(require){ event.preventDefault(); } env.notebook.command_mode(); - env.notebook.select_next(); + env.notebook.select_next(true); env.notebook.edit_mode(); var cm = env.notebook.get_selected_cell().code_mirror; cm.setCursor(0, 0); diff --git a/notebook/static/notebook/js/cell.js b/notebook/static/notebook/js/cell.js index 8594816e3..fc67293fd 100644 --- a/notebook/static/notebook/js/cell.js +++ b/notebook/static/notebook/js/cell.js @@ -1,4 +1,3 @@ -// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. /** @@ -21,7 +20,7 @@ define([ "use strict"; var overlayHack = CodeMirror.scrollbarModel.native.prototype.overlayHack; - + CodeMirror.scrollbarModel.native.prototype.overlayHack = function () { overlayHack.apply(this, arguments); // Reverse `min-height: 18px` scrollbar hack on OS X @@ -55,6 +54,7 @@ define([ this.placeholder = config.placeholder || ''; this.selected = false; + this.anchor = false; this.rendered = false; this.mode = 'command'; @@ -154,30 +154,29 @@ define([ } }; + /** + * trigger on focus and on click to bubble up to the notebook and + * potentially extend the selection if shift-click, contract the selection + * if just codemirror focus (so edit mode). + * We **might** be able to move that to notebook `handle_edit_mode`. + */ + Cell.prototype._on_click = function(event){ + if (!this.selected) { + this.events.trigger('select.Cell', {'cell':this, 'extendSelection':event.shiftKey}); + } + } + /** * Subclasses can implement override bind_events. - * Be carefull to call the parent method when overwriting as it fires event. - * this will be triggerd after create_element in constructor. + * Be careful to call the parent method when overwriting as it fires event. + * this will be triggered after create_element in constructor. * @method bind_events */ Cell.prototype.bind_events = function () { var that = this; // We trigger events so that Cell doesn't have to depend on Notebook. that.element.click(function (event) { - if (!that.selected) { - that.events.trigger('select.Cell', {'cell':that}); - } - - // Cmdtrl-click should mark the cell. - var isMac = navigator.platform.slice(0, 3).toLowerCase() === 'mac'; - if ((!isMac && event.ctrlKey) || (isMac && event.metaKey)) { - that.marked = !that.marked; - } - }); - that.element.focusin(function (event) { - if (!that.selected) { - that.events.trigger('select.Cell', {'cell':that}); - } + that._on_click(event) }); if (this.code_mirror) { this.code_mirror.on("change", function(cm, change) { @@ -186,6 +185,9 @@ define([ } if (this.code_mirror) { this.code_mirror.on('focus', function(cm, change) { + if (!that.selected) { + that.events.trigger('select.Cell', {'cell':that}); + } that.events.trigger('edit_mode.Cell', {cell: that}); }); } @@ -239,7 +241,7 @@ define([ /** - * Triger typsetting of math by mathjax on current cell element + * Triger typesetting of math by mathjax on current cell element * @method typeset */ Cell.prototype.typeset = function () { @@ -251,7 +253,13 @@ define([ * @method select * @return is the action being taken */ - Cell.prototype.select = function () { + Cell.prototype.select = function (moveanchor) { + // if anchor is true, set the move the anchor + moveanchor = (moveanchor === undefined)? true:moveanchor; + if(moveanchor){ + this.anchor=true; + } + if (!this.selected) { this.element.addClass('selected'); this.element.removeClass('unselected'); @@ -265,10 +273,14 @@ define([ /** * handle cell level logic when the cell is unselected * @method unselect - * @param {bool} leave_selected - true to move cursor away and extend selection * @return is the action being taken */ - Cell.prototype.unselect = function (leave_selected) { + Cell.prototype.unselect = function (moveanchor) { + // if anchor is true, remove also the anchor + moveanchor = (moveanchor === undefined)? true:moveanchor; + if (moveanchor){ + this.anchor = false + } if (this.selected) { this.element.addClass('unselected'); this.element.removeClass('selected'); @@ -279,32 +291,9 @@ define([ } }; - /** - * Whether or not the cell is marked. - * @return {boolean} - */ - Object.defineProperty(Cell.prototype, 'marked', { - get: function() { - return this.element.hasClass('marked'); - }, - set: function(value) { - var isMarked = this.element.hasClass('marked'); - // Use a casting comparison. Allows for the caller to assign 0 or - // 1 instead of a boolean value, which in return means the caller - // can do cell.marked ^= true to toggle the mark. - if (isMarked != value) { - if (value) { - this.element.addClass('marked'); - } else { - this.element.removeClass('marked'); - } - this.events.trigger('marked_changed.Cell', {cell: this, value: value}); - } - } - }); /** - * should be overritten by subclass + * should be overwritten by subclass * @method execute */ Cell.prototype.execute = function () { @@ -426,6 +415,7 @@ define([ */ Cell.prototype.focus_cell = function () { this.element.focus(); + this._on_click({}); }; /** diff --git a/notebook/static/notebook/js/codecell.js b/notebook/static/notebook/js/codecell.js index 59d8b4729..ecda8341b 100644 --- a/notebook/static/notebook/js/codecell.js +++ b/notebook/static/notebook/js/codecell.js @@ -188,7 +188,7 @@ define([ /** @method bind_events */ CodeCell.prototype.bind_events = function () { - Cell.prototype.bind_events.apply(this); + Cell.prototype.bind_events.apply(this, arguments); var that = this; this.element.focusout( @@ -393,7 +393,7 @@ define([ // Basic cell manipulation. CodeCell.prototype.select = function () { - var cont = Cell.prototype.select.apply(this); + var cont = Cell.prototype.select.apply(this, arguments); if (cont) { this.code_mirror.refresh(); this.auto_highlight(); @@ -402,7 +402,7 @@ define([ }; CodeCell.prototype.render = function () { - var cont = Cell.prototype.render.apply(this); + var cont = Cell.prototype.render.apply(this, arguments); // Always execute, even if we are already in the rendered state return cont; }; @@ -540,7 +540,7 @@ define([ * @return is the action being taken */ CodeCell.prototype.unselect = function() { - var cont = Cell.prototype.unselect.call(this); + var cont = Cell.prototype.unselect.apply(this, arguments); if (cont) { // When a code cell is unselected, make sure that the corresponding // tooltip and completer to that cell is closed. diff --git a/notebook/static/notebook/js/keyboardmanager.js b/notebook/static/notebook/js/keyboardmanager.js index 9955923df..21138b4ee 100644 --- a/notebook/static/notebook/js/keyboardmanager.js +++ b/notebook/static/notebook/js/keyboardmanager.js @@ -96,14 +96,14 @@ define([ 'i,i' : 'jupyter-notebook:interrupt-kernel', '0,0' : 'jupyter-notebook:confirm-restart-kernel', 'd,d' : 'jupyter-notebook:delete-cell', - 'esc': 'jupyter-notebook:close-pager-or-unmark-all-cells', + 'esc': 'jupyter-notebook:close-pager', 'up' : 'jupyter-notebook:select-previous-cell', 'k' : 'jupyter-notebook:select-previous-cell', 'j' : 'jupyter-notebook:select-next-cell', - 'shift-k': 'jupyter-notebook:extend-marked-cells-above', - 'shift-j': 'jupyter-notebook:extend-marked-cells-below', - 'shift-up': 'jupyter-notebook:extend-marked-cells-above', - 'shift-down': 'jupyter-notebook:extend-marked-cells-below', + 'shift-k': 'jupyter-notebook:extend-selection-above', + 'shift-j': 'jupyter-notebook:extend-selection-below', + 'shift-up': 'jupyter-notebook:extend-selection-above', + 'shift-down': 'jupyter-notebook:extend-selection-below', 'x' : 'jupyter-notebook:cut-cell', 'c' : 'jupyter-notebook:copy-cell', 'v' : 'jupyter-notebook:paste-cell-below', diff --git a/notebook/static/notebook/js/menubar.js b/notebook/static/notebook/js/menubar.js index 61207ff31..6ae6fc785 100644 --- a/notebook/static/notebook/js/menubar.js +++ b/notebook/static/notebook/js/menubar.js @@ -63,7 +63,7 @@ define([ // The selected cell loses focus when the menu is entered, so we // re-select it upon selection. var i = that.notebook.get_selected_index(); - that.notebook.select(i); + that.notebook.select(i, false); } ); }; diff --git a/notebook/static/notebook/js/notebook.js b/notebook/static/notebook/js/notebook.js index 5de9f14ba..14c8b42aa 100644 --- a/notebook/static/notebook/js/notebook.js +++ b/notebook/static/notebook/js/notebook.js @@ -29,6 +29,12 @@ define(function (require) { var scrollmanager = require('notebook/js/scrollmanager'); var commandpalette = require('notebook/js/commandpalette'); + var _SOFT_SELECTION_CLASS = 'jupyter-soft-selected'; + + function soft_selected(cell){ + return cell.element.hasClass(_SOFT_SELECTION_CLASS); + } + /** * Contains and manages cells. * @class Notebook @@ -187,9 +193,6 @@ define(function (require) { Notebook.prototype.bind_events = function () { var that = this; - this.events.on('marked_changed.Cell', function() { - that.update_marked_status(); - }); this.events.on('set_next_input.Notebook', function (event, data) { if (data.replace) { @@ -221,7 +224,7 @@ define(function (require) { this.events.on('select.Cell', function (event, data) { var index = that.find_cell_index(data.cell); - that.select(index); + that.select(index, !data.extendSelection); }); this.events.on('edit_mode.Cell', function (event, data) { @@ -298,9 +301,6 @@ define(function (require) { expand_time(time); }); - this.scroll_manager.onScroll(function () { - that.update_marked_status(); - }, 100); // Firefox 22 broke $(window).on("beforeunload") // I'm not sure why or how. @@ -584,13 +584,30 @@ define(function (require) { return i; }; + + Notebook.prototype.get_selected_cells = function () { + return this.get_cells().filter(function(cell, index){ return cell.selected || soft_selected(cell) || cell.anchor}) + }; + + Notebook.prototype.get_selected_cells_indices = function () { + + var result = []; + this.get_cells().filter(function (cell, index) { + if (cell.selected || soft_selected(cell) || cell.anchor) { + result.push(index); + } + }); + return result; + }; + + /** * Get the currently selected cell. * * @return {Cell} The selected cell */ Notebook.prototype.get_selected_cell = function () { - var index = this.get_selected_index(); + var index = this.get_selected_cells_indices(); return this.get_cell(index); }; @@ -608,6 +625,15 @@ define(function (require) { } }; + Notebook.prototype.get_anchor_index = function () { + var result = null; + this.get_cell_elements().filter(function (index) { + if ($(this).data("cell").anchor === true) { + result = index; + } + }); + return result; + }; /** * Get the index of the currently selected cell. * @@ -622,144 +648,52 @@ define(function (require) { }); return result; }; - - /** - * Toggles the marks on the cells - * @param {Cell[]} [cells] - optionally specify what cells should be toggled - */ - Notebook.prototype.toggle_cells_marked = function(cells) { - cells = cells || this.get_cells(); - cells.forEach(function(cell) { cell.marked = !cell.marked; }); - }; - - /** - * Mark all of the cells - * @param {Cell[]} [cells] - optionally specify what cells should be marked - */ - Notebook.prototype.mark_all_cells = function(cells) { - cells = cells || this.get_cells(); - cells.forEach(function(cell) { cell.marked = true; }); - }; - - /** - * Unmark all of the cells - * @param {Cell[]} [cells] - optionally specify what cells should be unmarked - */ - Notebook.prototype.unmark_all_cells = function(cells) { - this.get_marked_cells(cells).forEach(function(cell) { cell.marked = false; }); - }; - - /** - * Set the cells that should be marked, exclusively - * @param {Cell[]} cells - */ - Notebook.prototype.set_marked_cells = function(cells) { - this.unmark_all_cells(); - this.mark_all_cells(cells); - }; - - /** - * Gets the cells that are marked - * @param {Cell[]} [cells] - optionally provide the cells to search through - * @return {Cell[]} marked cells - */ - Notebook.prototype.get_marked_cells = function(cells) { - cells = cells || this.get_cells(); - return cells.filter(function(cell) { return (cell.marked || cell.selected); }); - }; - - /** - * Sets the cells that are marked by indices - * @param {number[]} indices - * @param {Cell[]} [cells] - optionally provide the cells to search through - */ - Notebook.prototype.set_marked_indices = function(indices, cells) { - cells = cells || this.get_cells(); - this.unmark_all_cells(cells); - this.mark_all_cells(cells.filter(function(cell, index) { return indices.indexOf(index) !== -1; })); - }; - - /** - * Gets the indices of the cells that are marked - * @param {Cell[]} [cells] - optionally provide the cells to search through - * @return {number[]} marked cell indices - */ - Notebook.prototype.get_marked_indices = function(cells) { - cells = cells || this.get_cells(); - var markedCells = this.get_marked_cells(cells); - return markedCells.map(function(cell) { return cells.indexOf(cell); }); - }; - - /** - * Checks if the marked cells are contiguous - * @param {Cell[]} [cells] - optionally provide the cells to search through - * @return {boolean} - */ - Notebook.prototype.are_marked_cells_contiguous = function(cells) { - // Get a numerically sorted list of the marked indices. - var markedIndices = this.get_marked_indices(cells).sort( - function(a,b) { return a-b; }); - // Check for contiguousness - for (var i = 0; i < markedIndices.length - 1; i++) { - if (markedIndices[i+1] - markedIndices[i] !== 1) { - return false; - } - } - return true; - }; - - /** - * Checks if the marked cells specified by their indices are contiguous - * @param {number[]} indices - the cell indices to search through - * @param {Cell[]} [cells] - the cells to search through - * @return {boolean} - */ - Notebook.prototype.are_marked_indices_contiguous = function(indices, cells) { - cells = cells || this.get_cells(); - return this.are_marked_cells_contiguous(cells.filter(function(cell, index) { - return indices.indexOf(index) !== -1; - })); - }; - /** - * Extend the selected range - * - * @param {number} offset - */ - Notebook.prototype.extend_marked = function(offset) { - // Mark currently selected cell - this.get_selected_cell().marked = true; + // Cell selection. - // Select the cell in the offset direction. Bound index between 0 and - // the number of cells -1. - var selectedIndex = Math.min(Math.max(this.get_selected_index() + offset, 0), this.ncells()-1); - this.select(selectedIndex); - this.ensure_focused(); + Notebook.prototype.extend_selection_by = function(delta) { + var index = this.get_selected_index(); + // do not move anchor + return this.select(index+delta, false); }; - Notebook.prototype.update_marked_status = function() { - var marked_cells = this.get_marked_cells(); - var num_offscreen = 0; - var i; - for (i = 0; i < marked_cells.length; i++) { - if (!this.scroll_manager.is_cell_visible(marked_cells[i])) { - num_offscreen += 1; - } - } - this.events.trigger('marked_offscreen.Cell', num_offscreen); - }; + Notebook.prototype.update_soft_selection = function(){ + var i1 = this.get_selected_index(); + var i2 = this.get_anchor_index(); + var low = Math.min(i1, i2); + var high = Math.max(i1, i2); + if (low !== high){ + $('body').addClass('jupyter-multi-select'); + } else { + $('body').removeClass('jupyter-multi-select'); + } + this.get_cells().map(function(cell, index, all){ + if( low <= index && index <= high ){ + cell.element.addClass(_SOFT_SELECTION_CLASS); + } else { + cell.element.removeClass(_SOFT_SELECTION_CLASS); + } + }) + } - // Cell selection. + Notebook.prototype._contract_selection = function(){ + var i = this.get_selected_index(); + this.select(i, true); + } /** * Programmatically select a cell. * * @param {integer} index - A cell's index + * @param {bool} moveanchor – whether to move the selection + * anchor, default to true. * @return {Notebook} This notebook */ - Notebook.prototype.select = function (index) { + Notebook.prototype.select = function (index, moveanchor) { + moveanchor = (moveanchor===undefined)? true : moveanchor; + if (this.is_valid_cell_index(index)) { var sindex = this.get_selected_index(); if (sindex !== null && index !== sindex) { @@ -768,11 +702,13 @@ define(function (require) { if (this.mode !== 'command') { this.command_mode(); } - this.get_cell(sindex).unselect(); + this.get_cell(sindex).unselect(moveanchor); + } + if(moveanchor){ + this.get_cell(this.get_anchor_index()).unselect(true); } var cell = this.get_cell(index); - cell.select(); - this.update_marked_status(); + cell.select(moveanchor); if (cell.cell_type === 'heading') { this.events.trigger('selected_cell_type_changed.Notebook', {'cell_type':cell.cell_type,level:cell.level} @@ -783,17 +719,20 @@ define(function (require) { ); } } + this.update_soft_selection(); return this; }; /** * Programmatically select the next cell. * + * @param {bool} moveanchor – whether to move the selection + * anchor, default to true. * @return {Notebook} This notebook */ - Notebook.prototype.select_next = function () { + Notebook.prototype.select_next = function (moveanchor) { var index = this.get_selected_index(); - this.select(index+1); + this.select(index+1, moveanchor); return this; }; @@ -802,9 +741,9 @@ define(function (require) { * * @return {Notebook} This notebook */ - Notebook.prototype.select_prev = function () { + Notebook.prototype.select_prev = function (moveanchor) { var index = this.get_selected_index(); - this.select(index-1); + this.select(index-1, moveanchor); return this; }; @@ -858,6 +797,7 @@ define(function (require) { * @param {Cell} [cell] Cell to enter edit mode on. */ Notebook.prototype.handle_edit_mode = function (cell) { + this._contract_selection(); if (cell && this.mode !== 'edit') { cell.edit_mode(); this.mode = 'edit'; @@ -870,6 +810,7 @@ define(function (require) { * Make a cell enter edit mode. */ Notebook.prototype.edit_mode = function () { + this._contract_selection(); var cell = this.get_selected_cell(); if (cell && this.mode !== 'edit') { cell.unrender(); @@ -978,7 +919,7 @@ define(function (require) { */ Notebook.prototype.delete_cells = function(indices) { if (indices === undefined) { - indices = this.get_marked_indices(); + indices = this.get_selected_cells_indices(); } this.undelete_backup = []; @@ -1418,7 +1359,7 @@ define(function (require) { * Copy cells. */ Notebook.prototype.copy_cell = function () { - var cells = this.get_marked_cells(); + var cells = this.get_selected_cells(); if (cells.length === 0) { cells = [this.get_selected_cell()]; } @@ -1507,7 +1448,6 @@ define(function (require) { // Unrender the new cell so we can call set_text. new_cell.unrender(); new_cell.set_text(texta); - new_cell.marked = cell.marked; } }; @@ -1564,14 +1504,13 @@ define(function (require) { this.delete_cells(indices); this.select(this.find_cell_index(target)); - this.unmark_all_cells(); }; /** * Merge the selected range of cells */ - Notebook.prototype.merge_marked_cells = function() { - this.merge_cells(this.get_marked_indices()); + Notebook.prototype.merge_selected_cells = function() { + this.merge_cells(this.get_selected_cells_indices()); }; /** @@ -2009,27 +1948,27 @@ define(function (require) { /** * Execute or render cell outputs and go into command mode. */ - Notebook.prototype.execute_marked_cells = function () { - this.execute_cells(this.get_marked_indices()); + Notebook.prototype.execute_selected_cells = function () { + this.execute_cells(this.get_selected_cells_indices()); }; + /** - * Alias for execute_marked_cells, for backwards compatibility -- + * Alias for execute_selected_cells, for backwards compatibility -- * previously, doing "Run Cell" would only ever run a single cell (hence * `execute_cell`), but now it runs all marked cells, so that's the * preferable function to use. But it is good to keep this function to avoid * breaking existing extensions, etc. */ Notebook.prototype.execute_cell = function () { - this.execute_marked_cells(); + this.execute_selected_cells(); }; /** * Execute or render cell outputs and insert a new cell below. */ Notebook.prototype.execute_cell_and_insert_below = function () { - // execute the marked cells, and don't insert anything - var indices = this.get_marked_indices(); + var indices = this.get_selected_cells_indices(); if (indices.length > 1) { this.execute_cells(indices); return; @@ -2061,8 +2000,7 @@ define(function (require) { * Execute or render cell outputs and select the next cell. */ Notebook.prototype.execute_cell_and_select_below = function () { - // execute the marked cells, and don't select anything - var indices = this.get_marked_indices(); + var indices = this.get_selected_cells_indices(); if (indices.length > 1) { this.execute_cells(indices); return; diff --git a/notebook/static/notebook/js/textcell.js b/notebook/static/notebook/js/textcell.js index e5bde67be..dc7471c37 100644 --- a/notebook/static/notebook/js/textcell.js +++ b/notebook/static/notebook/js/textcell.js @@ -118,7 +118,7 @@ define([ // Cell level actions TextCell.prototype.select = function () { - var cont = Cell.prototype.select.apply(this); + var cont = Cell.prototype.select.apply(this, arguments); if (cont) { if (this.mode === 'edit') { this.code_mirror.refresh(); diff --git a/notebook/static/notebook/less/cell.less b/notebook/static/notebook/less/cell.less index 85e0481ff..393626caa 100644 --- a/notebook/static/notebook/less/cell.less +++ b/notebook/static/notebook/less/cell.less @@ -1,59 +1,65 @@ @_cell_padding_minus_border: @cell_padding - @cell_border_width; -._marked_style(@n) { - border-left-width: @n; - padding-left: @cell_padding - @n; +._selected_style(@c1, @c2, @sep:0, @border_width:@cell_border_width) { + border-left-width: @border_width; + padding-left: @cell_padding - @border_width; + background: linear-gradient(to right, @c1 -40px,@c1 @sep,@c2 @sep,@c2 100%); } + div.cell { - border: @cell_border_width solid transparent; .vbox(); .corner-all(); .border-box-sizing(); + border-width: @cell_border_width; border-style: solid; + border-color: transparent; - &.marked { - ._marked_style(3px); - border-left-color: @marked_border_color_light; - - /* Don't border the cells when printing */ + width: 100%; + padding: @_cell_padding_minus_border; + /* This acts as a spacer between cells, that is outside the border */ + margin: 0px; + outline: none; + + ._selected_style(transparent, transparent, @cell_border_width); + + .jupyter-multi-select &.jupyter-soft-selected { + border-left-color: @selected_border_color_light; + border-left-color: @soft_select_color; + ._selected_style(@selected_border_color_light, @soft_select_color, 5px, 0px); + @media print { border-color: transparent; } } - &.selected { + &.selected { border-color: @border_color; - border-left-color: @marked_border_color; - ._marked_style(2px); - - &.marked { - ._marked_style(4px); - } - - /* Don't border the cells when printing */ + ._selected_style(@selected_border_color, transparent, 5px, 0px); + + @media print { border-color: transparent; } } + .jupyter-multi-select &.selected.jupyter-soft-selected { + ._selected_style(@selected_border_color, @soft_select_color, 7px, 0); + } + .edit_mode &.selected { border-color: @edit_mode_border_color; + ._selected_style(@edit_mode_border_color, transparent, 5px, 0px); - /* Don't border the cells when printing */ @media print { border-color: transparent; } } - width: 100%; - padding: @_cell_padding_minus_border; - /* This acts as a spacer between cells, that is outside the border */ - margin: 0px; - outline: none; } + .prompt { /* This needs to be wide enough for 3 digit prompt numbers: In[100]: */ min-width: 14ex; diff --git a/notebook/static/notebook/less/variables.less b/notebook/static/notebook/less/variables.less index 441395491..c1dad6927 100644 --- a/notebook/static/notebook/less/variables.less +++ b/notebook/static/notebook/less/variables.less @@ -11,12 +11,17 @@ @code_line_height: 1.21429em; // changed from 1.231 to get 17px even @code_padding: 0.4em; // 5.6 px @rendered_html_border_color: black; -@input_prompt_color: navy; -@output_prompt_color: darkred; +@input_prompt_color: #303F9F; +@output_prompt_color: #D84315; @output_pre_color: black; @notification_widget_bg: rgba(240, 240, 240, 0.5); -@marked_border_color: #009AF5; -@marked_border_color_light: #7AC7F5; -@edit_mode_border_color: green; + + +@selected_border_color: #42A5F5; +@selected_border_color_light: #90CAF9; +@soft_select_color: #E3F2FD; + + +@edit_mode_border_color: #66BB6A; @cell_padding: 6px; -@cell_border_width: 1px; \ No newline at end of file +@cell_border_width: 1px; diff --git a/notebook/tests/notebook/dualmode_merge.js b/notebook/tests/notebook/dualmode_merge.js index 2a6520595..b39f753fa 100644 --- a/notebook/tests/notebook/dualmode_merge.js +++ b/notebook/tests/notebook/dualmode_merge.js @@ -41,7 +41,7 @@ casper.notebook_test(function () { this.test.assertEquals(this.get_cell_text(1), 'cd', 'split; Verify that cell 1 has the second half.'); this.validate_notebook_state('split', 'edit', 1); this.select_cell(0); // Move up to cell 0 - this.evaluate(function() { IPython.notebook.extend_marked(1);}); + this.evaluate(function() { IPython.notebook.extend_selection_by(1);}); this.trigger_keydown('shift-m'); // Merge this.validate_notebook_state('merge', 'command', 0); this.test.assertEquals(this.get_cell_text(0), a, 'merge; Verify that cell 0 has the merged contents.'); diff --git a/notebook/tests/notebook/execute_marked_cells.js b/notebook/tests/notebook/execute_selected_cells.js similarity index 61% rename from notebook/tests/notebook/execute_marked_cells.js rename to notebook/tests/notebook/execute_selected_cells.js index 2ce0f08e0..67119f397 100644 --- a/notebook/tests/notebook/execute_marked_cells.js +++ b/notebook/tests/notebook/execute_selected_cells.js @@ -3,16 +3,18 @@ // casper.notebook_test(function () { var that = this; - var assert_outputs = function (expected) { + var assert_outputs = function (expected, msg_prefix) { var msg, i; + msg_prefix = "(assert_outputs) "+(msg_prefix || 'no prefix')+": "; for (i = 0; i < that.get_cells_length(); i++) { if (expected[i] === undefined) { - msg = 'cell ' + i + ' not executed'; + msg = msg_prefix + 'cell ' + i + ' not executed'; that.test.assertFalse(that.cell_has_outputs(i), msg); } else { - msg = 'cell ' + i + ' executed'; - that.test.assertEquals(that.get_output_cell(i).text, expected[i], msg); + msg = msg_prefix + 'cell ' + i + ' executed'; + var out = that.get_output_cell(i, undefined, msg_prefix).text + that.test.assertEquals(out, expected[i], msg + 'out is: '+out); } } }; @@ -23,69 +25,54 @@ casper.notebook_test(function () { this.append_cell('print("c")'); this.append_cell('print("d")'); this.test.assertEquals(this.get_cells_length(), 4, "correct number of cells"); - - this.evaluate(function () { - IPython.notebook.unmark_all_cells(); - IPython.notebook.set_marked_indices([1, 2]); - }); }); this.then(function () { - this.evaluate(function () { - IPython.notebook.clear_all_output(); - }); - this.select_cell(1); - this.validate_notebook_state('before execute', 'command', 1); - this.trigger_keydown('ctrl-enter'); - }); - - this.wait_for_output(1); - this.wait_for_output(2); - - this.then(function () { - assert_outputs([undefined, 'b\n', 'c\n', undefined]); - this.validate_notebook_state('run marked cells', 'command', 2); + this.select_cell(2, false); }); - // execute cells in place when there are marked cells this.then(function () { this.evaluate(function () { IPython.notebook.clear_all_output(); }); + }) + this.then(function(){ this.select_cell(1); - this.validate_notebook_state('before execute', 'command', 1); - this.trigger_keydown('shift-enter'); + this.validate_notebook_state('before execute 1', 'command', 1); + this.select_cell(1); + this.select_cell(2, false); + this.trigger_keydown('ctrl-enter'); }); this.wait_for_output(1); this.wait_for_output(2); this.then(function () { - assert_outputs([undefined, 'b\n', 'c\n', undefined]); - this.validate_notebook_state('run marked cells', 'command', 2); + assert_outputs([undefined, 'b\n', 'c\n', undefined], 'run selected 1'); + this.validate_notebook_state('run selected cells 1', 'command', 2); }); - // execute and insert below when there are marked cells + + // execute and insert below when there are selected cells this.then(function () { this.evaluate(function () { IPython.notebook.clear_all_output(); }); this.select_cell(1); - this.validate_notebook_state('before execute', 'command', 1); + this.validate_notebook_state('before execute 2', 'command', 1); this.evaluate(function () { $("#run_cell_insert_below").click(); }); }); this.wait_for_output(1); - this.wait_for_output(2); this.then(function () { - assert_outputs([undefined, 'b\n', 'c\n', undefined]); - this.validate_notebook_state('run marked cells', 'command', 2); + assert_outputs([undefined, 'b\n', undefined, undefined , undefined],'run selected cells 2'); + this.validate_notebook_state('run selected cells 2', 'edit', 2); }); // check that it doesn't affect run all above @@ -95,7 +82,7 @@ casper.notebook_test(function () { }); this.select_cell(1); - this.validate_notebook_state('before execute', 'command', 1); + this.validate_notebook_state('before execute 3', 'command', 1); this.evaluate(function () { $("#run_all_cells_above").click(); }); @@ -104,7 +91,7 @@ casper.notebook_test(function () { this.wait_for_output(0); this.then(function () { - assert_outputs(['a\n', undefined, undefined, undefined]); + assert_outputs(['a\n', undefined, undefined, undefined],'run cells above'); this.validate_notebook_state('run cells above', 'command', 0); }); @@ -115,7 +102,7 @@ casper.notebook_test(function () { }); this.select_cell(1); - this.validate_notebook_state('before execute', 'command', 1); + this.validate_notebook_state('before execute 4', 'command', 1); this.evaluate(function () { $("#run_all_cells_below").click(); }); @@ -126,8 +113,8 @@ casper.notebook_test(function () { this.wait_for_output(3); this.then(function () { - assert_outputs([undefined, 'b\n', 'c\n', 'd\n']); - this.validate_notebook_state('run cells below', 'command', 3); + assert_outputs([undefined, 'b\n', undefined, 'c\n', 'd\n'],'run cells below'); + this.validate_notebook_state('run cells below', 'command', 4); }); // check that it doesn't affect run all @@ -137,7 +124,7 @@ casper.notebook_test(function () { }); this.select_cell(1); - this.validate_notebook_state('before execute', 'command', 1); + this.validate_notebook_state('before execute 5', 'command', 1); this.evaluate(function () { $("#run_all_cells").click(); }); @@ -149,7 +136,7 @@ casper.notebook_test(function () { this.wait_for_output(3); this.then(function () { - assert_outputs(['a\n', 'b\n', 'c\n', 'd\n']); - this.validate_notebook_state('run all cells', 'command', 3); + assert_outputs(['a\n', 'b\n', undefined, 'c\n', 'd\n'],'run all cells'); + this.validate_notebook_state('run all cells', 'command', 4); }); }); diff --git a/notebook/tests/notebook/marks.js b/notebook/tests/notebook/marks.js deleted file mode 100644 index a59823491..000000000 --- a/notebook/tests/notebook/marks.js +++ /dev/null @@ -1,73 +0,0 @@ - -// Test -casper.notebook_test(function () { - var that = this; - - var a = 'print("a")'; - var index = this.append_cell(a); - - var b = 'print("b")'; - index = this.append_cell(b); - - var c = 'print("c")'; - index = this.append_cell(c); - - this.then(function () { - var selectedIndex = this.evaluate(function () { - Jupyter.notebook.select(0); - return Jupyter.notebook.get_selected_index(); - }); - - this.test.assertEquals(this.evaluate(function() { - return Jupyter.notebook.get_marked_cells().length; - }), 1, 'only one cell is marked programmatically'); - - this.test.assertEquals(this.evaluate(function() { - return Jupyter.notebook.get_marked_indices()[0]; - }), selectedIndex, 'marked cell is selected cell'); - - this.test.assertEquals(this.evaluate(function() { - return $('.cell.marked').length; - }), 0, 'no cells are marked visibily'); - - this.evaluate(function() { - Jupyter.notebook.mark_all_cells(); - }); - - var cellCount = this.evaluate(function() { - return Jupyter.notebook.ncells(); - }); - - this.test.assertEquals(this.evaluate(function() { - return Jupyter.notebook.get_marked_cells().length; - }), cellCount, 'mark_all'); - - this.test.assertEquals(this.evaluate(function() { - return $('.cell.marked').length; - }), cellCount, 'marked cells are marked visibily'); - - this.evaluate(function() { - Jupyter.notebook.unmark_all_cells(); - }); - - this.test.assertEquals(this.evaluate(function() { - return Jupyter.notebook.get_marked_cells().length; - }), 1, 'unmark_all'); - - this.test.assertEquals(this.evaluate(function() { - return Jupyter.notebook.get_marked_indices()[0]; - }), selectedIndex, 'marked cell is selected cell'); - - this.evaluate(function() { - Jupyter.notebook.set_marked_indices([1]); - }); - - this.test.assertEquals(this.evaluate(function() { - return Jupyter.notebook.get_marked_cells().length; - }), 2, 'two cells are marked'); - - this.test.assertEquals(this.evaluate(function() { - return Jupyter.notebook.get_marked_indices(); - }), [selectedIndex, 1], 'get/set_marked_indices'); - }); -}); diff --git a/notebook/tests/notebook/multiselect.js b/notebook/tests/notebook/multiselect.js new file mode 100644 index 000000000..0e0df023a --- /dev/null +++ b/notebook/tests/notebook/multiselect.js @@ -0,0 +1,49 @@ + +// Test +casper.notebook_test(function () { + var that = this; + + var a = 'print("a")'; + var index = this.append_cell(a); + + var b = 'print("b")'; + index = this.append_cell(b); + + var c = 'print("c")'; + index = this.append_cell(c); + + this.then(function () { + var selectedIndex = this.evaluate(function () { + Jupyter.notebook.select(0); + return Jupyter.notebook.get_selected_index(); + }); + + this.test.assertEquals(this.evaluate(function() { + return Jupyter.notebook.get_selected_cells().length; + }), 1, 'only one cell is selected programmatically'); + + this.test.assertEquals(this.evaluate(function() { + return $('.cell.jupyter-soft-selected').length; + }), 1, 'one cell is selected'); + + + + this.test.assertEquals(this.evaluate(function() { + Jupyter.notebook.extend_selection_by(1); + return Jupyter.notebook.get_selected_cells().length; + }), 2, 'extend selection by one'); + + + this.test.assertEquals(this.evaluate(function() { + Jupyter.notebook.extend_selection_by(-1); + return Jupyter.notebook.get_selected_cells().length; + }), 1, 'contract selection by one'); + + this.test.assertEquals(this.evaluate(function() { + Jupyter.notebook.select(1); + Jupyter.notebook.extend_selection_by(-1); + return Jupyter.notebook.get_selected_cells().length; + }), 2, 'extend selection by one up'); + + }); +}); diff --git a/notebook/tests/notebook/undelete.js b/notebook/tests/notebook/undelete.js index 6269b6f17..371c1838c 100644 --- a/notebook/tests/notebook/undelete.js +++ b/notebook/tests/notebook/undelete.js @@ -4,11 +4,11 @@ casper.notebook_test(function () { var that = this; - var assert_marked_cells = function (action, indices) { - var marked = that.evaluate(function () { - return IPython.notebook.get_marked_indices(); + var assert_selected_cells = function (action, indices) { + var selected = that.evaluate(function () { + return IPython.notebook.get_selected_cells_indices(); }); - that.test.assertEquals(marked, indices, action + "; verify marked cells"); + that.test.assertEquals( selected, indices, action + "; verify selected cells"); }; var assert_cells = function (action, cells, index) { @@ -22,7 +22,7 @@ casper.notebook_test(function () { } that.validate_notebook_state(action, 'command', index); - assert_marked_cells(action, [index]); + assert_selected_cells(action, [index]); }; var a = 'print("a")'; @@ -59,7 +59,7 @@ casper.notebook_test(function () { this.select_cell(1); this.trigger_keydown('esc'); this.trigger_keydown('shift-j'); - assert_marked_cells("select cells 1-2", [1, 2]); + assert_selected_cells("select cells 1-2", [1, 2]); this.trigger_keydown('shift-m'); this.trigger_keydown('esc'); assert_cells("merge cells 1-2", [a, bc, d], 1); @@ -75,7 +75,7 @@ casper.notebook_test(function () { this.select_cell(3); this.trigger_keydown('esc'); this.trigger_keydown('shift-k'); - assert_marked_cells("select cells 3-2", [2, 3]); + assert_selected_cells("select cells 3-2", [2, 3]); this.trigger_keydown('shift-m'); this.trigger_keydown('esc'); assert_cells("merge cells 3-2", [a, bc, cd], 2); diff --git a/notebook/tests/util.js b/notebook/tests/util.js index 8a15b1d56..8a82c9a37 100644 --- a/notebook/tests/util.js +++ b/notebook/tests/util.js @@ -169,11 +169,11 @@ casper.wait_for_output = function (cell_num, out_num) { }, function then() { }, function timeout() { - this.echo("wait_for_output timed out on cell "+cell_num+", waiting for "+out_num+"outputs ."); + this.echo("wait_for_output timed out on cell "+cell_num+", waiting for "+out_num+" outputs ."); var pn = this.evaluate(function get_prompt(c) { - return IPython.notebook.get_cell(c).input_prompt_number; + return (IPython.notebook.get_cell(c)|| {'input_prompt_number':'no cell'}).input_prompt_number; }); - this.echo("cell prompt was :'"+pn+"'.") + this.echo("cell prompt was :'"+pn+"'."); }); }); }; @@ -229,7 +229,8 @@ casper.cell_has_outputs = function (cell_num) { return result > 0; }; -casper.get_output_cell = function (cell_num, out_num) { +casper.get_output_cell = function (cell_num, out_num, message) { + messsge = message+': ' ||'no category :' // return an output of a given cell out_num = out_num || 0; var result = casper.evaluate(function (c, o) { @@ -244,7 +245,7 @@ casper.get_output_cell = function (cell_num, out_num) { }, {c : cell_num}); this.test.assertTrue(false, - "Cell " + cell_num + " has no output #" + out_num + " (" + num_outputs + " total)" + message+"Cell " + cell_num + " has no output #" + out_num + " (" + num_outputs + " total)" ); } else { return result; @@ -394,14 +395,19 @@ casper.cell_element_function = function(index, selector, function_name, function casper.validate_notebook_state = function(message, mode, cell_index) { // Validate the entire dual mode state of the notebook. Make sure no more than // one cell is selected, focused, in edit mode, etc... - // General tests. this.test.assertEquals(this.get_keyboard_mode(), this.get_notebook_mode(), message + '; keyboard and notebook modes match'); // Is the selected cell the only cell that is selected? if (cell_index!==undefined) { this.test.assert(this.is_only_cell_selected(cell_index), - message + '; cell ' + cell_index + ' is the only cell selected'); + message + '; expecting cell ' + cell_index + ' to be the only cell selected. Got selected cell(s):'+ + (function(){ + return casper.evaluate(function(){ + return IPython.notebook.get_selected_cells_indices(); + }) + })() + ); } // Mode specific tests. @@ -433,11 +439,11 @@ casper.validate_notebook_state = function(message, mode, cell_index) { } }; -casper.select_cell = function(index) { +casper.select_cell = function(index, moveanchor) { // Select a cell in the notebook. - this.evaluate(function (i) { - IPython.notebook.select(i); - }, {i: index}); + this.evaluate(function (i, moveanchor) { + IPython.notebook.select(i, moveanchor); + }, {i: index, moveanchor: moveanchor}); }; casper.click_cell_editor = function(index) {