diff --git a/package.json b/package.json index 3427bd8..74c109f 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "@jupyterlab/apputils": "^4.2.0", "@jupyterlab/coreutils": "^6.2.0", "@jupyterlab/filebrowser": "^4.2.0", + "@jupyterlab/launcher": "^4.3.3", "@jupyterlab/services": "^7.2.0", "@jupyterlab/settingregistry": "^4.2.0", "@jupyterlab/translation": "^4.2.0", @@ -113,7 +114,10 @@ }, "extension": true, "outputDir": "jupyter_drives/labextension", - "schemaDir": "schema" + "schemaDir": "schema", + "disabledExtensions": [ + "@jupyterlab/launcher-extension:plugin" + ] }, "eslintIgnore": [ "node_modules", @@ -175,7 +179,8 @@ "all" ], "eqeqeq": "error", - "prefer-arrow-callback": "error" + "prefer-arrow-callback": "error", + "prefer-const": "off" } }, "prettier": { diff --git a/src/index.ts b/src/index.ts index 3234d08..a1b6d1b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,7 @@ import { DriveIcon, driveBrowserIcon } from './icons'; import { Drive } from './contents'; import { getDrivesList, setListingLimit } from './requests'; import { IDriveInfo, IDrivesList } from './token'; +import { launcherPlugin } from './launcher'; /** * The command IDs used by the driveBrowser plugin. @@ -230,7 +231,7 @@ const driveFileBrowser: JupyterFrontEndPlugin = { driveBrowser.node.setAttribute('aria-label', 'Drive Browser Section'); driveBrowser.title.icon = driveBrowserIcon; driveBrowser.title.caption = 'Drive File Browser'; - driveBrowser.id = 'Drive-File-Browser'; + driveBrowser.id = 'drive-file-browser'; void Private.restoreBrowser(driveBrowser, commands, router, tree, labShell); @@ -313,7 +314,8 @@ const driveFileBrowser: JupyterFrontEndPlugin = { const plugins: JupyterFrontEndPlugin[] = [ driveFileBrowser, drivesListProvider, - openDriveDialogPlugin + openDriveDialogPlugin, + launcherPlugin ]; export default plugins; diff --git a/src/launcher.ts b/src/launcher.ts new file mode 100644 index 0000000..3889233 --- /dev/null +++ b/src/launcher.ts @@ -0,0 +1,156 @@ +import { + ILabShell, + JupyterFrontEnd, + JupyterFrontEndPlugin +} from '@jupyterlab/application'; +import { ICommandPalette, MainAreaWidget } from '@jupyterlab/apputils'; +import { FileBrowserModel, IFileBrowserFactory } from '@jupyterlab/filebrowser'; +import { ILauncher, Launcher, LauncherModel } from '@jupyterlab/launcher'; +import { ITranslator } from '@jupyterlab/translation'; +import { addIcon, launcherIcon } from '@jupyterlab/ui-components'; +import { find } from '@lumino/algorithm'; +import { ReadonlyPartialJSONObject } from '@lumino/coreutils'; +import { DockPanel, TabBar, Widget } from '@lumino/widgets'; + +/** + * The command IDs used by the launcher plugin. + */ +namespace CommandIDs { + export const launcher = 'launcher:create'; +} + +/** + * A service providing an interface to the the launcher. + */ +export const launcherPlugin: JupyterFrontEndPlugin = { + activate, + id: 'jupyter-drives:launcher-extension-plugin', + description: 'Provides the launcher tab service for the file browsers.', + requires: [ITranslator], + optional: [ILabShell, ICommandPalette, IFileBrowserFactory], + provides: ILauncher, + autoStart: true +}; + +/** + * Activate the launcher. + */ +function activate( + app: JupyterFrontEnd, + translator: ITranslator, + labShell: ILabShell | null, + palette: ICommandPalette | null, + factory: IFileBrowserFactory | null +): ILauncher { + const { commands, shell } = app; + const trans = translator.load('jupyter-drives'); + const model = new LauncherModel(); + + commands.addCommand(CommandIDs.launcher, { + label: trans.__('New Launcher'), + icon: args => (args.toolbar ? addIcon : undefined), + execute: (args: ReadonlyPartialJSONObject) => { + // get current file browser used + const currentBrowser = factory?.tracker.currentWidget; + const cwd = (args['cwd'] as string) ?? currentBrowser?.model.path ?? ''; + const id = `launcher-${Private.id++}`; + const callback = (item: Widget) => { + // If widget is attached to the main area replace the launcher + if (find(shell.widgets('main'), w => w === item)) { + shell.add(item, 'main', { ref: id }); + launcher.dispose(); + } + }; + const launcher = new Launcher({ + model, + cwd, + callback, + commands, + translator + }); + + launcher.model = model; + launcher.title.icon = launcherIcon; + launcher.title.label = trans.__('Launcher'); + + const main = new MainAreaWidget({ content: launcher }); + + // If there are any other widgets open, remove the launcher close icon. + main.title.closable = !!Array.from(shell.widgets('main')).length; + main.id = id; + + shell.add(main, 'main', { + activate: args['activate'] as boolean, + ref: args['ref'] as string + }); + + if (labShell) { + labShell.layoutModified.connect(() => { + // If there is only a launcher open, remove the close icon. + main.title.closable = Array.from(labShell.widgets('main')).length > 1; + }, main); + } + + if (currentBrowser) { + const onPathChanged = (model: FileBrowserModel) => { + launcher.cwd = model.path; + }; + currentBrowser.model.pathChanged.connect(onPathChanged); + launcher.disposed.connect(() => { + currentBrowser.model.pathChanged.disconnect(onPathChanged); + }); + } + + return main; + } + }); + + if (labShell) { + const currentBrowser = factory?.tracker.currentWidget; + void Promise.all([app.restored, currentBrowser?.model.restored]).then( + () => { + function maybeCreate() { + // Create a launcher if there are no open items. + if (labShell!.isEmpty('main')) { + void commands.execute(CommandIDs.launcher); + } + } + // When layout is modified, create a launcher if there are no open items. + labShell.layoutModified.connect(() => { + maybeCreate(); + }); + } + ); + } + + if (palette) { + palette.addItem({ + command: CommandIDs.launcher, + category: trans.__('Launcher') + }); + } + + if (labShell) { + labShell.addButtonEnabled = true; + labShell.addRequested.connect((sender: DockPanel, arg: TabBar) => { + // Get the ref for the current tab of the tabbar which the add button was clicked + const ref = + arg.currentTitle?.owner.id || + arg.titles[arg.titles.length - 1].owner.id; + + return commands.execute(CommandIDs.launcher, { ref }); + }); + } + + return model; +} + +/** + * The namespace for module private data. + */ +namespace Private { + /** + * The incrementing id used for launcher widgets. + */ + export let id = 0; +} diff --git a/yarn.lock b/yarn.lock index d64f3a8..d6b7212 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2357,6 +2357,24 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/launcher@npm:^4.3.3": + version: 4.3.3 + resolution: "@jupyterlab/launcher@npm:4.3.3" + dependencies: + "@jupyterlab/apputils": ^4.4.3 + "@jupyterlab/translation": ^4.3.3 + "@jupyterlab/ui-components": ^4.3.3 + "@lumino/algorithm": ^2.0.2 + "@lumino/commands": ^2.3.1 + "@lumino/coreutils": ^2.2.0 + "@lumino/disposable": ^2.1.3 + "@lumino/properties": ^2.0.2 + "@lumino/widgets": ^2.5.0 + react: ^18.2.0 + checksum: d91ab6375b23b6cad8498d855008f9299353f8bc411793ed8242ab7a37f5e0ab9f6b4799d5931368a30d27bf612d88b4fc37a4b0cab44f2231754ea38d732787 + languageName: node + linkType: hard + "@jupyterlab/lsp@npm:^4.3.3": version: 4.3.3 resolution: "@jupyterlab/lsp@npm:4.3.3" @@ -7207,6 +7225,7 @@ __metadata: "@jupyterlab/builder": ^4.2.0 "@jupyterlab/coreutils": ^6.2.0 "@jupyterlab/filebrowser": ^4.2.0 + "@jupyterlab/launcher": ^4.3.3 "@jupyterlab/services": ^7.2.0 "@jupyterlab/settingregistry": ^4.2.0 "@jupyterlab/testutils": ^4.2.0