import { inject, injectable, named, optional } from 'inversify'; import { PipelineDimension, PipelineDispatcher, PipelineRegistry, PipelineRenderer, } from './pipeline'; import { PlaygroundContribution, PlaygroundRegistry } from './playground-contribution'; import { PlaygroundConfig } from './playground-config'; import { ContributionProvider, Disposable, DisposableCollection, Emitter, Event, debounce } from '@gedit/utils'; import { ConfigEntity, Entity, EntityManager, EntityRegistry, AbleDispatchEvent, AbleManager, PlaygroundContext, TransformData } from '../common'; import { EditorState, EditorStateConfigEntity, PathPointSelectionEntity, PlaygroundConfigEntity, PlaygroundConfigRevealOpts, } from './layer/config'; import { PlaygroundCommandRegistry, PlaygroundId, toContextMenuPath } from './playground-registries'; import { ContextMenuRenderer } from '@gedit/layout'; import { Selectable, SelectPayload, SelectState, syncToSelection } from './able'; import { MenuPath, SelectionService } from '@gedit/application-common'; import { PlaygroundContextKeyService } from './playground-context-key-service'; import '../../src/browser/style/index.less'; import { domUtils } from '@gedit/utils/lib/browser'; import { Rectangle } from '@gedit/math'; import { SelectorExtendContribution, SelectorExtendContributionRegistry } from './selector-extend-contribution'; const playgroundInstances: Set = new Set(); @injectable() export class Playground implements Disposable { readonly toDispose = new DisposableCollection(); readonly node: HTMLElement; private _focused = false; readonly onBlur: Event; readonly onFocus: Event; readonly onZoom: Event; readonly onScroll: Event<{ scrollX: number, scrollY: number }>; private onRestoreStateEmitter = new Emitter(); private onContextmenuEmitter = new Emitter(); private onContextmenuHideEmitter = new Emitter(); readonly onRestoreState = this.onRestoreStateEmitter.event; onContextmenu(onShow: (e: MouseEvent) => void, onHide?: (e: MouseEvent) => void): Disposable { const dispose1 = this.onContextmenuEmitter.event(onShow); const dispose2 = onHide ? this.onContextmenuHideEmitter.event(onHide) : undefined; return Disposable.create(() => { dispose1.dispose(); dispose2?.dispose(); }); } static getSelection(selectionService: SelectionService): Entity[] { const selection = selectionService.selection; if (!selection || !Array.isArray(selection)) return []; if (selection.find(s => !(s instanceof Entity) || !s.hasAble(Selectable))) return []; return selection; } static getAllInstances(): Playground[] { const result: Playground[] = []; for (const p of playgroundInstances.values()) { result.push(p); } return result; } constructor( @inject(PlaygroundId) readonly id: PlaygroundId, @inject(EntityManager) readonly entityManager: EntityManager, @inject(AbleManager) readonly ableManager: AbleManager, @inject(PlaygroundRegistry) readonly registry: PlaygroundRegistry, @inject(SelectorExtendContributionRegistry) readonly selectorExtendRegistry: SelectorExtendContributionRegistry, @inject(PlaygroundContext) readonly context: CONTEXT, @inject(PipelineRenderer) protected readonly pipelineRenderer: PipelineRenderer, @inject(PlaygroundCommandRegistry) protected readonly commands: PlaygroundCommandRegistry, @inject(PipelineRegistry) protected readonly pipelineRegistry: PipelineRegistry, @inject(PipelineDispatcher) protected readonly dispatcher: PipelineDispatcher, @inject(PlaygroundConfig) protected readonly playgroundConfig: PlaygroundConfig, @inject(ContributionProvider) @named(PlaygroundContribution) protected readonly contributionProvider: ContributionProvider, @inject(ContributionProvider) @named(SelectorExtendContribution) protected readonly selectContributionProvider: ContributionProvider, @inject(PlaygroundContextKeyService) @optional() protected readonly contextKeyService?: PlaygroundContextKeyService, @inject(SelectionService) @optional() protected readonly selectionService?: SelectionService, @inject(ContextMenuRenderer) @optional() protected readonly contextMenuRenderer?: ContextMenuRenderer, ) { this.toDispose.pushAll([ this.pipelineRenderer, this.pipelineRegistry, this.entityManager, this.ableManager, this.onContextmenuEmitter, Disposable.create(() => { playgroundInstances.delete(this); this.node.remove(); }) ]); playgroundInstances.add(this); // Deafult entities added const editStates = this.entityManager.createEntity(EditorStateConfigEntity); editStates.playgroundId = this.id; this.entityManager.createEntity(PlaygroundConfigEntity); this.entityManager.createEntity(PathPointSelectionEntity); this.node = playgroundConfig.node || document.createElement('div'); this.node.classList.add('gedit-playground'); if (playgroundConfig.layers) playgroundConfig.layers.forEach(layer => this.registry.registerLayer(layer)); if (playgroundConfig.ables) playgroundConfig.ables.forEach(able => this.ableManager.registerAble(able)); if (playgroundConfig.entities) playgroundConfig.entities.forEach(entity => this.entityManager.registerEntity(entity)); if (playgroundConfig.editorStates) playgroundConfig.editorStates.forEach(state => editStates.registerState(state)); if (playgroundConfig.zoomEnable !== undefined) this.zoomEnable = playgroundConfig.zoomEnable; if (playgroundConfig.entityConfigs) { for (const [k, v] of playgroundConfig.entityConfigs) { const entity = this.entityManager.getEntity(k, true); entity?.updateConfig(v); } } // 绑定focus if (playgroundConfig.autoFocus) { this.node.addEventListener('blur', () => { this.blur(); }); this.node.addEventListener('focus', () => { this.focus(); }); } // 同步到selection if (this.selectionService) { this.toDispose.push(this.entityManager.onEntityChanged((type: string) => { if (!this.isFocused || this.entityManager.isConfigEntity(type)) return; this.syncToSelection(); })); } this.node.tabIndex = 0; this.node.appendChild(this.pipelineRenderer.node); this.onBlur = this.pipelineRegistry.onBlurEmitter.event; this.onFocus = this.pipelineRegistry.onFocusEmitter.event; this.onZoom = this.pipelineRegistry.onZoomEmitter.event; this.onScroll = this.pipelineRegistry.onScrollEmitter.event; if (this.contextMenuRenderer) { this.node.addEventListener('contextmenu', e => { // 选择元素更新会有延迟,所以这里要等选中元素更新后再执行 setTimeout(() => { this.onContextmenuEmitter.fire(e); this.contextMenuRenderer!.render({ menuPath: this.editorState.is(EditorState.EDIT_PATH_STATE.id) ? this.contextMenuPathEditPath : this.contextMenuPath, anchor: {x: e.clientX, y: e.clientY}, args: [{ playgroundId: this.id, clientX: e.clientX, clientY: e.clientY }], onHide: () => this.onContextmenuHideEmitter.fire(e) }); }, 50); e.preventDefault(); // 阻止默认 e.stopPropagation(); }); } const contributions = this.contributionProvider.getContributions(); for (const contrib of contributions) { if (contrib.registerPlayground) { contrib.registerPlayground(this.registry); }; } const selectContributions = this.selectContributionProvider.getContributions(); for (const contrib of selectContributions) { if (contrib.registerExtendSchema) { contrib.registerExtendSchema(this.selectorExtendRegistry); }; } this.pipelineRenderer.layers.forEach(layer => { layer.reloadEntities(); }); } protected syncToSelection = debounce(() => { if (!this.isFocused) return; syncToSelection(this.selectedNodes, this.selectionService!); }, 50); setParent(parent: HTMLElement): void { parent.appendChild(this.node); this.resize(); } get onDispatch(): Event { return this.ableManager.onAbleDispatch; } /** * 对应的右键菜单路径 */ get contextMenuPath(): string[] { return this.playgroundConfig.contextMenuPath ? this.playgroundConfig.contextMenuPath : toContextMenuPath(this.id); } get contextMenuPathEditPath(): string[] { return this.playgroundConfig.contextMenuPathEditPath ? this.playgroundConfig.contextMenuPathEditPath : toContextMenuPath(this.id); } get zoomEnable(): boolean { return this.config.zoomEnable; } set zoomEnable(zoomEnable) { this.config.zoomEnable = zoomEnable; } /** * 转换为内部的命令id * @param commandId */ toPlaygroundCommandId(commandId: string): string { return this.registry.commands.toCommandId(commandId); } /** * 转换为内部的右键菜单路径 * @param menuPath */ toPlaygroundContextMenuPath(menuPath: MenuPath): MenuPath { return this.registry.menus.toMenuPath(menuPath); } /** * 通知所有关联able的entity */ dispatch

(payloadKey: string | Symbol, payload: P): string[] { return this.ableManager.dispatch(payloadKey, payload); } /** * 刷新所有layer */ flush(): void { this.pipelineRenderer.flush(); } /** * 浏览器关闭触发,并存储layer的数据 */ storeState(): object { return { entities: this.entityManager.storeState(), }; } /** * 执行命令 * @param commandId * @param args */ // eslint-disable-next-line @typescript-eslint/no-explicit-any execCommand(commandId: string, ...args: any[]): Promise { return this.commands.executeCommand(commandId, ...args); } private isReady = false; ready(oldState?: object): void { if (this.isReady) return; this.isReady = true; if (oldState) this.restoreState(oldState); this.pipelineRegistry.ready(); this.pipelineRenderer.ready(); const contributions = this.contributionProvider.getContributions(); for (const contrib of contributions) { if (contrib.onReady) contrib.onReady(this); } if (this.playgroundConfig.autoResize) { const resize = debounce(() => { this.resize(); }, 100); // @ts-ignore if (ResizeObserver) { // @ts-ignore const resizeObserver = new ResizeObserver(resize); resizeObserver.observe(this.node); this.toDispose.push(Disposable.create(() => { resizeObserver.disconnect(); })); } else { this.toDispose.push(domUtils.addStandardDisposableListener(window.document.body, 'resize', resize)); } this.toDispose.push(domUtils.addStandardDisposableListener(window.document, 'scroll', resize)); } this.resize(); } /** * 浏览器强制刷新后会从localStorage读取layer的状态 * @param oldState */ restoreState(oldState: object): void { // eslint-disable-next-line @typescript-eslint/no-explicit-any this.entityManager.restoreState((oldState as any || {}).entities); this.onRestoreStateEmitter.fire(oldState); } /** * 切换Tab,或者关闭情况,需要清空重制画布 */ resetState(): void { this.entityManager.reset(); } /** * 按下边顺序执行 * 1. 指定的entity位置或pos位置 * 2. selection位置 * 3. 初始化位置 */ scrollToView(opts?: PlaygroundConfigRevealOpts): Promise { const playgroundEntity = this.entityManager.getEntity(PlaygroundConfigEntity)!; return playgroundEntity.scrollToView(opts); } /** * 这里会由widget透传进来 * @param msg */ resize(msg?: PipelineDimension): void { if (!msg) { const boundingRect = this.node.getBoundingClientRect(); msg = { clientX: boundingRect.left, clientY: boundingRect.top, width: boundingRect.width, height: boundingRect.height }; } this.pipelineRegistry.onResizeEmitter.fire(msg); } /** * 触发focus */ focus(): void { if (this._focused) return; if (this.contextKeyService) { this.contextKeyService.playgroundFocus.set(true); } this._focused = true; this.pipelineRegistry.onFocusEmitter.fire(); this.syncToSelection(); } /** * 触发blur */ blur(): void { if (!this._focused) return; if (this.contextKeyService) { this.contextKeyService.playgroundFocus.set(false); } this._focused = false; this.pipelineRegistry.onBlurEmitter.fire(); } isFocused(): boolean { return this._focused; } /** * @deprecated */ get configEntity(): PlaygroundConfigEntity { return this.config; } /** * 画布配置数据 */ get config(): PlaygroundConfigEntity { return this.entityManager.getEntity(PlaygroundConfigEntity)!; } /** * 画布编辑状态管理 */ get editorState(): EditorStateConfigEntity { return this.entityManager.getEntity(EditorStateConfigEntity)!; } /** * 获取 path selection */ get pathPointSelection(): PathPointSelectionEntity { return this.entityManager.getEntity(PathPointSelectionEntity)!; } getConfigEntity(r: EntityRegistry): T { return this.entityManager.getEntity(r, true) as T; } dispose(): void { if (this.disposed) return; const contributions = this.contributionProvider.getContributions(); for (const contrib of contributions) { if (contrib.onDispose) contrib.onDispose(this); } this.toDispose.dispose(); } get selectedNodes(): Entity[] { return this.entityManager.getEntitiesByAble(Selectable).filter(node => { const selectState = node.getData(SelectState); return selectState && selectState.selected; }); } get selectedBounds(): Rectangle { const selectedNodes = this.selectedNodes; const selectedTransforms: TransformData[] = selectedNodes.map(node => node.getData(TransformData)!); return Rectangle.enlarge(selectedTransforms.map(t => t.bounds)); } /** * 选择节点 * @param entities */ selectNodes(...entities: Entity[]): void { entities.forEach((entity, i) => { const index = Selectable.getZIndex(entity); if (index !== undefined) { this.dispatch(SelectPayload, { zIndex: index, additional: i !== 0, selectionService: this.selectionService }); } }); } get disposed(): boolean { return this.toDispose.disposed; } }