Skip to content

Commit f65e516

Browse files
authored
Migrate top insert toolbar to notebook view zone (microsoft#204185)
* Migrate top insert toolbar to notebook view zone * remove test command * Update docs.
1 parent b288546 commit f65e516

File tree

6 files changed

+248
-56
lines changed

6 files changed

+248
-56
lines changed

src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -754,10 +754,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD
754754
// between cell insert toolbar
755755
if (insertToolbarPosition === 'betweenCells' || insertToolbarPosition === 'both') {
756756
styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { display: flex; }`);
757-
styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .cell-list-top-cell-toolbar-container { display: flex; }`);
757+
styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .view-zones .cell-list-top-cell-toolbar-container { display: flex; }`);
758758
} else {
759759
styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { display: none; }`);
760-
styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .cell-list-top-cell-toolbar-container { display: none; }`);
760+
styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .view-zones .cell-list-top-cell-toolbar-container { display: none; }`);
761761
}
762762

763763
if (insertToolbarAlignment === 'left') {
@@ -844,7 +844,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD
844844
`);
845845

846846
styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-toolbar { height: ${bottomToolbarHeight}px }`);
847-
styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .cell-list-top-cell-toolbar-container .monaco-toolbar { height: ${bottomToolbarHeight}px }`);
847+
styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .view-zones .cell-list-top-cell-toolbar-container .monaco-toolbar { height: ${bottomToolbarHeight}px }`);
848848

