diff --git a/notebook/static/notebook/js/actions.js b/notebook/static/notebook/js/actions.js index 292665a3c..a169bcf80 100644 --- a/notebook/static/notebook/js/actions.js +++ b/notebook/static/notebook/js/actions.js @@ -423,6 +423,14 @@ define(function(require){ env.notebook.show_command_palette(); } }, + 'toggle-cell-marked': { + help_index : 'cj', + help: 'toggle marks', + icon: 'fa-check', + handler : function(env){ + env.notebook.toggle_cells_marked(env.notebook.get_selected_cells()); + } + }, }; /** diff --git a/notebook/static/notebook/js/cell.js b/notebook/static/notebook/js/cell.js index 9a35a48fa..c6150a307 100644 --- a/notebook/static/notebook/js/cell.js +++ b/notebook/static/notebook/js/cell.js @@ -169,6 +169,12 @@ define([ 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) { @@ -280,6 +286,26 @@ define([ } return was_selected_cell; }; + + /** + * 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'); + if (isMarked !== value) { + if (value) { + this.element.addClass('marked'); + } else { + this.element.removeClass('marked'); + } + } + } + }); /** * should be overritten by subclass diff --git a/notebook/static/notebook/js/keyboardmanager.js b/notebook/static/notebook/js/keyboardmanager.js index 6663438b1..407a7a11a 100644 --- a/notebook/static/notebook/js/keyboardmanager.js +++ b/notebook/static/notebook/js/keyboardmanager.js @@ -93,6 +93,7 @@ define([ 'enter' : 'ipython.enter-edit-mode', 'space' : 'ipython.scroll-down', 'down' : 'ipython.select-next-cell', + 'ctrl-space' : 'ipython.toggle-cell-marked', 'i,i' : 'ipython.interrupt-kernel', '0,0' : 'ipython.restart-kernel', 'd,d' : 'ipython.delete-cell', diff --git a/notebook/static/notebook/js/notebook.js b/notebook/static/notebook/js/notebook.js index 92e3a0e0e..fe2b4fc1e 100644 --- a/notebook/static/notebook/js/notebook.js +++ b/notebook/static/notebook/js/notebook.js @@ -401,7 +401,7 @@ define(function (require) { var st = sme.scrollTop(); var t = sme.offset().top; var ct = cells[index].element.offset().top; - var scroll_value = st + ct - (t + .01 * percent * h); + var scroll_value = st + ct - (t + 0.01 * percent * h); this.scroll_manager.element.animate({scrollTop:scroll_value}, time); return scroll_value; }; @@ -628,6 +628,105 @@ 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; }); + }; + + /** + * 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; + })); + }; /** * Get an array of the cells in the currently selected range diff --git a/notebook/static/notebook/less/cell.less b/notebook/static/notebook/less/cell.less index d44d0928e..888ff4cc8 100644 --- a/notebook/static/notebook/less/cell.less +++ b/notebook/static/notebook/less/cell.less @@ -16,11 +16,20 @@ div.cell { .edit_mode &.selected { border-color: green; + /* Don't border the cells when printing */ @media print { border-color: transparent; } } + + &.marked { + border-left-color: #009AF5; + border-left-width: 3px; + margin-left: -2px; + border-bottom-left-radius: 0px; + border-top-left-radius: 0px; + } width: 100%; padding: 5px; diff --git a/notebook/static/notebook/less/variables.less b/notebook/static/notebook/less/variables.less index 76d56398f..f93bcaa68 100644 --- a/notebook/static/notebook/less/variables.less +++ b/notebook/static/notebook/less/variables.less @@ -11,7 +11,7 @@ @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; +@input_prompt_color: #2F97C1; @output_prompt_color: darkred; @output_pre_color: black; @notification_widget_bg: rgba(240, 240, 240, 0.5); diff --git a/notebook/tests/notebook/marks.js b/notebook/tests/notebook/marks.js new file mode 100644 index 000000000..bb7dc4dc6 --- /dev/null +++ b/notebook/tests/notebook/marks.js @@ -0,0 +1,56 @@ + +// 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 () { + this.test.assertEquals(this.evaluate(function() { + return Jupyter.notebook.get_marked_cells().length; + }), 0, 'no cells are marked programmatically'); + + 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; + }), 0, 'unmark_all'); + + this.evaluate(function() { + Jupyter.notebook.set_marked_indices([1]); + }); + + this.test.assertEquals(this.evaluate(function() { + return Jupyter.notebook.get_marked_indices()[0]; + }), 1, 'get/set_marked_indices'); + }); +});