Skip to content

Commit

Permalink
Add editor modes and a basic ui showing the current one
Browse files Browse the repository at this point in the history
  • Loading branch information
hlxid committed Feb 11, 2024
1 parent 7b93a38 commit 192d4f4
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 0 deletions.
16 changes: 16 additions & 0 deletions src/features/editorMode/di.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ContainerModule } from "inversify";
import { TYPES, configureCommand } from "sprotty";
import { ChangeEditorModeCommand, EditorModeController } from "./editorModeController";
import { EditorModeSwitchUi } from "./modeSwitchUi";
import { EDITOR_TYPES } from "../../utils";

export const editorModeModule = new ContainerModule((bind, unbind, isBound, rebind) => {
const context = { bind, unbind, isBound, rebind };

bind(EditorModeController).toSelf().inSingletonScope();
bind(EditorModeSwitchUi).toSelf().inSingletonScope();
bind(TYPES.IUIExtension).toService(EditorModeSwitchUi);
bind(EDITOR_TYPES.DefaultUIElement).toService(EditorModeSwitchUi);

configureCommand(context, ChangeEditorModeCommand);
});
100 changes: 100 additions & 0 deletions src/features/editorMode/editorModeController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { inject, injectable } from "inversify";
import { Command, CommandExecutionContext, CommandReturn, TYPES } from "sprotty";
import { Action } from "sprotty-protocol";
import { DfdNodeImpl } from "../dfdElements/nodes";

export type EditorMode = "edit" | "validation" | "readonly";

/**
* Holds the current editor mode in a central place.
* Used to get the current mode in places where it is used.
*
* Changes to the mode should be done using the ChangeEditorModeCommand
* and not directly on this class when done interactively
* for undo/redo support and actions that are done to the model
* when the mode changes.
*/
@injectable()
export class EditorModeController {
private mode: EditorMode = "edit";
private modeChangeCallbacks: ((mode: EditorMode) => void)[] = [];

getCurrentMode(): EditorMode {
return this.mode;
}

setMode(mode: EditorMode) {
this.mode = mode;

this.modeChangeCallbacks.forEach((callback) => callback(mode));
}

onModeChange(callback: (mode: EditorMode) => void) {
this.modeChangeCallbacks.push(callback);
}
}

export interface ChangeEditorModeAction extends Action {
kind: typeof ChangeEditorModeAction.KIND;
newMode: EditorMode;
}
export namespace ChangeEditorModeAction {
export const KIND = "changeEditorMode";

export function create(newMode: EditorMode): ChangeEditorModeAction {
return {
kind: KIND,
newMode,
};
}
}

export class ChangeEditorModeCommand extends Command {
static readonly KIND = ChangeEditorModeAction.KIND;

private oldMode?: EditorMode;

@inject(EditorModeController)
private readonly controller?: EditorModeController;

constructor(@inject(TYPES.Action) private action: ChangeEditorModeAction) {
super();
}

execute(context: CommandExecutionContext): CommandReturn {
if (!this.controller) throw new Error("Missing injects");

this.oldMode = this.controller.getCurrentMode();
this.controller.setMode(this.action.newMode);
this.postModeSwitch(context);

return context.root;
}

private postModeSwitch(context: CommandExecutionContext): void {
if (this.oldMode === "validation" && this.action.newMode === "edit") {
// Remove validation errors when enabling editing
context.root.index.all().forEach((element) => {
if (element instanceof DfdNodeImpl) {
element.validationResult = undefined;
}
});
}
}

undo(context: CommandExecutionContext): CommandReturn {
if (!this.controller) throw new Error("Missing injects");

if (!this.oldMode) {
// This should never happen because execute() is called before undo() is called.
throw new Error("No old mode to restore");
}
this.controller.setMode(this.oldMode);

return context.root;
}

redo(context: CommandExecutionContext): CommandReturn {
return this.execute(context);
}
}
10 changes: 10 additions & 0 deletions src/features/editorMode/modeSwitchUi.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.editor-mode-switcher {
/* Position the switcher in the top left corner */
top: 40px;
padding: 8px;
left: 40px;

/* Make text non-selectable */
-webkit-user-select: none; /* Safari only supports user select using the -webkit prefix */
user-select: none;
}
77 changes: 77 additions & 0 deletions src/features/editorMode/modeSwitchUi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { AbstractUIExtension, ActionDispatcher, TYPES } from "sprotty";
import { ChangeEditorModeAction, EditorMode, EditorModeController } from "./editorModeController";
import { inject, injectable } from "inversify";

import "./modeSwitchUi.css";

/**
* UI that shows the current editor mode (unless it is edit mode)
* with details about the mode.
* For validation mode the user can also choose to enable editing
* and switch the editor to edit mode.
*/
@injectable()
export class EditorModeSwitchUi extends AbstractUIExtension {
static readonly ID = "editor-mode-switcher";

constructor(
@inject(EditorModeController)
private readonly editorModeController: EditorModeController,
@inject(TYPES.IActionDispatcher)
private readonly actionDispatcher: ActionDispatcher,
) {
super();
}

id(): string {
return EditorModeSwitchUi.ID;
}
containerClass(): string {
return this.id();
}

protected initializeContents(containerElement: HTMLElement): void {
containerElement.classList.add("ui-float");
this.editorModeController.onModeChange((mode) => this.reRender(mode));
// Only for testing, TODO: remove when mode is loaded from the model
this.editorModeController.setMode("validation");
}

private reRender(mode: EditorMode): void {
this.containerElement.innerHTML = "";
switch (mode) {
case "edit":
this.containerElement.style.visibility = "hidden";
break;
case "readonly":
this.containerElement.style.visibility = "visible";
this.renderReadonlyMode();
break;
case "validation":
this.containerElement.style.visibility = "visible";
this.renderValidationMode();
break;
default:
throw new Error(`Unknown editor mode: ${mode}`);
}
}

private renderValidationMode(): void {
this.containerElement.innerHTML = `
Currently validation errors from the analysis.</br>
Enabling editing will remove the validation errors.</br>
<button id="enableEditingButton">Enable editing</button>
`;
const enableEditingButton = this.containerElement.querySelector("#enableEditingButton");
enableEditingButton?.addEventListener("click", () => {
this.actionDispatcher.dispatch(ChangeEditorModeAction.create("edit"));
});
}

private renderReadonlyMode(): void {
this.containerElement.innerHTML = `
This diagram was generated from a palladio model.</br>
Model is readonly.
`;
}
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { LoadDefaultDiagramAction } from "./features/serialize/loadDefaultDiagra
import { dfdElementsModule } from "./features/dfdElements/di.config";
import { copyPasteModule } from "./features/copyPaste/di.config";
import { EDITOR_TYPES } from "./utils";
import { editorModeModule } from "./features/editorMode/di.config";

import "sprotty/css/sprotty.css";
import "sprotty/css/edit-label.css";
Expand All @@ -47,6 +48,7 @@ container.load(
dfdElementsModule,
serializeModule,
dfdLabelModule,
editorModeModule,
toolPaletteModule,
copyPasteModule,
);
Expand Down

0 comments on commit 192d4f4

Please sign in to comment.