Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Customize the shell layout with the settings #6921

Merged
merged 17 commits into from
Jun 15, 2023
32 changes: 32 additions & 0 deletions packages/application-extension/schema/shell.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"title": "Notebook Shell",
"description": "Notebook Shell layout settings.",
"properties": {
"layout": {
"$ref": "#/definitions/layout",
"type": "object",
"title": "Customize shell widget positioning",
"description": "Overrides default widget position in the application layout",
"default": {}
}
},
"additionalProperties": false,
"type": "object",
"definitions": {
"layout": {
"type": "object",
"properties": {
"[\\w-]+": {
"type": "object",
"properties": {
"area": {
"enum": ["left", "right"]
}
},
"additionalProperties": false
}
}
}
}
}
27 changes: 23 additions & 4 deletions packages/application-extension/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,14 +384,33 @@ const rendermime: JupyterFrontEndPlugin<IRenderMimeRegistry> = {
*/
const shell: JupyterFrontEndPlugin<INotebookShell> = {
id: '@jupyter-notebook/application-extension:shell',
activate: (app: JupyterFrontEnd) => {
autoStart: true,
provides: INotebookShell,
optional: [ISettingRegistry],
activate: (app: JupyterFrontEnd, settingRegistry: ISettingRegistry) => {
if (!(app.shell instanceof NotebookShell)) {
throw new Error(`${shell.id} did not find a NotebookShell instance.`);
}
return app.shell;
const notebookShell = app.shell;

if (settingRegistry) {
settingRegistry
.load(shell.id)
.then((settings) => {
// Add a layer of customization to support app shell mode
const customLayout = settings.composite['layout'] as any;

// Restore the layout.
void notebookShell.restoreLayout(customLayout);
})
.catch((reason) => {
console.error('Fail to load settings for the layout restorer.');
console.error(reason);
});
}

return notebookShell;
},
autoStart: true,
provides: INotebookShell,
};

/**
Expand Down
80 changes: 67 additions & 13 deletions packages/application/src/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,40 @@ export const INotebookShell = new Token<INotebookShell>(
*/
export interface INotebookShell extends NotebookShell {}

/**
* The namespace for INotebookShell type information.
*/
export namespace INotebookShell {
/**
* The areas of the application shell where widgets can reside.
*/
export type Area = 'main' | 'top' | 'menu' | 'left' | 'right';

/**
* Widget position
*/
export interface IWidgetPosition {
/**
* Widget area
*/
area?: Area;
/**
* Widget opening options
*/
options?: DocumentRegistry.IOpenOptions;
}

/**
* Mapping of widget type identifier and their user customized position
*/
export interface IUserLayout {
/**
* Widget customized position
*/
[k: string]: IWidgetPosition;
}
}

/**
* The default rank for ranked panels.
*/
Expand All @@ -36,6 +70,7 @@ export class NotebookShell extends Widget implements JupyterFrontEnd.IShell {
constructor() {
super();
this.id = 'main';
this._userLayout = {};

this._topHandler = new PanelHandler();
this._menuHandler = new PanelHandler();
Expand Down Expand Up @@ -217,7 +252,10 @@ export class NotebookShell extends Widget implements JupyterFrontEnd.IShell {
activateById(id: string): void {
// Search all areas that can have widgets for this widget, starting with main.
for (const area of ['main', 'top', 'left', 'right', 'menu']) {
const widget = find(this.widgets(area as Shell.Area), (w) => w.id === id);
const widget = find(
this.widgets(area as INotebookShell.Area),
(w) => w.id === id
);
if (widget) {
if (area === 'left') {
this.expandLeft(id);
Expand All @@ -243,9 +281,25 @@ export class NotebookShell extends Widget implements JupyterFrontEnd.IShell {
*/
add(
widget: Widget,
area?: Shell.Area,
area?: INotebookShell.Area,
options?: DocumentRegistry.IOpenOptions
): void {
let userPosition: INotebookShell.IWidgetPosition | undefined;
if (options?.type && this._userLayout[options.type]) {
userPosition = this._userLayout[options.type];
} else {
userPosition = this._userLayout[widget.id];
}

area = userPosition?.area ?? area;
options =
options || userPosition?.options
? {
...options,
...userPosition?.options,
}
: undefined;

const rank = options?.rank ?? DEFAULT_RANK;
switch (area) {
case 'top':
Expand Down Expand Up @@ -293,7 +347,7 @@ export class NotebookShell extends Widget implements JupyterFrontEnd.IShell {
*
* @param area The area
*/
*widgets(area: Shell.Area): IterableIterator<Widget> {
*widgets(area: INotebookShell.Area): IterableIterator<Widget> {
switch (area ?? 'main') {
case 'top':
yield* this._topHandler.panel.widgets;
Expand Down Expand Up @@ -348,6 +402,15 @@ export class NotebookShell extends Widget implements JupyterFrontEnd.IShell {
this._rightHandler.panel.hide();
}

/**
* Restore the layout state and configuration for the application shell.
*/
async restoreLayout(
configuration: INotebookShell.IUserLayout
): Promise<void> {
this._userLayout = configuration;
}

private _topWrapper: Panel;
private _topHandler: PanelHandler;
private _menuWrapper: Panel;
Expand All @@ -361,16 +424,7 @@ export class NotebookShell extends Widget implements JupyterFrontEnd.IShell {
private _translator: ITranslator = nullTranslator;
private _currentChanged = new Signal<this, void>(this);
private _mainWidgetLoaded = new PromiseDelegate<void>();
}

/**
* A namespace for Shell statics
*/
export namespace Shell {
/**
* The areas of the application shell where widgets can reside.
*/
export type Area = 'main' | 'top' | 'left' | 'right' | 'menu';
private _userLayout: INotebookShell.IUserLayout;
}

export namespace Private {
Expand Down
10 changes: 3 additions & 7 deletions packages/application/test/shell.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import {
INotebookShell,
NotebookShell,
Shell,
} from '@jupyter-notebook/application';
import { INotebookShell, NotebookShell } from '@jupyter-notebook/application';

import { JupyterFrontEnd } from '@jupyterlab/application';

Expand All @@ -30,7 +26,7 @@ describe('Shell for notebooks', () => {

it('should make some areas empty initially', () => {
['main', 'left', 'right', 'menu'].forEach((area) => {
const widgets = Array.from(shell.widgets(area as Shell.Area));
const widgets = Array.from(shell.widgets(area as INotebookShell.Area));
expect(widgets.length).toEqual(0);
});
});
Expand Down Expand Up @@ -139,7 +135,7 @@ describe('Shell for tree view', () => {

it('should make some areas empty initially', () => {
['main', 'left', 'right', 'menu'].forEach((area) => {
const widgets = Array.from(shell.widgets(area as Shell.Area));
const widgets = Array.from(shell.widgets(area as INotebookShell.Area));
expect(widgets.length).toEqual(0);
});
});
Expand Down
6 changes: 4 additions & 2 deletions packages/notebook-extension/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ const notebookToolsWidget: JupyterFrontEndPlugin<void> = {

// Add the notebook tools in right area.
if (notebookTools) {
shell.add(notebookTools, 'right');
shell.add(notebookTools, 'right', { type: 'Property Inspector' });
}
};
shell.currentChanged.connect(onChange);
Expand Down Expand Up @@ -388,7 +388,9 @@ const trusted: JupyterFrontEndPlugin<void> = {
await current.context.ready;

const widget = TrustedComponent.create({ notebook, translator });
notebookShell.add(widget, 'menu', { rank: 11_000 });
notebookShell.add(widget, 'menu', {
rank: 11_000,
});
};

notebookShell.currentChanged.connect(onChange);
Expand Down