Improved global keybind handling. General quest editor undo stack is now disabled when the script editor is focussed.

This commit is contained in:
Daan Vanden Bosch 2019-07-24 18:07:32 +02:00
parent 52376193ae
commit c0e3ac924a
8 changed files with 162 additions and 105 deletions

View File

@ -1,21 +1,39 @@
import { observable } from "mobx"; import { observable } from "mobx";
import { Server } from "../domain"; import { Server } from "../domain";
import { UndoStack } from "../undo";
class ApplicationStore { class ApplicationStore {
@observable current_server: Server = Server.Ephinea; @observable current_server: Server = Server.Ephinea;
@observable current_tool: string = this.init_tool(); @observable current_tool: string = this.init_tool();
private key_event_handlers = new Map<string, (e: KeyboardEvent) => void>(); private global_keyup_handlers = new Map<string, () => void>();
on_global_keyup = (tool: string, handler: (e: KeyboardEvent) => void) => { on_global_keyup(tool: string, binding: string, handler: () => void): void {
this.key_event_handlers.set(tool, handler); this.global_keyup_handlers.set(`${tool} ${binding}`, handler);
}; }
dispatch_global_keyup = (e: KeyboardEvent) => { dispatch_global_keyup = (e: KeyboardEvent) => {
const handler = this.key_event_handlers.get(this.current_tool); const binding_parts: string[] = [];
if (e.ctrlKey) binding_parts.push("Ctrl");
if (e.shiftKey) binding_parts.push("Shift");
if (e.altKey) binding_parts.push("Alt");
binding_parts.push(e.key.toUpperCase());
if (handler) { const binding = binding_parts.join("-");
handler(e);
switch (binding) {
case "Ctrl-Z":
UndoStack.current && UndoStack.current.undo();
break;
case "Ctrl-Shift-Z":
UndoStack.current && UndoStack.current.redo();
break;
default:
{
const handler = this.global_keyup_handlers.get(binding);
if (handler) handler();
}
break;
} }
}; };

View File

@ -7,6 +7,7 @@ import { Vec3 } from "../data_formats/vector";
import { Area, Episode, Quest, QuestEntity, Section } from "../domain"; import { Area, Episode, Quest, QuestEntity, Section } from "../domain";
import { read_file } from "../read_file"; import { read_file } from "../read_file";
import { UndoStack } from "../undo"; import { UndoStack } from "../undo";
import { application_store } from "./ApplicationStore";
import { area_store } from "./AreaStore"; import { area_store } from "./AreaStore";
import { create_new_quest } from "./quest_creation"; import { create_new_quest } from "./quest_creation";
@ -16,6 +17,7 @@ class QuestEditorStore {
@observable debug = false; @observable debug = false;
readonly undo_stack = new UndoStack(); readonly undo_stack = new UndoStack();
readonly script_undo_stack = new UndoStack();
@observable current_quest_filename?: string; @observable current_quest_filename?: string;
@observable current_quest?: Quest; @observable current_quest?: Quest;
@ -26,6 +28,10 @@ class QuestEditorStore {
@observable save_dialog_filename?: string; @observable save_dialog_filename?: string;
@observable save_dialog_open: boolean = false; @observable save_dialog_open: boolean = false;
constructor() {
application_store.on_global_keyup("quest_editor", "Ctrl-Alt-D", this.toggle_debug);
}
@action @action
toggle_debug = () => { toggle_debug = () => {
this.debug = !this.debug; this.debug = !this.debug;

View File

@ -11,6 +11,7 @@ import "./EntityInfoComponent.css";
export class EntityInfoComponent extends Component { export class EntityInfoComponent extends Component {
render(): ReactNode { render(): ReactNode {
const entity = quest_editor_store.selected_entity; const entity = quest_editor_store.selected_entity;
let body: ReactNode;
if (entity) { if (entity) {
const section_id = entity.section ? entity.section.id : entity.section_id; const section_id = entity.section ? entity.section.id : entity.section_id;
@ -32,8 +33,7 @@ export class EntityInfoComponent extends Component {
); );
} }
return ( body = (
<div className="EntityInfoComponent-container">
<table className="EntityInfoComponent-table"> <table className="EntityInfoComponent-table">
<tbody> <tbody>
{name} {name}
@ -95,17 +95,18 @@ export class EntityInfoComponent extends Component {
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div>
); );
} else { } else {
body = <DisabledTextComponent>No entity selected.</DisabledTextComponent>;
}
return ( return (
<div className="EntityInfoComponent-container"> <div className="EntityInfoComponent-container" tabIndex={-1}>
<DisabledTextComponent>No entity selected.</DisabledTextComponent> {body}
</div> </div>
); );
} }
} }
}
type CoordProps = { type CoordProps = {
entity: QuestEntity; entity: QuestEntity;

View File

@ -1,7 +1,8 @@
import GoldenLayout from "golden-layout"; import GoldenLayout from "golden-layout";
import Logger from "js-logger";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import React, { Component, createRef, ReactNode } from "react"; import React, { Component, createRef, FocusEvent, ReactNode } from "react";
import { application_store } from "../../stores/ApplicationStore"; import { quest_editor_ui_persister } from "../../persistence/QuestEditorUiPersister";
import { quest_editor_store } from "../../stores/QuestEditorStore"; import { quest_editor_store } from "../../stores/QuestEditorStore";
import { EntityInfoComponent } from "./EntityInfoComponent"; import { EntityInfoComponent } from "./EntityInfoComponent";
import "./QuestEditorComponent.less"; import "./QuestEditorComponent.less";
@ -9,8 +10,6 @@ import { QuestInfoComponent } from "./QuestInfoComponent";
import { QuestRendererComponent } from "./QuestRendererComponent"; import { QuestRendererComponent } from "./QuestRendererComponent";
import { ScriptEditorComponent } from "./ScriptEditorComponent"; import { ScriptEditorComponent } from "./ScriptEditorComponent";
import { Toolbar } from "./Toolbar"; import { Toolbar } from "./Toolbar";
import { quest_editor_ui_persister } from "../../persistence/QuestEditorUiPersister";
import Logger from "js-logger";
const logger = Logger.get("ui/quest_editor/QuestEditorComponent"); const logger = Logger.get("ui/quest_editor/QuestEditorComponent");
@ -74,7 +73,7 @@ export class QuestEditorComponent extends Component {
private layout?: GoldenLayout; private layout?: GoldenLayout;
componentDidMount(): void { componentDidMount(): void {
application_store.on_global_keyup("quest_editor", this.keyup); quest_editor_store.undo_stack.make_current();
window.addEventListener("resize", this.resize); window.addEventListener("resize", this.resize);
@ -117,6 +116,8 @@ export class QuestEditorComponent extends Component {
} }
componentWillUnmount(): void { componentWillUnmount(): void {
quest_editor_store.undo_stack.ensure_not_current();
window.removeEventListener("resize", this.resize); window.removeEventListener("resize", this.resize);
if (this.layout) { if (this.layout) {
@ -129,18 +130,27 @@ export class QuestEditorComponent extends Component {
return ( return (
<div className="qe-QuestEditorComponent"> <div className="qe-QuestEditorComponent">
<Toolbar /> <Toolbar />
<div className="qe-QuestEditorComponent-main" ref={this.layout_element} /> <div
className="qe-QuestEditorComponent-main"
onFocus={this.focus}
ref={this.layout_element}
/>
</div> </div>
); );
} }
private keyup = (e: KeyboardEvent) => { private focus = (e: FocusEvent) => {
if (e.ctrlKey && e.key === "z" && !e.altKey) { const scrip_editor_element = document.getElementById("qe-ScriptEditorComponent");
quest_editor_store.undo_stack.undo();
} else if (e.ctrlKey && e.key === "Z" && !e.altKey) { if (
quest_editor_store.undo_stack.redo(); scrip_editor_element &&
} else if (e.ctrlKey && e.altKey && e.key === "d") { scrip_editor_element.compareDocumentPosition(e.target) &
quest_editor_store.toggle_debug(); Node.DOCUMENT_POSITION_CONTAINED_BY
) {
// quest_editor_store.script_undo_stack.make_current();
quest_editor_store.undo_stack.ensure_not_current();
} else {
quest_editor_store.undo_stack.make_current();
} }
}; };

View File

@ -9,6 +9,7 @@ import { DisabledTextComponent } from "../DisabledTextComponent";
export class QuestInfoComponent extends Component { export class QuestInfoComponent extends Component {
render(): ReactNode { render(): ReactNode {
const quest = quest_editor_store.current_quest; const quest = quest_editor_store.current_quest;
let body: ReactNode;
if (quest) { if (quest) {
const episode = quest.episode === 4 ? "IV" : quest.episode === 2 ? "II" : "I"; const episode = quest.episode === 4 ? "IV" : quest.episode === 2 ? "II" : "I";
@ -34,8 +35,8 @@ export class QuestInfoComponent extends Component {
); );
}); });
return ( body = (
<div className="qe-QuestInfoComponent"> <>
<table> <table>
<tbody> <tbody>
<tr> <tr>
@ -68,14 +69,16 @@ export class QuestInfoComponent extends Component {
<tbody>{npc_count_rows}</tbody> <tbody>{npc_count_rows}</tbody>
</table> </table>
</div> </div>
</div> </>
); );
} else { } else {
body = <DisabledTextComponent>No quest loaded.</DisabledTextComponent>;
}
return ( return (
<div className="qe-QuestInfoComponent"> <div className="qe-QuestInfoComponent" tabIndex={-1}>
<DisabledTextComponent>No quest loaded.</DisabledTextComponent> {body}
</div> </div>
); );
} }
} }
}

View File

@ -112,7 +112,7 @@ editor.defineTheme("phantasmal-world", {
export class ScriptEditorComponent extends Component { export class ScriptEditorComponent extends Component {
render(): ReactNode { render(): ReactNode {
return ( return (
<section className="qe-ScriptEditorComponent"> <section id="qe-ScriptEditorComponent" className="qe-ScriptEditorComponent">
<AutoSizer> <AutoSizer>
{({ width, height }) => <MonacoComponent width={width} height={height} />} {({ width, height }) => <MonacoComponent width={width} height={height} />}
</AutoSizer> </AutoSizer>

View File

@ -6,11 +6,12 @@ import React, { ChangeEvent, Component, ReactNode } from "react";
import { Episode } from "../../domain"; import { Episode } from "../../domain";
import { quest_editor_store } from "../../stores/QuestEditorStore"; import { quest_editor_store } from "../../stores/QuestEditorStore";
import "./Toolbar.less"; import "./Toolbar.less";
import { UndoStack } from "../../undo";
@observer @observer
export class Toolbar extends Component { export class Toolbar extends Component {
render(): ReactNode { render(): ReactNode {
const undo = quest_editor_store.undo_stack; const undo = UndoStack.current;
const quest = quest_editor_store.current_quest; const quest = quest_editor_store.current_quest;
const areas = quest ? Array.from(quest.area_variants).map(a => a.area) : []; const areas = quest ? Array.from(quest.area_variants).map(a => a.area) : [];
const area = quest_editor_store.current_area; const area = quest_editor_store.current_area;
@ -47,16 +48,22 @@ export class Toolbar extends Component {
<Button <Button
icon="undo" icon="undo"
onClick={this.undo} onClick={this.undo}
title={"Undo" + (undo.first_undo ? ` "${undo.first_undo.description}"` : "")} title={
disabled={!undo.can_undo} "Undo" +
(undo && undo.first_undo ? ` "${undo.first_undo.description}"` : "")
}
disabled={!(undo && undo.can_undo)}
> >
Undo Undo
</Button> </Button>
<Button <Button
icon="redo" icon="redo"
onClick={this.redo} onClick={this.redo}
title={"Redo" + (undo.first_redo ? ` "${undo.first_redo.description}"` : "")} title={
disabled={!quest_editor_store.undo_stack.can_redo} "Redo" +
(undo && undo.first_redo ? ` "${undo.first_redo.description}"` : "")
}
disabled={!(undo && undo.can_redo)}
> >
Redo Redo
</Button> </Button>
@ -88,11 +95,11 @@ export class Toolbar extends Component {
} }
private undo(): void { private undo(): void {
quest_editor_store.undo_stack.undo(); UndoStack.current && UndoStack.current.undo();
} }
private redo(): void { private redo(): void {
quest_editor_store.undo_stack.redo(); UndoStack.current && UndoStack.current.redo();
} }
} }

View File

@ -9,6 +9,8 @@ export class Action {
} }
export class UndoStack { export class UndoStack {
@observable static current?: UndoStack;
@observable private readonly stack: IObservableArray<Action> = observable.array([], { @observable private readonly stack: IObservableArray<Action> = observable.array([], {
deep: false, deep: false,
}); });
@ -17,6 +19,16 @@ export class UndoStack {
*/ */
@observable private index = 0; @observable private index = 0;
make_current(): void {
UndoStack.current = this;
}
ensure_not_current(): void {
if (UndoStack.current === this) {
UndoStack.current = undefined;
}
}
@computed get can_undo(): boolean { @computed get can_undo(): boolean {
return this.index > 0; return this.index > 0;
} }