123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613 |
- /*-----------------------------------------------------------------------------
- | Copyright (c) Jupyter Development Team.
- | Distributed under the terms of the Modified BSD License.
- |----------------------------------------------------------------------------*/
- import {
- ILayoutRestorer,
- IRouter,
- JupyterFrontEnd,
- JupyterFrontEndPlugin
- } from '@jupyterlab/application';
- import {
- Dialog,
- ICommandPalette,
- ISplashScreen,
- IThemeManager,
- IWindowResolver,
- ThemeManager,
- WindowResolver,
- Printing
- } from '@jupyterlab/apputils';
- import {
- Debouncer,
- IRateLimiter,
- ISettingRegistry,
- IStateDB,
- SettingRegistry,
- StateDB,
- Throttler,
- URLExt
- } from '@jupyterlab/coreutils';
- import { IMainMenu } from '@jupyterlab/mainmenu';
- import { PromiseDelegate } from '@phosphor/coreutils';
- import { DisposableDelegate } from '@phosphor/disposable';
- import { Menu } from '@phosphor/widgets';
- import { Palette } from './palette';
- /**
- * The interval in milliseconds before recover options appear during splash.
- */
- const SPLASH_RECOVER_TIMEOUT = 12000;
- /**
- * The command IDs used by the apputils plugin.
- */
- namespace CommandIDs {
- export const changeTheme = 'apputils:change-theme';
- export const loadState = 'apputils:load-statedb';
- export const print = 'apputils:print';
- export const reset = 'apputils:reset';
- export const resetOnLoad = 'apputils:reset-on-load';
- }
- /**
- * The default command palette extension.
- */
- const palette: JupyterFrontEndPlugin<ICommandPalette> = {
- activate: Palette.activate,
- id: '@jupyterlab/apputils-extension:palette',
- provides: ICommandPalette,
- autoStart: true
- };
- /**
- * The default command palette's restoration extension.
- *
- * #### Notes
- * The command palette's restoration logic is handled separately from the
- * command palette provider extension because the layout restorer dependency
- * causes the command palette to be unavailable to other extensions earlier
- * in the application load cycle.
- */
- const paletteRestorer: JupyterFrontEndPlugin<void> = {
- activate: Palette.restore,
- id: '@jupyterlab/apputils-extension:palette-restorer',
- requires: [ILayoutRestorer],
- autoStart: true
- };
- /**
- * The default setting registry provider.
- */
- const settings: JupyterFrontEndPlugin<ISettingRegistry> = {
- id: '@jupyterlab/apputils-extension:settings',
- activate: async (app: JupyterFrontEnd): Promise<ISettingRegistry> => {
- const connector = app.serviceManager.settings;
- const plugins = (await connector.list()).values;
- return new SettingRegistry({ connector, plugins });
- },
- autoStart: true,
- provides: ISettingRegistry
- };
- /**
- * The default theme manager provider.
- */
- const themes: JupyterFrontEndPlugin<IThemeManager> = {
- id: '@jupyterlab/apputils-extension:themes',
- requires: [ISettingRegistry, JupyterFrontEnd.IPaths],
- optional: [ISplashScreen],
- activate: (
- app: JupyterFrontEnd,
- settings: ISettingRegistry,
- paths: JupyterFrontEnd.IPaths,
- splash: ISplashScreen | null
- ): IThemeManager => {
- const host = app.shell;
- const commands = app.commands;
- const url = URLExt.join(paths.urls.base, paths.urls.themes);
- const key = themes.id;
- const manager = new ThemeManager({ key, host, settings, splash, url });
- // Keep a synchronously set reference to the current theme,
- // since the asynchronous setting of the theme in `changeTheme`
- // can lead to an incorrect toggle on the currently used theme.
- let currentTheme: string;
- // Set data attributes on the application shell for the current theme.
- manager.themeChanged.connect((sender, args) => {
- currentTheme = args.newValue;
- document.body.dataset.jpThemeLight = String(
- manager.isLight(currentTheme)
- );
- document.body.dataset.jpThemeName = currentTheme;
- if (
- document.body.dataset.jpThemeScrollbars !==
- String(manager.themeScrollbars(currentTheme))
- ) {
- document.body.dataset.jpThemeScrollbars = String(
- manager.themeScrollbars(currentTheme)
- );
- }
- commands.notifyCommandChanged(CommandIDs.changeTheme);
- });
- commands.addCommand(CommandIDs.changeTheme, {
- label: args => {
- const theme = args['theme'] as string;
- return args['isPalette'] ? `Use ${theme} Theme` : theme;
- },
- isToggled: args => args['theme'] === currentTheme,
- execute: args => {
- const theme = args['theme'] as string;
- if (theme === manager.theme) {
- return;
- }
- return manager.setTheme(theme);
- }
- });
- return manager;
- },
- autoStart: true,
- provides: IThemeManager
- };
- /**
- * The default theme manager's UI command palette and main menu functionality.
- *
- * #### Notes
- * This plugin loads separately from the theme manager plugin in order to
- * prevent blocking of the theme manager while it waits for the command palette
- * and main menu to become available.
- */
- const themesPaletteMenu: JupyterFrontEndPlugin<void> = {
- id: '@jupyterlab/apputils-extension:themes-palette-menu',
- requires: [IThemeManager],
- optional: [ICommandPalette, IMainMenu],
- activate: (
- app: JupyterFrontEnd,
- manager: IThemeManager,
- palette: ICommandPalette | null,
- mainMenu: IMainMenu | null
- ): void => {
- const commands = app.commands;
- // If we have a main menu, add the theme manager to the settings menu.
- if (mainMenu) {
- const themeMenu = new Menu({ commands });
- themeMenu.title.label = 'JupyterLab Theme';
- void app.restored.then(() => {
- const command = CommandIDs.changeTheme;
- const isPalette = false;
- manager.themes.forEach(theme => {
- themeMenu.addItem({ command, args: { isPalette, theme } });
- });
- });
- mainMenu.settingsMenu.addGroup(
- [
- {
- type: 'submenu' as Menu.ItemType,
- submenu: themeMenu
- }
- ],
- 0
- );
- }
- // If we have a command palette, add theme switching options to it.
- if (palette) {
- void app.restored.then(() => {
- const category = 'Settings';
- const command = CommandIDs.changeTheme;
- const isPalette = true;
- manager.themes.forEach(theme => {
- palette.addItem({ command, args: { isPalette, theme }, category });
- });
- });
- }
- },
- autoStart: true
- };
- /**
- * The default window name resolver provider.
- */
- const resolver: JupyterFrontEndPlugin<IWindowResolver> = {
- id: '@jupyterlab/apputils-extension:resolver',
- autoStart: true,
- provides: IWindowResolver,
- requires: [JupyterFrontEnd.IPaths, IRouter],
- activate: async (
- _: JupyterFrontEnd,
- paths: JupyterFrontEnd.IPaths,
- router: IRouter
- ) => {
- const { hash, path, search } = router.current;
- const query = URLExt.queryStringToObject(search || '');
- const solver = new WindowResolver();
- const { urls } = paths;
- const match = path.match(new RegExp(`^${urls.workspaces}\/([^?\/]+)`));
- const workspace = (match && decodeURIComponent(match[1])) || '';
- const candidate = Private.candidate(paths, workspace);
- const rest = workspace
- ? path.replace(new RegExp(`^${urls.workspaces}\/${workspace}`), '')
- : path.replace(new RegExp(`^${urls.app}\/?`), '');
- try {
- await solver.resolve(candidate);
- return solver;
- } catch (error) {
- // Window resolution has failed so the URL must change. Return a promise
- // that never resolves to prevent the application from loading plugins
- // that rely on `IWindowResolver`.
- return new Promise<IWindowResolver>(() => {
- const { base, workspaces } = paths.urls;
- const pool =
- 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
- const random = pool[Math.floor(Math.random() * pool.length)];
- const path = URLExt.join(base, workspaces, `auto-${random}`, rest);
- // Clone the originally requested workspace after redirecting.
- query['clone'] = workspace;
- const url = path + URLExt.objectToQueryString(query) + (hash || '');
- router.navigate(url, { hard: true });
- });
- }
- }
- };
- /**
- * The default splash screen provider.
- */
- const splash: JupyterFrontEndPlugin<ISplashScreen> = {
- id: '@jupyterlab/apputils-extension:splash',
- autoStart: true,
- provides: ISplashScreen,
- activate: app => {
- const { commands, restored } = app;
- // Create splash element and populate it.
- const splash = document.createElement('div');
- const galaxy = document.createElement('div');
- const logo = document.createElement('div');
- splash.id = 'jupyterlab-splash';
- galaxy.id = 'galaxy';
- logo.id = 'main-logo';
- galaxy.appendChild(logo);
- ['1', '2', '3'].forEach(id => {
- const moon = document.createElement('div');
- const planet = document.createElement('div');
- moon.id = `moon${id}`;
- moon.className = 'moon orbit';
- planet.id = `planet${id}`;
- planet.className = 'planet';
- moon.appendChild(planet);
- galaxy.appendChild(moon);
- });
- splash.appendChild(galaxy);
- // Create debounced recovery dialog function.
- let dialog: Dialog<any>;
- const recovery: IRateLimiter = new Throttler(async () => {
- if (dialog) {
- return;
- }
- dialog = new Dialog({
- title: 'Loading...',
- body: `The loading screen is taking a long time.
- Would you like to clear the workspace or keep waiting?`,
- buttons: [
- Dialog.cancelButton({ label: 'Keep Waiting' }),
- Dialog.warnButton({ label: 'Clear Workspace' })
- ]
- });
- try {
- const result = await dialog.launch();
- dialog.dispose();
- dialog = null;
- if (result.button.accept && commands.hasCommand(CommandIDs.reset)) {
- return commands.execute(CommandIDs.reset);
- }
- // Re-invoke the recovery timer in the next frame.
- requestAnimationFrame(() => {
- // Because recovery can be stopped, handle invocation rejection.
- void recovery.invoke().catch(_ => undefined);
- });
- } catch (error) {
- /* no-op */
- }
- }, SPLASH_RECOVER_TIMEOUT);
- // Return ISplashScreen.
- let splashCount = 0;
- return {
- show: (light = true) => {
- splash.classList.remove('splash-fade');
- splash.classList.toggle('light', light);
- splash.classList.toggle('dark', !light);
- splashCount++;
- document.body.appendChild(splash);
- // Because recovery can be stopped, handle invocation rejection.
- void recovery.invoke().catch(_ => undefined);
- return new DisposableDelegate(async () => {
- await restored;
- if (--splashCount === 0) {
- void recovery.stop();
- if (dialog) {
- dialog.dispose();
- dialog = null;
- }
- splash.classList.add('splash-fade');
- window.setTimeout(() => {
- document.body.removeChild(splash);
- }, 200);
- }
- });
- }
- };
- }
- };
- const print: JupyterFrontEndPlugin<void> = {
- id: '@jupyterlab/apputils-extension:print',
- autoStart: true,
- activate: (app: JupyterFrontEnd) => {
- app.commands.addCommand(CommandIDs.print, {
- label: 'Print...',
- isEnabled: () => {
- const widget = app.shell.currentWidget;
- return Printing.getPrintFunction(widget) !== null;
- },
- execute: async () => {
- const widget = app.shell.currentWidget;
- const printFunction = Printing.getPrintFunction(widget);
- if (printFunction) {
- await printFunction();
- }
- }
- });
- }
- };
- /**
- * The default state database for storing application state.
- *
- * #### Notes
- * If this extension is loaded with a window resolver, it will automatically add
- * state management commands, URL support for `clone` and `reset`, and workspace
- * auto-saving. Otherwise, it will return a simple in-memory state database.
- */
- const state: JupyterFrontEndPlugin<IStateDB> = {
- id: '@jupyterlab/apputils-extension:state',
- autoStart: true,
- provides: IStateDB,
- requires: [JupyterFrontEnd.IPaths, IRouter],
- optional: [ISplashScreen, IWindowResolver],
- activate: (
- app: JupyterFrontEnd,
- paths: JupyterFrontEnd.IPaths,
- router: IRouter,
- splash: ISplashScreen | null,
- resolver: IWindowResolver | null
- ) => {
- if (resolver === null) {
- return new StateDB();
- }
- let resolved = false;
- const { commands, serviceManager } = app;
- const { workspaces } = serviceManager;
- const workspace = resolver.name;
- const transform = new PromiseDelegate<StateDB.DataTransform>();
- const db = new StateDB({ transform: transform.promise });
- const save = new Debouncer(async () => {
- const id = workspace;
- const metadata = { id };
- const data = await db.toJSON();
- await workspaces.save(id, { data, metadata });
- });
- commands.addCommand(CommandIDs.loadState, {
- execute: async (args: IRouter.ILocation) => {
- // Since the command can be executed an arbitrary number of times, make
- // sure it is safe to call multiple times.
- if (resolved) {
- return;
- }
- const { hash, path, search } = args;
- const { urls } = paths;
- const query = URLExt.queryStringToObject(search || '');
- const clone =
- typeof query['clone'] === 'string'
- ? query['clone'] === ''
- ? URLExt.join(urls.base, urls.app)
- : URLExt.join(urls.base, urls.workspaces, query['clone'])
- : null;
- const source = clone || workspace || null;
- if (source === null) {
- console.error(`${CommandIDs.loadState} cannot load null workspace.`);
- return;
- }
- // Any time the local state database changes, save the workspace.
- db.changed.connect(() => void save.invoke(), db);
- try {
- const saved = await workspaces.fetch(source);
- // If this command is called after a reset, the state database
- // will already be resolved.
- if (!resolved) {
- resolved = true;
- transform.resolve({ type: 'overwrite', contents: saved.data });
- }
- } catch ({ message }) {
- console.log(`Fetching workspace "${workspace}" failed.`, message);
- // If the workspace does not exist, cancel the data transformation
- // and save a workspace with the current user state data.
- if (!resolved) {
- resolved = true;
- transform.resolve({ type: 'cancel', contents: null });
- }
- }
- if (source === clone) {
- // Maintain the query string parameters but remove `clone`.
- delete query['clone'];
- const url = path + URLExt.objectToQueryString(query) + hash;
- const cloned = save.invoke().then(() => router.stop);
- // After the state has been cloned, navigate to the URL.
- void cloned.then(() => {
- router.navigate(url);
- });
- return cloned;
- }
- // After the state database has finished loading, save it.
- await save.invoke();
- }
- });
- commands.addCommand(CommandIDs.reset, {
- label: 'Reset Application State',
- execute: async () => {
- await db.clear();
- await save.invoke();
- router.reload();
- }
- });
- commands.addCommand(CommandIDs.resetOnLoad, {
- execute: (args: IRouter.ILocation) => {
- const { hash, path, search } = args;
- const query = URLExt.queryStringToObject(search || '');
- const reset = 'reset' in query;
- const clone = 'clone' in query;
- if (!reset) {
- return;
- }
- // If a splash provider exists, launch the splash screen.
- const loading = splash
- ? splash.show()
- : new DisposableDelegate(() => undefined);
- // If the state database has already been resolved, resetting is
- // impossible without reloading.
- if (resolved) {
- return router.reload();
- }
- // Empty the state database.
- resolved = true;
- transform.resolve({ type: 'clear', contents: null });
- // Maintain the query string parameters but remove `reset`.
- delete query['reset'];
- const url = path + URLExt.objectToQueryString(query) + hash;
- const cleared = db.clear().then(() => router.stop);
- // After the state has been reset, navigate to the URL.
- if (clone) {
- void cleared.then(() => {
- router.navigate(url, { hard: true });
- });
- } else {
- void cleared.then(() => {
- router.navigate(url);
- loading.dispose();
- });
- }
- return cleared;
- }
- });
- router.register({
- command: CommandIDs.loadState,
- pattern: /.?/,
- rank: 30 // High priority: 30:100.
- });
- router.register({
- command: CommandIDs.resetOnLoad,
- pattern: /(\?reset|\&reset)($|&)/,
- rank: 20 // High priority: 20:100.
- });
- return db;
- }
- };
- /**
- * Export the plugins as default.
- */
- const plugins: JupyterFrontEndPlugin<any>[] = [
- palette,
- paletteRestorer,
- resolver,
- settings,
- state,
- splash,
- themes,
- themesPaletteMenu,
- print
- ];
- export default plugins;
- /**
- * The namespace for module private data.
- */
- namespace Private {
- /**
- * Generate a workspace name candidate.
- *
- * @param workspace - A potential workspace name parsed from the URL.
- *
- * @returns A workspace name candidate.
- */
- export function candidate(
- { urls }: JupyterFrontEnd.IPaths,
- workspace = ''
- ): string {
- return workspace
- ? URLExt.join(urls.workspaces, workspace)
- : URLExt.join(urls.app);
- }
- }
|