Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork3.8k
Improve context handling#4453
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes from1 commit
7a5bd0078df852db400671e298d72e93f839c7c4a892d58d038167666ede2da0ff19f64aa4de658a7a4b3fc6bde1107f6912dfc8eaf12ced55f8b32631d0e18b3b7e1db7fc2dc90b59bc3496d1f7b03da78e85c3b60aaf51a6b58d11ffaafcbb1b6a6554c24b0a32345d9d5db7ecb3c8447ab04ebac5f0ae8b29b087d1d90468ec2ae892b4ba2802f672e84357b5c0799c6c455a21d5a7b618baec7d693d72fd54ce1abda5da476064212a65027e9ef12be487bb239a56e5766427a67e4efcad221040a0bf94b1ed031011dba0b63414fd43413f40163c41c13ab63726749792693810cac907afFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
…ic into the factory.This uses basic parts in a composable way, but puts a lot of burden on the factory and externalizes a lot of the logic that maybe should be inside the document widget.
- Loading branch information
Uh oh!
There was an error while loading.Please reload this page.
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -30,6 +30,20 @@ import { | ||
| * This widget is `closable` by default. | ||
| * This widget is automatically disposed when closed. | ||
| * This widget ensures its own focus when activated. | ||
| * | ||
| * You can pass in a ready promise - when the ready promise resolves, the content widget will be shown. In the case of a document widget, the ready promise passed in should probably be combined with the context ready promise? Or perhaps it's the responsibility of the one creating the document widget to hook the context up? | ||
| * | ||
| * I want to support a class that lets you just pass in a content widget and a ready promise, and I also want to support the use case of a subclass that creates the content widget. | ||
| * | ||
| * The case of creating the content widget probably doesn't need some of the things here, such as the automatic migration of the title attribute. So perhaps that *is* a separate case? When I create my own widget. Same thing about the toolbar - some widgets will want to create their own toolbar instead of pass in one. | ||
| * | ||
| * The other way to deal with this is to have the factory create the content widget and toolbar, and pass those into a mainareawidget. The document widget merely has the toolbar, content, and context accessors, and a ready promise/isReady function. The IDocumentWidget interface makes it so that we don't actually expose the constructor. So we actually *do* create the content widget and toolbar in the factory function. | ||
| * | ||
| * For the notebook factory function, MainAreaWidget takes the place of the notebook panel | ||
| * | ||
| * So what about the content widget that needs access to the context? Does the IDocumentWidget have a *content* that has a context attribute? That may make more sense than having the context on the overall panel? | ||
| * | ||
| * So how about this: Let's explore having the main widget like it is, and the factory providing a content that is context-aware | ||
| */ | ||
| export | ||
| class MainAreaWidget<T extends Widget = Widget> extends Widget { | ||
| @@ -67,11 +81,11 @@ class MainAreaWidget<T extends Widget = Widget> extends Widget { | ||
| this.title.changed.connect(this._updateContentTitle, this); | ||
| content.disposed.connect(() => this.dispose()); | ||
| if (options.ready) { | ||
| layout.addWidget(spinner); | ||
| this._ready = options.ready; | ||
| this.ready.then(() => { | ||
| this._isReady = true; | ||
| const active = document.activeElement === spinner.node; | ||
| spinner.dispose(); | ||
| layout.addWidget(content); | ||
| @@ -93,8 +107,8 @@ class MainAreaWidget<T extends Widget = Widget> extends Widget { | ||
| } else { | ||
| spinner.dispose(); | ||
| layout.addWidget(content); | ||
| this._isReady = true; | ||
| this._ready = Promise.resolve(void 0); | ||
| } | ||
| } | ||
| @@ -111,14 +125,16 @@ class MainAreaWidget<T extends Widget = Widget> extends Widget { | ||
| /** | ||
| * Whether the widget is fully populated. | ||
| */ | ||
| getisReady(): boolean { | ||
| return this._isReady; | ||
| } | ||
| /** | ||
| * A promise that resolves when the widget is fully populated. | ||
| */ | ||
| get ready(): Promise<void> { | ||
| return this._ready; | ||
| } | ||
| /** | ||
| * Handle the DOM events for the widget. | ||
| @@ -135,7 +151,7 @@ class MainAreaWidget<T extends Widget = Widget> extends Widget { | ||
| case 'mouseup': | ||
| case 'mouseout': | ||
| let target = event.target as HTMLElement; | ||
| if (this._isReady && | ||
| this.toolbar.node.contains(document.activeElement) && | ||
| target.tagName !== 'SELECT') { | ||
| this._focusContent(); | ||
| @@ -166,7 +182,7 @@ class MainAreaWidget<T extends Widget = Widget> extends Widget { | ||
| * Handle `'activate-request'` messages. | ||
| */ | ||
| protected onActivateRequest(msg: Message): void { | ||
| if (this._isReady) { | ||
| this._focusContent(); | ||
| } else { | ||
| this._spinner.node.focus(); | ||
| @@ -222,6 +238,7 @@ class MainAreaWidget<T extends Widget = Widget> extends Widget { | ||
| * Give focus to the content. | ||
| */ | ||
| private _focusContent(): void { | ||
| // TODO: perhaps we give the widget a chance to activate, and if that doesn't change the focus, *then* we focus the content node. | ||
| ||
| if (!this.content.node.contains(document.activeElement)) { | ||
| this.content.node.focus(); | ||
| } | ||
| @@ -230,8 +247,10 @@ class MainAreaWidget<T extends Widget = Widget> extends Widget { | ||
| } | ||
| private _changeGuard = false; | ||
| private _spinner = new Spinner(); | ||
| private _isReady = false; | ||
| private _ready: Promise<void>; | ||
| } | ||
| @@ -258,6 +277,6 @@ namespace MainAreaWidget { | ||
| /** | ||
| * An optional promise for when the content is ready to be shown. | ||
| */ | ||
| ready?: Promise<void>; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -2,7 +2,7 @@ | ||
| // Distributed under the terms of the Modified BSD License. | ||
| import { | ||
| showErrorMessage, MainAreaWidget | ||
| } from '@jupyterlab/apputils'; | ||
| import { | ||
| @@ -22,7 +22,7 @@ import { | ||
| } from '@phosphor/messaging'; | ||
| import { | ||
| SingletonLayout, Widget | ||
| } from '@phosphor/widgets'; | ||
| import { | ||
| @@ -35,29 +35,40 @@ import { | ||
| /** | ||
| * Acontentwidget for a rendered mimetype document. | ||
| */ | ||
| export | ||
| classMimeContent extendsWidget { | ||
| /** | ||
| * Construct a new widget. | ||
| */ | ||
| constructor(options:MimeContent.IOptions) { | ||
| super(); | ||
| this.addClass('jp-MimeDocument'); // maybe move to the MainAreaWidget in the factory? | ||
| ||
| this._mimeType = options.mimeType; | ||
| this._dataType = options.dataType || 'string'; | ||
| this._context = options.context; | ||
| this._renderer = options.renderer; | ||
| const layout = new SingletonLayout(); | ||
| layout.widget = this._renderer; | ||
| this.layout = layout; | ||
| this._context.ready.then(() => { | ||
| return this._render(); | ||
| }).then(() => { | ||
| // After rendering for the first time, send an activation request if we | ||
| // are currently focused. | ||
| if (this.node === document.activeElement) { | ||
| // We want to synchronously send (not post) the activate message, while | ||
| // we know this node still has focus. | ||
| MessageLoop.sendMessage(this._renderer, Widget.Msg.ActivateRequest); | ||
| } | ||
| // Throttle the rendering rate of the widget. | ||
| this._monitor = new ActivityMonitor({ | ||
| signal: this._context.model.contentChanged, | ||
| timeout: options.renderTimeout | ||
| }); | ||
| this._monitor.activityStopped.connect(this.update, this); | ||
| @@ -87,39 +98,33 @@ class MimeDocument extends DocumentWidget<IRenderMime.IRenderer> { | ||
| super.dispose(); | ||
| } | ||
| /** | ||
| * Handle an `update-request` message to the widget. | ||
| */ | ||
| protected onUpdateRequest(msg: Message): void { | ||
| if (this._context.isReady) { | ||
| this._render(); | ||
| } | ||
| } | ||
| /** | ||
| * Render the mime content. | ||
| */ | ||
| private async _render(): Promise<void> { | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. An aside: is TS capable of emitting the ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. I'm not sure - TSnever emits the async keyword for us since we are compiling for es2015, and async/await is es2016. But I doubt it will automatically emit an async keyword if one is not there in the source. | ||
| if (this.isDisposed) { | ||
| return; | ||
| } | ||
| // Since rendering is async, we note render requests that happen while we | ||
| // actually are rendering for a future rendering. | ||
| if (this._isRendering) { | ||
| this._renderRequested = true; | ||
| return; | ||
| } | ||
| // Set up for this rendering pass. | ||
| this._renderRequested = false; | ||
| let context = this._context; | ||
| let model = context.model; | ||
| let data: JSONObject = {}; | ||
| if (this._dataType === 'string') { | ||
| @@ -129,23 +134,21 @@ class MimeDocument extends DocumentWidget<IRenderMime.IRenderer> { | ||
| } | ||
| let mimeModel = new MimeModel({ data, callback: this._changeCallback }); | ||
| try { | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Ooh, async/await error handling! | ||
| // Do the rendering asynchronously. | ||
| this._isRendering = true; | ||
| await this._renderer.renderModel(mimeModel); | ||
| this._isRendering = false; | ||
| // If there is an outstanding request to render, go ahead and render | ||
| if (this._renderRequested) { | ||
| return this._render(); | ||
| } | ||
| }catch (error) { | ||
| // Dispose the document if rendering fails. | ||
| requestAnimationFrame(() => { this._renderer.dispose(); }); | ||
| showErrorMessage(`Renderer Failure: ${context.path}`, error); | ||
| } | ||
| } | ||
| /** | ||
| @@ -157,17 +160,18 @@ class MimeDocument extends DocumentWidget<IRenderMime.IRenderer> { | ||
| } | ||
| let data = options.data[this._mimeType]; | ||
| if (typeof data === 'string') { | ||
| this._context.model.fromString(data); | ||
| } else { | ||
| this._context.model.fromJSON(data); | ||
| } | ||
| } | ||
| private _context: DocumentRegistry.IContext<DocumentRegistry.IModel>; | ||
| private _renderer: IRenderMime.IRenderer; | ||
| private _monitor: ActivityMonitor<any, any> | null; | ||
| private _mimeType: string; | ||
| private _ready = new PromiseDelegate<void>(); | ||
| private _dataType: 'string' | 'json'; | ||
| private _isRendering = false; | ||
| private _renderRequested = false; | ||
| } | ||
| @@ -177,16 +181,21 @@ class MimeDocument extends DocumentWidget<IRenderMime.IRenderer> { | ||
| * The namespace for MimeDocument class statics. | ||
| */ | ||
| export | ||
| namespaceMimeContent { | ||
| /** | ||
| * The options used to initialize a MimeDocument. | ||
| */ | ||
| export | ||
| interface IOptions extends DocumentWidget.IOptions<IRenderMime.IRenderer> { | ||
| /** | ||
| *Context | ||
| */ | ||
| context: DocumentRegistry.IContext<DocumentRegistry.IModel>; | ||
| /** | ||
| * The renderer instance. | ||
| */ | ||
| renderer: IRenderMime.IRenderer; | ||
| /** | ||
| * The mime type. | ||
| @@ -210,7 +219,7 @@ namespace MimeDocument { | ||
| * An implementation of a widget factory for a rendered mimetype document. | ||
| */ | ||
| export | ||
| class MimeDocumentFactory extends ABCWidgetFactory<IDocumentWidget<MimeContent>, DocumentRegistry.IModel> { | ||
| /** | ||
| * Construct a new markdown widget factory. | ||
| */ | ||
| @@ -225,26 +234,28 @@ class MimeDocumentFactory extends ABCWidgetFactory<MimeDocument, DocumentRegistr | ||
| /** | ||
| * Create a new widget given a context. | ||
| */ | ||
| protected createNewWidget(context: DocumentRegistry.Context):MainAreaWidget { | ||
| const ft = this._fileType; | ||
| const mimeType = ft.mimeTypes.length ? ft.mimeTypes[0] : 'text/plain'; | ||
| const rendermime = this._rendermime.clone({ | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Why did you move this out of the constructor for ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Good question. I think it was basically to simplify the mimecontent widget to not need a full rendermime, but just to take a single renderer. I think it's probably fine to put it back in if you think that's better. I suppose it makes it easier for the caller to not have this logic. Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. I think this is fine. | ||
| resolver: context.urlResolver | ||
| }); | ||
| const renderer = rendermime.createRenderer(mimeType); | ||
| const content = newMimeContent({ | ||
| context, | ||
| renderer, | ||
| mimeType, | ||
| renderTimeout: this._renderTimeout, | ||
| dataType: this._dataType, | ||
| }); | ||
| content.title.iconClass = ft.iconClass; | ||
| content.title.iconLabel = ft.iconLabel; | ||
| const widget = new DocumentWidget({content}); | ||
| return widget; | ||
| } | ||
Uh oh!
There was an error while loading.Please reload this page.