mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
Improved global keybind handling. General quest editor undo stack is now disabled when the script editor is focussed.
This commit is contained in:
parent
52376193ae
commit
c0e3ac924a
@ -1,21 +1,39 @@
|
||||
import { observable } from "mobx";
|
||||
import { Server } from "../domain";
|
||||
import { UndoStack } from "../undo";
|
||||
|
||||
class ApplicationStore {
|
||||
@observable current_server: Server = Server.Ephinea;
|
||||
@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) => {
|
||||
this.key_event_handlers.set(tool, handler);
|
||||
};
|
||||
on_global_keyup(tool: string, binding: string, handler: () => void): void {
|
||||
this.global_keyup_handlers.set(`${tool} ${binding}`, handler);
|
||||
}
|
||||
|
||||
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) {
|
||||
handler(e);
|
||||
const binding = binding_parts.join("-");
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -7,6 +7,7 @@ import { Vec3 } from "../data_formats/vector";
|
||||
import { Area, Episode, Quest, QuestEntity, Section } from "../domain";
|
||||
import { read_file } from "../read_file";
|
||||
import { UndoStack } from "../undo";
|
||||
import { application_store } from "./ApplicationStore";
|
||||
import { area_store } from "./AreaStore";
|
||||
import { create_new_quest } from "./quest_creation";
|
||||
|
||||
@ -16,6 +17,7 @@ class QuestEditorStore {
|
||||
@observable debug = false;
|
||||
|
||||
readonly undo_stack = new UndoStack();
|
||||
readonly script_undo_stack = new UndoStack();
|
||||
|
||||
@observable current_quest_filename?: string;
|
||||
@observable current_quest?: Quest;
|
||||
@ -26,6 +28,10 @@ class QuestEditorStore {
|
||||
@observable save_dialog_filename?: string;
|
||||
@observable save_dialog_open: boolean = false;
|
||||
|
||||
constructor() {
|
||||
application_store.on_global_keyup("quest_editor", "Ctrl-Alt-D", this.toggle_debug);
|
||||
}
|
||||
|
||||
@action
|
||||
toggle_debug = () => {
|
||||
this.debug = !this.debug;
|
||||
|
@ -11,6 +11,7 @@ import "./EntityInfoComponent.css";
|
||||
export class EntityInfoComponent extends Component {
|
||||
render(): ReactNode {
|
||||
const entity = quest_editor_store.selected_entity;
|
||||
let body: ReactNode;
|
||||
|
||||
if (entity) {
|
||||
const section_id = entity.section ? entity.section.id : entity.section_id;
|
||||
@ -32,78 +33,78 @@ export class EntityInfoComponent extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="EntityInfoComponent-container">
|
||||
<table className="EntityInfoComponent-table">
|
||||
<tbody>
|
||||
{name}
|
||||
<tr>
|
||||
<td>Section: </td>
|
||||
<td>{section_id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan={2}>World position: </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan={2}>
|
||||
<table>
|
||||
<tbody>
|
||||
<CoordRow
|
||||
entity={entity}
|
||||
position_type="position"
|
||||
coord="x"
|
||||
/>
|
||||
<CoordRow
|
||||
entity={entity}
|
||||
position_type="position"
|
||||
coord="y"
|
||||
/>
|
||||
<CoordRow
|
||||
entity={entity}
|
||||
position_type="position"
|
||||
coord="z"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan={2}>Section position: </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan={2}>
|
||||
<table>
|
||||
<tbody>
|
||||
<CoordRow
|
||||
entity={entity}
|
||||
position_type="section_position"
|
||||
coord="x"
|
||||
/>
|
||||
<CoordRow
|
||||
entity={entity}
|
||||
position_type="section_position"
|
||||
coord="y"
|
||||
/>
|
||||
<CoordRow
|
||||
entity={entity}
|
||||
position_type="section_position"
|
||||
coord="z"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
body = (
|
||||
<table className="EntityInfoComponent-table">
|
||||
<tbody>
|
||||
{name}
|
||||
<tr>
|
||||
<td>Section: </td>
|
||||
<td>{section_id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan={2}>World position: </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan={2}>
|
||||
<table>
|
||||
<tbody>
|
||||
<CoordRow
|
||||
entity={entity}
|
||||
position_type="position"
|
||||
coord="x"
|
||||
/>
|
||||
<CoordRow
|
||||
entity={entity}
|
||||
position_type="position"
|
||||
coord="y"
|
||||
/>
|
||||
<CoordRow
|
||||
entity={entity}
|
||||
position_type="position"
|
||||
coord="z"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan={2}>Section position: </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan={2}>
|
||||
<table>
|
||||
<tbody>
|
||||
<CoordRow
|
||||
entity={entity}
|
||||
position_type="section_position"
|
||||
coord="x"
|
||||
/>
|
||||
<CoordRow
|
||||
entity={entity}
|
||||
position_type="section_position"
|
||||
coord="y"
|
||||
/>
|
||||
<CoordRow
|
||||
entity={entity}
|
||||
position_type="section_position"
|
||||
coord="z"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="EntityInfoComponent-container">
|
||||
<DisabledTextComponent>No entity selected.</DisabledTextComponent>
|
||||
</div>
|
||||
);
|
||||
body = <DisabledTextComponent>No entity selected.</DisabledTextComponent>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="EntityInfoComponent-container" tabIndex={-1}>
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
import GoldenLayout from "golden-layout";
|
||||
import Logger from "js-logger";
|
||||
import { observer } from "mobx-react";
|
||||
import React, { Component, createRef, ReactNode } from "react";
|
||||
import { application_store } from "../../stores/ApplicationStore";
|
||||
import React, { Component, createRef, FocusEvent, ReactNode } from "react";
|
||||
import { quest_editor_ui_persister } from "../../persistence/QuestEditorUiPersister";
|
||||
import { quest_editor_store } from "../../stores/QuestEditorStore";
|
||||
import { EntityInfoComponent } from "./EntityInfoComponent";
|
||||
import "./QuestEditorComponent.less";
|
||||
@ -9,8 +10,6 @@ import { QuestInfoComponent } from "./QuestInfoComponent";
|
||||
import { QuestRendererComponent } from "./QuestRendererComponent";
|
||||
import { ScriptEditorComponent } from "./ScriptEditorComponent";
|
||||
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");
|
||||
|
||||
@ -74,7 +73,7 @@ export class QuestEditorComponent extends Component {
|
||||
private layout?: GoldenLayout;
|
||||
|
||||
componentDidMount(): void {
|
||||
application_store.on_global_keyup("quest_editor", this.keyup);
|
||||
quest_editor_store.undo_stack.make_current();
|
||||
|
||||
window.addEventListener("resize", this.resize);
|
||||
|
||||
@ -117,6 +116,8 @@ export class QuestEditorComponent extends Component {
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
quest_editor_store.undo_stack.ensure_not_current();
|
||||
|
||||
window.removeEventListener("resize", this.resize);
|
||||
|
||||
if (this.layout) {
|
||||
@ -129,18 +130,27 @@ export class QuestEditorComponent extends Component {
|
||||
return (
|
||||
<div className="qe-QuestEditorComponent">
|
||||
<Toolbar />
|
||||
<div className="qe-QuestEditorComponent-main" ref={this.layout_element} />
|
||||
<div
|
||||
className="qe-QuestEditorComponent-main"
|
||||
onFocus={this.focus}
|
||||
ref={this.layout_element}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private keyup = (e: KeyboardEvent) => {
|
||||
if (e.ctrlKey && e.key === "z" && !e.altKey) {
|
||||
quest_editor_store.undo_stack.undo();
|
||||
} else if (e.ctrlKey && e.key === "Z" && !e.altKey) {
|
||||
quest_editor_store.undo_stack.redo();
|
||||
} else if (e.ctrlKey && e.altKey && e.key === "d") {
|
||||
quest_editor_store.toggle_debug();
|
||||
private focus = (e: FocusEvent) => {
|
||||
const scrip_editor_element = document.getElementById("qe-ScriptEditorComponent");
|
||||
|
||||
if (
|
||||
scrip_editor_element &&
|
||||
scrip_editor_element.compareDocumentPosition(e.target) &
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -9,6 +9,7 @@ import { DisabledTextComponent } from "../DisabledTextComponent";
|
||||
export class QuestInfoComponent extends Component {
|
||||
render(): ReactNode {
|
||||
const quest = quest_editor_store.current_quest;
|
||||
let body: ReactNode;
|
||||
|
||||
if (quest) {
|
||||
const episode = quest.episode === 4 ? "IV" : quest.episode === 2 ? "II" : "I";
|
||||
@ -34,8 +35,8 @@ export class QuestInfoComponent extends Component {
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="qe-QuestInfoComponent">
|
||||
body = (
|
||||
<>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
@ -68,14 +69,16 @@ export class QuestInfoComponent extends Component {
|
||||
<tbody>{npc_count_rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="qe-QuestInfoComponent">
|
||||
<DisabledTextComponent>No quest loaded.</DisabledTextComponent>
|
||||
</div>
|
||||
);
|
||||
body = <DisabledTextComponent>No quest loaded.</DisabledTextComponent>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="qe-QuestInfoComponent" tabIndex={-1}>
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ editor.defineTheme("phantasmal-world", {
|
||||
export class ScriptEditorComponent extends Component {
|
||||
render(): ReactNode {
|
||||
return (
|
||||
<section className="qe-ScriptEditorComponent">
|
||||
<section id="qe-ScriptEditorComponent" className="qe-ScriptEditorComponent">
|
||||
<AutoSizer>
|
||||
{({ width, height }) => <MonacoComponent width={width} height={height} />}
|
||||
</AutoSizer>
|
||||
|
@ -6,11 +6,12 @@ import React, { ChangeEvent, Component, ReactNode } from "react";
|
||||
import { Episode } from "../../domain";
|
||||
import { quest_editor_store } from "../../stores/QuestEditorStore";
|
||||
import "./Toolbar.less";
|
||||
import { UndoStack } from "../../undo";
|
||||
|
||||
@observer
|
||||
export class Toolbar extends Component {
|
||||
render(): ReactNode {
|
||||
const undo = quest_editor_store.undo_stack;
|
||||
const undo = UndoStack.current;
|
||||
const quest = quest_editor_store.current_quest;
|
||||
const areas = quest ? Array.from(quest.area_variants).map(a => a.area) : [];
|
||||
const area = quest_editor_store.current_area;
|
||||
@ -47,16 +48,22 @@ export class Toolbar extends Component {
|
||||
<Button
|
||||
icon="undo"
|
||||
onClick={this.undo}
|
||||
title={"Undo" + (undo.first_undo ? ` "${undo.first_undo.description}"` : "")}
|
||||
disabled={!undo.can_undo}
|
||||
title={
|
||||
"Undo" +
|
||||
(undo && undo.first_undo ? ` "${undo.first_undo.description}"` : "")
|
||||
}
|
||||
disabled={!(undo && undo.can_undo)}
|
||||
>
|
||||
Undo
|
||||
</Button>
|
||||
<Button
|
||||
icon="redo"
|
||||
onClick={this.redo}
|
||||
title={"Redo" + (undo.first_redo ? ` "${undo.first_redo.description}"` : "")}
|
||||
disabled={!quest_editor_store.undo_stack.can_redo}
|
||||
title={
|
||||
"Redo" +
|
||||
(undo && undo.first_redo ? ` "${undo.first_redo.description}"` : "")
|
||||
}
|
||||
disabled={!(undo && undo.can_redo)}
|
||||
>
|
||||
Redo
|
||||
</Button>
|
||||
@ -88,11 +95,11 @@ export class Toolbar extends Component {
|
||||
}
|
||||
|
||||
private undo(): void {
|
||||
quest_editor_store.undo_stack.undo();
|
||||
UndoStack.current && UndoStack.current.undo();
|
||||
}
|
||||
|
||||
private redo(): void {
|
||||
quest_editor_store.undo_stack.redo();
|
||||
UndoStack.current && UndoStack.current.redo();
|
||||
}
|
||||
}
|
||||
|
||||
|
12
src/undo.ts
12
src/undo.ts
@ -9,6 +9,8 @@ export class Action {
|
||||
}
|
||||
|
||||
export class UndoStack {
|
||||
@observable static current?: UndoStack;
|
||||
|
||||
@observable private readonly stack: IObservableArray<Action> = observable.array([], {
|
||||
deep: false,
|
||||
});
|
||||
@ -17,6 +19,16 @@ export class UndoStack {
|
||||
*/
|
||||
@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 {
|
||||
return this.index > 0;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user