Skip to content

Commit

Permalink
Backport PR #11963: Support dynamic toolbar definition (#12078)
Browse files Browse the repository at this point in the history
Co-authored-by: Eric Charles <eric@datalayer.io>
  • Loading branch information
fcollonval and echarles committed Feb 17, 2022
1 parent 6871e89 commit 3e449cd
Show file tree
Hide file tree
Showing 15 changed files with 532 additions and 116 deletions.
193 changes: 99 additions & 94 deletions packages/apputils/src/toolbar/factory.ts
@@ -1,7 +1,7 @@
import { IObservableList, ObservableList } from '@jupyterlab/observables';
import { ISettingRegistry, SettingRegistry } from '@jupyterlab/settingregistry';
import { ITranslator, TranslationBundle } from '@jupyterlab/translation';
import { findIndex, toArray } from '@lumino/algorithm';
import { toArray } from '@lumino/algorithm';
import { JSONExt, PartialJSONObject } from '@lumino/coreutils';
import { Widget } from '@lumino/widgets';
import { Dialog, showDialog } from '../dialog';
Expand Down Expand Up @@ -38,22 +38,26 @@ async function displayInformation(trans: TranslationBundle): Promise<void> {
}

/**
* Accumulate the toolbar definition from all settings and set the default value from it.
* Set the toolbar definition by accumulating all settings definition.
*
* The list will be populated only with the enabled items.
*
* @param toolbarItems Observable list to populate
* @param registry Application settings registry
* @param factoryName Widget factory name that needs a toolbar
* @param pluginId Settings plugin id
* @param translator Translator object
* @param propertyId Property holding the toolbar definition in the settings; default 'toolbar'
* @returns List of toolbar items
*/
async function getToolbarItems(
async function setToolbarItems(
toolbarItems: IObservableList<ISettingRegistry.IToolbarItem>,
registry: ISettingRegistry,
factoryName: string,
pluginId: string,
translator: ITranslator,
propertyId: string = 'toolbar'
): Promise<IObservableList<ISettingRegistry.IToolbarItem>> {
): Promise<void> {
const trans = translator.load('jupyterlab');
let canonical: ISettingRegistry.ISchema | null;
let loaded: { [name: string]: ISettingRegistry.IToolbarItem[] } = {};
Expand Down Expand Up @@ -93,13 +97,11 @@ async function getToolbarItems(
pluginDefaults,
schema.properties![propertyId].default as any[],
true
)!
// flatten one level
.sort(
(a, b) =>
(a.rank ?? DEFAULT_TOOLBAR_ITEM_RANK) -
(b.rank ?? DEFAULT_TOOLBAR_ITEM_RANK)
);
)!.sort(
(a, b) =>
(a.rank ?? DEFAULT_TOOLBAR_ITEM_RANK) -
(b.rank ?? DEFAULT_TOOLBAR_ITEM_RANK)
);
}

// Transform the plugin object to return different schema than the default.
Expand All @@ -119,12 +121,17 @@ async function getToolbarItems(
// Overrides the value with using the aggregated default for the toolbar property
user[propertyId] =
(plugin.data.user[propertyId] as ISettingRegistry.IToolbarItem[]) ?? [];
composite[propertyId] =
composite[propertyId] = (
SettingRegistry.reconcileToolbarItems(
defaults as ISettingRegistry.IToolbarItem[],
user[propertyId] as ISettingRegistry.IToolbarItem[],
false
) ?? [];
) ?? []
).sort(
(a, b) =>
(a.rank ?? DEFAULT_TOOLBAR_ITEM_RANK) -
(b.rank ?? DEFAULT_TOOLBAR_ITEM_RANK)
);

plugin.data = { composite, user };

Expand Down Expand Up @@ -153,21 +160,12 @@ async function getToolbarItems(

const settings = await registry.load(pluginId);

const toolbarItems: IObservableList<ISettingRegistry.IToolbarItem> = new ObservableList(
{
values: JSONExt.deepCopy(settings.composite[propertyId] as any) ?? [],
itemCmp: (a, b) => JSONExt.deepEqual(a, b)
}
);

// React to customization by the user
settings.changed.connect(() => {
// As extension may change the toolbar through API,
// prompt the user to reload if the toolbar definition has been updated.
const newItems = (settings.composite[propertyId] as any) ?? [];
if (!JSONExt.deepEqual(toArray(toolbarItems.iter()), newItems)) {
void displayInformation(trans);
}
const newItems: ISettingRegistry.IToolbarItem[] =
(settings.composite[propertyId] as any) ?? [];

transferSettings(newItems);
});

// React to plugin changes
Expand All @@ -190,31 +188,33 @@ async function getToolbarItems(
} else {
// The plugin was not yet loaded => update the toolbar items list
loaded[plugin] = JSONExt.deepCopy(newItems);
const newList =
const newList = (
SettingRegistry.reconcileToolbarItems(
toArray(toolbarItems),
newItems,
false
) ?? [];

// Existing items cannot be removed.
newList?.forEach(item => {
const index = findIndex(
toolbarItems,
value => item.name === value.name
);
if (index < 0) {
toolbarItems.push(item);
} else {
toolbarItems.set(index, item);
}
});
) ?? []
).sort(
(a, b) =>
(a.rank ?? DEFAULT_TOOLBAR_ITEM_RANK) -
(b.rank ?? DEFAULT_TOOLBAR_ITEM_RANK)
);
transferSettings(newList);
}
}
}
});

