Skip to content

Commit

Permalink
Use responsive design for the main notebook toolbar (#13663)
Browse files Browse the repository at this point in the history
* main toolbar adjusting to maximum size by putting items in to a context menu

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* fixed memory leak

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* fixed issues with max and min hidden items

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* fixed lint

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* fixed when incresing width of notebook

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

---------

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>
  • Loading branch information
jonah-iden authored Apr 30, 2024
1 parent c1f222d commit b20a751
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,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 {
Expand Down Expand Up @@ -344,4 +346,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';
}
1 change: 1 addition & 0 deletions packages/notebook/src/browser/style/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@

.theia-notebook-main-toolbar-item-text {
padding: 0 4px;
white-space: nowrap;
}

.theia-notebook-toolbar-separator {
Expand Down
89 changes: 82 additions & 7 deletions packages/notebook/src/browser/view/notebook-main-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -32,6 +32,7 @@ export interface NotebookMainToolbarProps {
contextKeyService: ContextKeyService;
editorNode: HTMLElement;
notebookContextManager: NotebookContextManager;
contextMenuRenderer: ContextMenuRenderer;
}

@injectable()
Expand All @@ -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 <NotebookMainToolbar notebookModel={notebookModel}
Expand All @@ -49,22 +51,39 @@ export class NotebookMainToolbarRenderer {
commandRegistry={this.commandRegistry}
contextKeyService={this.contextKeyService}
editorNode={editorNode}
notebookContextManager={this.notebookContextManager} />;
notebookContextManager={this.notebookContextManager}
contextMenuRenderer={this.contextMenuRenderer} />;
}
}

export class NotebookMainToolbar extends React.Component<NotebookMainToolbarProps, { selectedKernelLabel?: string }> {
interface NotebookMainToolbarState {
selectedKernelLabel?: string;
numberOfHiddenItems: number;
}

export class NotebookMainToolbar extends React.Component<NotebookMainToolbarProps, NotebookMainToolbarState> {

// 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();

protected nativeSubmenus = [
NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_CELL_ADD_GROUP[NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_CELL_ADD_GROUP.length - 1],
NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_EXECUTION_GROUP[NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_EXECUTION_GROUP.length - 1]];

protected gapElement: HTMLDivElement | undefined;
protected lastGapElementWidth: number = 0;

protected resizeObserver: ResizeObserver = new ResizeObserver(() => 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 });
Expand Down Expand Up @@ -97,18 +116,68 @@ export class NotebookMainToolbar extends React.Component<NotebookMainToolbarProp
this.toDispose.dispose();
}

override componentDidUpdate(): void {
this.calculateItemsToHide();
}

override componentDidMount(): void {
this.calculateItemsToHide();
}

protected calculateItemsToHide(): void {
const numberOfMenuItems = this.getMenuItems().length;
if (this.gapElement && this.gapElement.getBoundingClientRect().width < NotebookMainToolbar.MIN_FREE_AREA && this.state.numberOfHiddenItems < numberOfMenuItems) {
this.setState({ ...this.state, numberOfHiddenItems: this.state.numberOfHiddenItems + 1 });
this.lastGapElementWidth = this.gapElement.getBoundingClientRect().width;
} else if (this.gapElement && this.gapElement.getBoundingClientRect().width > 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 <div className='theia-notebook-main-toolbar'>
{this.getMenuItems().map(item => this.renderMenuItem(item))}
<div style={{ flexGrow: 1 }}></div>
{menuItems.slice(0, menuItems.length - this.calculateNumberOfHiddenItems(menuItems)).map(item => this.renderMenuItem(item))}
{
this.state.numberOfHiddenItems > 0 &&
<span className={`${codicon('ellipsis')} action-label theia-notebook-main-toolbar-item`} onClick={e => this.renderContextMenu(e.nativeEvent, menuItems)} />
}
<div ref={element => this.gapElementChanged(element)} style={{ flexGrow: 1 }}></div>
<div className='theia-notebook-main-toolbar-item action-label'
onClick={() => this.props.commandRegistry.executeCommand(NotebookCommands.SELECT_KERNEL_COMMAND.id, this.props.notebookModel)}>
<span className={codicon('server-environment')} />
<span className=' theia-notebook-main-toolbar-item-text'>
{this.state.selectedKernelLabel ?? nls.localizeByDefault('Select Kernel')}
</span>
</div>
</div>;
</div >;
}

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 {
Expand Down Expand Up @@ -157,4 +226,10 @@ export class NotebookMainToolbar extends React.Component<NotebookMainToolbarProp
menus.filter(item => 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;
}
}

0 comments on commit b20a751

Please sign in to comment.