Browse Source

Clean up theme manager implementation

Steven Silvester 7 years ago
parent
commit
adb95fc2d6
1 changed files with 79 additions and 30 deletions
  1. 79 30
      packages/apputils/src/thememanager.ts

+ 79 - 30
packages/apputils/src/thememanager.ts

@@ -6,7 +6,7 @@ import {
 } from '@jupyterlab/coreutils';
 
 import {
-  Token
+  PromiseDelegate, Token
 } from '@phosphor/coreutils';
 
 import {
@@ -45,19 +45,26 @@ class ThemeManager {
    */
   constructor(options: ThemeManager.IOptions) {
     this._baseUrl = options.baseUrl;
-    this._registry = options.settingRegistry;
+    let registry = options.settingRegistry;
     this._host = options.host;
-    this._registry.load('jupyter.services.theme-manager').then(settings => {
-      settings.changed.connect(this._onSettingsChanged, this);
-      this.setTheme(settings.composite['theme'] as string);
+    let id = 'jupyter.services.theme-manager';
+    this.ready = registry.load(id).then(settings => {
+      this._settings = settings;
+      this._settings.changed.connect(this._onSettingsChanged, this);
+      this._onSettingsChanged(this._settings);
     });
   }
 
+  /**
+   * A promise that resolves when the theme manager is ready.
+   */
+  readonly ready: Promise<void>;
+
   /**
    * Get the name of the current theme.
    */
-  get theme(): string {
-    return this._theme;
+  get theme(): string | null {
+    return this._loadedTheme;
   }
 
   /**
@@ -71,17 +78,8 @@ class ThemeManager {
    * Set the current theme.
    */
   setTheme(name: string): Promise<void> {
-    if (name === this._theme) {
-      return;
-    }
-    let theme = this._themes[name];
-    if (!theme) {
-      return;
-    }
-    return theme.load().then(paths => {
-      paths.forEach(path => {
-        this._loadFile(path);
-      });
+    return this.ready.then(() => {
+      this._settings.set('theme', name);
     });
   }
 
@@ -93,9 +91,16 @@ class ThemeManager {
    * @returns A disposable that can be used to unregister the theme.
    */
   register(theme: ThemeManager.ITheme): IDisposable {
-    this._themes[theme.name] = theme;
+    let name = theme.name;
+    if (this._themes[name]) {
+      throw new Error(`Theme already registered for ${name}`);
+    }
+    this._themes[name] = theme;
+    if (theme.name === this._pendingTheme) {
+      this._loadTheme();
+    }
     return new DisposableDelegate(() => {
-      delete this._themes[theme.name];
+      delete this._themes[name];
     });
   }
 
@@ -104,29 +109,74 @@ class ThemeManager {
    *
    * @param path - The path of the file to load.
    */
-  private _loadFile(path: string): Promise<void> {
+  loadCSS(path: string): Promise<void> {
     let link = document.createElement('link');
     link.rel = 'stylesheet';
     link.type = 'text/css';
     link.href = path;
+    let promise = new PromiseDelegate<void>();
     link.onload = () => {
-        this._host.fit();
+      promise.resolve(void 0);
     };
     document.body.appendChild(link);
     this._links.push(link);
-    return Promise.resolve(void 0);
+    return promise.promise;
   }
 
+  /**
+   * Handle a change to the settings.
+   */
   private _onSettingsChanged(sender: ISettingRegistry.ISettings): void {
-    this.setTheme(sender.composite['theme'] as string);
+    this._pendingTheme = sender.composite['theme'] as string;
+    if (this._loadPromise) {
+      return;
+    }
+    this._loadTheme();
+  }
+
+  /**
+   * Load the theme.
+   */
+  private _loadTheme(): void {
+    let newTheme = this._themes[this._pendingTheme];
+    if (!newTheme) {
+      return;
+    }
+    if (this._pendingTheme === this._loadedTheme) {
+      return;
+    }
+    let oldPromise = Promise.resolve(void 0);
+    let oldTheme = this._themes[this._loadedTheme];
+    if (oldTheme) {
+      oldPromise = oldTheme.unload();
+    }
+    this._pendingTheme = '';
+    this._loadPromise = oldPromise.then(() => {
+      this._links.forEach(link => {
+        if (link.parentElement) {
+          link.parentElement.removeChild(link);
+        }
+      });
+      this._links.length = 0;
+      return newTheme.load();
+    }).then(() => {
+      this._host.fit();
+      this._loadPromise = null;
+      this._loadedTheme = newTheme.name;
+      if (this._pendingTheme) {
+        this._loadTheme();
+      }
+    });
   }
 
   private _baseUrl: string;
-  private _registry: ISettingRegistry;
-  private _theme: string;
   private _themes: { [key: string]: ThemeManager.ITheme } = {};
   private _links: HTMLLinkElement[] = [];
   private _host: Widget;
+  private _settings: ISettingRegistry.ISettings;
+  private _pendingTheme = '';
+  private _loadedTheme: string | null = null;
+  private _loadPromise: Promise<void> | null = null;
 }
 
 
@@ -169,15 +219,14 @@ namespace ThemeManager {
     /**
      * Load the theme.
      *
-     * @returns A promise that resolves with the paths of the CSS
-     * files to load.
+     * @returns A promise that resolves when the theme has loaded.
      */
-    load(): Promise<ReadonlyArray<string>>;
+    load(): Promise<void>;
 
     /**
      * Unload the theme.
      *
-     * @returns A promise that resolves when the theme is unloaded.
+     * @returns A promise that resolves when the theme has unloaded.
      */
     unload(): Promise<void>;
   }