return toolbarItems;
const transferSettings = (newItems: ISettingRegistry.IToolbarItem[]) => {
// This is not optimal but safer because a toolbar item with the same
// name cannot be inserted (it will be a no-op). But that could happen
// if the settings are changing the items order.
toolbarItems.clear();
toolbarItems.pushAll(newItems.filter(item => !item.disabled));
};

// Initialize the toolbar
transferSettings((settings.composite[propertyId] as any) ?? []);
}

/**
Expand All @@ -236,66 +236,71 @@ export function createToolbarFactory(
pluginId: string,
translator: ITranslator,
propertyId: string = 'toolbar'
): (widget: Widget) => ToolbarRegistry.IToolbarItem[] {
const items: ToolbarRegistry.IWidget[] = [];
let rawItems: IObservableList<ToolbarRegistry.IWidget>;

const transfer = (
list: IObservableList<ToolbarRegistry.IWidget>,
change: IObservableList.IChangedArgs<ToolbarRegistry.IWidget>
) => {
switch (change.type) {
case 'move':
break;
case 'add':
case 'remove':
case 'set':
items.length = 0;
items.push(
...toArray(list)
.filter(item => !item.disabled)
.sort(
(a, b) =>
(a.rank ?? DEFAULT_TOOLBAR_ITEM_RANK) -
(b.rank ?? DEFAULT_TOOLBAR_ITEM_RANK)
)
);
break;
}
};
): (widget: Widget) => IObservableList<ToolbarRegistry.IToolbarItem> {
const items = new ObservableList<ISettingRegistry.IToolbarItem>({
itemCmp: (a, b) => JSONExt.deepEqual(a as any, b as any)
});

// Get toolbar definition from the settings
getToolbarItems(
setToolbarItems(
items,
settingsRegistry,
factoryName,
pluginId,
translator,
propertyId
)
.then(candidates => {
rawItems = candidates;
rawItems.changed.connect(transfer);
// Force initialization of items
transfer(rawItems, {
type: 'add',
newIndex: 0,
newValues: [],
oldIndex: 0,
oldValues: []
});
})
.catch(reason => {
console.error(
`Failed to load toolbar items for factory ${factoryName} from ${pluginId}`,
reason
);
).catch(reason => {
console.error(
`Failed to load toolbar items for factory ${factoryName} from ${pluginId}`,
reason
);
});

return (widget: Widget) => {
const updateToolbar = (
list: IObservableList<ToolbarRegistry.IWidget>,
change: IObservableList.IChangedArgs<ToolbarRegistry.IWidget>
) => {
switch (change.type) {
case 'move':
toolbar.move(change.oldIndex, change.newIndex);
break;
case 'add':
change.newValues.forEach(item =>
toolbar.push({
name: item.name,
widget: toolbarRegistry.createWidget(factoryName, widget, item)
})
);
break;
case 'remove':
change.oldValues.forEach(() => toolbar.remove(change.oldIndex));
break;
case 'set':
change.newValues.forEach(item =>
toolbar.set(change.newIndex, {
name: item.name,
widget: toolbarRegistry.createWidget(factoryName, widget, item)
})
);
break;
}
};

const toolbar = new ObservableList<ToolbarRegistry.IToolbarItem>({
values: toArray(items).map(item => {
return {
name: item.name,
widget: toolbarRegistry.createWidget(factoryName, widget, item)
};
})
});

return (widget: Widget) =>
items.map(item => {
return {
name: item.name,
widget: toolbarRegistry.createWidget(factoryName, widget, item)
};
items.changed.connect(updateToolbar);
widget.disposed.connect(() => {
items.changed.disconnect(updateToolbar);
});

return toolbar;
};
}

0 comments on commit 3e449cd

Please sign in to comment.