widgettracker.ts 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { IRestorable, RestorablePool } from '@jupyterlab/statedb';
  4. import { IDisposable } from '@lumino/disposable';
  5. import { ISignal, Signal } from '@lumino/signaling';
  6. import { FocusTracker, Widget } from '@lumino/widgets';
  7. /**
  8. * A tracker that tracks widgets.
  9. *
  10. * @typeparam T - The type of widget being tracked. Defaults to `Widget`.
  11. */
  12. export interface IWidgetTracker<T extends Widget = Widget> extends IDisposable {
  13. /**
  14. * A signal emitted when a widget is added.
  15. */
  16. readonly widgetAdded: ISignal<this, T>;
  17. /**
  18. * The current widget is the most recently focused or added widget.
  19. *
  20. * #### Notes
  21. * It is the most recently focused widget, or the most recently added
  22. * widget if no widget has taken focus.
  23. */
  24. readonly currentWidget: T | null;
  25. /**
  26. * A signal emitted when the current instance changes.
  27. *
  28. * #### Notes
  29. * If the last instance being tracked is disposed, `null` will be emitted.
  30. */
  31. readonly currentChanged: ISignal<this, T | null>;
  32. /**
  33. * The number of instances held by the tracker.
  34. */
  35. readonly size: number;
  36. /**
  37. * A promise that is resolved when the widget tracker has been
  38. * restored from a serialized state.
  39. *
  40. * #### Notes
  41. * Most client code will not need to use this, since they can wait
  42. * for the whole application to restore. However, if an extension
  43. * wants to perform actions during the application restoration, but
  44. * after the restoration of another widget tracker, they can use
  45. * this promise.
  46. */
  47. readonly restored: Promise<void>;
  48. /**
  49. * A signal emitted when a widget is updated.
  50. */
  51. readonly widgetUpdated: ISignal<this, T>;
  52. /**
  53. * Find the first instance in the tracker that satisfies a filter function.
  54. *
  55. * @param - fn The filter function to call on each instance.
  56. *
  57. * #### Notes
  58. * If nothing is found, the value returned is `undefined`.
  59. */
  60. find(fn: (obj: T) => boolean): T | undefined;
  61. /**
  62. * Iterate through each instance in the tracker.
  63. *
  64. * @param fn - The function to call on each instance.
  65. */
  66. forEach(fn: (obj: T) => void): void;
  67. /**
  68. * Filter the instances in the tracker based on a predicate.
  69. *
  70. * @param fn - The function by which to filter.
  71. */
  72. filter(fn: (obj: T) => boolean): T[];
  73. /**
  74. * Check if this tracker has the specified instance.
  75. *
  76. * @param obj - The object whose existence is being checked.
  77. */
  78. has(obj: Widget): boolean;
  79. /**
  80. * Inject an instance into the widget tracker without the tracker handling
  81. * its restoration lifecycle.
  82. *
  83. * @param obj - The instance to inject into the tracker.
  84. */
  85. inject(obj: T): void;
  86. }
  87. /**
  88. * A class that keeps track of widget instances on an Application shell.
  89. *
  90. * @typeparam T - The type of widget being tracked. Defaults to `Widget`.
  91. *
  92. * #### Notes
  93. * The API surface area of this concrete implementation is substantially larger
  94. * than the widget tracker interface it implements. The interface is intended
  95. * for export by JupyterLab plugins that create widgets and have clients who may
  96. * wish to keep track of newly created widgets. This class, however, can be used
  97. * internally by plugins to restore state as well.
  98. */
  99. export class WidgetTracker<T extends Widget = Widget>
  100. implements IWidgetTracker<T>, IRestorable<T> {
  101. /**
  102. * Create a new widget tracker.
  103. *
  104. * @param options - The instantiation options for a widget tracker.
  105. */
  106. constructor(options: WidgetTracker.IOptions) {
  107. const focus = (this._focusTracker = new FocusTracker());
  108. const pool = (this._pool = new RestorablePool(options));
  109. this.namespace = options.namespace;
  110. focus.currentChanged.connect((_, current) => {
  111. if (current.newValue !== this.currentWidget) {
  112. pool.current = current.newValue;
  113. }
  114. }, this);
  115. pool.added.connect((_, widget) => {
  116. this._widgetAdded.emit(widget);
  117. }, this);
  118. pool.currentChanged.connect((_, widget) => {
  119. // If the pool's current reference is `null` but the focus tracker has a
  120. // current widget, update the pool to match the focus tracker.
  121. if (widget === null && focus.currentWidget) {
  122. pool.current = focus.currentWidget;
  123. return;
  124. }
  125. this.onCurrentChanged(widget);
  126. this._currentChanged.emit(widget);
  127. }, this);
  128. pool.updated.connect((_, widget) => {
  129. this._widgetUpdated.emit(widget);
  130. }, this);
  131. }
  132. /**
  133. * A namespace for all tracked widgets, (e.g., `notebook`).
  134. */
  135. readonly namespace: string;
  136. /**
  137. * A signal emitted when the current widget changes.
  138. */
  139. get currentChanged(): ISignal<this, T | null> {
  140. return this._currentChanged;
  141. }
  142. /**
  143. * The current widget is the most recently focused or added widget.
  144. *
  145. * #### Notes
  146. * It is the most recently focused widget, or the most recently added
  147. * widget if no widget has taken focus.
  148. */
  149. get currentWidget(): T | null {
  150. return this._pool.current || null;
  151. }
  152. /**
  153. * A promise resolved when the tracker has been restored.
  154. */
  155. get restored(): Promise<void> {
  156. return this._pool.restored;
  157. }
  158. /**
  159. * The number of widgets held by the tracker.
  160. */
  161. get size(): number {
  162. return this._pool.size;
  163. }
  164. /**
  165. * A signal emitted when a widget is added.
  166. *
  167. * #### Notes
  168. * This signal will only fire when a widget is added to the tracker. It will
  169. * not fire if a widget is injected into the tracker.
  170. */
  171. get widgetAdded(): ISignal<this, T> {
  172. return this._widgetAdded;
  173. }
  174. /**
  175. * A signal emitted when a widget is updated.
  176. */
  177. get widgetUpdated(): ISignal<this, T> {
  178. return this._widgetUpdated;
  179. }
  180. /**
  181. * Add a new widget to the tracker.
  182. *
  183. * @param widget - The widget being added.
  184. *
  185. * #### Notes
  186. * The widget passed into the tracker is added synchronously; its existence in
  187. * the tracker can be checked with the `has()` method. The promise this method
  188. * returns resolves after the widget has been added and saved to an underlying
  189. * restoration connector, if one is available.
  190. *
  191. * The newly added widget becomes the current widget unless the focus tracker
  192. * already had a focused widget.
  193. */
  194. async add(widget: T): Promise<void> {
  195. this._focusTracker.add(widget);
  196. await this._pool.add(widget);
  197. if (!this._focusTracker.activeWidget) {
  198. this._pool.current = widget;
  199. }
  200. }
  201. /**
  202. * Test whether the tracker is disposed.
  203. */
  204. get isDisposed(): boolean {
  205. return this._isDisposed;
  206. }
  207. /**
  208. * Dispose of the resources held by the tracker.
  209. */
  210. dispose(): void {
  211. if (this.isDisposed) {
  212. return;
  213. }
  214. this._isDisposed = true;
  215. this._pool.dispose();
  216. this._focusTracker.dispose();
  217. Signal.clearData(this);
  218. }
  219. /**
  220. * Find the first widget in the tracker that satisfies a filter function.
  221. *
  222. * @param - fn The filter function to call on each widget.
  223. *
  224. * #### Notes
  225. * If no widget is found, the value returned is `undefined`.
  226. */
  227. find(fn: (widget: T) => boolean): T | undefined {
  228. return this._pool.find(fn);
  229. }
  230. /**
  231. * Iterate through each widget in the tracker.
  232. *
  233. * @param fn - The function to call on each widget.
  234. */
  235. forEach(fn: (widget: T) => void): void {
  236. return this._pool.forEach(fn);
  237. }
  238. /**
  239. * Filter the widgets in the tracker based on a predicate.
  240. *
  241. * @param fn - The function by which to filter.
  242. */
  243. filter(fn: (widget: T) => boolean): T[] {
  244. return this._pool.filter(fn);
  245. }
  246. /**
  247. * Inject a foreign widget into the widget tracker.
  248. *
  249. * @param widget - The widget to inject into the tracker.
  250. *
  251. * #### Notes
  252. * Injected widgets will not have their state saved by the tracker.
  253. *
  254. * The primary use case for widget injection is for a plugin that offers a
  255. * sub-class of an extant plugin to have its instances share the same commands
  256. * as the parent plugin (since most relevant commands will use the
  257. * `currentWidget` of the parent plugin's widget tracker). In this situation,
  258. * the sub-class plugin may well have its own widget tracker for layout and
  259. * state restoration in addition to injecting its widgets into the parent
  260. * plugin's widget tracker.
  261. */
  262. inject(widget: T): Promise<void> {
  263. return this._pool.inject(widget);
  264. }
  265. /**
  266. * Check if this tracker has the specified widget.
  267. *
  268. * @param widget - The widget whose existence is being checked.
  269. */
  270. has(widget: Widget): boolean {
  271. return this._pool.has(widget as any);
  272. }
  273. /**
  274. * Restore the widgets in this tracker's namespace.
  275. *
  276. * @param options - The configuration options that describe restoration.
  277. *
  278. * @returns A promise that resolves when restoration has completed.
  279. *
  280. * #### Notes
  281. * This function should not typically be invoked by client code.
  282. * Its primary use case is to be invoked by a restorer.
  283. */
  284. async restore(options: IRestorable.IOptions<T>): Promise<any> {
  285. return this._pool.restore(options);
  286. }
  287. /**
  288. * Save the restore data for a given widget.
  289. *
  290. * @param widget - The widget being saved.
  291. */
  292. async save(widget: T): Promise<void> {
  293. return this._pool.save(widget);
  294. }
  295. /**
  296. * Handle the current change event.
  297. *
  298. * #### Notes
  299. * The default implementation is a no-op.
  300. */
  301. protected onCurrentChanged(value: T | null): void {
  302. /* no-op */
  303. }
  304. private _currentChanged = new Signal<this, T | null>(this);
  305. private _focusTracker: FocusTracker<T>;
  306. private _pool: RestorablePool<T>;
  307. private _isDisposed = false;
  308. private _widgetAdded = new Signal<this, T>(this);
  309. private _widgetUpdated = new Signal<this, T>(this);
  310. }
  311. /**
  312. * A namespace for `WidgetTracker` statics.
  313. */
  314. export namespace WidgetTracker {
  315. /**
  316. * The instantiation options for a widget tracker.
  317. */
  318. export interface IOptions {
  319. /**
  320. * A namespace for all tracked widgets, (e.g., `notebook`).
  321. */
  322. namespace: string;
  323. }
  324. }