849849
// cell toolbar
850850
styleSheets.push(`.monaco-workbench .notebookOverlay.cell-title-toolbar-right > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar {
@@ -923,7 +923,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD
923923
multipleSelectionSupport: true,
924924
selectionNavigation: true,
925925
typeNavigationEnabled: true,
926-
paddingTop: this._notebookOptions.computeTopInsertToolbarHeight(this.viewModel?.viewType),
926+
paddingTop: 0,
927927
paddingBottom: 0,
928928
transformOptimization: false, //(isMacintosh && isNative) || getTitleBarStyle(this.configurationService, this.environmentService) === 'native',
929929
initialSize: this._dimension,
@@ -973,7 +973,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD
973973
this._register(combinedDisposable(...renderers));
974974

975975
// top cell toolbar
976-
this._listTopCellToolbar = this._register(this.instantiationService.createInstance(ListTopCellToolbar, this, this.scopedContextKeyService, this._list.rowsContainer));
976+
this._listTopCellToolbar = this._register(this.instantiationService.createInstance(ListTopCellToolbar, this, this.notebookOptions));
977977

978978
// transparent cover
979979
this._webviewTransparentCover = DOM.append(this._list.rowsContainer, $('.webview-cover'));
@@ -1131,15 +1131,12 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD
11311131

11321132
async setModel(textModel: NotebookTextModel, viewState: INotebookEditorViewState | undefined, perf?: NotebookPerfMarks): Promise<void> {
11331133
if (this.viewModel === undefined || !this.viewModel.equal(textModel)) {
1134-
const oldTopInsertToolbarHeight = this._notebookOptions.computeTopInsertToolbarHeight(this.viewModel?.viewType);
11351134
const oldBottomToolbarDimensions = this._notebookOptions.computeBottomToolbarDimensions(this.viewModel?.viewType);
11361135
this._detachModel();
11371136
await this._attachModel(textModel, viewState, perf);
1138-
const newTopInsertToolbarHeight = this._notebookOptions.computeTopInsertToolbarHeight(this.viewModel?.viewType);
11391137
const newBottomToolbarDimensions = this._notebookOptions.computeBottomToolbarDimensions(this.viewModel?.viewType);
11401138

1141-
if (oldTopInsertToolbarHeight !== newTopInsertToolbarHeight
1142-
|| oldBottomToolbarDimensions.bottomToolbarGap !== newBottomToolbarDimensions.bottomToolbarGap
1139+
if (oldBottomToolbarDimensions.bottomToolbarGap !== newBottomToolbarDimensions.bottomToolbarGap
11431140
|| oldBottomToolbarDimensions.bottomToolbarHeight !== newBottomToolbarDimensions.bottomToolbarHeight) {
11441141
this._styleElement?.remove();
11451142
this._createLayoutStyles();
@@ -1848,16 +1845,15 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD
18481845
const newBodyHeight = this.getBodyHeight(dimension.height) - this.getLayoutInfo().stickyHeight;
18491846
DOM.size(this._body, dimension.width, newBodyHeight);
18501847

1851-
const topInserToolbarHeight = this._notebookOptions.computeTopInsertToolbarHeight(this.viewModel?.viewType);
18521848
const newCellListHeight = newBodyHeight;
18531849
if (this._list.getRenderHeight() < newCellListHeight) {
18541850
// the new dimension is larger than the list viewport, update its additional height first, otherwise the list view will move down a bit (as the `scrollBottom` will move down)
1855-
this._list.updateOptions({ paddingBottom: this._allowScrollBeyondLastLine() ? Math.max(0, (newCellListHeight - 50)) : 0, paddingTop: topInserToolbarHeight });
1851+
this._list.updateOptions({ paddingBottom: this._allowScrollBeyondLastLine() ? Math.max(0, (newCellListHeight - 50)) : 0, paddingTop: 0 });
18561852
this._list.layout(newCellListHeight, dimension.width);
18571853
} else {
18581854
// the new dimension is smaller than the list viewport, if we update the additional height, the `scrollBottom` will move up, which moves the whole list view upwards a bit. So we run a layout first.
18591855
this._list.layout(newCellListHeight, dimension.width);
1860-
this._list.updateOptions({ paddingBottom: this._allowScrollBeyondLastLine() ? Math.max(0, (newCellListHeight - 50)) : 0, paddingTop: topInserToolbarHeight });
1856+
this._list.updateOptions({ paddingBottom: this._allowScrollBeyondLastLine() ? Math.max(0, (newCellListHeight - 50)) : 0, paddingTop: 0 });
18611857
}
18621858

18631859
this._overlayContainer.style.visibility = 'visible';

src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1254,7 +1254,9 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
12541254
}
12551255

12561256
changeViewZones(callback: (accessor: INotebookViewZoneChangeAccessor) => void): void {
1257-
this.viewZones.changeViewZones(callback);
1257+
if (this.viewZones.changeViewZones(callback)) {
1258+
this.viewZones.layout();
1259+
}
12581260
}
12591261

12601262
// override

src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,31 @@ export class NotebookCellsLayout implements IRangeMap {
147147
}
148148
}
149149

150+
/**
151+
* find position of whitespace
152+
* @param id: id of the whitespace
153+
* @returns: position in the list view
154+
*/
155+
getWhitespacePosition(id: string): number {
156+
const whitespace = this._whitespace.find(ws => ws.id === id);
157+
if (!whitespace) {
158+
throw new Error('Whitespace not found');
159+
}
160+
161+
const afterPosition = whitespace.afterPosition;
162+
if (afterPosition === 0) {
163+
return this.paddingTop;
164+
}
165+
166+
const whitespaceBeforeFirstItem = this._whitespace.length > 0 && this._whitespace[0].afterPosition === 0 ? this._whitespace[0].size : 0;
167+
168+
// previous item index
169+
const index = afterPosition - 1;
170+
const previousItemPosition = this._prefixSumComputer.getPrefixSum(index);
171+
const previousItemSize = this._items[index].size;
172+
return previousItemPosition + previousItemSize + whitespaceBeforeFirstItem + this.paddingTop;
173+
}
174+
150175
indexAt(position: number): number {
151176
if (position < 0) {
152177
return -1;
@@ -238,4 +263,8 @@ export class NotebookCellListView<T> extends ListView<T> {
238263
this.notebookRangeMap.removeWhitespace(id);
239264
this.eventuallyUpdateScrollDimensions();
240265
}
266+
267+
getWhitespacePosition(id: string): number {
268+
return this.notebookRangeMap.getWhitespacePosition(id);
269+
}
241270
}

src/vs/workbench/contrib/notebook/browser/viewParts/notebookTopCellToolbar.ts

Lines changed: 103 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,69 +4,136 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as DOM from 'vs/base/browser/dom';
7-
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
7+
import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle';
88
import { HiddenItemStrategy, MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar';
99
import { IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions';
10-
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
1110
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
1211
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
1312
import { INotebookActionContext } from 'vs/workbench/contrib/notebook/browser/controller/coreActions';
1413
import { INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
14+
import { NotebookOptions } from 'vs/workbench/contrib/notebook/browser/notebookOptions';
1515
import { CodiconActionViewItem } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellActionView';
1616

1717
export class ListTopCellToolbar extends Disposable {
18+
private readonly topCellToolbarContainer: HTMLElement;
1819
private topCellToolbar: HTMLElement;
19-
private toolbar: MenuWorkbenchToolBar;
20+
private viewZone: MutableDisposable<DisposableStore> = this._register(new MutableDisposable());
2021
private readonly _modelDisposables = this._register(new DisposableStore());
2122
constructor(
2223
protected readonly notebookEditor: INotebookEditorDelegate,
23-
24-
contextKeyService: IContextKeyService,
25-
insertionIndicatorContainer: HTMLElement,
24+
private readonly notebookOptions: NotebookOptions,
2625
@IInstantiationService protected readonly instantiationService: IInstantiationService,
2726
@IContextMenuService protected readonly contextMenuService: IContextMenuService,
2827
@IMenuService protected readonly menuService: IMenuService
2928
) {
3029
super();
3130

32-
this.topCellToolbar = DOM.append(insertionIndicatorContainer, DOM.$('.cell-list-top-cell-toolbar-container'));
33-
34-
this.toolbar = this._register(instantiationService.createInstance(MenuWorkbenchToolBar, this.topCellToolbar, this.notebookEditor.creationOptions.menuIds.cellTopInsertToolbar, {
35-
actionViewItemProvider: action => {
36-
if (action instanceof MenuItemAction) {
37-
const item = this.instantiationService.createInstance(CodiconActionViewItem, action, undefined);
38-
return item;
39-
}
31+
this.topCellToolbarContainer = DOM.$('div');
32+
this.topCellToolbar = DOM.$('.cell-list-top-cell-toolbar-container');
33+
this.topCellToolbarContainer.appendChild(this.topCellToolbar);
4034

41-
return undefined;
42-
},
43-
menuOptions: {
44-
shouldForwardArgs: true
45-
},
46-
toolbarOptions: {
47-
primaryGroup: (g: string) => /^inline/.test(g),
48-
},
49-
hiddenItemStrategy: HiddenItemStrategy.Ignore,
35+
this._register(this.notebookEditor.onDidAttachViewModel(() => {
36+
this.updateTopToolbar();
5037
}));
5138

52-
this.toolbar.context = <INotebookActionContext>{
53-
notebookEditor
54-
};
39+
this._register(this.notebookOptions.onDidChangeOptions(e => {
40+
if (e.insertToolbarAlignment || e.insertToolbarPosition || e.cellToolbarLocation) {
41+
this.updateTopToolbar();
42+
}
43+
}));
44+
}
5545

56-
// update toolbar container css based on cell list length
57-
this._register(this.notebookEditor.onDidChangeModel(() => {
58-
this._modelDisposables.clear();
46+
private updateTopToolbar() {
47+
const layoutInfo = this.notebookOptions.getLayoutConfiguration();
48+
this.viewZone.value = new DisposableStore();
5949

60-
if (this.notebookEditor.hasModel()) {
61-
this._modelDisposables.add(this.notebookEditor.onDidChangeViewCells(() => {
62-
this.updateClass();
63-
}));
50+
if (layoutInfo.insertToolbarPosition === 'hidden' || layoutInfo.insertToolbarPosition === 'notebookToolbar') {
51+
const height = this.notebookOptions.computeTopInsertToolbarHeight(this.notebookEditor.textModel?.viewType);
6452

65-
this.updateClass();
53+
if (height !== 0) {
54+
// reserve whitespace to avoid overlap with cell toolbar
55+
this.notebookEditor.changeViewZones(accessor => {
56+
const id = accessor.addZone({
57+
afterModelPosition: 0,
58+
heightInPx: height,
59+
domNode: DOM.$('div')
60+
});
61+
accessor.layoutZone(id);
62+
this.viewZone.value?.add({
63+
dispose: () => {
64+
if (!this.notebookEditor.isDisposed) {
65+
this.notebookEditor.changeViewZones(accessor => {
66+
accessor.removeZone(id);
67+
});
68+
}
69+
}
70+
});
71+
});
6672
}
67-
}));
73+
return;
74+
}
75+
76+
77+
this.notebookEditor.changeViewZones(accessor => {
78+
const height = this.notebookOptions.computeTopInsertToolbarHeight(this.notebookEditor.textModel?.viewType);
79+
const id = accessor.addZone({
80+
afterModelPosition: 0,
81+
heightInPx: height,
82+
domNode: this.topCellToolbarContainer
83+
});
84+
accessor.layoutZone(id);
85+
86+
this.viewZone.value?.add({
87+
dispose: () => {
88+
if (!this.notebookEditor.isDisposed) {
89+
this.notebookEditor.changeViewZones(accessor => {
90+
accessor.removeZone(id);
91+
});
92+
}
93+
}
94+
});
95+
96+
DOM.clearNode(this.topCellToolbar);
97+
98+
const toolbar = this.instantiationService.createInstance(MenuWorkbenchToolBar, this.topCellToolbar, this.notebookEditor.creationOptions.menuIds.cellTopInsertToolbar, {
99+
actionViewItemProvider: action => {
100+
if (action instanceof MenuItemAction) {
101+
const item = this.instantiationService.createInstance(CodiconActionViewItem, action, undefined);
102+
return item;
103+
}
104+
105+
return undefined;
106+
},
107+
menuOptions: {
108+
shouldForwardArgs: true
109+
},
110+
toolbarOptions: {
111+
primaryGroup: (g: string) => /^inline/.test(g),
112+
},
113+
hiddenItemStrategy: HiddenItemStrategy.Ignore,
114+
});
115+
116+
toolbar.context = <INotebookActionContext>{
117+
notebookEditor: this.notebookEditor
118+
};
119+
120+
this.viewZone.value?.add(toolbar);
121+
122+
// update toolbar container css based on cell list length
123+
this.viewZone.value?.add(this.notebookEditor.onDidChangeModel(() => {
124+
this._modelDisposables.clear();
125+
126+
if (this.notebookEditor.hasModel()) {
127+
this._modelDisposables.add(this.notebookEditor.onDidChangeViewCells(() => {
128+
this.updateClass();
129+
}));
130+
131+
this.updateClass();
132+
}
133+
}));
68134

69-
this.updateClass();
135+
this.updateClass();
136+
});
70137
}
71138

72139
private updateClass() {

src/vs/workbench/contrib/notebook/browser/viewParts/notebookViewZones.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { NotebookCellListView } from 'vs/workbench/contrib/notebook/browser/view
1111
import { ICoordinatesConverter } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon';
1212
import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl';
1313

14+
const invalidFunc = () => { throw new Error(`Invalid notebook view zone change accessor`); };
15+
1416
interface IZoneWidget {
1517
whitespaceId: string;
1618
isInHiddenArea: boolean;
@@ -29,25 +31,39 @@ export class NotebookViewZones extends Disposable {
2931
this.domNode.setPosition('absolute');
3032
this.domNode.setAttribute('role', 'presentation');
3133
this.domNode.setAttribute('aria-hidden', 'true');
34+
this.domNode.setWidth('100%');
3235
this._zones = {};
3336

3437
this.listView.containerDomNode.appendChild(this.domNode.domNode);
3538
}
3639

37-
changeViewZones(callback: (changeAccessor: INotebookViewZoneChangeAccessor) => void): void {
40+
changeViewZones(callback: (changeAccessor: INotebookViewZoneChangeAccessor) => void): boolean {
41+
let zonesHaveChanged = false;
3842
const changeAccessor: INotebookViewZoneChangeAccessor = {
3943
addZone: (zone: INotebookViewZone): string => {
44+
zonesHaveChanged = true;
4045
return this._addZone(zone);
4146
},
4247
removeZone: (id: string): void => {
48+
zonesHaveChanged = true;
49+
// TODO: validate if zones have changed layout
4350
this._removeZone(id);
4451
},
4552
layoutZone: (id: string): void => {
53+
zonesHaveChanged = true;
54+
// TODO: validate if zones have changed layout
4655
this._layoutZone(id);
4756
}
4857
};
4958

5059
safeInvoke1Arg(callback, changeAccessor);
60+
61+
// Invalidate changeAccessor
62+
changeAccessor.addZone = invalidFunc;
63+
changeAccessor.removeZone = invalidFunc;
64+
changeAccessor.layoutZone = invalidFunc;
65+
66+
return zonesHaveChanged;
5167
}
5268

5369
onCellsChanged(e: INotebookViewCellsUpdateEvent): void {
@@ -138,13 +154,10 @@ export class NotebookViewZones extends Disposable {
138154
if (isInHiddenArea) {
139155
zoneWidget.domNode.setDisplay('none');
140156
} else {
141-
const afterPosition = zoneWidget.zone.afterModelPosition;
142-
const index = afterPosition - 1;
143-
const viewIndex = this.coordinator.convertModelIndexToViewIndex(index);
144-
const top = this.listView.elementTop(viewIndex);
145-
const height = this.listView.elementHeight(viewIndex);
146-
zoneWidget.domNode.setTop(top + height);
157+
const top = this.listView.getWhitespacePosition(zoneWidget.whitespaceId);
158+
zoneWidget.domNode.setTop(top);
147159
zoneWidget.domNode.setDisplay('block');
160+
zoneWidget.domNode.setHeight(zoneWidget.zone.heightInPx);
148161
}
149162
}
150163

0 commit comments

Comments
 (0)