diff --git a/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts b/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts index 00ae102716ac7..4b00c42f13d5c 100644 --- a/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts +++ b/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts @@ -286,6 +286,8 @@ export class NotebookActionsContribution implements CommandContribution, MenuCon order: '30', when: NOTEBOOK_HAS_OUTPUTS }); + + menus.registerIndependentSubmenu(NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_HIDDEN_ITEMS_CONTEXT_MENU, ''); } registerKeybindings(keybindings: KeybindingRegistry): void { @@ -326,4 +328,5 @@ export namespace NotebookMenus { export const NOTEBOOK_MAIN_TOOLBAR = 'notebook/toolbar'; export const NOTEBOOK_MAIN_TOOLBAR_CELL_ADD_GROUP = [NOTEBOOK_MAIN_TOOLBAR, 'cell-add-group']; export const NOTEBOOK_MAIN_TOOLBAR_EXECUTION_GROUP = [NOTEBOOK_MAIN_TOOLBAR, 'cell-execution-group']; + export const NOTEBOOK_MAIN_TOOLBAR_HIDDEN_ITEMS_CONTEXT_MENU = 'notebook-main-toolbar-hidden-items-context-menu'; } diff --git a/packages/notebook/src/browser/style/index.css b/packages/notebook/src/browser/style/index.css index ce8afa1a658b4..5fd8ce1e2f904 100644 --- a/packages/notebook/src/browser/style/index.css +++ b/packages/notebook/src/browser/style/index.css @@ -225,6 +225,7 @@ .theia-notebook-main-toolbar-item-text { padding: 0 4px; + white-space: nowrap; } .theia-notebook-toolbar-separator { diff --git a/packages/notebook/src/browser/view/notebook-main-toolbar.tsx b/packages/notebook/src/browser/view/notebook-main-toolbar.tsx index 696bb496232a5..f3d20a1eb63c7 100644 --- a/packages/notebook/src/browser/view/notebook-main-toolbar.tsx +++ b/packages/notebook/src/browser/view/notebook-main-toolbar.tsx @@ -15,7 +15,7 @@ // ***************************************************************************** import { ArrayUtils, CommandRegistry, CompoundMenuNodeRole, DisposableCollection, MenuModelRegistry, MenuNode, nls } from '@theia/core'; import * as React from '@theia/core/shared/react'; -import { codicon } from '@theia/core/lib/browser'; +import { codicon, ContextMenuRenderer } from '@theia/core/lib/browser'; import { NotebookCommands, NotebookMenus } from '../contributions/notebook-actions-contribution'; import { NotebookModel } from '../view-model/notebook-model'; import { NotebookKernelService } from '../service/notebook-kernel-service'; @@ -32,6 +32,7 @@ export interface NotebookMainToolbarProps { contextKeyService: ContextKeyService; editorNode: HTMLElement; notebookContextManager: NotebookContextManager; + contextMenuRenderer: ContextMenuRenderer; } @injectable() @@ -41,6 +42,7 @@ export class NotebookMainToolbarRenderer { @inject(MenuModelRegistry) protected readonly menuRegistry: MenuModelRegistry; @inject(ContextKeyService) protected readonly contextKeyService: ContextKeyService; @inject(NotebookContextManager) protected readonly notebookContextManager: NotebookContextManager; + @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer; render(notebookModel: NotebookModel, editorNode: HTMLElement): React.ReactNode { return ; + notebookContextManager={this.notebookContextManager} + contextMenuRenderer={this.contextMenuRenderer} />; } } -export class NotebookMainToolbar extends React.Component { +interface NotebookMainToolbarState { + selectedKernelLabel?: string; + numberOfHiddenItems: number; +} + +export class NotebookMainToolbar extends React.Component { + + // The minimum area between items and kernel select before hiding items in a context menu + static readonly MIN_FREE_AREA = 10; protected toDispose = new DisposableCollection(); @@ -61,10 +72,18 @@ export class NotebookMainToolbar extends React.Component this.calculateItemsToHide()); + constructor(props: NotebookMainToolbarProps) { super(props); - this.state = { selectedKernelLabel: props.notebookKernelService.getSelectedOrSuggestedKernel(props.notebookModel)?.label }; + this.state = { + selectedKernelLabel: props.notebookKernelService.getSelectedOrSuggestedKernel(props.notebookModel)?.label, + numberOfHiddenItems: 0, + }; this.toDispose.push(props.notebookKernelService.onDidChangeSelectedKernel(event => { if (props.notebookModel.uri.isEqual(event.notebook)) { this.setState({ selectedKernelLabel: props.notebookKernelService.getKernel(event.newKernel ?? '')?.label }); @@ -97,10 +116,49 @@ export class NotebookMainToolbar extends React.Component this.lastGapElementWidth && this.state.numberOfHiddenItems > 0) { + this.setState({ ...this.state, numberOfHiddenItems: 0 }); + this.lastGapElementWidth = this.gapElement.getBoundingClientRect().width; + } + } + + protected renderContextMenu(event: MouseEvent, menuItems: readonly MenuNode[]): void { + const hiddenItems = menuItems.slice(menuItems.length - this.calculateNumberOfHiddenItems(menuItems)); + const contextMenu = this.props.menuRegistry.getMenu([NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_HIDDEN_ITEMS_CONTEXT_MENU]); + + contextMenu.children.map(item => item.id).forEach(id => contextMenu.removeNode(id)); + hiddenItems.forEach(item => contextMenu.addNode(item)); + + this.props.contextMenuRenderer.render({ + anchor: event, + menuPath: [NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_HIDDEN_ITEMS_CONTEXT_MENU], + context: this.props.editorNode, + args: [this.props.notebookModel.uri] + }); + } + override render(): React.ReactNode { + const menuItems = this.getMenuItems(); return
- {this.getMenuItems().map(item => this.renderMenuItem(item))} -
+ {menuItems.slice(0, menuItems.length - this.calculateNumberOfHiddenItems(menuItems)).map(item => this.renderMenuItem(item))} + { + this.state.numberOfHiddenItems > 0 && + this.renderContextMenu(e.nativeEvent, menuItems)} /> + } +
this.gapElementChanged(element)} style={{ flexGrow: 1 }}>
this.props.commandRegistry.executeCommand(NotebookCommands.SELECT_KERNEL_COMMAND.id, this.props.notebookModel)}> @@ -108,7 +166,18 @@ export class NotebookMainToolbar extends React.Component
-
; + ; + } + + protected gapElementChanged(element: HTMLDivElement | null): void { + if (this.gapElement) { + this.resizeObserver.unobserve(this.gapElement); + } + this.gapElement = element ?? undefined; + if (this.gapElement) { + this.lastGapElementWidth = this.gapElement.getBoundingClientRect().width; + this.resizeObserver.observe(this.gapElement); + } } protected renderMenuItem(item: MenuNode, submenu?: string): React.ReactNode { @@ -157,4 +226,10 @@ export class NotebookMainToolbar extends React.Component item.children && item.children.length > 0) .forEach(item => this.getAllContextKeys(item.children!, keySet)); } + + protected calculateNumberOfHiddenItems(allMenuItems: readonly MenuNode[]): number { + return this.state.numberOfHiddenItems >= allMenuItems.length ? + allMenuItems.length : + this.state.numberOfHiddenItems % allMenuItems.length; + } }