UNPKG

@itwin/unified-selection

Version:

Package for managing unified selection in iTwin.js applications.

374 lines 17.3 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) { if (value !== null && value !== void 0) { if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected."); var dispose, inner; if (async) { if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined."); dispose = value[Symbol.asyncDispose]; } if (dispose === void 0) { if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined."); dispose = value[Symbol.dispose]; if (async) inner = dispose; } if (typeof dispose !== "function") throw new TypeError("Object not disposable."); if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } }; env.stack.push({ value: value, dispose: dispose, async: async }); } else if (async) { env.stack.push({ async: true }); } return value; }; var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) { return function (env) { function fail(e) { env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e; env.hasError = true; } var r, s = 0; function next() { while (r = env.stack.pop()) { try { if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next); if (r.dispose) { var result = r.dispose.call(r.value); if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); }); } else s |= 1; } catch (e) { fail(e); } } if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve(); if (env.hasError) throw env.error; } return next(); }; })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }); import "./DisposePolyfill.js"; import { EMPTY, firstValueFrom, from, map, merge, Subject, takeUntil, toArray } from "rxjs"; import { Guid } from "@itwin/core-bentley"; import { createHiliteSetProvider } from "./HiliteSetProvider.js"; import { createIModelHiliteSetProvider } from "./IModelHiliteSetProvider.js"; import { Selectables } from "./Selectable.js"; import { computeSelection } from "./SelectionScope.js"; import { CoreSelectionSetEventType } from "./types/IModel.js"; import { safeDispose } from "./Utils.js"; /** * Enables synchronization between iModel selection and unified selection. * @returns function for disposing the synchronization. * @public */ export function enableUnifiedSelectionSyncWithIModel(props) { const selectionHandler = new IModelSelectionHandler(props); return () => selectionHandler[Symbol.dispose](); } /** * A handler that syncs selection between unified selection storage (`SelectionStorage`) and * an iModel (`iModel.selectionSet`, `iModel.hilited`). * * @internal */ export class IModelSelectionHandler { _selectionSourceName = "Tool"; _imodelAccess; _selectionStorage; _imodelHiliteSetProvider; _activeScopeProvider; _isSuspended; _selectionStorageChangeTracker = 0; _cancelOngoingChanges = new Subject(); _unregisterUnifiedSelectionListener; _unregisterIModelSelectionSetListener; _disposeInternalHiliteSetProvider; #componentId; constructor(props) { this.#componentId = Guid.createValue(); this._imodelAccess = props.imodelAccess; this._selectionStorage = props.selectionStorage; this._activeScopeProvider = props.activeScopeProvider; this._isSuspended = false; [this._imodelHiliteSetProvider, this._disposeInternalHiliteSetProvider] = (() => { if (props.imodelHiliteSetProvider) { return [props.imodelHiliteSetProvider, () => { }]; } // eslint-disable-next-line @typescript-eslint/no-deprecated if (props.cachingHiliteSetProvider) { return [ createIModelHiliteSetProviderFromCachingProvider( // eslint-disable-next-line @typescript-eslint/no-deprecated props.cachingHiliteSetProvider, props.hiliteSetProvider ?? createHiliteSetProvider({ imodelAccess: props.imodelAccess })), // don't need to dispose anything, because our wrapper has no dispose logic, and `cachingHiliteSetProvider` should be disposed // by whoever owns it () => { }, ]; } /* c8 ignore start */ const internalProvider = createIModelHiliteSetProvider({ selectionStorage: this._selectionStorage, imodelProvider: () => this._imodelAccess, createHiliteSetProvider: () => props.hiliteSetProvider ?? createHiliteSetProvider({ imodelAccess: props.imodelAccess }), }); return [internalProvider, () => safeDispose(internalProvider)]; /* c8 ignore end */ })(); this._unregisterIModelSelectionSetListener = this._imodelAccess.selectionSet.onChanged.addListener(this.onIModelSelectionChanged); this._unregisterUnifiedSelectionListener = this._selectionStorage.selectionChangeEvent.addListener(this.onUnifiedSelectionChanged); if (!is5xSelectionSet(this._imodelAccess.selectionSet)) { // itwinjs-core@4: stop imodel from syncing tool selection with hilited list - we want to manage that sync ourselves this._imodelAccess.hiliteSet.wantSyncWithSelectionSet = false; } this.applyCurrentHiliteSet({ activeSelectionAction: "clearAll" }); } [Symbol.dispose]() { this._cancelOngoingChanges.next(); this._unregisterIModelSelectionSetListener(); this._unregisterUnifiedSelectionListener(); this._disposeInternalHiliteSetProvider(); } /** Temporarily suspends tool selection synchronization until the returned disposable object is disposed. */ suspendIModelToolSelectionSync() { const wasSuspended = this._isSuspended; this._isSuspended = true; return { [Symbol.dispose]: () => { this._isSuspended = wasSuspended; }, }; } syncHiliteSet(props) { const { changeType, selectables, source } = props; switch (changeType) { case "clear": return this.applyCurrentHiliteSet({ activeSelectionAction: "clearAll" }); case "replace": return this.applyCurrentHiliteSet({ activeSelectionAction: source === this._selectionSourceName ? is5xSelectionSet(this._imodelAccess.selectionSet) ? // with 5x core we don't need to clear anything when event is triggered by a Tool (hilite and selection sets are in sync already) "keep" : // with 4x core we need to clear hilite set, because it's not synced with selection set "clearHilited" : // when event is triggered not by a Tool, we need to clear everything "clearAll", }); case "add": return void from(this._imodelHiliteSetProvider.getHiliteSetProvider({ imodelKey: this._imodelAccess.key }).getHiliteSet({ selectables })) .pipe(takeUntil(this._cancelOngoingChanges)) .subscribe({ next: (set) => { this.addHiliteSet(set); }, }); case "remove": return void from(this._imodelHiliteSetProvider.getHiliteSetProvider({ imodelKey: this._imodelAccess.key }).getHiliteSet({ selectables })) .pipe(takeUntil(this._cancelOngoingChanges)) .subscribe({ next: (set) => { this.removeHiliteSet(set); }, complete: () => { this.applyCurrentHiliteSet({ activeSelectionAction: "keep" }); }, }); } } onUnifiedSelectionChanged = (args) => { // iModels are only interested in top-level selection changes if (args.imodelKey !== this._imodelAccess.key || args.level !== 0) { return; } // update the selection storage change tracker so we know the selection storage changed this._selectionStorageChangeTracker = (this._selectionStorageChangeTracker + 1) % Number.MAX_SAFE_INTEGER; if (args.changeType === "replace" || args.changeType === "clear") { this._cancelOngoingChanges.next(); } this.syncHiliteSet(args); }; applyCurrentHiliteSet({ activeSelectionAction }) { if (activeSelectionAction !== "keep") { const env_1 = { stack: [], error: void 0, hasError: false }; try { const _dispose = __addDisposableResource(env_1, this.suspendIModelToolSelectionSync(), false); if (!is5xSelectionSet(this._imodelAccess.selectionSet)) { this._imodelAccess.hiliteSet.clear(); } if (activeSelectionAction === "clearAll") { this._imodelAccess.selectionSet.emptyAll(); } } catch (e_1) { env_1.error = e_1; env_1.hasError = true; } finally { __disposeResources(env_1); } } from(this._imodelHiliteSetProvider.getCurrentHiliteSet({ imodelKey: this._imodelAccess.key })) .pipe(takeUntil(this._cancelOngoingChanges)) .subscribe({ next: (ids) => { this.addHiliteSet(ids); }, }); } addHiliteSet(set) { const env_2 = { stack: [], error: void 0, hasError: false }; try { const _dispose = __addDisposableResource(env_2, this.suspendIModelToolSelectionSync(), false); if (is5xSelectionSet(this._imodelAccess.selectionSet)) { // with 5.x core we can simply add the set as a whole this._imodelAccess.selectionSet.add({ models: set.models, subcategories: set.subCategories, elements: set.elements, }); } else { // pre-5.0 core requires adding models and subcategories to hilite set separately if (set.models.length) { this._imodelAccess.hiliteSet.models.addIds(set.models); } if (set.subCategories.length) { this._imodelAccess.hiliteSet.subcategories.addIds(set.subCategories); } if (set.elements.length) { this._imodelAccess.hiliteSet.elements.addIds(set.elements); this._imodelAccess.selectionSet.add(set.elements); } } } catch (e_2) { env_2.error = e_2; env_2.hasError = true; } finally { __disposeResources(env_2); } } removeHiliteSet(set) { const env_3 = { stack: [], error: void 0, hasError: false }; try { const _dispose = __addDisposableResource(env_3, this.suspendIModelToolSelectionSync(), false); if (is5xSelectionSet(this._imodelAccess.selectionSet)) { // with 5.x core we can simply remove the set as a whole this._imodelAccess.selectionSet.remove({ models: set.models, subcategories: set.subCategories, elements: set.elements, }); } else { if (set.models.length) { this._imodelAccess.hiliteSet.models.deleteIds(set.models); } if (set.subCategories.length) { this._imodelAccess.hiliteSet.subcategories.deleteIds(set.subCategories); } if (set.elements.length) { this._imodelAccess.hiliteSet.elements.deleteIds(set.elements); this._imodelAccess.selectionSet.remove(set.elements); } } } catch (e_3) { env_3.error = e_3; env_3.hasError = true; } finally { __disposeResources(env_3); } } onIModelSelectionChanged = async (event) => { if (this._isSuspended) { return; } if (CoreSelectionSetEventType.Clear === event.type) { this._selectionStorage.clearSelection({ imodelKey: this._imodelAccess.key, source: this._selectionSourceName }); return; } const ids = getSelectionSetChangeIds(event); const scopedSelection = merge(ids.elements ? from(computeSelection({ queryExecutor: this._imodelAccess, elementIds: ids.elements, scope: this._activeScopeProvider(), componentId: this.#componentId, })) : /* c8 ignore next */ EMPTY, ids.models ? from(ids.models).pipe(map((id) => ({ className: "BisCore.Model", id }))) : /* c8 ignore next */ EMPTY, ids.subcategories ? from(ids.subcategories).pipe(map((id) => ({ className: "BisCore.SubCategory", id }))) : /* c8 ignore next */ EMPTY); const selectionStorageVersion = this._selectionStorageChangeTracker; const changeSelectionStorageProps = { imodelKey: this._imodelAccess.key, source: this._selectionSourceName, selectables: await firstValueFrom(scopedSelection.pipe(toArray())), }; try { switch (event.type) { case CoreSelectionSetEventType.Add: return this._selectionStorage.addToSelection(changeSelectionStorageProps); case CoreSelectionSetEventType.Remove: return this._selectionStorage.removeFromSelection(changeSelectionStorageProps); case CoreSelectionSetEventType.Replace: return this._selectionStorage.replaceSelection(changeSelectionStorageProps); } } finally { if (this._selectionStorageChangeTracker === selectionStorageVersion) { // if the storage wasn't changed while we were processing the selection change, we have to re-sync the // hilite set (otherwise it's done by the selection storage change handler) this.syncHiliteSet({ ...changeSelectionStorageProps, selectables: Selectables.create(changeSelectionStorageProps.selectables), changeType: getUnifiedSelectionChangeType(event.type), }); } } }; } function getSelectionSetChangeIds(event) { switch (event.type) { case CoreSelectionSetEventType.Add: return event.additions ?? (event.added ? { elements: event.added } : /* c8 ignore next */ {}); case CoreSelectionSetEventType.Remove: return event.removals ?? (event.removed ? { elements: event.removed } : /* c8 ignore next */ {}); case CoreSelectionSetEventType.Replace: return "active" in event.set ? event.set.active : { elements: event.set.elements }; } } function getUnifiedSelectionChangeType(coreChangeType) { switch (coreChangeType) { case CoreSelectionSetEventType.Add: return "add"; case CoreSelectionSetEventType.Remove: return "remove"; case CoreSelectionSetEventType.Replace: return "replace"; case CoreSelectionSetEventType.Clear: return "clear"; } } function is5xSelectionSet(selectionSet) { return "active" in selectionSet; } /* c8 ignore start */ function createIModelHiliteSetProviderFromCachingProvider(cachingHiliteSetProvider, hiliteSetProvider) { return { getHiliteSetProvider: () => hiliteSetProvider, getCurrentHiliteSet: (props) => cachingHiliteSetProvider.getHiliteSet(props), [Symbol.dispose]: () => { }, }; } /* c8 ignore end */ //# sourceMappingURL=EnableUnifiedSelectionSyncWithIModel.js.map