settingeditor.tsx 12 KB

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