Skip to content

Commit

Permalink
add support for remote selection (#323)
Browse files Browse the repository at this point in the history
Co-authored-by: Matthias Lehner <[email protected]>
  • Loading branch information
loreanvictor and matthiaslehnertum authored Nov 24, 2023
1 parent 4b4a1ef commit bbd593c
Show file tree
Hide file tree
Showing 16 changed files with 266 additions and 9 deletions.
24 changes: 24 additions & 0 deletions src/main/apollon-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,30 @@ export class ApollonEditor {
return id;
}

/**
* Displays given elements and relationships as selected or deselected by
* a given remote selector, identified by a name and a color.
* @param selectorName name of the remote selector
* @param selectorColor color of the remote selector
* @param select ids of elements and relationships to be selected
* @param deselect ids of elements and relationships to be deselected
*/
remoteSelect(selectorName: string, selectorColor: string, select: string[], deselect?: string[]): void {
this.store?.dispatch(
UMLElementRepository.remoteSelectDeselect({ name: selectorName, color: selectorColor }, select, deselect || []),
);
}

/**
* Allows a given set of remote selectors for remotely selecting and deselecting
* elements and relationships, removing all other selectors. This won't have an effect
* on future remote selections.
* @param allowedSelectors allowed remote selectors
*/
pruneRemoteSelectors(allowedSelectors: { name: string; color: string }[]): void {
this.store?.dispatch(UMLElementRepository.pruneRemoteSelectors(allowedSelectors));
}

/**
* Removes error subscription, so that the corresponding callback is no longer executed when an error occurs.
* @param subscriptionId subscription identifier
Expand Down
2 changes: 2 additions & 0 deletions src/main/components/store/model-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { UMLDiagram } from '../../services/uml-diagram/uml-diagram';
import { CopyState } from '../../services/copypaste/copy-types';
import { LastActionState } from '../../services/last-action/last-action-types';
import { arrayToInclusionMap, inclusionMapToArray } from './util';
import { RemoteSelectionState } from '../../services/uml-element/remote-selectable/remote-selectable-types';

export type PartialModelState = Omit<Partial<ModelState>, 'editor'> & { editor?: Partial<EditorState> };

Expand All @@ -35,6 +36,7 @@ export interface ModelState {
diagram: UMLDiagramState;
hovered: HoverableState;
selected: SelectableState;
remoteSelection: RemoteSelectionState;
moving: MovableState;
resizing: ResizableState;
connecting: ConnectableState;
Expand Down
9 changes: 6 additions & 3 deletions src/main/components/uml-element/canvas-element.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { UMLElementRepository } from '../../services/uml-element/uml-element-rep
import { ModelState } from '../store/model-state';
import { withTheme, withThemeProps } from '../theme/styles';
import { UMLElementComponentProps } from './uml-element-component-props';
import { UMLElementSelectorType } from '../../packages/uml-element-selector-type';

const STROKE = 5;

Expand All @@ -19,6 +20,7 @@ type OwnProps = { child?: ComponentClass<UMLElementComponentProps> } & UMLElemen
type StateProps = {
hovered: boolean;
selected: boolean;
remoteSelectors: UMLElementSelectorType[];
moving: boolean;
interactive: boolean;
interactable: boolean;
Expand All @@ -36,6 +38,7 @@ const enhance = compose<ComponentClass<OwnProps>>(
(state, props) => ({
hovered: state.hovered[0] === props.id,
selected: state.selected.includes(props.id),
remoteSelectors: state.remoteSelection[props.id] || [],
moving: state.moving.includes(props.id),
interactive: state.interactive.includes(props.id),
interactable: state.editor.view === ApollonView.Exporting || state.editor.view === ApollonView.Highlight,
Expand All @@ -51,6 +54,7 @@ class CanvasElementComponent extends Component<Props> {
const {
hovered,
selected,
remoteSelectors,
moving,
interactive,
interactable,
Expand Down Expand Up @@ -79,7 +83,6 @@ class CanvasElementComponent extends Component<Props> {
? element.fillColor
: theme.color.background;

const selectedByList = element.selectedBy || [];
return (
<svg
{...props}
Expand All @@ -105,9 +108,9 @@ class CanvasElementComponent extends Component<Props> {
pointerEvents="none"
/>
)}
{selectedByList.length > 0 && (
{remoteSelectors.length > 0 && (
<g>
{selectedByList.map((selectedBy, index) => {
{remoteSelectors.map((selectedBy, index) => {
const indicatorPosition = 'translate(' + (element.bounds.width + STROKE) + ' ' + index * 32 + ')';
return (
<g key={selectedBy.name + '_' + selectedBy.color} id={selectedBy.name + '_' + selectedBy.color}>
Expand Down
15 changes: 15 additions & 0 deletions src/main/components/uml-element/canvas-relationship.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import { getClientEventCoordinates } from '../../utils/touch-event';
import { ModelState } from '../store/model-state';
import { withTheme, withThemeProps } from '../theme/styles';
import { UMLElementComponentProps } from './uml-element-component-props';
import { UMLElementSelectorType } from '../../packages/uml-element-selector-type';

type OwnProps = UMLElementComponentProps & SVGProps<SVGSVGElement>;

type StateProps = {
hovered: boolean;
selected: boolean;
remoteSelectors: UMLElementSelectorType[];
interactive: boolean;
interactable: boolean;
reconnecting: boolean;
Expand Down Expand Up @@ -55,6 +57,7 @@ const enhance = compose<ComponentClass<OwnProps>>(
(state, props) => ({
hovered: state.hovered[0] === props.id,
selected: state.selected.includes(props.id),
remoteSelectors: state.remoteSelection[props.id] || [],
interactive: state.interactive.includes(props.id),
interactable: state.editor.view === ApollonView.Exporting || state.editor.view === ApollonView.Highlight,
reconnecting: !!state.reconnecting[props.id],
Expand All @@ -77,6 +80,7 @@ export class CanvasRelationshipComponent extends Component<Props, State> {
const {
hovered,
selected,
remoteSelectors,
interactive,
interactable,
reconnecting,
Expand Down Expand Up @@ -127,6 +131,17 @@ export class CanvasRelationshipComponent extends Component<Props, State> {
pointerEvents={disabled ? 'none' : 'stroke'}
>
<polyline points={points} stroke={highlight} fill="none" strokeWidth={STROKE} />
{remoteSelectors.length > 0 &&
remoteSelectors.map((selector) => (
<polyline
key={selector.name}
points={points}
stroke={selector.color}
strokeOpacity="0.2"
strokeWidth={STROKE}
fill="none"
/>
))}
<ChildComponent element={UMLRelationshipRepository.get(relationship)} />
{children}
{midPoints.map((point, index) => {
Expand Down
1 change: 0 additions & 1 deletion src/main/packages/uml-element-selector-type.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export type UMLElementSelectorType = {
elementId: string;
name: string;
color: string;
};
2 changes: 2 additions & 0 deletions src/main/services/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { MovingActions } from './uml-element/movable/moving-types';
import { ResizableActions } from './uml-element/resizable/resizable-types';
import { ResizingActions } from './uml-element/resizable/resizing-types';
import { SelectableActions } from './uml-element/selectable/selectable-types';
import { RemoteSelectionActions } from './uml-element/remote-selectable/remote-selectable-types';
import { UMLElementActions } from './uml-element/uml-element-types';
import { UpdatableActions } from './uml-element/updatable/updatable-types';
import { ReconnectableActions } from './uml-relationship/reconnectable/reconnectable-types';
Expand All @@ -36,6 +37,7 @@ export type Actions =
| ResizableActions
| ResizingActions
| SelectableActions
| RemoteSelectionActions
| UpdatableActions
| AssessmentActions
| UndoActions
Expand Down
2 changes: 2 additions & 0 deletions src/main/services/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { ReconnectableReducer } from './uml-relationship/reconnectable/reconnect
import { UMLRelationshipReducer } from './uml-relationship/uml-relationship-reducer';
import { CopyReducer } from './copypaste/copy-reducer';
import { LastActionReducer } from './last-action/last-action-reducer';
import { RemoteSelectionReducer } from './uml-element/remote-selectable/remote-selection-reducer';

const reduce =
<S, T extends Action>(intial: S, ...reducerList: Reducer<S, T>[]): Reducer<S, T> =>
Expand All @@ -40,6 +41,7 @@ export const reducers: ReducersMapObject<ModelState, Actions> = {
updating: UpdatableReducer,
copy: CopyReducer,
lastAction: LastActionReducer,
remoteSelection: RemoteSelectionReducer,
elements: reduce<UMLElementState, Actions>(
{},
UMLContainerReducer,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { UMLElementSelectorType } from '../../../packages/uml-element-selector-type';
import { Action } from '../../../utils/actions/actions';
import { UMLElementState } from '../uml-element-types';

export const enum RemoteSelectionActionTypes {
SELECTION_CHANGE = '@@element/remote-selection/CHANGE',
PRUNE_SELECTORS = '@@element/remote-selection/PRUNE_SELECTORS',
}

export const enum RemoteSelectionChangeTypes {
SELECT = '@@element/remote-selection/SELECT',
DESELECT = '@@element/remote-selection/DESELECT',
}

export interface RemoteSelectionChange {
type: RemoteSelectionChangeTypes.SELECT | RemoteSelectionChangeTypes.DESELECT;
id: string;
}

export type RemoteSelectionChangeAction = Action<RemoteSelectionActionTypes.SELECTION_CHANGE> & {
payload: {
changes: RemoteSelectionChange[];
selector: UMLElementSelectorType;
};
};

export type RemoteSelectionPruneSelectorsAction = Action<RemoteSelectionActionTypes.PRUNE_SELECTORS> & {
payload: {
allowedSelectors: UMLElementSelectorType[];
};
};

export type RemoteSelectionState = {
[id: string]: UMLElementSelectorType[];
};

export type RemoteSelectionActions = RemoteSelectionChangeAction | RemoteSelectionPruneSelectorsAction;
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Reducer } from 'redux';
import {
RemoteSelectionActionTypes,
RemoteSelectionChangeTypes,
RemoteSelectionState,
} from './remote-selectable-types';
import { Actions } from '../../actions';
import { UMLElementSelectorType } from '../../../packages/uml-element-selector-type';

const sameSelector = (a: UMLElementSelectorType, b: UMLElementSelectorType) => {
return a.name === b.name && a.color === b.color;
};

export const RemoteSelectionReducer: Reducer<RemoteSelectionState, Actions> = (state = {}, action) => {
switch (action.type) {
case RemoteSelectionActionTypes.SELECTION_CHANGE:
const { payload } = action;
const { selector, changes } = payload;

return changes.reduce<RemoteSelectionState>((selection, change) => {
const { id } = change;
const selectors: UMLElementSelectorType[] = [...(selection[id] ?? [])];

if (change.type === RemoteSelectionChangeTypes.SELECT && !selectors.some((s) => sameSelector(s, selector))) {
selectors.push(selector);
} else if (change.type === RemoteSelectionChangeTypes.DESELECT) {
const index = selectors.findIndex((s) => sameSelector(s, selector));
if (index >= 0) {
selectors.splice(index, 1);
}
}

return {
...selection,
[id]: selectors,
};
}, state);

case RemoteSelectionActionTypes.PRUNE_SELECTORS:
const { allowedSelectors } = action.payload;

return Object.fromEntries(
Object.entries(state).map(([id, selectors]) => {
return [id, selectors.filter((s) => allowedSelectors.some((selector) => sameSelector(s, selector)))];
}),
);
}

return state;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { UMLElementSelectorType } from '../../../packages/uml-element-selector-type';
import {
RemoteSelectionActionTypes,
RemoteSelectionChangeTypes,
RemoteSelectionChange,
RemoteSelectionChangeAction,
RemoteSelectionPruneSelectorsAction,
} from './remote-selectable-types';

export const RemoteSelectable = {
remoteSelectionChange: (
selector: UMLElementSelectorType,
changes: RemoteSelectionChange[],
): RemoteSelectionChangeAction => ({
type: RemoteSelectionActionTypes.SELECTION_CHANGE,
payload: {
selector,
changes,
},
undoable: false,
}),

remoteSelect: (selector: UMLElementSelectorType, ids: string[]): RemoteSelectionChangeAction =>
RemoteSelectable.remoteSelectionChange(
selector,
ids.map((id) => ({ type: RemoteSelectionChangeTypes.SELECT, id })),
),

remoteDeselect: (selector: UMLElementSelectorType, ids: string[]): RemoteSelectionChangeAction =>
RemoteSelectable.remoteSelectionChange(
selector,
ids.map((id) => ({ type: RemoteSelectionChangeTypes.DESELECT, id })),
),

remoteSelectDeselect: (
selector: UMLElementSelectorType,
select: string[],
deselect: string[],
): RemoteSelectionChangeAction =>
RemoteSelectable.remoteSelectionChange(selector, [
...select.map((id) => ({ type: RemoteSelectionChangeTypes.SELECT, id })),
...deselect.map((id) => ({ type: RemoteSelectionChangeTypes.DESELECT, id })),
]),

pruneRemoteSelectors: (allowedSelectors: UMLElementSelectorType[]): RemoteSelectionPruneSelectorsAction => ({
type: RemoteSelectionActionTypes.PRUNE_SELECTORS,
payload: {
allowedSelectors,
},
undoable: false,
}),
};
2 changes: 2 additions & 0 deletions src/main/services/uml-element/uml-element-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import { Interactable } from './interactable/interactable-repository';
import { Movable } from './movable/movable-repository';
import { Resizable } from './resizable/resizable-repository';
import { Selectable } from './selectable/selectable-repository';
import { RemoteSelectable } from './remote-selectable/remote-selection-repository';
import { UMLElementCommonRepository } from './uml-element-common-repository';
import { Updatable } from './updatable/updatable-repository';

export const UMLElementRepository = {
...UMLElementCommonRepository,
...Hoverable,
...Selectable,
...RemoteSelectable,
...Movable,
...Resizable,
...Connectable,
Expand Down
4 changes: 0 additions & 4 deletions src/main/services/uml-element/uml-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export interface IUMLElement {
textColor?: string;
/** Note to show for element's assessment */
assessmentNote?: string;
selectedBy?: UMLElementSelectorType[];
isManuallyLayouted?: boolean;
}

