123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552 |
- /*-----------------------------------------------------------------------------
- | Copyright (c) Jupyter Development Team.
- | Distributed under the terms of the Modified BSD License.
- |----------------------------------------------------------------------------*/
- import {
- CodeEditor
- } from '@jupyterlab/codeeditor';
- import {
- ISettingRegistry, IStateDB
- } from '@jupyterlab/coreutils';
- import {
- RenderMimeRegistry
- } from '@jupyterlab/rendermime';
- import {
- CommandRegistry
- } from '@phosphor/commands';
- import {
- JSONExt, JSONObject, JSONValue
- } from '@phosphor/coreutils';
- import {
- Message
- } from '@phosphor/messaging';
- import {
- ISignal
- } from '@phosphor/signaling';
- import {
- h, VirtualDOM
- } from '@phosphor/virtualdom';
- import {
- PanelLayout, Widget
- } from '@phosphor/widgets';
- import {
- PluginEditor
- } from './plugineditor';
- import {
- PluginList
- } from './pluginlist';
- import {
- SplitPanel
- } from './splitpanel';
- /**
- * The ratio panes in the setting editor.
- */
- const DEFAULT_LAYOUT: SettingEditor.ILayoutState = {
- sizes: [1, 3],
- container: {
- editor: 'raw',
- plugin: '',
- sizes: [1, 1]
- }
- };
- /**
- * The class name added to all setting editors.
- */
- const SETTING_EDITOR_CLASS = 'jp-SettingEditor';
- /**
- * The class name added to the top level split panel of the setting editor.
- */
- const SETTING_EDITOR_MAIN_PANEL_CLASS = 'jp-SettingEditor-main';
- /**
- * The class name added to the instructions widget.
- */
- const INSTRUCTIONS_CLASS = 'jp-SettingEditorInstructions';
- /**
- * The class name added to the instructions icon.
- */
- const INSTRUCTIONS_ICON_CLASS = 'jp-SettingEditorInstructions-icon';
- /**
- * The class name added to the instructions title.
- */
- const INSTRUCTIONS_TITLE_CLASS = 'jp-SettingEditorInstructions-title';
- /**
- * The class name added to the instructions text.
- */
- const INSTRUCTIONS_TEXT_CLASS = 'jp-SettingEditorInstructions-text';
- /**
- * The title of the instructions pane.
- */
- const INSTRUCTIONS_TITLE = 'Settings';
- /**
- * The instructions for using the setting editor.
- */
- const INSTRUCTIONS_TEXT = `
- Select a plugin from the list to view and edit its preferences.
- `;
- /**
- * An interface for modifying and saving application settings.
- */
- export
- class SettingEditor extends Widget {
- /**
- * Create a new setting editor.
- */
- constructor(options: SettingEditor.IOptions) {
- super();
- this.addClass(SETTING_EDITOR_CLASS);
- this.key = options.key;
- this.state = options.state;
- const { commands, editorFactory, rendermime } = options;
- const layout = this.layout = new PanelLayout();
- const registry = this.registry = options.registry;
- const panel = this._panel = new SplitPanel({
- orientation: 'horizontal',
- renderer: SplitPanel.defaultRenderer,
- spacing: 1
- });
- const instructions = this._instructions = new Widget({
- node: Private.createInstructionsNode()
- });
- const editor = this._editor = new PluginEditor({
- commands, editorFactory, registry, rendermime
- });
- const confirm = () => editor.confirm();
- const list = this._list = new PluginList({ confirm, registry });
- const when = options.when;
- if (when) {
- this._when = Array.isArray(when) ? Promise.all(when) : when;
- }
- panel.addClass(SETTING_EDITOR_MAIN_PANEL_CLASS);
- layout.addWidget(panel);
- panel.addWidget(list);
- panel.addWidget(instructions);
- editor.stateChanged.connect(this._onStateChanged, this);
- list.changed.connect(this._onStateChanged, this);
- panel.handleMoved.connect(this._onStateChanged, this);
- }
- /**
- * The state database key for the editor's state management.
- */
- readonly key: string;
- /**
- * The setting registry used by the editor.
- */
- readonly registry: ISettingRegistry;
- /**
- * The state database used to store layout.
- */
- readonly state: IStateDB;
- /**
- * Whether the raw editor revert functionality is enabled.
- */
- get canRevertRaw(): boolean {
- return this._editor.raw.canRevert;
- }
- /**
- * Whether the raw editor save functionality is enabled.
- */
- get canSaveRaw(): boolean {
- return this._editor.raw.canSave;
- }
- /**
- * Emits when the commands passed in at instantiation change.
- */
- get commandsChanged(): ISignal<any, string[]> {
- return this._editor.raw.commandsChanged;
- }
- /**
- * Whether the debug panel is visible.
- */
- get isDebugVisible(): boolean {
- return this._editor.raw.isDebugVisible;
- }
- /**
- * The currently loaded settings.
- */
- get settings(): ISettingRegistry.ISettings {
- return this._editor.settings;
- }
- /**
- * The inspectable raw user editor source for the currently loaded settings.
- */
- get source(): CodeEditor.IEditor {
- return this._editor.raw.source;
- }
- /**
- * Dispose of the resources held by the setting editor.
- */
- dispose(): void {
- if (this.isDisposed) {
- return;
- }
- super.dispose();
- this._editor.dispose();
- this._instructions.dispose();
- this._list.dispose();
- this._panel.dispose();
- }
- /**
- * Revert raw editor back to original settings.
- */
- revert(): void {
- this._editor.raw.revert();
- }
- /**
- * Save the contents of the raw editor.
- */
- save(): Promise<void> {
- return this._editor.raw.save();
- }
- /**
- * Toggle the debug functionality.
- */
- toggleDebug(): void {
- this._editor.raw.toggleDebug();
- }
- /**
- * Handle `'after-attach'` messages.
- */
- protected onAfterAttach(msg: Message): void {
- super.onAfterAttach(msg);
- this._panel.hide();
- this._fetchState().then(() => {
- this._panel.show();
- this._setState();
- }).catch(reason => {
- console.error('Fetching setting editor state failed', reason);
- this._panel.show();
- this._setState();
- });
- }
- /**
- * Handle `'close-request'` messages.
- */
- protected onCloseRequest(msg: Message): void {
- this._editor.confirm().then(() => {
- super.onCloseRequest(msg);
- this.dispose();
- }).catch(() => { /* no op */ });
- }
- /**
- * Get the state of the panel.
- */
- private _fetchState(): Promise<void> {
- if (this._fetching) {
- return this._fetching;
- }
- const { key, state } = this;
- const promises = [state.fetch(key), this._when];
- return this._fetching = Promise.all(promises).then(([saved]) => {
- this._fetching = null;
- if (this._saving) {
- return;
- }
- this._state = Private.normalizeState(saved, this._state);
- });
- }
- /**
- * Handle root level layout state changes.
- */
- private _onStateChanged(): void {
- this._state.sizes = this._panel.relativeSizes();
- this._state.container = this._editor.state;
- this._state.container.editor = this._list.editor;
- this._state.container.plugin = this._list.selection;
- this._saveState()
- .then(() => { this._setState(); })
- .catch(reason => {
- console.error('Saving setting editor state failed', reason);
- this._setState();
- });
- }
- /**
- * Set the state of the setting editor.
- */
- private _saveState(): Promise<void> {
- const { key, state } = this;
- const value = this._state;
- this._saving = true;
- return state.save(key, value)
- .then(() => { this._saving = false; })
- .catch((reason: any) => {
- this._saving = false;
- throw reason;
- });
- }
- /**
- * Set the layout sizes.
- */
- private _setLayout(): void {
- const editor = this._editor;
- const panel = this._panel;
- const state = this._state;
- editor.state = state.container;
- // Allow the message queue (which includes fit requests that might disrupt
- // setting relative sizes) to clear before setting sizes.
- requestAnimationFrame(() => { panel.setRelativeSizes(state.sizes); });
- }
- /**
- * Set the presets of the setting editor.
- */
- private _setState(): void {
- const editor = this._editor;
- const list = this._list;
- const panel = this._panel;
- const { container } = this._state;
- if (!container.plugin) {
- editor.settings = null;
- list.selection = '';
- this._setLayout();
- return;
- }
- if (editor.settings && editor.settings.plugin === container.plugin) {
- this._setLayout();
- return;
- }
- const instructions = this._instructions;
- this.registry.load(container.plugin).then(settings => {
- if (instructions.isAttached) {
- instructions.parent = null;
- }
- if (!editor.isAttached) {
- panel.addWidget(editor);
- }
- editor.settings = settings;
- list.editor = container.editor;
- list.selection = container.plugin;
- this._setLayout();
- }).catch((reason: Error) => {
- console.error(`Loading settings failed: ${reason.message}`);
- list.selection = this._state.container.plugin = '';
- editor.settings = null;
- this._setLayout();
- });
- }
- private _editor: PluginEditor;
- private _fetching: Promise<void> | null = null;
- private _instructions: Widget;
- private _list: PluginList;
- private _panel: SplitPanel;
- private _saving = false;
- private _state: SettingEditor.ILayoutState = JSONExt.deepCopy(DEFAULT_LAYOUT);
- private _when: Promise<any>;
- }
- /**
- * A namespace for `SettingEditor` statics.
- */
- export
- namespace SettingEditor {
- /**
- * The instantiation options for a setting editor.
- */
- export
- interface IOptions {
- /**
- * The toolbar commands and registry for the setting editor toolbar.
- */
- commands: {
- /**
- * The command registry.
- */
- registry: CommandRegistry;
- /**
- * The debug command ID.
- */
- debug: string;
- /**
- * The revert command ID.
- */
- revert: string;
- /**
- * The save command ID.
- */
- save: string;
- };
- /**
- * The editor factory used by the setting editor.
- */
- editorFactory: CodeEditor.Factory;
- /**
- * The state database key for the editor's state management.
- */
- key: string;
- /**
- * The setting registry the editor modifies.
- */
- registry: ISettingRegistry;
- /**
- * The optional MIME renderer to use for rendering debug messages.
- */
- rendermime?: RenderMimeRegistry;
- /**
- * The state database used to store layout.
- */
- state: IStateDB;
- /**
- * The point after which the editor should restore its state.
- */
- when?: Promise<any> | Array<Promise<any>>;
- }
- /**
- * The layout state for the setting editor.
- */
- export
- interface ILayoutState extends JSONObject {
- /**
- * The layout state for a plugin editor container.
- */
- container: IPluginLayout;
- /**
- * The relative sizes of the plugin list and plugin editor.
- */
- sizes: number[];
- }
- /**
- * The layout information that is stored and restored from the state database.
- */
- export
- interface IPluginLayout extends JSONObject {
- /**
- * The current plugin being displayed.
- */
- plugin: string;
- editor: 'raw' | 'table';
- sizes: number[];
- }
- }
- /**
- * A namespace for private module data.
- */
- namespace Private {
- /**
- * Create the instructions text node.
- */
- export
- function createInstructionsNode(): HTMLElement {
- return VirtualDOM.realize(h.div({ className: INSTRUCTIONS_CLASS },
- h.h2(
- h.span({ className: `${INSTRUCTIONS_ICON_CLASS} jp-JupyterIcon` }),
- h.span({ className: INSTRUCTIONS_TITLE_CLASS }, INSTRUCTIONS_TITLE)),
- h.span({ className: INSTRUCTIONS_TEXT_CLASS }, INSTRUCTIONS_TEXT)));
- }
- /**
- * Return a normalized restored layout state that defaults to the presets.
- */
- export
- function normalizeState(saved: JSONObject | null, current: SettingEditor.ILayoutState): SettingEditor.ILayoutState {
- if (!saved) {
- return JSONExt.deepCopy(DEFAULT_LAYOUT);
- }
- if (!('sizes' in saved) || !numberArray(saved.sizes)) {
- saved.sizes = JSONExt.deepCopy(DEFAULT_LAYOUT.sizes);
- }
- if (!('container' in saved)) {
- saved.container = JSONExt.deepCopy(DEFAULT_LAYOUT.container);
- return saved as SettingEditor.ILayoutState;
- }
- const container = ('container' in saved) &&
- saved.container &&
- typeof saved.container === 'object' ? saved.container as JSONObject
- : { };
- saved.container = {
- editor: container.editor === 'raw' || container.editor === 'table' ?
- container.editor : DEFAULT_LAYOUT.container.editor,
- plugin: typeof container.plugin === 'string' ? container.plugin
- : DEFAULT_LAYOUT.container.plugin,
- sizes: numberArray(container.sizes) ? container.sizes
- : JSONExt.deepCopy(DEFAULT_LAYOUT.container.sizes)
- };
- return saved as SettingEditor.ILayoutState;
- }
- /**
- * Tests whether an array consists exclusively of numbers.
- */
- function numberArray(value: JSONValue): boolean {
- return Array.isArray(value) && value.every(x => typeof x === 'number');
- }
- }
|