123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293 |
- // Copyright (c) Jupyter Development Team.
- // Distributed under the terms of the Modified BSD License.
- import {
- IKernel
- } from 'jupyter-js-services';
- import {
- DisposableSet
- } from 'phosphor/lib/core/disposable';
- import {
- IMessageHandler, Message, installMessageHook
- } from 'phosphor/lib/core/messaging';
- import {
- AttachedProperty
- } from 'phosphor/lib/core/properties';
- import {
- Widget
- } from 'phosphor/lib/ui/widget';
- import {
- showDialog
- } from '../dialog';
- import {
- DocumentRegistry, IDocumentContext, IDocumentModel
- } from '../docregistry';
- import {
- ContextManager
- } from './context';
- /**
- * The class name added to document widgets.
- */
- const DOCUMENT_CLASS = 'jp-Document';
- /**
- * A class that maintains the lifecyle of file-backed widgets.
- */
- export
- class DocumentWidgetManager {
- /**
- * Construct a new document widget manager.
- */
- constructor(options: DocumentWidgetManager.IOptions) {
- this._contextManager = options.contextManager;
- this._registry = options.registry;
- }
- /**
- * Dispose of the resources used by the widget manager.
- */
- dispose(): void {
- this._registry = null;
- this._contextManager = null;
- for (let id in this._widgets) {
- for (let widget of this._widgets[id]) {
- widget.dispose();
- }
- }
- }
- /**
- * Create a widget for a document and handle its lifecycle.
- */
- createWidget(name: string, id: string, kernel?: IKernel.IModel): Widget {
- let factory = this._registry.getWidgetFactory(name);
- let context = this._contextManager.getContext(id);
- let widget = factory.createNew(context, kernel);
- Private.nameProperty.set(widget, name);
- // Handle widget extensions.
- let disposables = new DisposableSet();
- for (let extender of this._registry.getWidgetExtensions(name)) {
- disposables.add(extender.createNew(widget, context));
- }
- widget.disposed.connect(() => {
- disposables.dispose();
- });
- this.adoptWidget(id, widget);
- return widget;
- }
- /**
- * Install the message hook for the widget and add to list
- * of known widgets.
- */
- adoptWidget(id: string, widget: Widget): void {
- if (!(id in this._widgets)) {
- this._widgets[id] = [];
- }
- this._widgets[id].push(widget);
- installMessageHook(widget, (handler: IMessageHandler, msg: Message) => {
- return this.filterMessage(handler, msg);
- });
- widget.addClass(DOCUMENT_CLASS);
- widget.title.closable = true;
- widget.disposed.connect(() => {
- // Remove the widget from the widget registry.
- let index = this._widgets[id].indexOf(widget);
- this._widgets[id].splice(index, 1);
- // Dispose of the context if this is the last widget using it.
- if (!this._widgets[id].length) {
- this._contextManager.removeContext(id);
- }
- });
- Private.idProperty.set(widget, id);
- }
- /**
- * See if a widget already exists for the given path and widget name.
- *
- * #### Notes
- * This can be used to use an existing widget instead of opening
- * a new widget.
- */
- findWidget(path: string, widgetName: string): Widget {
- let ids = this._contextManager.getIdsForPath(path);
- for (let id of ids) {
- for (let widget of this._widgets[id]) {
- let name = Private.nameProperty.get(widget);
- if (name === widgetName) {
- return widget;
- }
- }
- }
- }
- /**
- * Get the document context for a widget.
- */
- contextForWidget(widget: Widget): IDocumentContext<IDocumentModel> {
- let id = Private.idProperty.get(widget);
- return this._contextManager.getContext(id);
- }
- /**
- * Clone a widget.
- *
- * #### Notes
- * This will create a new widget with the same model and context
- * as this widget.
- */
- clone(widget: Widget): Widget {
- let id = Private.idProperty.get(widget);
- let name = Private.nameProperty.get(widget);
- let newWidget = this.createWidget(name, id);
- this.adoptWidget(id, newWidget);
- return widget;
- }
- /**
- * Close the widgets associated with a given path.
- */
- closeFile(path: string): void {
- let ids = this._contextManager.getIdsForPath(path);
- for (let id of ids) {
- let widgets: Widget[] = this._widgets[id] || [];
- for (let w of widgets) {
- w.close();
- }
- }
- }
- /**
- * Close all of the open documents.
- */
- closeAll(): void {
- for (let id in this._widgets) {
- for (let w of this._widgets[id]) {
- w.close();
- }
- }
- }
- /**
- * Filter a message sent to a message handler.
- *
- * @param handler - The target handler of the message.
- *
- * @param msg - The message dispatched to the handler.
- *
- * @returns `true` if the message should be filtered, of `false`
- * if the message should be dispatched to the handler as normal.
- */
- protected filterMessage(handler: IMessageHandler, msg: Message): boolean {
- if (msg.type === 'close-request') {
- if (this._closeGuard) {
- return false;
- }
- this.onClose(handler as Widget);
- return true;
- }
- return false;
- }
- /**
- * Handle `'close-request'` messages.
- */
- protected onClose(widget: Widget): void {
- // Handle dirty state.
- this._maybeClose(widget).then(result => {
- if (result) {
- this._closeGuard = true;
- widget.close();
- this._closeGuard = false;
- // Dispose of document widgets when they are closed.
- widget.dispose();
- }
- }).catch(() => {
- widget.dispose();
- });
- }
- /**
- * Ask the user whether to close an unsaved file.
- */
- private _maybeClose(widget: Widget): Promise<boolean> {
- // Bail if the model is not dirty or other widgets are using the model.
- let id = Private.idProperty.get(widget);
- let widgets = this._widgets[id];
- let model = this._contextManager.getModel(id);
- if (!model.dirty || widgets.length > 1) {
- return Promise.resolve(true);
- }
- let fileName = widget.title.label;
- return showDialog({
- title: 'Close without saving?',
- body: `File "${fileName}" has unsaved changes, close without saving?`
- }).then(value => {
- if (value && value.text === 'OK') {
- return true;
- }
- return false;
- });
- }
- private _closeGuard = false;
- private _contextManager: ContextManager = null;
- private _registry: DocumentRegistry = null;
- private _widgets: { [key: string]: Widget[] } = Object.create(null);
- }
- /**
- * A namespace for document widget manager statics.
- */
- export
- namespace DocumentWidgetManager {
- /**
- * The options used to initialize a document widget manager.
- */
- export
- interface IOptions {
- /**
- * A document registry instance.
- */
- registry: DocumentRegistry;
- /**
- * A context manager instance.
- */
- contextManager: ContextManager;
- }
- }
- /**
- * A private namespace for DocumentManager data.
- */
- namespace Private {
- /**
- * A private attached property for a widget context id.
- */
- export
- const idProperty = new AttachedProperty<Widget, string>({
- name: 'id'
- });
- /**
- * A private attached property for a widget factory name.
- */
- export
- const nameProperty = new AttachedProperty<Widget, string>({
- name: 'name'
- });
- }
|