sprotty
Version:
A next-gen framework for graphical views
280 lines (247 loc) • 11.9 kB
text/typescript
/********************************************************************************
* Copyright (c) 2017-2021 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { inject, injectable, optional } from 'inversify';
import { VNode } from 'snabbdom';
import {
Action, BringToFrontAction, GetSelectionAction, ResponseAction, SelectAction, SelectAllAction, SelectionResult
} from 'sprotty-protocol/lib/actions';
import { Selectable } from 'sprotty-protocol/lib/model';
import { Command, CommandExecutionContext } from '../../base/commands/command';
import { ModelRequestCommand } from '../../base/commands/request-command';
import { SChildElementImpl, SModelElementImpl, SModelRootImpl, SParentElementImpl } from '../../base/model/smodel';
import { findParentByFeature } from '../../base/model/smodel-utils';
import { TYPES } from '../../base/types';
import { KeyListener } from '../../base/views/key-tool';
import { MouseListener } from '../../base/views/mouse-tool';
import { setClass } from '../../base/views/vnode-utils';
import { isCtrlOrCmd } from '../../utils/browser';
import { toArray } from '../../utils/iterable';
import { matchesKeystroke } from '../../utils/keyboard';
import { ButtonHandlerRegistry } from '../button/button-handler';
import { SButtonImpl } from '../button/model';
import { SwitchEditModeAction } from '../edit/edit-routing';
import { SRoutingHandleImpl } from '../routing/model';
import { SRoutableElementImpl } from '../routing/model';
import { findViewportScrollbar } from '../viewport/scroll';
import { isSelectable } from './model';
export class SelectCommand extends Command {
static readonly KIND = SelectAction.KIND;
protected selected: (SChildElementImpl & Selectable)[] = [];
protected deselected: (SChildElementImpl & Selectable)[] = [];
constructor( public action: SelectAction) {
super();
}
execute(context: CommandExecutionContext): SModelRootImpl {
const model = context.root;
this.action.selectedElementsIDs.forEach(id => {
const element = model.index.getById(id);
if (element instanceof SChildElementImpl && isSelectable(element)) {
this.selected.push(element);
}
});
this.action.deselectedElementsIDs.forEach(id => {
const element = model.index.getById(id);
if (element instanceof SChildElementImpl && isSelectable(element)) {
this.deselected.push(element);
}
});
return this.redo(context);
}
undo(context: CommandExecutionContext): SModelRootImpl {
for (const element of this.selected) {
element.selected = false;
}
for (const element of this.deselected) {
element.selected = true;
}
return context.root;
}
redo(context: CommandExecutionContext): SModelRootImpl {
for (const element of this.deselected) {
element.selected = false;
}
for (const element of this.selected) {
element.selected = true;
}
return context.root;
}
}
export class SelectAllCommand extends Command {
static readonly KIND = SelectAllAction.KIND;
protected previousSelection: Record<string, boolean> = {};
constructor( protected readonly action: SelectAllAction) {
super();
}
execute(context: CommandExecutionContext): SModelRootImpl {
this.selectAll(context.root, this.action.select);
return context.root;
}
protected selectAll(element: SParentElementImpl, newState: boolean): void {
if (isSelectable(element)) {
this.previousSelection[element.id] = element.selected;
element.selected = newState;
}
for (const child of element.children) {
this.selectAll(child, newState);
}
}
undo(context: CommandExecutionContext): SModelRootImpl {
const index = context.root.index;
Object.keys(this.previousSelection).forEach(id => {
const element = index.getById(id);
if (element !== undefined && isSelectable(element))
element.selected = this.previousSelection[id];
});
return context.root;
}
redo(context: CommandExecutionContext): SModelRootImpl {
this.selectAll(context.root, this.action.select);
return context.root;
}
}
export class SelectMouseListener extends MouseListener {
protected buttonHandlerRegistry: ButtonHandlerRegistry;
wasSelected = false;
hasDragged = false;
isMouseDown = false;
override mouseDown(target: SModelElementImpl, event: MouseEvent): (Action | Promise<Action>)[] {
if (event.button !== 0) {
return [];
}
this.isMouseDown = true;
const buttonHandled = this.handleButton(target, event);
if (buttonHandled) {
return buttonHandled;
}
const selectableTarget = findParentByFeature(target, isSelectable);
if (selectableTarget !== undefined || target instanceof SModelRootImpl) {
this.hasDragged = false;
}
if (selectableTarget !== undefined) {
let deselectedElements: SModelElementImpl[] = [];
// multi-selection?
if (!isCtrlOrCmd(event)) {
deselectedElements = this.collectElementsToDeselect(target, selectableTarget);
}
if (!selectableTarget.selected) {
this.wasSelected = false;
return this.handleSelectTarget(selectableTarget, deselectedElements, event);
} else if (isCtrlOrCmd(event)) {
this.wasSelected = false;
return this.handleDeselectTarget(selectableTarget, event);
} else {
this.wasSelected = true;
}
}
return [];
}
protected collectElementsToDeselect(target: SModelElementImpl, selectableTarget: (SModelElementImpl & Selectable) | undefined): SModelElementImpl[] {
return toArray(target.root.index.all()
.filter(element => isSelectable(element) && element.selected
&& !(selectableTarget instanceof SRoutingHandleImpl && element === selectableTarget.parent as SModelElementImpl)));
}
protected handleButton(target: SModelElementImpl, event: MouseEvent): (Action | Promise<Action>)[] | undefined {
if (this.buttonHandlerRegistry !== undefined && target instanceof SButtonImpl && target.enabled) {
const buttonHandler = this.buttonHandlerRegistry.get(target.type);
if (buttonHandler !== undefined) {
return buttonHandler.buttonPressed(target);
}
}
return undefined;
}
protected handleSelectTarget(selectableTarget: SModelElementImpl & Selectable, deselectedElements: SModelElementImpl[], event: MouseEvent): (Action | Promise<Action>)[] {
const result: Action[] = [];
result.push(SelectAction.create({ selectedElementsIDs: [selectableTarget.id], deselectedElementsIDs: deselectedElements.map(e => e.id) }));
result.push(BringToFrontAction.create([selectableTarget.id]));
const routableDeselect = deselectedElements.filter(e => e instanceof SRoutableElementImpl).map(e => e.id);
if (selectableTarget instanceof SRoutableElementImpl) {
result.push(SwitchEditModeAction.create({ elementsToActivate: [selectableTarget.id], elementsToDeactivate: routableDeselect }));
} else if (routableDeselect.length > 0) {
result.push(SwitchEditModeAction.create({ elementsToDeactivate: routableDeselect }));
}
return result;
}
protected handleDeselectTarget(selectableTarget: SModelElementImpl & Selectable, event: MouseEvent): (Action | Promise<Action>)[] {
const result: Action[] = [];
result.push(SelectAction.create({ selectedElementsIDs: [], deselectedElementsIDs: [selectableTarget.id] }));
if (selectableTarget instanceof SRoutableElementImpl) {
result.push(SwitchEditModeAction.create({ elementsToDeactivate: [selectableTarget.id] }));
}
return result;
}
protected handleDeselectAll(deselectedElements: SModelElementImpl[], event: MouseEvent): (Action | Promise<Action>)[] {
const result: Action[] = [];
result.push(SelectAction.create({ selectedElementsIDs: [], deselectedElementsIDs: deselectedElements.map(e => e.id) }));
const routableDeselect = deselectedElements.filter(e => e instanceof SRoutableElementImpl).map(e => e.id);
if (routableDeselect.length > 0) {
result.push(SwitchEditModeAction.create({ elementsToDeactivate: routableDeselect }));
}
return result;
}
override mouseMove(target: SModelElementImpl, event: MouseEvent): Action[] {
this.hasDragged = this.isMouseDown;
return [];
}
override mouseUp(target: SModelElementImpl, event: MouseEvent): (Action | Promise<Action>)[] {
if (event.button === 0) {
if (!this.hasDragged) {
const selectableTarget = findParentByFeature(target, isSelectable);
if (selectableTarget !== undefined) {
if (this.wasSelected) {
return [SelectAction.create({ selectedElementsIDs: [selectableTarget.id], deselectedElementsIDs: [] })];
}
} else if ((target instanceof SModelRootImpl && !findViewportScrollbar(event)) || !(target instanceof SModelRootImpl)) {
// Mouse up on everything that's not root or root but not over ViewPort's scroll bars > deselect all
return this.handleDeselectAll(this.collectElementsToDeselect(target, undefined), event);
}
}
}
this.isMouseDown = false;
this.hasDragged = false;
return [];
}
override decorate(vnode: VNode, element: SModelElementImpl): VNode {
const selectableTarget = findParentByFeature(element, isSelectable);
if (selectableTarget !== undefined) {
setClass(vnode, 'selected', selectableTarget.selected);
}
return vnode;
}
}
export class GetSelectionCommand extends ModelRequestCommand {
static readonly KIND = GetSelectionAction.KIND;
protected previousSelection: Record<string, boolean> = {};
constructor( protected readonly action: GetSelectionAction) {
super();
}
protected retrieveResult(context: CommandExecutionContext): ResponseAction {
const selection = context.root.index.all()
.filter(e => isSelectable(e) && e.selected)
.map(e => e.id);
return SelectionResult.create(toArray(selection), this.action.requestId);
}
}
export class SelectKeyboardListener extends KeyListener {
override keyDown(element: SModelElementImpl, event: KeyboardEvent): Action[] {
if (matchesKeystroke(event, 'KeyA', 'ctrlCmd')) {
return [ SelectAllAction.create()];
}
return [];
}
}