Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for remote selection #323

Merged
merged 5 commits into from
Nov 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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