From c840fba293880ae93db5c8a63c90cd2bd51110bb Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 6 Apr 2016 12:08:56 -0500 Subject: [PATCH 1/5] Implement multi-cell selection --- example/package.json | 5 +- example/src/index.ts | 127 ++++++++++++++-------------------------- src/fonts/icomoon.eot | Bin 0 -> 2640 bytes src/fonts/icomoon.svg | 23 ++++++++ src/fonts/icomoon.ttf | Bin 0 -> 2476 bytes src/fonts/icomoon.woff | Bin 0 -> 2552 bytes src/notebook/manager.ts | 55 +++++++++++++++++ src/notebook/model.ts | 39 ++++++++++-- src/notebook/widget.ts | 10 +++- src/theme.css | 10 ++++ 10 files changed, 178 insertions(+), 91 deletions(-) create mode 100755 src/fonts/icomoon.eot create mode 100755 src/fonts/icomoon.svg create mode 100755 src/fonts/icomoon.ttf create mode 100755 src/fonts/icomoon.woff diff --git a/example/package.json b/example/package.json index 9bbe2e1..0613623 100644 --- a/example/package.json +++ b/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", diff --git a/example/src/index.ts b/example/src/index.ts index 4a0238a..3fc04fb 100644 --- a/example/src/index.ts +++ b/example/src/index.ts @@ -209,6 +209,18 @@ function main(): void { shortcut: 'B', handler: () => { nbManager.insertBelow() ; } }, + { + category: 'Notebook Cell', + text: 'Extend Selection Above', + shortcut: 'Accel J', + handler: () => { nbManager.extendSelectionAbove() ; } + }, + { + category: 'Notebook Cell', + text: 'Extend Selection Below', + shortcut: 'Accel K', + handler: () => { nbManager.extendSelectionBelow() ; } + }, { category: 'Notebook Cell', text: 'Merge Selected', @@ -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: () => { nbModel.activeCellIndex += 1; } }, { selector: '.jp-Notebook.jp-mod-commandMode', sequence: ['ArrowDown'], - handler: () => { - nbModel.activeCellIndex += 1; - return true; - } + handler: () => { nbModel.activeCellIndex += 1; } }, { selector: '.jp-Notebook.jp-mod-commandMode', sequence: ['K'], - handler: () => { - nbModel.activeCellIndex -= 1; - return true; - } + handler: () => { nbModel.activeCellIndex -= 1; } }, { selector: '.jp-Notebook.jp-mod-commandMode', sequence: ['ArrowUp'], - handler: () => { - nbModel.activeCellIndex -= 1; - return true; - } + handler: () => { nbModel.activeCellIndex -= 1; } + }, + { + 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); diff --git a/src/fonts/icomoon.eot b/src/fonts/icomoon.eot new file mode 100755 index 0000000000000000000000000000000000000000..2023478573b61e2b5a8ada1360b07ef918005150 GIT binary patch literal 2640 zcmaJ@UrbY182`?>h0@}RmlkRUF}+j@DGDu@a?4Vn3?nJbj6Xt@WLXQ8iqbzq&1m8> z#=}e|nM8*Ryv&DXTjKs?;lTu%Hk;8s$cx(&6U}Baw@J1vCT364+kNNucC<+RZqE7s zobR0Pcfa$uR}b)o4uAlKd?K(wnB}$Jd^_{a(D|5YWqTWd0VZG+q7cRsg$OzrpdW$| zg3~Yw0hmHdISip?3Z~JQ`B=aS_25u^xA#UP3s?-Zz=7lSfriGC(n20BYgqiF;Xuqh zboU}URib_*6qt@7-;FxsMWM;WxbtagIqEY21)m=e2F7&pZ{G&cTClzOI2yFyad%Na ziu%6e;rPreKz>I34C)6bqoaYMW!*j0ldL@)n2CWIe-N~fP`5+^;owrY?iq&CV2t-- z(djtiMAFT(cHp@CWC02Q=n}nymh%}-{sNI;-t&kc?0oV9Z0zEa=_GhLEZE3$ktK^F zuxPAQVq1oDidQ9Qz>Ri26yfAjv{^QGrBms&(#x^|icHeKnKjricU%Gly-IRyxHzO1 z#bwLy#Wo5;I?4TtsI{;QpjzMs9TE5fp-gZQkt~x^S&#*+|KYB0zWNQBAfL*^@<)jo zoFj@$_p-m7kQzK`Yy;ma`ghaO5Tw>$c;I2L++SU{L|?FF8(VNFi&38 z5s@d$cL*%YAj2{tt@8KeGjd<9mXEOn2+8<`$dXc#B~-X$SSiw0 zgEB@0|M3Fi+=m0YIHRj^FD)|}sI{t|x~!(XRBUXcuBLiwby9fttS!>-aP&vo7$0j7 zUmO^?7;a~rK3z{k8rGcyv3754z=_;DxES+#V~c}FE=9c<3uiXHhW~gIj8FsnF!Jn(TvGjfwv^c> zNjAD+lPXNciVCBtVsn*l%(Gq2fg9Js54aAB;APm0I0Oa&*P&Sva*^Pq2=rbmS?l&w z??UQHt=nBoce6V6gzn~&7${EX-(XCMH*b?tW;)MK5Oq&Dp&bsxF!(V}8&Ou*|M_p0 z%vzR$+@)cwk%;Ib+RV>q65-_YG?J^~J|HeDIjhqPQjfi^+ikJ9yX(s9jERI1nX21+ zBtfrBkn!cz*;RXMtKIJL*t497#U&-htdMbeJ%mp5tV(-?oQ^t%E`X^43Z5mU?8!KGtVz9~tn(v}EThJ4H2lsc;(c z)!oy8(> z8P9D$t_$US(3)R(K0J6J`Md1)F2BV1QOQ5+^UX44*2lfH^6=q`-8bTvMtpYUQuo}5 z+dVSZ-7^M*Vv3!Av&8c+6ysHd^kbc8c8ja02VPWo7i0+gDR&EA6pXM|N6Iu~ z&1YINI_rkIz-&1svQ2j9=*;i3(pwKE+!-D?g4g{5+#-6?OFksGXfHiMFVmlt$UHs| z3KZ`wLPb$41AL|8BY3A|u~L-%s+~i*S(P;?pH}4pyhqAZxe)C=s;tM|`aZ4*qS2wn zfGruBq8w6X4&_!=)}VYwl?(6@V^QTowD+sB9-82jiP2~{8ja+?goc7s(-YB%#aZvz z`T4VBuN{s3!O-c+z*PRbYDdpQ(Ma493Pyrcfp~DtGLo?PMx%!jTRj|7o?dbMix57> gBFcj-0CD7FXd8h9);{GacUXPO)nnFi|GbF)1DjkB`Tzg` literal 0 HcmV?d00001 diff --git a/src/fonts/icomoon.svg b/src/fonts/icomoon.svg new file mode 100755 index 0000000..8c55ae9 --- /dev/null +++ b/src/fonts/icomoon.svg @@ -0,0 +1,23 @@ + + + +Generated by IcoMoon + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/fonts/icomoon.ttf b/src/fonts/icomoon.ttf new file mode 100755 index 0000000000000000000000000000000000000000..37b8bf77f27b7d42f7e580257b1390ab7b850ef6 GIT binary patch literal 2476 zcmaJ@Z%k8H6hG&_LMb@#(L&82rjJS?MWLmXSC#^07)c>!0z#BzSqqek(mz7YXyP)) zhnY+=jZPQ%GCwTa60&|_B9R-cIx%d3e zx#!;Vd*}WuAOMuW90=feW3Z{Yth`Vp%L>)NSR@!X58pixfYqcQ3NF*`)8sN{QpCobl@R-F%$u# zAA)6VTPB^!sG}@vpkmDYX4cWT{CN?Oyvp)y3_xyET{iq)Zet*2Qo^sqS`XU*Y9&!J zpd=PZ6_OiWxP;}3q)62MBV1p9{TrOb&y*46lE49jD zo`B6U>I|qipxJ;j>L9GpFN}+7gNvjHhpAD~Mu$2_l)mvC@!Uldx&@=Vc_*tdl`?Bh zBXe6#JDIDwgSlH8nbpbQ*|U!5fWt8m?cj2}Gje`#@O-3`OZId%0~uI#4#qou@j)j^ z-_V7)&lkTibns%#N3m#SGb{9sH^B&Xu$$u5lv1pX5CV_c(m^4!94Oh6$l&Pm>HhxH z%SUr^=t8WkD|TT>lNrE4_q9l`g5d)h)KztWK%9vcgnot+BaVT3wVG7wtlq&~~kK@9y1uORv?o zwA8X`-YU1ttE$R*#kTZ^BY1s87^`=?>t$QoY?EahTeHbkCSz5V(Nwj*%+?n8DHp&) z```!K2SxBI>?9r%CqVnqtb%+dv?vn0mrm7t0?cbeU<+?@ zp~hRV$!RlN;5UeNCY;a-`(XqE6sH5#-SvO|TV=DJryzf8*y?0-xly0}_-rDa;wBCA zH980Aw&E#6u_X7|8+tt!i>J4tvcZ^48cArny-${k4N06>N}pP`x3}BvUavhT(N$7b zR>CVzD63&|VxK%eHu-!6Wzofww2XdgtZtn@m zTpp4GbAJCESLXb}D_0&qykhr{dgM{Rouu45KkD&}&iD4s2itfac)4PVpLwgy`!AHx zQ$*#*2JhU4)NT)YsOT-o78<9%E%Z?ELajDawwqcp+mqG#Fw!M%%d62X*psJozf0=q zVldIk@WMfe!dbY5#drWe!dvVBJIXGxpVY`A1o=Lvl4GbUT2l&NY5b7hlpI$^IH1`D z!mS$C5k8^e0(wU(G+apbJ`ESsY5jn91nLZADW#q)W`x5U76`X%SV#Dzh6|vQc2Cy7 zkn95*E`}EPbaE^fiN&HYNq-b#5TP1_=x}IyW-=DFI2#=>3?Z0?8FHWwn#T!^;CSJv z7a9jb;S*EAX$l%5q9{DYWD0-Nt giK>i(kRUluwoyn@>sLQufA#~2qk4Yw7iBB{5C6CTWB>pF literal 0 HcmV?d00001 diff --git a/src/fonts/icomoon.woff b/src/fonts/icomoon.woff new file mode 100755 index 0000000000000000000000000000000000000000..827e122c173b607851aa08cb6f780d7141294bc2 GIT binary patch literal 2552 zcmaJ?Z%A8L6hG&_XpDC1Giu6|l4oX=*lJ>oFOH~j79~n))heqnmZ&k>n#5{K8`zlh z!wRdcrCmGp%le^ggYFNb9}3pBvPs(y^-H&b(6SZQZIz8tD1$-G%g%jysi~FT$N78D zedl-2{datSM+YEKGyDg6s{E4k?b`mnx_SW2p*}ySf}(73I2@QH-x%2kIhh9To(l!0 zxo?SV9?PjPG!Y*q-wHt17n~eVib}_Vfe`@x1oby^GQ_@ncZ{6myF~VDob=xdcgMo9 z6Xd&1_Fhh}1O7Y_9j3UP9qG871;bi6aDwvY*vV&B$ppiZKsZRg1C;v%PV-%cr<2j? z7|nO>S)4$cAl$<{%KBzZHd zT+KpSIrVY9oH&mv2OYRc=2E2?eF*0D9mzy8Nj@684yumH-%L7fgxWz20V%3L{Y%Qn z0OTe$^=AH;`xr>cW#Lystp}=Rr6fuQl*BBlL~@}M7qCc?6p8ph!u5?ezQu9;Oc_)@ ziJzb)p&ZF&0=3L{4vH30a!I07luDTItm4C~XYne&r?laXS-hdN&8qQ9ihrNx3Ryf- zbQ;hp;=&z-1qBpXKx|fiz|ZlXQmGu`b9z2Ul>zkzG#OAv9fTD6LO8E7oF_v#L`>B- zJJf$fXpUzIC+#V82}W1_E>=>U&ny+S%w;Lw#hmpm%+*lKEDi=wpSDDL?e^YC3zs9U z;d6a`=fbUAvL~xaNW!Y4Z?e@p+2(%q<-`vNYF| zmK%QW(F^_k7mj+C@Q42 zM~@Uc`#TLIEfX7=P&gzi31LasexA;U_J_*D&XJK$PL~t6Sd*~QZr}LD-rjC!-~6$0B-q&*6f{DQ})mJd^6mb@d>Y7dGO$h%{SzhhkQ1Ya@XvT z+dVYf)jb<%;&tHdO7Z0Bw+lS~LY_{wf2{G$a3v+R+k+l0dJ9sG#;I=$JrukV*Z!31 zCN8FWQZ^q(w#0oI6xpJ?Gi)ArQ5~HN#dI<}^qNQD4BW(AJb)kLO?H4CW*6B{YGx4v ze4kTGBUBBI<-^w+KBPA#jg^t^*ZczMjhe0_{g|d_(K}M2>DlD(*7RIDtsl~kK%Id+ z`P7rr8R;QS7f5f`bRFsMXnGcu((XycXOq8I({rH#E{zXI!_jC2#$gzu5GIO3WFR;- zJsypi9kq5CfFMl4GzCxx{l@{dVBdDsw$9#Q=-5PHijoEi=rNd}!cA=(*%6Jz%%NZ; zI2DKmN6bTUvu`-MkFeE3hnhP^*+cZ3h>9oxF_I(X8-h4-pE`kksR 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(); } /** @@ -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(); } /** @@ -154,6 +159,7 @@ class NotebookManager { this._copied.push(this.cloneCell(cell)); } } + this.deselectCells(); } /** @@ -170,6 +176,7 @@ class NotebookManager { model.cells.remove(cell); } } + this.deselectCells(); } /** @@ -193,6 +200,7 @@ class NotebookManager { } this._copied = []; this._cut = []; + this.deselectCells(); } /** @@ -221,6 +229,7 @@ class NotebookManager { model.cells.remove(cell); model.cells.insert(i, newCell); } + this.deselectCells(); } /** @@ -240,6 +249,7 @@ class NotebookManager { model.activeCellIndex = cells.indexOf(cell); model.runActiveCell(); } + this.deselectCells(); } /** @@ -258,6 +268,7 @@ class NotebookManager { model.mode = 'edit'; } model.activeCellIndex += 1; + this.deselectCells(); } /** @@ -269,6 +280,7 @@ class NotebookManager { let cell = model.createCodeCell(); model.cells.insert(model.activeCellIndex, cell); model.mode = 'edit'; + this.deselectCells(); } /** @@ -296,6 +308,49 @@ class NotebookManager { .then(() => { model.dirty = false; }); } + /** + * Extend the selection to the previous cell. + */ + extendSelectionAbove(): void { + // Do not wrap around. + if (this.model.activeCellIndex === 0) { + return; + } + let cell = this.model.cells.get(this.model.activeCellIndex); + this.model.select(cell); + this.model.activeCellIndex -= 1; + cell = this.model.cells.get(this.model.activeCellIndex); + this.model.select(cell); + } + + /** + * Extend the selection to the next cell. + */ + extendSelectionBelow(): void { + // Do not wrap around. + if (this.model.activeCellIndex === this.model.cells.length - 1) { + return; + } + let cell = this.model.cells.get(this.model.activeCellIndex); + this.model.select(cell); + this.model.activeCellIndex += 1; + cell = this.model.cells.get(this.model.activeCellIndex); + this.model.select(cell); + } + + /** + * 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. */ diff --git a/src/notebook/model.ts b/src/notebook/model.ts index c3787ab..1a6c08d 100644 --- a/src/notebook/model.ts +++ b/src/notebook/model.ts @@ -87,6 +87,16 @@ interface INotebookModel extends IDisposable { */ stateChanged: ISignal>; + /** + * A signal emitted when a user metadata state changes. + */ + metadataChanged: ISignal; + + /** + * A signal emitted when the selection changes. + */ + selectionChanged: ISignal; + /** * 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 { + return NotebookModelPrivate.selectionChangedSignal.bind(this); + } + /** * Get the observable list of notebook cells. * @@ -414,6 +431,13 @@ class NotebookModel implements INotebookModel { let oldValue = this._mode; this._mode = newValue; NotebookModelPrivate.modeChanged(this, newValue); + // Edit mode deselects all cells. + if (value === '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,6 +491,7 @@ class NotebookModel implements INotebookModel { */ select(cell: ICellModel): void { NotebookModelPrivate.selectedProperty.set(cell, true); + this.selectionChanged.emit(void 0); } /** @@ -474,6 +499,7 @@ class NotebookModel implements INotebookModel { */ deselect(cell: ICellModel): void { NotebookModelPrivate.selectedProperty.set(cell, false); + this.selectionChanged.emit(void 0); } /** @@ -627,13 +653,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 +753,12 @@ namespace NotebookModelPrivate { export const metadataChangedSignal = new Signal(); + /** + * A signal emitted when a the selection state changes. + */ + export + const selectionChangedSignal = new Signal(); + /** * An attached property for the selected state of a cell. */ @@ -746,7 +777,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'; } } } diff --git a/src/notebook/widget.ts b/src/notebook/widget.ts index fb6d466..5775bd8 100644 --- a/src/notebook/widget.ts +++ b/src/notebook/widget.ts @@ -306,6 +306,7 @@ class NotebookWidget extends Widget { } model.cells.changed.connect(this.onCellsChanged, this); model.stateChanged.connect(this.onModelChanged, this); + model.selectionChanged.connect(this.onSelectionChanged, this); } /** @@ -442,7 +443,7 @@ class NotebookWidget extends Widget { if (i !== model.activeCellIndex) { widget.removeClass(ACTIVE_CLASS); } - if (i === model.activeCellIndex || model.isSelected(cell)) { + if (model.isSelected(cell)) { widget.addClass(SELECTED_CLASS); } else { widget.removeClass(SELECTED_CLASS); @@ -520,6 +521,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. */ diff --git a/src/theme.css b/src/theme.css index eb78e8d..6f00e2e 100644 --- a/src/theme.css +++ b/src/theme.css @@ -112,6 +112,16 @@ } +.jp-Notebook.jp-mod-commandMode .jp-Notebook-cell.jp-mod-selected.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; From f58e85d3f9d5aa9209c1d7d8dbe51e441e2800cd Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 6 Apr 2016 14:41:33 -0500 Subject: [PATCH 2/5] Better handling of selection --- src/notebook/manager.ts | 44 +++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/src/notebook/manager.ts b/src/notebook/manager.ts index f889234..d5dc1bd 100644 --- a/src/notebook/manager.ts +++ b/src/notebook/manager.ts @@ -316,11 +316,25 @@ class NotebookManager { if (this.model.activeCellIndex === 0) { return; } - let cell = this.model.cells.get(this.model.activeCellIndex); - this.model.select(cell); + let current = this.model.cells.get(this.model.activeCellIndex); + this.model.select(current); this.model.activeCellIndex -= 1; - cell = this.model.cells.get(this.model.activeCellIndex); - this.model.select(cell); + let prev = this.model.cells.get(this.model.activeCellIndex); + if (this.model.isSelected(prev)) { + this.model.deselect(current); + let count = 0; + for (let i = 0; i < this.model.cells.length; i++) { + let cell = this.model.cells.get(i); + if (this.model.isSelected(cell)) { + count++; + } + } + if (count === 1) { + this.model.deselect(prev); + } + } else { + this.model.select(prev); + } } /** @@ -331,11 +345,25 @@ class NotebookManager { if (this.model.activeCellIndex === this.model.cells.length - 1) { return; } - let cell = this.model.cells.get(this.model.activeCellIndex); - this.model.select(cell); + let current = this.model.cells.get(this.model.activeCellIndex); + this.model.select(current); this.model.activeCellIndex += 1; - cell = this.model.cells.get(this.model.activeCellIndex); - this.model.select(cell); + let next = this.model.cells.get(this.model.activeCellIndex); + if (this.model.isSelected(next)) { + this.model.deselect(current); + let count = 0; + for (let i = 0; i < this.model.cells.length; i++) { + let cell = this.model.cells.get(i); + if (this.model.isSelected(cell)) { + count++; + } + } + if (count === 1) { + this.model.deselect(next); + } + } else { + this.model.select(next); + } } /** From c35d5b025530621e8665b1aa6fbdfede04a2b87d Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 6 Apr 2016 15:47:50 -0500 Subject: [PATCH 3/5] More improvements to multiselection --- example/src/index.ts | 12 +++--- src/notebook/manager.ts | 82 ++++++++++++++++++++++++++--------------- 2 files changed, 58 insertions(+), 36 deletions(-) diff --git a/example/src/index.ts b/example/src/index.ts index 3fc04fb..9caa6f0 100644 --- a/example/src/index.ts +++ b/example/src/index.ts @@ -249,13 +249,13 @@ function main(): void { category: 'Notebook Cell', text: 'Select Previous', shortcut: 'ArrowUp', - handler: () => { nbModel.activeCellIndex -= 1; } + handler: () => { nbManager.selectPrev(); } }, { category: 'Notebook Cell', text: 'Select Next', shortcut: 'ArrowDown', - handler: () => { nbModel.activeCellIndex += 1; } + handler: () => { nbManager.selectNext(); } }, ]; pModel.addItems(items); @@ -349,22 +349,22 @@ function main(): void { { selector: '.jp-Notebook.jp-mod-commandMode', sequence: ['J'], - handler: () => { nbModel.activeCellIndex += 1; } + handler: () => { nbManager.selectNext(); } }, { selector: '.jp-Notebook.jp-mod-commandMode', sequence: ['ArrowDown'], - handler: () => { nbModel.activeCellIndex += 1; } + handler: () => { nbManager.selectNext(); } }, { selector: '.jp-Notebook.jp-mod-commandMode', sequence: ['K'], - handler: () => { nbModel.activeCellIndex -= 1; } + handler: () => { nbManager.selectPrev(); } }, { selector: '.jp-Notebook.jp-mod-commandMode', sequence: ['ArrowUp'], - handler: () => { nbModel.activeCellIndex -= 1; } + handler: () => { nbManager.selectPrev(); } }, { selector: '.jp-Notebook.jp-mod-commandMode', diff --git a/src/notebook/manager.ts b/src/notebook/manager.ts index d5dc1bd..96907ef 100644 --- a/src/notebook/manager.ts +++ b/src/notebook/manager.ts @@ -308,29 +308,51 @@ class NotebookManager { .then(() => { model.dirty = false; }); } + /** + * Select the next cell. + */ + selectNext(): void { + if (this.model.activeCellIndex === this.model.cells.length - 1) { + return; + } + this.model.activeCellIndex += 1; + this.deselectCells(); + } + + /** + * Select the previous cell. + */ + selectPrev(): void { + if (this.model.activeCellIndex === 0) { + return; + } + this.model.activeCellIndex -= 1; + this.deselectCells(); + } + /** * Extend the selection to the previous cell. */ extendSelectionAbove(): void { + let model = this.model; + let cells = model.cells; // Do not wrap around. - if (this.model.activeCellIndex === 0) { + if (model.activeCellIndex === 0) { return; } - let current = this.model.cells.get(this.model.activeCellIndex); - this.model.select(current); - this.model.activeCellIndex -= 1; - let prev = this.model.cells.get(this.model.activeCellIndex); - if (this.model.isSelected(prev)) { - this.model.deselect(current); - let count = 0; - for (let i = 0; i < this.model.cells.length; i++) { - let cell = this.model.cells.get(i); - if (this.model.isSelected(cell)) { - count++; + let current = cells.get(model.activeCellIndex); + model.select(current); + model.activeCellIndex -= 1; + let prev = cells.get(model.activeCellIndex); + 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); } - } - if (count === 1) { - this.model.deselect(prev); + } else { + model.deselect(prev); } } else { this.model.select(prev); @@ -341,25 +363,25 @@ class NotebookManager { * Extend the selection to the next cell. */ extendSelectionBelow(): void { + let model = this.model; + let cells = model.cells; // Do not wrap around. - if (this.model.activeCellIndex === this.model.cells.length - 1) { + if (model.activeCellIndex === cells.length - 1) { return; } - let current = this.model.cells.get(this.model.activeCellIndex); - this.model.select(current); - this.model.activeCellIndex += 1; - let next = this.model.cells.get(this.model.activeCellIndex); - if (this.model.isSelected(next)) { - this.model.deselect(current); - let count = 0; - for (let i = 0; i < this.model.cells.length; i++) { - let cell = this.model.cells.get(i); - if (this.model.isSelected(cell)) { - count++; + let current = cells.get(model.activeCellIndex); + model.select(current); + model.activeCellIndex += 1; + let next = cells.get(model.activeCellIndex); + 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); } - } - if (count === 1) { - this.model.deselect(next); + } else { + model.deselect(next); } } else { this.model.select(next); From 80fc3d58bdb6f4f84e41ea8025e10cc3ef963a53 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 6 Apr 2016 16:59:19 -0500 Subject: [PATCH 4/5] Fix variable name --- src/notebook/model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/notebook/model.ts b/src/notebook/model.ts index 1a6c08d..908f17c 100644 --- a/src/notebook/model.ts +++ b/src/notebook/model.ts @@ -432,7 +432,7 @@ class NotebookModel implements INotebookModel { this._mode = newValue; NotebookModelPrivate.modeChanged(this, newValue); // Edit mode deselects all cells. - if (value === 'edit') { + if (newValue === 'edit') { for (let i = 0; i < this.cells.length; i++) { let cell = this.cells.get(i); this.deselect(cell); From 0c7995fa42d1b4a4704a9a5a70675f913161d922 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 7 Apr 2016 15:32:16 -0500 Subject: [PATCH 5/5] Clean up selection logic and rename next/prev to above/below --- example/src/index.ts | 20 +++++++++---------- src/notebook/manager.ts | 43 +++++++++++++++++++---------------------- src/notebook/model.ts | 11 +++++++---- src/notebook/widget.ts | 12 ++++++++++++ src/theme.css | 4 ++-- 5 files changed, 51 insertions(+), 39 deletions(-) diff --git a/example/src/index.ts b/example/src/index.ts index 9caa6f0..540a525 100644 --- a/example/src/index.ts +++ b/example/src/index.ts @@ -212,13 +212,13 @@ function main(): void { { category: 'Notebook Cell', text: 'Extend Selection Above', - shortcut: 'Accel J', + shortcut: 'Shift J', handler: () => { nbManager.extendSelectionAbove() ; } }, { category: 'Notebook Cell', text: 'Extend Selection Below', - shortcut: 'Accel K', + shortcut: 'Shift K', handler: () => { nbManager.extendSelectionBelow() ; } }, { @@ -247,15 +247,15 @@ function main(): void { }, { category: 'Notebook Cell', - text: 'Select Previous', + text: 'Select Above', shortcut: 'ArrowUp', - handler: () => { nbManager.selectPrev(); } + handler: () => { nbManager.selectAbove(); } }, { category: 'Notebook Cell', - text: 'Select Next', + text: 'Select Below', shortcut: 'ArrowDown', - handler: () => { nbManager.selectNext(); } + handler: () => { nbManager.selectBelow(); } }, ]; pModel.addItems(items); @@ -349,22 +349,22 @@ function main(): void { { selector: '.jp-Notebook.jp-mod-commandMode', sequence: ['J'], - handler: () => { nbManager.selectNext(); } + handler: () => { nbManager.selectBelow(); } }, { selector: '.jp-Notebook.jp-mod-commandMode', sequence: ['ArrowDown'], - handler: () => { nbManager.selectNext(); } + handler: () => { nbManager.selectBelow(); } }, { selector: '.jp-Notebook.jp-mod-commandMode', sequence: ['K'], - handler: () => { nbManager.selectPrev(); } + handler: () => { nbManager.selectAbove(); } }, { selector: '.jp-Notebook.jp-mod-commandMode', sequence: ['ArrowUp'], - handler: () => { nbManager.selectPrev(); } + handler: () => { nbManager.selectAbove(); } }, { selector: '.jp-Notebook.jp-mod-commandMode', diff --git a/src/notebook/manager.ts b/src/notebook/manager.ts index 96907ef..949399b 100644 --- a/src/notebook/manager.ts +++ b/src/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); } @@ -97,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) { @@ -155,7 +155,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)) { this._copied.push(this.cloneCell(cell)); } } @@ -171,7 +171,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)) { this._cut.push(this.cloneCell(cell)); model.cells.remove(cell); } @@ -210,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; @@ -241,15 +241,14 @@ 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(); } - this.deselectCells(); } /** @@ -309,9 +308,9 @@ class NotebookManager { } /** - * Select the next cell. + * Select the cell below the active cell. */ - selectNext(): void { + selectBelow(): void { if (this.model.activeCellIndex === this.model.cells.length - 1) { return; } @@ -320,9 +319,9 @@ class NotebookManager { } /** - * Select the previous cell. + * Select the above the active cell. */ - selectPrev(): void { + selectAbove(): void { if (this.model.activeCellIndex === 0) { return; } @@ -331,7 +330,7 @@ class NotebookManager { } /** - * Extend the selection to the previous cell. + * Extend the selection to the cell above. */ extendSelectionAbove(): void { let model = this.model; @@ -341,9 +340,7 @@ class NotebookManager { return; } let current = cells.get(model.activeCellIndex); - model.select(current); - model.activeCellIndex -= 1; - let prev = cells.get(model.activeCellIndex); + let prev = cells.get(model.activeCellIndex - 1); if (model.isSelected(prev)) { model.deselect(current); if (model.activeCellIndex >= 1) { @@ -355,12 +352,13 @@ class NotebookManager { model.deselect(prev); } } else { - this.model.select(prev); + model.select(current); } + model.activeCellIndex -= 1; } /** - * Extend the selection to the next cell. + * Extend the selection to the cell below. */ extendSelectionBelow(): void { let model = this.model; @@ -370,9 +368,7 @@ class NotebookManager { return; } let current = cells.get(model.activeCellIndex); - model.select(current); - model.activeCellIndex += 1; - let next = cells.get(model.activeCellIndex); + let next = cells.get(model.activeCellIndex + 1); if (model.isSelected(next)) { model.deselect(current); if (model.activeCellIndex < cells.length - 1) { @@ -384,8 +380,9 @@ class NotebookManager { model.deselect(next); } } else { - this.model.select(next); + model.select(current); } + model.activeCellIndex += 1; } /** diff --git a/src/notebook/model.ts b/src/notebook/model.ts index 908f17c..5519369 100644 --- a/src/notebook/model.ts +++ b/src/notebook/model.ts @@ -400,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; @@ -496,6 +495,9 @@ class NotebookModel implements INotebookModel { /** * Deselect a cell. + * + * #### Notes + * This has no effect on the "active" cell. */ deselect(cell: ICellModel): void { NotebookModelPrivate.selectedProperty.set(cell, false); @@ -503,10 +505,11 @@ class NotebookModel implements INotebookModel { } /** - * 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); } /** diff --git a/src/notebook/widget.ts b/src/notebook/widget.ts index 5775bd8..ce1469c 100644 --- a/src/notebook/widget.ts +++ b/src/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. @@ -437,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); } + 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); + } } /** diff --git a/src/theme.css b/src/theme.css index 6f00e2e..5d66e21 100644 --- a/src/theme.css +++ b/src/theme.css @@ -105,14 +105,14 @@ } -.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-selected.jp-mod-active { +.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%); }