notebook toolbar rewrite. Adopt WorkbenchToolbar (#185168)

* white

* okay now it all works (except the corner cases i didn't find yet)

* testing + small fixes

* yet another corner case woohoo

* setting changes + test skips

Co-authored-by: Peng Lyu <penn.lv@gmail.com>

---------

Co-authored-by: Peng Lyu <penn.lv@gmail.com>
This commit is contained in:
Michael Lively 2023-06-15 17:08:03 -07:00 committed by GitHub
parent 22705e6f41
commit 1eec40969e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 691 additions and 29 deletions

View file

@ -65,12 +65,12 @@ import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/v
import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel';
import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl';
import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext';
import { NotebookEditorToolbar } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar';
import { NotebookEditorToolbar, NotebookEditorWorkbenchToolbar, RenderLabel, RenderLabelWithFallback, convertConfiguration } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar';
import { NotebookEditorContextKeys } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys';
import { NotebookOverviewRuler } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookOverviewRuler';
import { ListTopCellToolbar } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookTopCellToolbar';
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
import { CellEditType, CellKind, INotebookSearchOptions, RENDERER_NOT_AVAILABLE, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CellEditType, CellKind, INotebookSearchOptions, NotebookSetting, RENDERER_NOT_AVAILABLE, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { NOTEBOOK_CURSOR_NAVIGATION_MODE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_OUTPUT_FOCUSED, NOTEBOOK_OUPTUT_INPUT_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys';
import { INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService';
import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService';
@ -170,7 +170,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD
//#endregion
private _overlayContainer!: HTMLElement;
private _notebookTopToolbarContainer!: HTMLElement;
private _notebookTopToolbar!: NotebookEditorToolbar;
private _notebookTopToolbar!: NotebookEditorToolbar | NotebookEditorWorkbenchToolbar;
private _notebookOverviewRulerContainer!: HTMLElement;
private _notebookOverviewRuler!: NotebookOverviewRuler;
private _body!: HTMLElement;
@ -1013,12 +1013,42 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD
}
private _registerNotebookActionsToolbar() {
this._notebookTopToolbar = this._register(this.instantiationService.createInstance(NotebookEditorToolbar, this, this.scopedContextKeyService, this._notebookOptions, this._notebookTopToolbarContainer));
this._register(this._notebookTopToolbar.onDidChangeVisibility(() => {
if (this._dimension && this._isVisible) {
this.layout(this._dimension);
const store = new DisposableStore();
let currentLabel = convertConfiguration(this.configurationService.getValue<RenderLabelWithFallback>(NotebookSetting.globalToolbarShowLabel));
const render = () => {
if (currentLabel === RenderLabel.Dynamic) {
this._notebookTopToolbar = this._register(this.instantiationService.createInstance(NotebookEditorToolbar, this, this.scopedContextKeyService, this._notebookOptions, this._notebookTopToolbarContainer));
} else {
this._notebookTopToolbar = this._register(this.instantiationService.createInstance(NotebookEditorWorkbenchToolbar, this, this.scopedContextKeyService, this._notebookOptions, this._notebookTopToolbarContainer));
}
store.add(this._notebookTopToolbar.onDidChangeVisibility(() => {
if (this._dimension && this._isVisible) {
this.layout(this._dimension);
}
}));
};
render();
this._register(this.configurationService.onDidChangeConfiguration(e => {
if (!e.affectsConfiguration(NotebookSetting.globalToolbarShowLabel)) {
return;
}
const newRenderLabel = convertConfiguration(this.configurationService.getValue<RenderLabelWithFallback>(NotebookSetting.globalToolbarShowLabel));
if (newRenderLabel !== currentLabel && (newRenderLabel === RenderLabel.Dynamic || currentLabel === RenderLabel.Dynamic)) {
// switch to the other implementation
store.clear();
this._notebookTopToolbar.dispose();
DOM.clearNode(this._notebookTopToolbarContainer);
currentLabel = newRenderLabel;
render();
}
}));
this._register(store);
}
private _updateOutputRenderers() {

View file

@ -11,7 +11,7 @@ import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { IMenu, IMenuService, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions';
import { IMenu, IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
@ -28,6 +28,7 @@ import { NotebookOptions } from 'vs/workbench/contrib/notebook/browser/notebookO
import { IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar';
import { disposableTimeout } from 'vs/base/common/async';
import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
import { HiddenItemStrategy, IWorkbenchToolBarOptions, WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar';
interface IActionModel {
action: IAction;
@ -36,13 +37,28 @@ interface IActionModel {
renderLabel: boolean;
}
enum RenderLabel {
export enum RenderLabel {
Always = 0,
Never = 1,
Dynamic = 2
}
type RenderLabelWithFallback = true | false | 'always' | 'never' | 'dynamic';
export type RenderLabelWithFallback = true | false | 'always' | 'never' | 'dynamic';
export function convertConfiguration(value: RenderLabelWithFallback): RenderLabel {
switch (value) {
case true:
return RenderLabel.Always;
case false:
return RenderLabel.Never;
case 'always':
return RenderLabel.Always;
case 'never':
return RenderLabel.Never;
case 'dynamic':
return RenderLabel.Dynamic;
}
}
const ICON_ONLY_ACTION_WIDTH = 21;
const TOGGLE_MORE_ACTION_WIDTH = 21;
@ -104,7 +120,6 @@ class FixedLabelStrategy implements IActionLayoutStrategy {
}
}
class FixedLabellessStrategy extends FixedLabelStrategy {
constructor(
notebookEditor: INotebookEditorDelegate,
@ -354,7 +369,7 @@ export class NotebookEditorToolbar extends Disposable {
this._register(this._notebookGlobalActionsMenu);
this._useGlobalToolbar = this.notebookOptions.getLayoutConfiguration().globalToolbar;
this._renderLabel = this._convertConfiguration(this.configurationService.getValue<RenderLabelWithFallback>(NotebookSetting.globalToolbarShowLabel));
this._renderLabel = convertConfiguration(this.configurationService.getValue<RenderLabelWithFallback>(NotebookSetting.globalToolbarShowLabel));
this._updateStrategy();
const context = {
@ -433,7 +448,7 @@ export class NotebookEditorToolbar extends Disposable {
this._register(this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(NotebookSetting.globalToolbarShowLabel)) {
this._renderLabel = this._convertConfiguration(this.configurationService.getValue<RenderLabelWithFallback>(NotebookSetting.globalToolbarShowLabel));
this._renderLabel = convertConfiguration(this.configurationService.getValue<RenderLabelWithFallback>(NotebookSetting.globalToolbarShowLabel));
this._updateStrategy();
const oldElement = this._notebookLeftToolbar.getElement();
oldElement.parentElement?.removeChild(oldElement);
@ -479,21 +494,6 @@ export class NotebookEditorToolbar extends Disposable {
}
}
private _convertConfiguration(value: RenderLabelWithFallback): RenderLabel {
switch (value) {
case true:
return RenderLabel.Always;
case false:
return RenderLabel.Never;
case 'always':
return RenderLabel.Always;
case 'never':
return RenderLabel.Never;
case 'dynamic':
return RenderLabel.Dynamic;
}
}
private _showNotebookActionsinEditorToolbar() {
// when there is no view model, just ignore.
if (!this.notebookEditor.hasModel()) {
@ -642,3 +642,532 @@ export class NotebookEditorToolbar extends Disposable {
super.dispose();
}
}
class WorkbenchAlwaysLabelStrategy implements IActionLayoutStrategy {
constructor(
readonly notebookEditor: INotebookEditorDelegate,
readonly editorToolbar: NotebookEditorWorkbenchToolbar,
readonly instantiationService: IInstantiationService) { }
actionProvider(action: IAction): ActionViewItem | undefined {
if (action.id === SELECT_KERNEL_ID) {
// this is being disposed by the consumer
return this.instantiationService.createInstance(NotebooKernelActionViewItem, action, this.notebookEditor);
}
return action instanceof MenuItemAction ? this.instantiationService.createInstance(ActionViewWithLabel, action, undefined) : undefined;
}
calculateActions(leftToolbarContainerMaxWidth: number): { primaryActions: IAction[]; secondaryActions: IAction[] } {
const initialPrimaryActions = this.editorToolbar.primaryActions;
const initialSecondaryActions = this.editorToolbar.secondaryActions;
return workbenchCalculateActions(initialPrimaryActions, initialSecondaryActions, leftToolbarContainerMaxWidth);
}
}
class WorkbenchNeverLabelStrategy implements IActionLayoutStrategy {
constructor(
readonly notebookEditor: INotebookEditorDelegate,
readonly editorToolbar: NotebookEditorWorkbenchToolbar,
readonly instantiationService: IInstantiationService) { }
actionProvider(action: IAction): ActionViewItem | undefined {
if (action.id === SELECT_KERNEL_ID) {
// this is being disposed by the consumer
return this.instantiationService.createInstance(NotebooKernelActionViewItem, action, this.notebookEditor);
}
return action instanceof MenuItemAction ? this.instantiationService.createInstance(MenuEntryActionViewItem, action, undefined) : undefined;
}
calculateActions(leftToolbarContainerMaxWidth: number): { primaryActions: IAction[]; secondaryActions: IAction[] } {
const initialPrimaryActions = this.editorToolbar.primaryActions;
const initialSecondaryActions = this.editorToolbar.secondaryActions;
return workbenchCalculateActions(initialPrimaryActions, initialSecondaryActions, leftToolbarContainerMaxWidth);
}
}
// class WorkbenchDynamicLabelStrategy implements IActionLayoutStrategy {
// constructor(
// readonly notebookEditor: INotebookEditorDelegate,
// readonly editorToolbar: NotebookEditorToolbar,
// readonly instantiationService: IInstantiationService) {
// }
// actionProvider(action: IAction) {
// if (action.id === SELECT_KERNEL_ID) {
// // // this is being disposed by the consumer
// return this.instantiationService.createInstance(NotebooKernelActionViewItem, action, this.notebookEditor);
// }
// const a = this.editorToolbar.primaryActions.find(a => a.action.id === action.id);
// if (!a || a.renderLabel) {
// // render new action with label to get a correct full width.
// return action instanceof MenuItemAction ? this.instantiationService.createInstance(ActionViewWithLabel, action, undefined) : undefined;
// } else {
// return action instanceof MenuItemAction ? this.instantiationService.createInstance(MenuEntryActionViewItem, action, undefined) : undefined;
// }
// }
// calculateActions(leftToolbarContainerMaxWidth: number) {
// }
// }
export class NotebookEditorWorkbenchToolbar extends Disposable {
private _leftToolbarScrollable!: DomScrollableElement;
private _notebookTopLeftToolbarContainer!: HTMLElement;
private _notebookTopRightToolbarContainer!: HTMLElement;
private _notebookGlobalActionsMenu!: IMenu;
private _notebookLeftToolbar!: WorkbenchToolBar;
private _primaryActions: IActionModel[];
get primaryActions(): IActionModel[] {
return this._primaryActions;
}
private _secondaryActions: IAction[];
get secondaryActions(): IAction[] {
return this._secondaryActions;
}
private _notebookRightToolbar!: ToolBar;
private _useGlobalToolbar: boolean = false;
private _strategy!: IActionLayoutStrategy;
private _renderLabel: RenderLabel = RenderLabel.Always;
private _visible: boolean = false;
set visible(visible: boolean) {
if (this._visible !== visible) {
this._visible = visible;
this._onDidChangeVisibility.fire(visible);
}
}
private readonly _onDidChangeVisibility = this._register(new Emitter<boolean>());
onDidChangeVisibility: Event<boolean> = this._onDidChangeVisibility.event;
get useGlobalToolbar(): boolean {
return this._useGlobalToolbar;
}
private _dimension: DOM.Dimension | null = null;
private _deferredActionUpdate: IDisposable | undefined;
constructor(
readonly notebookEditor: INotebookEditorDelegate,
readonly contextKeyService: IContextKeyService,
readonly notebookOptions: NotebookOptions,
readonly domNode: HTMLElement,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IContextMenuService private readonly contextMenuService: IContextMenuService,
@IMenuService private readonly menuService: IMenuService,
@IEditorService private readonly editorService: IEditorService,
@IKeybindingService private readonly keybindingService: IKeybindingService,
@IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService,
) {
super();
this._primaryActions = [];
this._secondaryActions = [];
this._buildBody();
this._register(Event.debounce<void, void>(
this.editorService.onDidActiveEditorChange,
(last, _current) => last,
200
)(this._updatePerEditorChange, this));
this._registerNotebookActionsToolbar();
}
private _buildBody() {
this._notebookTopLeftToolbarContainer = document.createElement('div');
this._notebookTopLeftToolbarContainer.classList.add('notebook-toolbar-left');
this._leftToolbarScrollable = new DomScrollableElement(this._notebookTopLeftToolbarContainer, {
vertical: ScrollbarVisibility.Hidden,
horizontal: ScrollbarVisibility.Visible,
horizontalScrollbarSize: 3,
useShadows: false,
scrollYToX: true
});
this._register(this._leftToolbarScrollable);
DOM.append(this.domNode, this._leftToolbarScrollable.getDomNode());
this._notebookTopRightToolbarContainer = document.createElement('div');
this._notebookTopRightToolbarContainer.classList.add('notebook-toolbar-right');
DOM.append(this.domNode, this._notebookTopRightToolbarContainer);
}
private _updatePerEditorChange() {
if (this.editorService.activeEditorPane?.getId() === NOTEBOOK_EDITOR_ID) {
const notebookEditor = this.editorService.activeEditorPane.getControl() as INotebookEditorDelegate;
if (notebookEditor === this.notebookEditor) {
// this is the active editor
this._showNotebookActionsinEditorToolbar();
return;
}
}
}
private _registerNotebookActionsToolbar() {
this._notebookGlobalActionsMenu = this._register(this.menuService.createMenu(this.notebookEditor.creationOptions.menuIds.notebookToolbar, this.contextKeyService));
this._register(this._notebookGlobalActionsMenu);
this._useGlobalToolbar = this.notebookOptions.getLayoutConfiguration().globalToolbar;
this._renderLabel = this._convertConfiguration(this.configurationService.getValue(NotebookSetting.globalToolbarShowLabel));
this._updateStrategy();
const context = {
ui: true,
notebookEditor: this.notebookEditor
};
const actionProvider = (action: IAction) => {
if (action.id === SELECT_KERNEL_ID) {
// this is being disposed by the consumer
return this.instantiationService.createInstance(NotebooKernelActionViewItem, action, this.notebookEditor);
}
if (this._renderLabel !== RenderLabel.Never) {
const a = this._primaryActions.find(a => a.action.id === action.id);
// if (a && a.renderLabel) {
if (a) {
return action instanceof MenuItemAction ? this.instantiationService.createInstance(ActionViewWithLabel, action, undefined) : undefined;
} else {
return action instanceof MenuItemAction ? this.instantiationService.createInstance(MenuEntryActionViewItem, action, undefined) : undefined;
}
} else {
return action instanceof MenuItemAction ? this.instantiationService.createInstance(MenuEntryActionViewItem, action, undefined) : undefined;
}
};
const leftToolbarOptions: IWorkbenchToolBarOptions = {
hiddenItemStrategy: HiddenItemStrategy.RenderInSecondaryGroup,
resetMenu: MenuId.NotebookToolbar,
actionViewItemProvider: (action, options) => {
return this._strategy.actionProvider(action, options);
},
getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id),
renderDropdownAsChildElement: true,
};
this._notebookLeftToolbar = this.instantiationService.createInstance(
WorkbenchToolBar,
this._notebookTopLeftToolbarContainer,
leftToolbarOptions
);
this._register(this._notebookLeftToolbar);
this._notebookLeftToolbar.context = context;
this._notebookRightToolbar = new ToolBar(this._notebookTopRightToolbarContainer, this.contextMenuService, {
getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id),
actionViewItemProvider: actionProvider,
renderDropdownAsChildElement: true
});
this._register(this._notebookRightToolbar);
this._notebookRightToolbar.context = context;
this._showNotebookActionsinEditorToolbar();
let dropdownIsVisible = false;
let deferredUpdate: (() => void) | undefined;
this._register(this._notebookGlobalActionsMenu.onDidChange(() => {
if (dropdownIsVisible) {
deferredUpdate = () => this._showNotebookActionsinEditorToolbar();
return;
}
if (this.notebookEditor.isVisible) {
this._showNotebookActionsinEditorToolbar();
}
}));
this._register(this._notebookLeftToolbar.onDidChangeDropdownVisibility(visible => {
dropdownIsVisible = visible;
if (deferredUpdate && !visible) {
setTimeout(() => {
deferredUpdate?.();
}, 0);
deferredUpdate = undefined;
}
}));
this._register(this.notebookOptions.onDidChangeOptions(e => {
if (e.globalToolbar !== undefined) {
this._useGlobalToolbar = this.notebookOptions.getLayoutConfiguration().globalToolbar;
this._showNotebookActionsinEditorToolbar();
}
}));
this._register(this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(NotebookSetting.globalToolbarShowLabel)) {
this._renderLabel = this._convertConfiguration(this.configurationService.getValue<RenderLabelWithFallback>(NotebookSetting.globalToolbarShowLabel));
this._updateStrategy();
const oldElement = this._notebookLeftToolbar.getElement();
oldElement.parentElement?.removeChild(oldElement);
this._notebookLeftToolbar.dispose();
this._notebookLeftToolbar = this.instantiationService.createInstance(
WorkbenchToolBar,
this._notebookTopLeftToolbarContainer,
leftToolbarOptions
);
this._register(this._notebookLeftToolbar);
this._notebookLeftToolbar.context = context;
this._showNotebookActionsinEditorToolbar();
return;
}
}));
if (this.experimentService) {
this.experimentService.getTreatment<boolean>('nbtoolbarineditor').then(treatment => {
if (treatment === undefined) {
return;
}
if (this._useGlobalToolbar !== treatment) {
this._useGlobalToolbar = treatment;
this._showNotebookActionsinEditorToolbar();
}
});
}
}
private _updateStrategy() {
switch (this._renderLabel) {
case RenderLabel.Always:
this._strategy = new WorkbenchAlwaysLabelStrategy(this.notebookEditor, this, this.instantiationService);
break;
case RenderLabel.Never:
this._strategy = new WorkbenchNeverLabelStrategy(this.notebookEditor, this, this.instantiationService);
break;
case RenderLabel.Dynamic:
this._strategy = new WorkbenchAlwaysLabelStrategy(this.notebookEditor, this, this.instantiationService); // todo: defer to always, incorrect
break;
}
}
private _convertConfiguration(value: RenderLabelWithFallback): RenderLabel {
switch (value) {
case true:
return RenderLabel.Always;
case false:
return RenderLabel.Never;
case 'always':
return RenderLabel.Always;
case 'never':
return RenderLabel.Never;
case 'dynamic':
return RenderLabel.Dynamic;
}
}
private _showNotebookActionsinEditorToolbar() {
// when there is no view model, just ignore.
if (!this.notebookEditor.hasModel()) {
this._deferredActionUpdate?.dispose();
this._deferredActionUpdate = undefined;
this.visible = false;
return;
}
if (this._deferredActionUpdate) {
return;
}
if (!this._useGlobalToolbar) {
this.domNode.style.display = 'none';
this._deferredActionUpdate = undefined;
this.visible = false;
} else {
this._deferredActionUpdate = disposableTimeout(async () => {
await this._setNotebookActions();
this.visible = true;
this._deferredActionUpdate = undefined;
}, 50);
}
}
private async _setNotebookActions() {
const groups = this._notebookGlobalActionsMenu.getActions({ shouldForwardArgs: true, renderShortTitle: true });
this.domNode.style.display = 'flex';
const primaryLeftGroups = groups.filter(group => /^navigation/.test(group[0]));
const primaryActions: IAction[] = [];
primaryLeftGroups.sort((a, b) => {
if (a[0] === 'navigation') {
return 1;
}
if (b[0] === 'navigation') {
return -1;
}
return 0;
}).forEach((group, index) => {
primaryActions.push(...group[1]);
if (index < primaryLeftGroups.length - 1) {
primaryActions.push(new Separator());
}
});
const primaryRightGroup = groups.find(group => /^status/.test(group[0]));
const primaryRightActions = primaryRightGroup ? primaryRightGroup[1] : [];
const secondaryActions = groups.filter(group => !/^navigation/.test(group[0]) && !/^status/.test(group[0])).reduce((prev: (MenuItemAction | SubmenuItemAction)[], curr) => { prev.push(...curr[1]); return prev; }, []);
this._notebookLeftToolbar.setActions([], []);
// this._primaryActions.forEach(action => action.renderLabel = true);
this._notebookLeftToolbar.setActions(primaryActions, secondaryActions);
this._primaryActions = primaryActions.map(action => ({
action: action,
size: 0,
renderLabel: true,
visible: true
}));
this._secondaryActions = secondaryActions;
this._notebookRightToolbar.setActions(primaryRightActions, []);
this._secondaryActions = secondaryActions;
// flush to make sure it can be updated later
// this._primaryActions = [];
if (this._dimension && this._dimension.width >= 0 && this._dimension.height >= 0) {
this._cacheItemSizes(this._notebookLeftToolbar);
}
this._computeSizes();
}
private _cacheItemSizes(toolbar: WorkbenchToolBar) {
for (let i = 0; i < toolbar.getItemsLength(); i++) {
const action = toolbar.getItemAction(i);
if (action && action.id !== 'toolbar.toggle.more') {
const existing = this._primaryActions.find(a => a.action.id === action.id);
if (existing) {
existing.size = toolbar.getItemWidth(i);
existing.renderLabel = true;
}
}
}
}
private _computeSizes() {
const toolbar = this._notebookLeftToolbar;
const rightToolbar = this._notebookRightToolbar;
if (toolbar && rightToolbar && this._dimension && this._dimension.height >= 0 && this._dimension.width >= 0) {
// compute size only if it's visible
if (this._primaryActions.length === 0 && toolbar.getItemsLength() !== this._primaryActions.length) {
this._cacheItemSizes(this._notebookLeftToolbar);
}
if (this._primaryActions.length === 0) {
return;
}
const kernelWidth = (rightToolbar.getItemsLength() ? rightToolbar.getItemWidth(0) : 0) + ACTION_PADDING;
const leftToolbarContainerMaxWidth = this._dimension.width - kernelWidth - (ACTION_PADDING + TOGGLE_MORE_ACTION_WIDTH) - (/** toolbar left margin */ACTION_PADDING) - (/** toolbar right margin */ACTION_PADDING);
const calculatedActions = this._strategy.calculateActions(leftToolbarContainerMaxWidth);
this._notebookLeftToolbar.setActions(calculatedActions.primaryActions, calculatedActions.secondaryActions);
}
}
layout(dimension: DOM.Dimension) {
this._dimension = dimension;
if (!this._useGlobalToolbar) {
this.domNode.style.display = 'none';
} else {
this.domNode.style.display = 'flex';
}
this._computeSizes();
}
override dispose() {
this._notebookLeftToolbar.context = undefined;
this._notebookRightToolbar.context = undefined;
this._notebookLeftToolbar.dispose();
this._notebookRightToolbar.dispose();
this._notebookLeftToolbar = null!;
this._notebookRightToolbar = null!;
this._deferredActionUpdate?.dispose();
this._deferredActionUpdate = undefined;
super.dispose();
}
}
export function workbenchCalculateActions(initialPrimaryActions: IActionModel[], initialSecondaryActions: IAction[], leftToolbarContainerMaxWidth: number): { primaryActions: IAction[]; secondaryActions: IAction[] } {
let currentSize = 0;
const renderActions: IActionModel[] = [];
const overflow: IAction[] = [];
let containerFull = false;
let nonZeroAction = false;
if (initialPrimaryActions.length === 0) {
return { primaryActions: [], secondaryActions: initialSecondaryActions };
}
for (let i = 0; i < initialPrimaryActions.length; i++) {
const actionModel = initialPrimaryActions[i];
const itemSize = actionModel.size;
// if two separators in a row, ignore the second
if (actionModel.action instanceof Separator && renderActions.length > 0 && renderActions[renderActions.length - 1].action instanceof Separator) {
continue;
}
// if a separator is the first nonZero action, ignore it
if (actionModel.action instanceof Separator && !nonZeroAction) {
continue;
}
if (currentSize + itemSize <= leftToolbarContainerMaxWidth && !containerFull) { // if next item fits within left container width, push to rendered actions
currentSize += ACTION_PADDING + itemSize;
renderActions.push(actionModel);
if (itemSize !== 0) {
nonZeroAction = true;
}
} else {
containerFull = true;
if (itemSize === 0) { // size 0 implies a hidden item, keep in primary to allow for Workbench to handle visibility
renderActions.push(actionModel);
} else {
if (actionModel.action instanceof Separator) { // never push a separator to overflow
continue;
}
overflow.push(actionModel.action);
}
}
}
for (let i = (renderActions.length - 1); i !== 0; i--) {
const temp = renderActions[i];
if (temp.size === 0) {
continue;
}
if (temp.action instanceof Separator) {
renderActions.splice(i, 1);
}
break;
}
if (renderActions.length && renderActions[renderActions.length - 1].action instanceof Separator) {
renderActions.pop();
}
if (overflow.length !== 0) {
overflow.push(new Separator());
}
return {
primaryActions: renderActions.map(action => action.action),
secondaryActions: [...overflow, ...initialSecondaryActions]
};
}

View file

@ -78,7 +78,7 @@ suite('NotebookCellList focus/selection', () => {
});
});
test('notebook cell list setFocus', async function () {
test('notebook cell list setFocus2', async function () {
await withTestNotebook(
[
['var a = 1;', 'javascript', CellKind.Code, [], {}],
@ -97,6 +97,7 @@ suite('NotebookCellList focus/selection', () => {
cellList.setSelection([1]);
assert.deepStrictEqual(viewModel.getSelections(), [{ start: 1, end: 2 }]);
cellList.detachViewModel();
});
});

View file

@ -0,0 +1,102 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { workbenchCalculateActions } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar';
import { Action, IAction, Separator } from 'vs/base/common/actions';
import * as assert from 'assert';
interface IActionModel {
action: IAction;
size: number;
visible: boolean;
renderLabel: boolean;
}
/**
* Calculate the visible actions in the toolbar.
* @param action The action to measure.
* @param container The container the action will be placed in.
* @returns The primary and secondary actions to be rendered
*
* NOTE: every action requires space for ACTION_PADDING +8 to the right.
*
* ex: action with size 50 requires 58px of space
*/
suite('workbenchCalculateActions', () => {
test('should return empty primary and secondary actions when given empty initial actions', () => {
const result = workbenchCalculateActions([], [], 100);
assert.deepEqual(result.primaryActions, []);
assert.deepEqual(result.secondaryActions, []);
});
test('should return all primary actions when they fit within the container width', () => {
const actions: IActionModel[] = [
{ action: new Action('action0', 'Action 0'), size: 50, visible: true, renderLabel: true },
{ action: new Action('action1', 'Action 1'), size: 50, visible: true, renderLabel: true },
{ action: new Action('action2', 'Action 2'), size: 50, visible: true, renderLabel: true },
];
const result = workbenchCalculateActions(actions, [], 200);
assert.deepEqual(result.primaryActions, actions.map(action => action.action));
assert.deepEqual(result.secondaryActions, []);
});
test.skip('should move actions to secondary when they do not fit within the container width', () => {
const actions: IActionModel[] = [
{ action: new Action('action0', 'Action 0'), size: 50, visible: true, renderLabel: true },
{ action: new Action('action1', 'Action 1'), size: 50, visible: true, renderLabel: true },
{ action: new Action('action2', 'Action 2'), size: 50, visible: true, renderLabel: true },
];
const result = workbenchCalculateActions(actions, [], 100);
assert.deepEqual(result.primaryActions, [actions[0]].map(action => action.action));
assert.deepEqual(result.secondaryActions, [actions[1], actions[2]].map(action => action.action));
});
test('should ignore second separator when two separators are in a row', () => {
const actions: IActionModel[] = [
{ action: new Action('action0', 'Action 0'), size: 50, visible: true, renderLabel: true },
{ action: new Separator(), size: 1, visible: true, renderLabel: true },
{ action: new Separator(), size: 1, visible: true, renderLabel: true },
{ action: new Action('action1', 'Action 1'), size: 50, visible: true, renderLabel: true },
];
const result = workbenchCalculateActions(actions, [], 125);
assert.deepEqual(result.primaryActions, [actions[0], actions[1], actions[3]].map(action => action.action));
assert.deepEqual(result.secondaryActions, []);
});
test('should ignore separators when they are at the end of the resulting primary actions', () => {
const actions: IActionModel[] = [
{ action: new Action('action0', 'Action 0'), size: 50, visible: true, renderLabel: true },
{ action: new Separator(), size: 1, visible: true, renderLabel: true },
{ action: new Action('action1', 'Action 1'), size: 50, visible: true, renderLabel: true },
{ action: new Separator(), size: 1, visible: true, renderLabel: true },
];
const result = workbenchCalculateActions(actions, [], 200);
assert.deepEqual(result.primaryActions, [actions[0], actions[1], actions[2]].map(action => action.action));
assert.deepEqual(result.secondaryActions, []);
});
test.skip('should keep actions with size 0 in primary actions', () => {
const actions: IActionModel[] = [
{ action: new Action('action0', 'Action 0'), size: 50, visible: true, renderLabel: true },
{ action: new Action('action1', 'Action 1'), size: 50, visible: true, renderLabel: true },
{ action: new Action('action2', 'Action 2'), size: 50, visible: true, renderLabel: true },
{ action: new Action('action3', 'Action 3'), size: 0, visible: true, renderLabel: true },
];
const result = workbenchCalculateActions(actions, [], 116);
assert.deepEqual(result.primaryActions, [actions[0], actions[1], actions[3]].map(action => action.action));
assert.deepEqual(result.secondaryActions, [actions[2]].map(action => action.action));
});
test('should not render separator if preceeded by size 0 action(s).', () => {
const actions: IActionModel[] = [
{ action: new Action('action0', 'Action 0'), size: 0, visible: true, renderLabel: true },
{ action: new Separator(), size: 1, visible: true, renderLabel: true },
{ action: new Action('action1', 'Action 1'), size: 50, visible: true, renderLabel: true },
];
const result = workbenchCalculateActions(actions, [], 116);
assert.deepEqual(result.primaryActions, [actions[0], actions[2]].map(action => action.action));
assert.deepEqual(result.secondaryActions, []);
});
});