|
@@ -24,7 +24,7 @@ import {
|
|
|
} from '@phosphor/widgets';
|
|
|
|
|
|
import {
|
|
|
- InstanceTracker
|
|
|
+ ApplicationShell, InstanceTracker
|
|
|
} from '../application';
|
|
|
|
|
|
import {
|
|
@@ -72,79 +72,6 @@ interface IInstanceRestorer {
|
|
|
*/
|
|
|
export
|
|
|
namespace IInstanceRestorer {
|
|
|
- /**
|
|
|
- * An application layout data store.
|
|
|
- */
|
|
|
- export
|
|
|
- interface ILayoutDB {
|
|
|
- /**
|
|
|
- * Fetch the layout state for the application.
|
|
|
- *
|
|
|
- * #### Notes
|
|
|
- * Fetching the layout relies on all widget restoration to be complete, so
|
|
|
- * calls to `fetch` are guaranteed to return after restoration is complete.
|
|
|
- */
|
|
|
- fetch(): Promise<IInstanceRestorer.ILayout>;
|
|
|
-
|
|
|
- /**
|
|
|
- * Save the layout state for the application.
|
|
|
- */
|
|
|
- save(data: IInstanceRestorer.ILayout): Promise<void>;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * A description of the application's user interface layout.
|
|
|
- */
|
|
|
- export
|
|
|
- interface ILayout {
|
|
|
- /**
|
|
|
- * The current widget that has application focus.
|
|
|
- */
|
|
|
- readonly currentWidget: Widget | null;
|
|
|
-
|
|
|
- /**
|
|
|
- * Indicates whether fetched session restore data was actually retrieved
|
|
|
- * from the state database or whether it is a fresh blank slate.
|
|
|
- *
|
|
|
- * #### Notes
|
|
|
- * This attribute is only relevant when the layout data is retrieved via a
|
|
|
- * `fetch` call. If it is set when being passed into `save`, it will be
|
|
|
- * ignored.
|
|
|
- */
|
|
|
- readonly fresh?: boolean;
|
|
|
-
|
|
|
- /**
|
|
|
- * The left area of the user interface.
|
|
|
- */
|
|
|
- readonly leftArea: ISideArea;
|
|
|
-
|
|
|
- /**
|
|
|
- * The right area of the user interface.
|
|
|
- */
|
|
|
- readonly rightArea: ISideArea;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * The restorable description of a sidebar in the user interface.
|
|
|
- */
|
|
|
- export
|
|
|
- interface ISideArea {
|
|
|
- /**
|
|
|
- * A flag denoting whether the sidebar has been collapsed.
|
|
|
- */
|
|
|
- readonly collapsed: boolean;
|
|
|
-
|
|
|
- /**
|
|
|
- * The current widget that has side area focus.
|
|
|
- */
|
|
|
- readonly currentWidget: Widget | null;
|
|
|
-
|
|
|
- /**
|
|
|
- * The collection of widgets held by the sidebar.
|
|
|
- */
|
|
|
- readonly widgets: Array<Widget> | null;
|
|
|
- }
|
|
|
-
|
|
|
/**
|
|
|
* The state restoration configuration options.
|
|
|
*/
|
|
@@ -268,12 +195,9 @@ class InstanceRestorer implements IInstanceRestorer {
|
|
|
* Fetching the layout relies on all widget restoration to be complete, so
|
|
|
* calls to `fetch` are guaranteed to return after restoration is complete.
|
|
|
*/
|
|
|
- fetch(): Promise<IInstanceRestorer.ILayout> {
|
|
|
- const blank: IInstanceRestorer.ILayout = {
|
|
|
- currentWidget: null,
|
|
|
- fresh: true,
|
|
|
- leftArea: { collapsed: true, currentWidget: null, widgets: null },
|
|
|
- rightArea: { collapsed: true, currentWidget: null, widgets: null }
|
|
|
+ fetch(): Promise<ApplicationShell.ILayout> {
|
|
|
+ const blank: ApplicationShell.ILayout = {
|
|
|
+ fresh: true, mainArea: null, leftArea: null, rightArea: null
|
|
|
};
|
|
|
let layout = this._state.fetch(KEY);
|
|
|
|
|
@@ -282,14 +206,13 @@ class InstanceRestorer implements IInstanceRestorer {
|
|
|
return blank;
|
|
|
}
|
|
|
|
|
|
- let { current, left, right } = data as InstanceRestorer.IDehydratedLayout;
|
|
|
+ const { main, left, right } = data as Private.ILayout;
|
|
|
|
|
|
// If any data exists, then this is not a fresh session.
|
|
|
const fresh = false;
|
|
|
|
|
|
- // Rehydrate main area. Coerce type of `current` in case of bad data.
|
|
|
- const currentWidget = current && this._widgets.has(`${current}`) ?
|
|
|
- this._widgets.get(`${current}`) : null;
|
|
|
+ // Rehydrate main area.
|
|
|
+ const mainArea = this._rehydrateMainArea(main);
|
|
|
|
|
|
// Rehydrate left area.
|
|
|
const leftArea = this._rehydrateSideArea(left);
|
|
@@ -297,7 +220,7 @@ class InstanceRestorer implements IInstanceRestorer {
|
|
|
// Rehydrate right area.
|
|
|
const rightArea = this._rehydrateSideArea(right);
|
|
|
|
|
|
- return { currentWidget, fresh, leftArea, rightArea };
|
|
|
+ return { fresh, mainArea, leftArea, rightArea };
|
|
|
}).catch(() => blank); // Let fetch fail gracefully; return blank slate.
|
|
|
}
|
|
|
|
|
@@ -341,7 +264,7 @@ class InstanceRestorer implements IInstanceRestorer {
|
|
|
/**
|
|
|
* Save the layout state for the application.
|
|
|
*/
|
|
|
- save(data: IInstanceRestorer.ILayout): Promise<void> {
|
|
|
+ save(data: ApplicationShell.ILayout): Promise<void> {
|
|
|
// If there are promises that are unresolved, bail.
|
|
|
if (this._promises) {
|
|
|
let warning = 'save() was called prematurely.';
|
|
@@ -349,16 +272,10 @@ class InstanceRestorer implements IInstanceRestorer {
|
|
|
return Promise.reject(warning);
|
|
|
}
|
|
|
|
|
|
- let dehydrated: InstanceRestorer.IDehydratedLayout = {};
|
|
|
- let current: string;
|
|
|
+ let dehydrated: Private.ILayout = {};
|
|
|
|
|
|
// Dehydrate main area.
|
|
|
- if (data.currentWidget) {
|
|
|
- current = Private.nameProperty.get(data.currentWidget);
|
|
|
- if (current) {
|
|
|
- dehydrated.current = current;
|
|
|
- }
|
|
|
- }
|
|
|
+ dehydrated.main = this._dehydrateMainArea(data.mainArea);
|
|
|
|
|
|
// Dehydrate left area.
|
|
|
dehydrated.left = this._dehydrateSideArea(data.leftArea);
|
|
@@ -370,10 +287,28 @@ class InstanceRestorer implements IInstanceRestorer {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Dehydrate a side area into a serialized description object.
|
|
|
+ * Dehydrate a main area description into a serializable object.
|
|
|
*/
|
|
|
- private _dehydrateSideArea(area: IInstanceRestorer.ISideArea): InstanceRestorer.ISideArea {
|
|
|
- let dehydrated: InstanceRestorer.ISideArea = { collapsed: area.collapsed };
|
|
|
+ private _dehydrateMainArea(area: ApplicationShell.IMainArea): Private.IMainArea {
|
|
|
+ return Private.serializeMain(area);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Reydrate a serialized main area description object.
|
|
|
+ *
|
|
|
+ * #### Notes
|
|
|
+ * This function consumes data that can become corrupted, so it uses type
|
|
|
+ * coercion to guarantee the dehydrated object is safely processed.
|
|
|
+ */
|
|
|
+ private _rehydrateMainArea(area: Private.IMainArea): ApplicationShell.IMainArea {
|
|
|
+ return Private.deserializeMain(area, this._widgets);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Dehydrate a side area description into a serializable object.
|
|
|
+ */
|
|
|
+ private _dehydrateSideArea(area: ApplicationShell.ISideArea): Private.ISideArea {
|
|
|
+ let dehydrated: Private.ISideArea = { collapsed: area.collapsed };
|
|
|
if (area.currentWidget) {
|
|
|
let current = Private.nameProperty.get(area.currentWidget);
|
|
|
if (current) {
|
|
@@ -395,7 +330,7 @@ class InstanceRestorer implements IInstanceRestorer {
|
|
|
* This function consumes data that can become corrupted, so it uses type
|
|
|
* coercion to guarantee the dehydrated object is safely processed.
|
|
|
*/
|
|
|
- private _rehydrateSideArea(area: InstanceRestorer.ISideArea): IInstanceRestorer.ISideArea {
|
|
|
+ private _rehydrateSideArea(area: Private.ISideArea): ApplicationShell.ISideArea {
|
|
|
if (!area) {
|
|
|
return { collapsed: true, currentWidget: null, widgets: null };
|
|
|
}
|
|
@@ -457,23 +392,27 @@ namespace InstanceRestorer {
|
|
|
*/
|
|
|
state: IStateDB;
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
+/*
|
|
|
+ * A namespace for private data.
|
|
|
+ */
|
|
|
+namespace Private {
|
|
|
/**
|
|
|
* The dehydrated state of the application layout.
|
|
|
*
|
|
|
* #### Notes
|
|
|
- * This format is JSON serializable and only used internally by the instance
|
|
|
- * restorer to read and write to the state database. It is meant to be a data
|
|
|
- * structure that the instance restorer can translate into an
|
|
|
- * `IInstanceTracker.ILayout` data structure for consumption by the
|
|
|
- * application shell.
|
|
|
+ * This format is JSON serializable and saved in the state database.
|
|
|
+ * It is meant to be a data structure can translate into an
|
|
|
+ * `ApplicationShell.ILayout` data structure for consumption by
|
|
|
+ * the application shell.
|
|
|
*/
|
|
|
export
|
|
|
- interface IDehydratedLayout extends JSONObject {
|
|
|
+ interface ILayout extends JSONObject {
|
|
|
/**
|
|
|
- * The current widget that has application focus.
|
|
|
+ * The main area of the user interface.
|
|
|
*/
|
|
|
- current?: string | null;
|
|
|
+ main?: IMainArea | null;
|
|
|
|
|
|
/**
|
|
|
* The left area of the user interface.
|
|
@@ -486,6 +425,22 @@ namespace InstanceRestorer {
|
|
|
right?: ISideArea | null;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * The restorable description of the main application area.
|
|
|
+ */
|
|
|
+ export
|
|
|
+ interface IMainArea extends JSONObject {
|
|
|
+ /**
|
|
|
+ * The current widget that has application focus.
|
|
|
+ */
|
|
|
+ current?: string | null;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The main application dock panel.
|
|
|
+ */
|
|
|
+ dock?: ISplitArea | ITabArea | null;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* The restorable description of a sidebar in the user interface.
|
|
|
*/
|
|
@@ -506,12 +461,54 @@ namespace InstanceRestorer {
|
|
|
*/
|
|
|
widgets?: Array<string> | null;
|
|
|
}
|
|
|
-}
|
|
|
|
|
|
-/*
|
|
|
- * A namespace for private data.
|
|
|
- */
|
|
|
-namespace Private {
|
|
|
+ /**
|
|
|
+ * The restorable description of a tab area in the user interface.
|
|
|
+ */
|
|
|
+ export
|
|
|
+ interface ITabArea extends JSONObject {
|
|
|
+ /**
|
|
|
+ * The type indicator of the serialized tab area.
|
|
|
+ */
|
|
|
+ type: 'tab-area';
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The widgets in the tab area.
|
|
|
+ */
|
|
|
+ widgets: Array<string> | null;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The index of the selected tab.
|
|
|
+ */
|
|
|
+ currentIndex: number;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The restorable description of a split area in the user interface.
|
|
|
+ */
|
|
|
+ export
|
|
|
+ interface ISplitArea extends JSONObject {
|
|
|
+ /**
|
|
|
+ * The type indicator of the serialized split area.
|
|
|
+ */
|
|
|
+ type: 'split-area';
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The orientation of the split area.
|
|
|
+ */
|
|
|
+ orientation: 'horizontal' | 'vertical';
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The children in the split area.
|
|
|
+ */
|
|
|
+ children: Array<ITabArea | ISplitArea> | null;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The sizes of the children.
|
|
|
+ */
|
|
|
+ sizes: Array<number>;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* An attached property for a widget's ID in the state database.
|
|
|
*/
|
|
@@ -520,4 +517,124 @@ namespace Private {
|
|
|
name: 'name',
|
|
|
create: owner => ''
|
|
|
});
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Serialize individual areas within the main area.
|
|
|
+ */
|
|
|
+ function serializeArea(area: ApplicationShell.AreaConfig): ITabArea | ISplitArea | null {
|
|
|
+ if (!area || !area.type) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (area.type === 'tab-area') {
|
|
|
+ return {
|
|
|
+ type: 'tab-area',
|
|
|
+ currentIndex: area.currentIndex,
|
|
|
+ widgets: area.widgets
|
|
|
+ .map(widget => nameProperty.get(widget))
|
|
|
+ .filter(name => !!name)
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ type: 'split-area',
|
|
|
+ orientation: area.orientation,
|
|
|
+ sizes: area.sizes,
|
|
|
+ children: area.children.map(serializeArea)
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Return a dehydrated, serializable version of the main dock panel.
|
|
|
+ */
|
|
|
+ export
|
|
|
+ function serializeMain(area: ApplicationShell.IMainArea): IMainArea {
|
|
|
+ let dehydrated: IMainArea = {
|
|
|
+ dock: area && area.dock && serializeArea(area.dock.main) || null
|
|
|
+ };
|
|
|
+ if (area && area.currentWidget) {
|
|
|
+ let current = Private.nameProperty.get(area.currentWidget);
|
|
|
+ if (current) {
|
|
|
+ dehydrated.current = current;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return dehydrated;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Deserialize individual areas within the main area.
|
|
|
+ *
|
|
|
+ * #### Notes
|
|
|
+ * Because this data comes from a potentially unreliable foreign source, it is
|
|
|
+ * typed as a `JSONObject`; but the actual expected type is:
|
|
|
+ * `ITabArea | ISplitArea`.
|
|
|
+ *
|
|
|
+ * For fault tolerance, types are manually checked in deserialization.
|
|
|
+ */
|
|
|
+ function deserializeArea(area: JSONObject, names: Map<string, Widget>): ApplicationShell.AreaConfig | null {
|
|
|
+ if (!area) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Because this data is saved to a foreign data source, its type safety is
|
|
|
+ // not guaranteed when it is retrieved, so exhaustive checks are necessary.
|
|
|
+ const type = (area as any).type as string || 'unknown';
|
|
|
+ if (type === 'unknown' || (type !== 'tab-area' && type !== 'split-area')) {
|
|
|
+ console.warn(`Attempted to deserialize unknown type: ${type}`);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (type === 'tab-area') {
|
|
|
+ const { currentIndex, widgets } = area as ITabArea;
|
|
|
+ let hydrated: ApplicationShell.AreaConfig = {
|
|
|
+ type: 'tab-area',
|
|
|
+ currentIndex: currentIndex || 0,
|
|
|
+ widgets: widgets && widgets.map(widget => names.get(widget))
|
|
|
+ .filter(widget => !!widget) || []
|
|
|
+ };
|
|
|
+
|
|
|
+ // Make sure the current index is within bounds.
|
|
|
+ if (hydrated.currentIndex > hydrated.widgets.length - 1) {
|
|
|
+ hydrated.currentIndex = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ return hydrated;
|
|
|
+ }
|
|
|
+
|
|
|
+ const { orientation, sizes, children } = area as ISplitArea;
|
|
|
+ let hydrated: ApplicationShell.AreaConfig = {
|
|
|
+ type: 'split-area',
|
|
|
+ orientation: orientation,
|
|
|
+ sizes: sizes || [],
|
|
|
+ children: children &&
|
|
|
+ children.map(child => deserializeArea(child, names))
|
|
|
+ .filter(widget => !!widget) || []
|
|
|
+ };
|
|
|
+
|
|
|
+ return hydrated;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Return the hydrated version of the main dock panel, ready to restore.
|
|
|
+ *
|
|
|
+ * #### Notes
|
|
|
+ * Because this data comes from a potentially unreliable foreign source, it is
|
|
|
+ * typed as a `JSONObject`; but the actual expected type is: `IMainArea`.
|
|
|
+ *
|
|
|
+ * For fault tolerance, types are manually checked in deserialization.
|
|
|
+ */
|
|
|
+ export
|
|
|
+ function deserializeMain(area: JSONObject, names: Map<string, Widget>): ApplicationShell.IMainArea | null {
|
|
|
+ if (!area) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const name = (area as any).current || null;
|
|
|
+ const dock = (area as any).dock || null;
|
|
|
+
|
|
|
+ return {
|
|
|
+ currentWidget: name && names.has(name) && names.get(name) || null,
|
|
|
+ dock: dock ? { main: deserializeArea(dock, names) } : null
|
|
|
+ };
|
|
|
+ }
|
|
|
}
|