Expand Down Expand Up @@ -91,7 +90,6 @@ export abstract class UMLElement implements IUMLElement, ILayoutable {
strokeColor?: string;
textColor?: string;
assessmentNote?: string;
selectedBy?: UMLElementSelectorType[];
resizeFrom: ResizeFrom = ResizeFrom.BOTTOMRIGHT;

constructor(values?: DeepPartial<IUMLElement>) {
Expand Down Expand Up @@ -123,7 +121,6 @@ export abstract class UMLElement implements IUMLElement, ILayoutable {
strokeColor: this.strokeColor,
textColor: this.textColor,
assessmentNote: this.assessmentNote,
selectedBy: this.selectedBy,
};
}

Expand All @@ -139,7 +136,6 @@ export abstract class UMLElement implements IUMLElement, ILayoutable {
this.strokeColor = values.strokeColor;
this.textColor = values.textColor;
this.assessmentNote = values.assessmentNote;
this.selectedBy = values.selectedBy;
}

abstract render(canvas: ILayer): ILayoutable[];
Expand Down
1 change: 0 additions & 1 deletion src/main/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ export type UMLModelElement = {
strokeColor?: string;
textColor?: string;
assessmentNote?: string;
selectedBy?: UMLElementSelectorType[];
};

export type UMLElement = UMLModelElement & {
Expand Down
Loading

0 comments on commit bbd593c

Please sign in to comment.