settingeditor.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. /*-----------------------------------------------------------------------------
  2. | Copyright (c) Jupyter Development Team.
  3. | Distributed under the terms of the Modified BSD License.
  4. |----------------------------------------------------------------------------*/
  5. import {
  6. CodeEditor
  7. } from '@jupyterlab/codeeditor';
  8. import {
  9. ISettingRegistry, IStateDB
  10. } from '@jupyterlab/coreutils';
  11. import {
  12. RenderMimeRegistry
  13. } from '@jupyterlab/rendermime';
  14. import {
  15. CommandRegistry
  16. } from '@phosphor/commands';
  17. import {
  18. JSONExt, JSONObject, JSONValue
  19. } from '@phosphor/coreutils';
  20. import {
  21. Message
  22. } from '@phosphor/messaging';
  23. import {
  24. ISignal
  25. } from '@phosphor/signaling';
  26. import {
  27. h, VirtualDOM
  28. } from '@phosphor/virtualdom';
  29. import {
  30. PanelLayout, Widget
  31. } from '@phosphor/widgets';
  32. import {
  33. PluginEditor
  34. } from './plugineditor';
  35. import {
  36. PluginList
  37. } from './pluginlist';
  38. import {
  39. SplitPanel
  40. } from './splitpanel';
  41. /**
  42. * The ratio panes in the setting editor.
  43. */
  44. const DEFAULT_LAYOUT: SettingEditor.ILayoutState = {
  45. sizes: [1, 3],
  46. container: {
  47. editor: 'raw',
  48. plugin: '',
  49. sizes: [1, 1]
  50. }
  51. };
  52. /**
  53. * The class name added to all setting editors.
  54. */
  55. const SETTING_EDITOR_CLASS = 'jp-SettingEditor';
  56. /**
  57. * The class name added to the top level split panel of the setting editor.
  58. */
  59. const SETTING_EDITOR_MAIN_PANEL_CLASS = 'jp-SettingEditor-main';
  60. /**
  61. * The class name added to the instructions widget.
  62. */
  63. const INSTRUCTIONS_CLASS = 'jp-SettingEditorInstructions';
  64. /**
  65. * The class name added to the instructions icon.
  66. */
  67. const INSTRUCTIONS_ICON_CLASS = 'jp-SettingEditorInstructions-icon';
  68. /**
  69. * The class name added to the instructions title.
  70. */
  71. const INSTRUCTIONS_TITLE_CLASS = 'jp-SettingEditorInstructions-title';
  72. /**
  73. * The class name added to the instructions text.
  74. */
  75. const INSTRUCTIONS_TEXT_CLASS = 'jp-SettingEditorInstructions-text';
  76. /**
  77. * The title of the instructions pane.
  78. */
  79. const INSTRUCTIONS_TITLE = 'Settings';
  80. /**
  81. * The instructions for using the setting editor.
  82. */
  83. const INSTRUCTIONS_TEXT = `
  84. Select a plugin from the list to view and edit its preferences.
  85. `;
  86. /**
  87. * An interface for modifying and saving application settings.
  88. */
  89. export
  90. class SettingEditor extends Widget {
  91. /**
  92. * Create a new setting editor.
  93. */
  94. constructor(options: SettingEditor.IOptions) {
  95. super();
  96. this.addClass(SETTING_EDITOR_CLASS);
  97. this.key = options.key;
  98. this.state = options.state;
  99. const { commands, editorFactory, rendermime } = options;
  100. const layout = this.layout = new PanelLayout();
  101. const registry = this.registry = options.registry;
  102. const panel = this._panel = new SplitPanel({
  103. orientation: 'horizontal',
  104. renderer: SplitPanel.defaultRenderer,
  105. spacing: 1
  106. });
  107. const instructions = this._instructions = new Widget({
  108. node: Private.createInstructionsNode()
  109. });
  110. const editor = this._editor = new PluginEditor({
  111. commands, editorFactory, registry, rendermime
  112. });
  113. const confirm = () => editor.confirm();
  114. const list = this._list = new PluginList({ confirm, registry });
  115. const when = options.when;
  116. if (when) {
  117. this._when = Array.isArray(when) ? Promise.all(when) : when;
  118. }
  119. panel.addClass(SETTING_EDITOR_MAIN_PANEL_CLASS);
  120. layout.addWidget(panel);
  121. panel.addWidget(list);
  122. panel.addWidget(instructions);
  123. editor.stateChanged.connect(this._onStateChanged, this);
  124. list.changed.connect(this._onStateChanged, this);
  125. panel.handleMoved.connect(this._onStateChanged, this);
  126. }
  127. /**
  128. * The state database key for the editor's state management.
  129. */
  130. readonly key: string;
  131. /**
  132. * The setting registry used by the editor.
  133. */
  134. readonly registry: ISettingRegistry;
  135. /**
  136. * The state database used to store layout.
  137. */
  138. readonly state: IStateDB;
  139. /**
  140. * Whether the raw editor revert functionality is enabled.
  141. */
  142. get canRevertRaw(): boolean {
  143. return this._editor.raw.canRevert;
  144. }
  145. /**
  146. * Whether the raw editor save functionality is enabled.
  147. */
  148. get canSaveRaw(): boolean {
  149. return this._editor.raw.canSave;
  150. }
  151. /**
  152. * Emits when the commands passed in at instantiation change.
  153. */
  154. get commandsChanged(): ISignal<any, string[]> {
  155. return this._editor.raw.commandsChanged;
  156. }
  157. /**
  158. * Whether the debug panel is visible.
  159. */
  160. get isDebugVisible(): boolean {
  161. return this._editor.raw.isDebugVisible;
  162. }
  163. /**
  164. * The currently loaded settings.
  165. */
  166. get settings(): ISettingRegistry.ISettings {
  167. return this._editor.settings;
  168. }
  169. /**
  170. * The inspectable raw user editor source for the currently loaded settings.
  171. */
  172. get source(): CodeEditor.IEditor {
  173. return this._editor.raw.source;
  174. }
  175. /**
  176. * Dispose of the resources held by the setting editor.
  177. */
  178. dispose(): void {
  179. if (this.isDisposed) {
  180. return;
  181. }
  182. super.dispose();
  183. this._editor.dispose();
  184. this._instructions.dispose();
  185. this._list.dispose();
  186. this._panel.dispose();
  187. }
  188. /**
  189. * Revert raw editor back to original settings.
  190. */
  191. revert(): void {
  192. this._editor.raw.revert();
  193. }
  194. /**
  195. * Save the contents of the raw editor.
  196. */
  197. save(): Promise<void> {
  198. return this._editor.raw.save();
  199. }
  200. /**
  201. * Toggle the debug functionality.
  202. */
  203. toggleDebug(): void {
  204. this._editor.raw.toggleDebug();
  205. }
  206. /**
  207. * Handle `'after-attach'` messages.
  208. */
  209. protected onAfterAttach(msg: Message): void {
  210. super.onAfterAttach(msg);
  211. this._panel.hide();
  212. this._fetchState().then(() => {
  213. this._panel.show();
  214. this._setState();
  215. }).catch(reason => {
  216. console.error('Fetching setting editor state failed', reason);
  217. this._panel.show();
  218. this._setState();
  219. });
  220. }
  221. /**
  222. * Handle `'close-request'` messages.
  223. */
  224. protected onCloseRequest(msg: Message): void {
  225. this._editor.confirm().then(() => {
  226. super.onCloseRequest(msg);
  227. this.dispose();
  228. }).catch(() => { /* no op */ });
  229. }
  230. /**
  231. * Get the state of the panel.
  232. */
  233. private _fetchState(): Promise<void> {
  234. if (this._fetching) {
  235. return this._fetching;
  236. }
  237. const { key, state } = this;
  238. const promises = [state.fetch(key), this._when];
  239. return this._fetching = Promise.all(promises).then(([saved]) => {
  240. this._fetching = null;
  241. if (this._saving) {
  242. return;
  243. }
  244. this._state = Private.normalizeState(saved, this._state);
  245. });
  246. }
  247. /**
  248. * Handle root level layout state changes.
  249. */
  250. private _onStateChanged(): void {
  251. this._state.sizes = this._panel.relativeSizes();
  252. this._state.container = this._editor.state;
  253. this._state.container.editor = this._list.editor;
  254. this._state.container.plugin = this._list.selection;
  255. this._saveState()
  256. .then(() => { this._setState(); })
  257. .catch(reason => {
  258. console.error('Saving setting editor state failed', reason);
  259. this._setState();
  260. });
  261. }
  262. /**
  263. * Set the state of the setting editor.
  264. */
  265. private _saveState(): Promise<void> {
  266. const { key, state } = this;
  267. const value = this._state;
  268. this._saving = true;
  269. return state.save(key, value)
  270. .then(() => { this._saving = false; })
  271. .catch((reason: any) => {
  272. this._saving = false;
  273. throw reason;
  274. });
  275. }
  276. /**
  277. * Set the layout sizes.
  278. */
  279. private _setLayout(): void {
  280. const editor = this._editor;
  281. const panel = this._panel;
  282. const state = this._state;
  283. editor.state = state.container;
  284. // Allow the message queue (which includes fit requests that might disrupt
  285. // setting relative sizes) to clear before setting sizes.
  286. requestAnimationFrame(() => { panel.setRelativeSizes(state.sizes); });
  287. }
  288. /**
  289. * Set the presets of the setting editor.
  290. */
  291. private _setState(): void {
  292. const editor = this._editor;
  293. const list = this._list;
  294. const panel = this._panel;
  295. const { container } = this._state;
  296. if (!container.plugin) {
  297. editor.settings = null;
  298. list.selection = '';
  299. this._setLayout();
  300. return;
  301. }
  302. if (editor.settings && editor.settings.plugin === container.plugin) {
  303. this._setLayout();
  304. return;
  305. }
  306. const instructions = this._instructions;
  307. this.registry.load(container.plugin).then(settings => {
  308. if (instructions.isAttached) {
  309. instructions.parent = null;
  310. }
  311. if (!editor.isAttached) {
  312. panel.addWidget(editor);
  313. }
  314. editor.settings = settings;
  315. list.editor = container.editor;
  316. list.selection = container.plugin;
  317. this._setLayout();
  318. }).catch((reason: Error) => {
  319. console.error(`Loading settings failed: ${reason.message}`);
  320. list.selection = this._state.container.plugin = '';
  321. editor.settings = null;
  322. this._setLayout();
  323. });
  324. }
  325. private _editor: PluginEditor;
  326. private _fetching: Promise<void> | null = null;
  327. private _instructions: Widget;
  328. private _list: PluginList;
  329. private _panel: SplitPanel;
  330. private _saving = false;
  331. private _state: SettingEditor.ILayoutState = JSONExt.deepCopy(DEFAULT_LAYOUT);
  332. private _when: Promise<any>;
  333. }
  334. /**
  335. * A namespace for `SettingEditor` statics.
  336. */
  337. export
  338. namespace SettingEditor {
  339. /**
  340. * The instantiation options for a setting editor.
  341. */
  342. export
  343. interface IOptions {
  344. /**
  345. * The toolbar commands and registry for the setting editor toolbar.
  346. */
  347. commands: {
  348. /**
  349. * The command registry.
  350. */
  351. registry: CommandRegistry;
  352. /**
  353. * The debug command ID.
  354. */
  355. debug: string;
  356. /**
  357. * The revert command ID.
  358. */
  359. revert: string;
  360. /**
  361. * The save command ID.
  362. */
  363. save: string;
  364. };
  365. /**
  366. * The editor factory used by the setting editor.
  367. */
  368. editorFactory: CodeEditor.Factory;
  369. /**
  370. * The state database key for the editor's state management.
  371. */
  372. key: string;
  373. /**
  374. * The setting registry the editor modifies.
  375. */
  376. registry: ISettingRegistry;
  377. /**
  378. * The optional MIME renderer to use for rendering debug messages.
  379. */
  380. rendermime?: RenderMimeRegistry;
  381. /**
  382. * The state database used to store layout.
  383. */
  384. state: IStateDB;
  385. /**
  386. * The point after which the editor should restore its state.
  387. */
  388. when?: Promise<any> | Array<Promise<any>>;
  389. }
  390. /**
  391. * The layout state for the setting editor.
  392. */
  393. export
  394. interface ILayoutState extends JSONObject {
  395. /**
  396. * The layout state for a plugin editor container.
  397. */
  398. container: IPluginLayout;
  399. /**
  400. * The relative sizes of the plugin list and plugin editor.
  401. */
  402. sizes: number[];
  403. }
  404. /**
  405. * The layout information that is stored and restored from the state database.
  406. */
  407. export
  408. interface IPluginLayout extends JSONObject {
  409. /**
  410. * The current plugin being displayed.
  411. */
  412. plugin: string;
  413. editor: 'raw' | 'table';
  414. sizes: number[];
  415. }
  416. }
  417. /**
  418. * A namespace for private module data.
  419. */
  420. namespace Private {
  421. /**
  422. * Create the instructions text node.
  423. */
  424. export
  425. function createInstructionsNode(): HTMLElement {
  426. return VirtualDOM.realize(h.div({ className: INSTRUCTIONS_CLASS },
  427. h.h2(
  428. h.span({ className: `${INSTRUCTIONS_ICON_CLASS} jp-JupyterIcon` }),
  429. h.span({ className: INSTRUCTIONS_TITLE_CLASS }, INSTRUCTIONS_TITLE)),
  430. h.span({ className: INSTRUCTIONS_TEXT_CLASS }, INSTRUCTIONS_TEXT)));
  431. }
  432. /**
  433. * Return a normalized restored layout state that defaults to the presets.
  434. */
  435. export
  436. function normalizeState(saved: JSONObject | null, current: SettingEditor.ILayoutState): SettingEditor.ILayoutState {
  437. if (!saved) {
  438. return JSONExt.deepCopy(DEFAULT_LAYOUT);
  439. }
  440. if (!('sizes' in saved) || !numberArray(saved.sizes)) {
  441. saved.sizes = JSONExt.deepCopy(DEFAULT_LAYOUT.sizes);
  442. }
  443. if (!('container' in saved)) {
  444. saved.container = JSONExt.deepCopy(DEFAULT_LAYOUT.container);
  445. return saved as SettingEditor.ILayoutState;
  446. }
  447. const container = ('container' in saved) &&
  448. saved.container &&
  449. typeof saved.container === 'object' ? saved.container as JSONObject
  450. : { };
  451. saved.container = {
  452. editor: container.editor === 'raw' || container.editor === 'table' ?
  453. container.editor : DEFAULT_LAYOUT.container.editor,
  454. plugin: typeof container.plugin === 'string' ? container.plugin
  455. : DEFAULT_LAYOUT.container.plugin,
  456. sizes: numberArray(container.sizes) ? container.sizes
  457. : JSONExt.deepCopy(DEFAULT_LAYOUT.container.sizes)
  458. };
  459. return saved as SettingEditor.ILayoutState;
  460. }
  461. /**
  462. * Tests whether an array consists exclusively of numbers.
  463. */
  464. function numberArray(value: JSONValue): boolean {
  465. return Array.isArray(value) && value.every(x => typeof x === 'number');
  466. }
  467. }