@itwin/presentation-frontend
Version:
Frontend of iModel.js Presentation library
652 lines • 27.8 kB
JavaScript
"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module UnifiedSelection
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ToolSelectionSyncHandler = exports.SelectionManager = void 0;
const rxjs_1 = require("rxjs");
const core_bentley_1 = require("@itwin/core-bentley");
const core_frontend_1 = require("@itwin/core-frontend");
const presentation_common_1 = require("@itwin/presentation-common");
const unified_selection_1 = require("@itwin/unified-selection");
const Presentation_1 = require("../Presentation");
const HiliteSetProvider_1 = require("./HiliteSetProvider");
const SelectionChangeEvent_1 = require("./SelectionChangeEvent");
const SelectionScopesManager_1 = require("./SelectionScopesManager");
/**
* The selection manager which stores the overall selection.
* @public
*/
class SelectionManager {
/**
* Creates an instance of SelectionManager.
*/
constructor(props) {
this._imodelToolSelectionSyncHandlers = new Map();
this._hiliteSetProviders = new Map();
this._knownIModels = new Set();
this._currentSelection = new CurrentSelectionStorage();
this._selectionChanges = new rxjs_1.Subject();
this._listeners = [];
this.selectionChange = new SelectionChangeEvent_1.SelectionChangeEvent();
this.scopes = props.scopes;
this._selectionStorage = props.selectionStorage ?? (0, unified_selection_1.createStorage)();
this._ownsStorage = props.selectionStorage === undefined;
this._selectionStorage.selectionChangeEvent.addListener((args) => this._selectionChanges.next(args));
this._selectionEventsSubscription = this.streamSelectionEvents();
this._listeners.push(core_frontend_1.IModelConnection.onOpen.addListener((imodel) => {
this._knownIModels.add(imodel);
}));
this._listeners.push(core_frontend_1.IModelConnection.onClose.addListener((imodel) => {
this.onConnectionClose(imodel);
}));
}
dispose() {
this._selectionEventsSubscription.unsubscribe();
this._listeners.forEach((dispose) => dispose());
}
onConnectionClose(imodel) {
this._hiliteSetProviders.delete(imodel);
this._knownIModels.delete(imodel);
this._currentSelection.clear(imodel.key);
if (this._ownsStorage) {
this.clearSelection("Connection Close Event", imodel);
this._selectionStorage.clearStorage({ iModelKey: imodel.key });
}
}
/** @internal */
// istanbul ignore next
getToolSelectionSyncHandler(imodel) {
return this._imodelToolSelectionSyncHandlers.get(imodel)?.handler;
}
/**
* Request the manager to sync with imodel's tool selection (see `IModelConnection.selectionSet`).
*/
setSyncWithIModelToolSelection(imodel, sync = true) {
const registration = this._imodelToolSelectionSyncHandlers.get(imodel);
if (sync) {
if (!registration || registration.requestorsCount === 0) {
this._imodelToolSelectionSyncHandlers.set(imodel, { requestorsCount: 1, handler: new ToolSelectionSyncHandler(imodel, this) });
}
else {
this._imodelToolSelectionSyncHandlers.set(imodel, { ...registration, requestorsCount: registration.requestorsCount + 1 });
}
}
else {
if (registration && registration.requestorsCount > 0) {
const requestorsCount = registration.requestorsCount - 1;
if (requestorsCount > 0) {
this._imodelToolSelectionSyncHandlers.set(imodel, { ...registration, requestorsCount });
}
else {
this._imodelToolSelectionSyncHandlers.delete(imodel);
registration.handler.dispose();
}
}
}
}
/**
* Temporarily suspends tool selection synchronization until the returned `IDisposable`
* is disposed.
*/
suspendIModelToolSelectionSync(imodel) {
const registration = this._imodelToolSelectionSyncHandlers.get(imodel);
if (!registration) {
return { dispose: () => { } };
}
const wasSuspended = registration.handler.isSuspended;
registration.handler.isSuspended = true;
return { dispose: () => (registration.handler.isSuspended = wasSuspended) };
}
/** Get the selection levels currently stored in this manager for the specified imodel */
getSelectionLevels(imodel) {
return this._selectionStorage.getSelectionLevels({ iModelKey: imodel.key });
}
/**
* Get the selection currently stored in this manager
*
* @note Calling immediately after `add*`|`replace*`|`remove*`|`clear*` method call does not guarantee
* that returned `KeySet` will include latest changes. Listen for `selectionChange` event to get the
* latest selection after changes.
*/
getSelection(imodel, level = 0) {
return this._currentSelection.getSelection(imodel.key, level);
}
handleEvent(evt) {
this._knownIModels.add(evt.imodel);
switch (evt.changeType) {
case SelectionChangeEvent_1.SelectionChangeType.Add:
this._selectionStorage.addToSelection({
iModelKey: evt.imodel.key,
source: evt.source,
level: evt.level,
selectables: keysToSelectable(evt.imodel, evt.keys),
});
break;
case SelectionChangeEvent_1.SelectionChangeType.Remove:
this._selectionStorage.removeFromSelection({
iModelKey: evt.imodel.key,
source: evt.source,
level: evt.level,
selectables: keysToSelectable(evt.imodel, evt.keys),
});
break;
case SelectionChangeEvent_1.SelectionChangeType.Replace:
this._selectionStorage.replaceSelection({
iModelKey: evt.imodel.key,
source: evt.source,
level: evt.level,
selectables: keysToSelectable(evt.imodel, evt.keys),
});
break;
case SelectionChangeEvent_1.SelectionChangeType.Clear:
this._selectionStorage.clearSelection({ iModelKey: evt.imodel.key, source: evt.source, level: evt.level });
break;
}
}
/**
* Add keys to the selection
* @param source Name of the selection source
* @param imodel iModel associated with the selection
* @param keys Keys to add
* @param level Selection level (see [selection levels documentation section]($docs/presentation/unified-selection/index#selection-levels))
* @param rulesetId ID of the ruleset in case the selection was changed from a rules-driven control
*/
addToSelection(source, imodel, keys, level = 0, rulesetId) {
const evt = {
source,
level,
imodel,
changeType: SelectionChangeEvent_1.SelectionChangeType.Add,
keys: new presentation_common_1.KeySet(keys),
timestamp: new Date(),
rulesetId,
};
this.handleEvent(evt);
}
/**
* Remove keys from current selection
* @param source Name of the selection source
* @param imodel iModel associated with the selection
* @param keys Keys to remove
* @param level Selection level (see [selection levels documentation section]($docs/presentation/unified-selection/index#selection-levels))
* @param rulesetId ID of the ruleset in case the selection was changed from a rules-driven control
*/
removeFromSelection(source, imodel, keys, level = 0, rulesetId) {
const evt = {
source,
level,
imodel,
changeType: SelectionChangeEvent_1.SelectionChangeType.Remove,
keys: new presentation_common_1.KeySet(keys),
timestamp: new Date(),
rulesetId,
};
this.handleEvent(evt);
}
/**
* Replace current selection
* @param source Name of the selection source
* @param imodel iModel associated with the selection
* @param keys Keys to add
* @param level Selection level (see [selection levels documentation section]($docs/presentation/unified-selection/index#selection-levels))
* @param rulesetId ID of the ruleset in case the selection was changed from a rules-driven control
*/
replaceSelection(source, imodel, keys, level = 0, rulesetId) {
const evt = {
source,
level,
imodel,
changeType: SelectionChangeEvent_1.SelectionChangeType.Replace,
keys: new presentation_common_1.KeySet(keys),
timestamp: new Date(),
rulesetId,
};
this.handleEvent(evt);
}
/**
* Clear current selection
* @param source Name of the selection source
* @param imodel iModel associated with the selection
* @param level Selection level (see [selection levels documentation section]($docs/presentation/unified-selection/index#selection-levels))
* @param rulesetId ID of the ruleset in case the selection was changed from a rules-driven control
*/
clearSelection(source, imodel, level = 0, rulesetId) {
const evt = {
source,
level,
imodel,
changeType: SelectionChangeEvent_1.SelectionChangeType.Clear,
keys: new presentation_common_1.KeySet(),
timestamp: new Date(),
rulesetId,
};
this.handleEvent(evt);
}
/**
* Add keys to selection after applying [selection scope]($docs/presentation/unified-selection/index#selection-scopes) on them.
* @param source Name of the selection source
* @param imodel iModel associated with the selection
* @param ids Element IDs to add
* @param scope Selection scope to apply
* @param level Selection level (see [selection levels documentation section]($docs/presentation/unified-selection/index#selection-levels))
* @param rulesetId ID of the ruleset in case the selection was changed from a rules-driven control
*/
async addToSelectionWithScope(source, imodel, ids, scope, level = 0, rulesetId) {
const scopedKeys = await this.scopes.computeSelection(imodel, ids, scope);
this.addToSelection(source, imodel, scopedKeys, level, rulesetId);
}
/**
* Remove keys from current selection after applying [selection scope]($docs/presentation/unified-selection/index#selection-scopes) on them.
* @param source Name of the selection source
* @param imodel iModel associated with the selection
* @param ids Element IDs to remove
* @param scope Selection scope to apply
* @param level Selection level (see [selection levels documentation section]($docs/presentation/unified-selection/index#selection-levels))
* @param rulesetId ID of the ruleset in case the selection was changed from a rules-driven control
*/
async removeFromSelectionWithScope(source, imodel, ids, scope, level = 0, rulesetId) {
const scopedKeys = await this.scopes.computeSelection(imodel, ids, scope);
this.removeFromSelection(source, imodel, scopedKeys, level, rulesetId);
}
/**
* Replace current selection with keys after applying [selection scope]($docs/presentation/unified-selection/index#selection-scopes) on them.
* @param source Name of the selection source
* @param imodel iModel associated with the selection
* @param ids Element IDs to replace with
* @param scope Selection scope to apply
* @param level Selection level (see [selection levels documentation section]($docs/presentation/unified-selection/index#selection-levels))
* @param rulesetId ID of the ruleset in case the selection was changed from a rules-driven control
*/
async replaceSelectionWithScope(source, imodel, ids, scope, level = 0, rulesetId) {
const scopedKeys = await this.scopes.computeSelection(imodel, ids, scope);
this.replaceSelection(source, imodel, scopedKeys, level, rulesetId);
}
/**
* Get the current hilite set for the specified imodel
* @public
*/
async getHiliteSet(imodel) {
return this.getHiliteSetProvider(imodel).getHiliteSet(this.getSelection(imodel));
}
/**
* Get the current hilite set iterator for the specified imodel.
* @public
*/
getHiliteSetIterator(imodel) {
return this.getHiliteSetProvider(imodel).getHiliteSetIterator(this.getSelection(imodel));
}
getHiliteSetProvider(imodel) {
let provider = this._hiliteSetProviders.get(imodel);
if (!provider) {
provider = HiliteSetProvider_1.HiliteSetProvider.create({ imodel });
this._hiliteSetProviders.set(imodel, provider);
}
return provider;
}
streamSelectionEvents() {
return this._selectionChanges
.pipe((0, rxjs_1.mergeMap)((args) => {
const currentSelectables = this._selectionStorage.getSelection({ iModelKey: args.imodelKey, level: args.level });
return this._currentSelection.computeSelection(args.imodelKey, args.level, currentSelectables, args.selectables).pipe((0, rxjs_1.mergeMap)(({ level, changedSelection }) => {
const imodel = findIModel(this._knownIModels, args.imodelKey);
// istanbul ignore if
if (!imodel) {
return rxjs_1.EMPTY;
}
return (0, rxjs_1.of)({
imodel,
keys: changedSelection,
level,
source: args.source,
timestamp: args.timestamp,
changeType: getChangeType(args.changeType),
});
}));
}))
.subscribe({
next: (args) => {
this.selectionChange.raiseEvent(args, this);
},
});
}
}
exports.SelectionManager = SelectionManager;
function findIModel(set, key) {
for (const imodel of set) {
if (imodel.key === key) {
return imodel;
}
}
return undefined;
}
/** @internal */
class ToolSelectionSyncHandler {
constructor(imodel, logicalSelection) {
this._selectionSourceName = "Tool";
this._asyncsTracker = new presentation_common_1.AsyncTasksTracker();
this.onToolSelectionChanged = async (ev) => {
// ignore selection change event if the handler is suspended
if (this.isSuspended) {
return;
}
// this component only cares about its own imodel
const imodel = ev.set.iModel;
if (imodel !== this._imodel) {
return;
}
// determine the level of selection changes
// wip: may want to allow selecting at different levels?
const selectionLevel = 0;
let ids;
switch (ev.type) {
case core_frontend_1.SelectionSetEventType.Add:
ids = ev.added;
break;
case core_frontend_1.SelectionSetEventType.Replace:
ids = ev.set.elements;
break;
default:
ids = ev.removed;
break;
}
// we're always using scoped selection changer even if the scope is set to "element" - that
// makes sure we're adding to selection keys with concrete classes and not "BisCore:Element", which
// we can't because otherwise our keys compare fails (presentation components load data with
// concrete classes)
const changer = new ScopedSelectionChanger(this._selectionSourceName, this._imodel, this._logicalSelection, (0, SelectionScopesManager_1.createSelectionScopeProps)(this._logicalSelection.scopes.activeScope));
// we know what to do immediately on `clear` events
if (core_frontend_1.SelectionSetEventType.Clear === ev.type) {
await changer.clear(selectionLevel);
return;
}
const parsedIds = parseIds(ids);
await (0, core_bentley_1.using)(this._asyncsTracker.trackAsyncTask(), async (_r) => {
switch (ev.type) {
case core_frontend_1.SelectionSetEventType.Add:
await changer.add(parsedIds.transient, parsedIds.persistent, selectionLevel);
break;
case core_frontend_1.SelectionSetEventType.Replace:
await changer.replace(parsedIds.transient, parsedIds.persistent, selectionLevel);
break;
case core_frontend_1.SelectionSetEventType.Remove:
await changer.remove(parsedIds.transient, parsedIds.persistent, selectionLevel);
break;
}
});
};
this._imodel = imodel;
this._logicalSelection = logicalSelection;
this._imodelToolSelectionListenerDisposeFunc = imodel.selectionSet.onChanged.addListener(this.onToolSelectionChanged);
}
dispose() {
this._imodelToolSelectionListenerDisposeFunc();
}
/** note: used only it tests */
get pendingAsyncs() {
return this._asyncsTracker.pendingAsyncs;
}
}
exports.ToolSelectionSyncHandler = ToolSelectionSyncHandler;
const parseIds = (ids) => {
let allPersistent = true;
let allTransient = true;
for (const id of core_bentley_1.Id64.iterable(ids)) {
if (core_bentley_1.Id64.isTransient(id)) {
allPersistent = false;
}
else {
allTransient = false;
}
if (!allPersistent && !allTransient) {
break;
}
}
// avoid making a copy if ids are only persistent or only transient
if (allPersistent) {
return { persistent: ids, transient: [] };
}
else if (allTransient) {
return { persistent: [], transient: ids };
}
// if `ids` contain mixed ids, we have to copy.. use Array instead of
// a Set for performance
const persistentElementIds = [];
const transientElementIds = [];
for (const id of core_bentley_1.Id64.iterable(ids)) {
if (core_bentley_1.Id64.isTransient(id)) {
transientElementIds.push(id);
}
else {
persistentElementIds.push(id);
}
}
return { persistent: persistentElementIds, transient: transientElementIds };
};
function addTransientKeys(transientIds, keys) {
for (const id of core_bentley_1.Id64.iterable(transientIds)) {
keys.add({ className: unified_selection_1.TRANSIENT_ELEMENT_CLASSNAME, id });
}
}
/** @internal */
class ScopedSelectionChanger {
constructor(name, imodel, manager, scope) {
this.name = name;
this.imodel = imodel;
this.manager = manager;
this.scope = scope;
}
async clear(level) {
this.manager.clearSelection(this.name, this.imodel, level);
}
async add(transientIds, persistentIds, level) {
const keys = await this.manager.scopes.computeSelection(this.imodel, persistentIds, this.scope);
addTransientKeys(transientIds, keys);
this.manager.addToSelection(this.name, this.imodel, keys, level);
}
async remove(transientIds, persistentIds, level) {
const keys = await this.manager.scopes.computeSelection(this.imodel, persistentIds, this.scope);
addTransientKeys(transientIds, keys);
this.manager.removeFromSelection(this.name, this.imodel, keys, level);
}
async replace(transientIds, persistentIds, level) {
const keys = await this.manager.scopes.computeSelection(this.imodel, persistentIds, this.scope);
addTransientKeys(transientIds, keys);
this.manager.replaceSelection(this.name, this.imodel, keys, level);
}
}
/** Stores current selection in `KeySet` format per iModel. */
class CurrentSelectionStorage {
constructor() {
this._currentSelection = new Map();
}
getCurrentSelectionStorage(imodelKey) {
let storage = this._currentSelection.get(imodelKey);
if (!storage) {
storage = new IModelSelectionStorage();
this._currentSelection.set(imodelKey, storage);
}
return storage;
}
getSelection(imodelKey, level) {
return this.getCurrentSelectionStorage(imodelKey).getSelection(level);
}
clear(imodelKey) {
this._currentSelection.delete(imodelKey);
}
computeSelection(imodelKey, level, currSelectables, changedSelectables) {
return this.getCurrentSelectionStorage(imodelKey).computeSelection(level, currSelectables, changedSelectables);
}
}
/**
* Computes and stores current selection in `KeySet` format.
* It always stores result of latest resolved call to `computeSelection`.
*/
class IModelSelectionStorage {
constructor() {
this._currentSelection = new Map();
}
getSelection(level) {
let entry = this._currentSelection.get(level);
if (!entry) {
entry = { value: new presentation_common_1.KeySet(), ongoingComputationDisposers: new Set() };
this._currentSelection.set(level, entry);
}
return entry.value;
}
clearSelections(level) {
const clearedLevels = [];
for (const [storedLevel] of this._currentSelection.entries()) {
if (storedLevel > level) {
clearedLevels.push(storedLevel);
}
}
clearedLevels.forEach((storedLevel) => {
const entry = this._currentSelection.get(storedLevel);
// istanbul ignore if
if (!entry) {
return;
}
for (const disposer of entry.ongoingComputationDisposers) {
disposer.next();
}
this._currentSelection.delete(storedLevel);
});
}
addDisposer(level, disposer) {
const entry = this._currentSelection.get(level);
if (!entry) {
this._currentSelection.set(level, { value: new presentation_common_1.KeySet(), ongoingComputationDisposers: new Set([disposer]) });
return;
}
entry.ongoingComputationDisposers.add(disposer);
}
setSelection(level, keys, disposer) {
const currEntry = this._currentSelection.get(level);
// istanbul ignore else
if (currEntry) {
currEntry.ongoingComputationDisposers.delete(disposer);
}
this._currentSelection.set(level, {
value: keys,
ongoingComputationDisposers: currEntry?.ongoingComputationDisposers ?? /* istanbul ignore next */ new Set(),
});
}
computeSelection(level, currSelectables, changedSelectables) {
this.clearSelections(level);
const prevComputationsDisposers = [...(this._currentSelection.get(level)?.ongoingComputationDisposers ?? [])];
const currDisposer = new rxjs_1.Subject();
this.addDisposer(level, currDisposer);
return (0, rxjs_1.defer)(async () => {
const convertedSelectables = [];
const [current, changed] = await Promise.all([
selectablesToKeys(currSelectables, convertedSelectables),
selectablesToKeys(changedSelectables, convertedSelectables),
]);
const currentSelection = new presentation_common_1.KeySet([...current.keys, ...current.selectableKeys.flatMap((selectable) => selectable.keys)]);
const changedSelection = new presentation_common_1.KeySet([...changed.keys, ...changed.selectableKeys.flatMap((selectable) => selectable.keys)]);
return {
level,
currentSelection,
changedSelection,
};
}).pipe((0, rxjs_1.takeUntil)(currDisposer), (0, rxjs_1.tap)({
next: (val) => {
prevComputationsDisposers.forEach((disposer) => disposer.next());
this.setSelection(val.level, val.currentSelection, currDisposer);
},
}));
}
}
function keysToSelectable(imodel, keys) {
const selectables = [];
keys.forEach((key) => {
if ("id" in key) {
selectables.push(key);
return;
}
const customSelectable = {
identifier: key.pathFromRoot.join("/"),
data: key,
loadInstanceKeys: () => createInstanceKeysIterator(imodel, key),
};
selectables.push(customSelectable);
});
return selectables;
}
async function selectablesToKeys(selectables, convertedList) {
const keys = [];
const selectableKeys = [];
for (const [className, ids] of selectables.instanceKeys) {
for (const id of ids) {
keys.push({ id, className });
}
}
for (const [_, selectable] of selectables.custom) {
if (isNodeKey(selectable.data)) {
selectableKeys.push({ identifier: selectable.identifier, keys: [selectable.data] });
continue;
}
const converted = convertedList.find((con) => con.identifier === selectable.identifier);
if (converted) {
selectableKeys.push(converted);
continue;
}
const newConverted = { identifier: selectable.identifier, keys: [] };
convertedList.push(newConverted);
for await (const instanceKey of selectable.loadInstanceKeys()) {
newConverted.keys.push(instanceKey);
}
selectableKeys.push(newConverted);
}
return { keys, selectableKeys };
}
async function* createInstanceKeysIterator(imodel, nodeKey) {
if (presentation_common_1.NodeKey.isInstancesNodeKey(nodeKey)) {
for (const key of nodeKey.instanceKeys) {
yield key;
}
return;
}
const content = await Presentation_1.Presentation.presentation.getContentInstanceKeys({
imodel,
keys: new presentation_common_1.KeySet([nodeKey]),
rulesetOrId: {
id: "grouped-instances",
rules: [
{
ruleType: "Content",
specifications: [
{
specType: "SelectedNodeInstances",
},
],
},
],
},
});
for await (const key of content.items()) {
yield key;
}
}
function isNodeKey(data) {
const key = data;
return key.pathFromRoot !== undefined && key.type !== undefined;
}
function getChangeType(type) {
switch (type) {
case "add":
return SelectionChangeEvent_1.SelectionChangeType.Add;
case "remove":
return SelectionChangeEvent_1.SelectionChangeType.Remove;
case "replace":
return SelectionChangeEvent_1.SelectionChangeType.Replace;
case "clear":
return SelectionChangeEvent_1.SelectionChangeType.Clear;
}
}
//# sourceMappingURL=SelectionManager.js.map