import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { create_element, el } from "../../core/gui/dom"; import { QuestEditorToolBar } from "./QuestEditorToolBar"; import GoldenLayout, { Container, ContentItem, ItemConfigType } from "golden-layout"; import { QuestInfoView } from "./QuestInfoView"; import "golden-layout/src/css/goldenlayout-base.css"; import "../../core/gui/golden_layout_theme.css"; import { NpcCountsView } from "./NpcCountsView"; import { QuestEditorRendererView } from "./QuestEditorRendererView"; import { AsmEditorView } from "./AsmEditorView"; import { EntityInfoView } from "./EntityInfoView"; import { GuiStore, GuiTool } from "../../core/stores/GuiStore"; import { NpcListView } from "./NpcListView"; import { ObjectListView } from "./ObjectListView"; import { EventsView } from "./EventsView"; import { RegistersView } from "./RegistersView"; import { LogView } from "./LogView"; import { QuestRunnerRendererView } from "./QuestRunnerRendererView"; import { QuestEditorStore } from "../stores/QuestEditorStore"; import { AsmEditorStore } from "../stores/AsmEditorStore"; import { AreaStore } from "../stores/AreaStore"; import { EntityImageRenderer } from "../rendering/EntityImageRenderer"; import { AreaAssetLoader } from "../loading/AreaAssetLoader"; import { EntityAssetLoader } from "../loading/EntityAssetLoader"; import { DisposableThreeRenderer } from "../../core/rendering/Renderer"; import { QuestEditorUiPersister } from "../persistence/QuestEditorUiPersister"; import Logger = require("js-logger"); const logger = Logger.get("quest_editor/gui/QuestEditorView"); const DEFAULT_LAYOUT_CONFIG = { settings: { showPopoutIcon: false, showMaximiseIcon: true, showCloseIcon: true, }, dimensions: { headerHeight: 24, }, labels: { close: "Close", maximise: "Maximise", minimise: "Minimise", popout: "Open in new window", }, }; export class QuestEditorView extends ResizableWidget { readonly element = el.div({ class: "quest_editor_QuestEditorView" }); /** * Maps views to names and creation functions. */ private readonly view_map: Map< new (...args: never) => ResizableWidget, { name: string; create(): ResizableWidget } >; private readonly tool_bar: QuestEditorToolBar; private readonly layout_element = create_element("div", { class: "quest_editor_gl_container" }); private readonly layout: Promise; private loaded_layout: GoldenLayout | undefined; private readonly sub_views = new Map(); constructor( private readonly gui_store: GuiStore, area_store: AreaStore, quest_editor_store: QuestEditorStore, asm_editor_store: AsmEditorStore, area_asset_loader: AreaAssetLoader, entity_asset_loader: EntityAssetLoader, entity_image_renderer: EntityImageRenderer, private readonly quest_editor_ui_persister: QuestEditorUiPersister, create_three_renderer: () => DisposableThreeRenderer, ) { super(); // Don't change the values of this map, as they are persisted in the user's browser. this.view_map = new Map< new (...args: never) => ResizableWidget, { name: string; create(): ResizableWidget } >([ [ QuestInfoView, { name: "quest_info", create: () => new QuestInfoView(quest_editor_store) }, ], [ NpcCountsView, { name: "npc_counts", create: () => new NpcCountsView(quest_editor_store) }, ], [ QuestEditorRendererView, { name: "quest_renderer", create: () => new QuestEditorRendererView( gui_store, quest_editor_store, area_asset_loader, entity_asset_loader, create_three_renderer(), ), }, ], [ AsmEditorView, { name: "asm_editor", create: () => new AsmEditorView( gui_store, quest_editor_store.quest_runner, asm_editor_store, ), }, ], [ EntityInfoView, { name: "entity_info", create: () => new EntityInfoView(quest_editor_store) }, ], [ NpcListView, { name: "npc_list_view", create: () => new NpcListView(quest_editor_store, entity_image_renderer), }, ], [ ObjectListView, { name: "object_list_view", create: () => new ObjectListView(quest_editor_store, entity_image_renderer), }, ], ]); if (gui_store.feature_active("events")) { this.view_map.set(EventsView, { name: "events_view", create: () => new EventsView(quest_editor_store), }); } if (gui_store.feature_active("vm")) { this.view_map.set(QuestRunnerRendererView, { name: "quest_runner", create: () => new QuestRunnerRendererView( gui_store, quest_editor_store, area_asset_loader, entity_asset_loader, create_three_renderer(), ), }); this.view_map.set(LogView, { name: "log_view", create: () => new LogView() }); this.view_map.set(RegistersView, { name: "registers_view", create: () => new RegistersView(quest_editor_store.quest_runner), }); } this.tool_bar = this.disposable( new QuestEditorToolBar(gui_store, area_store, quest_editor_store), ); this.element.append(this.tool_bar.element, this.layout_element); this.layout = this.init_golden_layout(); this.layout.then(layout => (this.loaded_layout = layout)); this.disposables( gui_store.on_global_keydown( GuiTool.QuestEditor, "Ctrl-Alt-D", () => (quest_editor_store.debug.val = !quest_editor_store.debug.val), ), quest_editor_store.quest_runner.running.observe(async ({ value: running }) => { const layout = await this.layout; if (quest_editor_store.quest_runner.running.val === running) { const runner_items = layout.root.getItemsById( this.view_map.get(QuestRunnerRendererView)!.name, ); if (running) { if (runner_items.length === 0) { const renderer_item = layout.root.getItemsById( this.view_map.get(QuestEditorRendererView)!.name, )[0]; renderer_item.parent.addChild({ id: this.view_map.get(QuestRunnerRendererView)!.name, title: "Quest", type: "component", componentName: this.view_map.get(QuestRunnerRendererView)!.name, isClosable: false, }); } } else { for (const item of runner_items) { item.remove(); } } } }), ); this.finalize_construction(); } resize(width: number, height: number): this { super.resize(width, height); const layout_height = Math.max(0, height - this.tool_bar.height); this.layout_element.style.width = `${width}px`; this.layout_element.style.height = `${layout_height}px`; this.layout.then(layout => layout.updateSize(width, layout_height)); return this; } dispose(): void { super.dispose(); this.layout.then(layout => layout.destroy()); for (const view of this.sub_views.values()) { view.dispose(); } this.sub_views.clear(); } private async init_golden_layout(): Promise { const default_layout_content = this.get_default_layout_content(); try { const content = await this.quest_editor_ui_persister.load_layout_config( default_layout_content, ); if (content) { const gl = this.attempt_gl_init({ ...DEFAULT_LAYOUT_CONFIG, content, }); logger.info("Instantiated golden layout with persisted layout."); return gl; } } catch (e) { logger.warn("Couldn't instantiate golden layout with persisted layout.", e); } logger.info("Instantiating golden layout with default layout."); return this.attempt_gl_init({ ...DEFAULT_LAYOUT_CONFIG, content: default_layout_content, }); } private attempt_gl_init(config: GoldenLayout.Config): GoldenLayout { const layout = new GoldenLayout(config, this.layout_element); // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; try { for (const { name, create } of this.view_map.values()) { // registerComponent expects a regular function and not an arrow function. This // function will be called with new. layout.registerComponent(name, function(container: Container) { const view = create(); container.on("close", () => view.dispose()); container.on("resize", () => // Subtract 4 from height to work around bug in Golden Layout related to // headerHeight. view.resize(container.width, container.height - 4), ); view.resize(container.width, container.height); self.sub_views.set(name, view); container.getElement().append(view.element); }); } layout.on("stateChanged", () => { this.quest_editor_ui_persister.persist_layout_config(layout.toConfig().content); }); layout.on("stackCreated", (stack: ContentItem) => { stack.on("activeContentItemChanged", (item: ContentItem) => { if ("componentName" in item.config) { const view = this.sub_views.get(item.config.componentName); if (view) view.focus(); } }); }); layout.init(); return layout; } catch (e) { layout.destroy(); throw e; } } private get_default_layout_content(): ItemConfigType[] { return [ { type: "row", content: [ { type: "column", width: 2, content: [ { type: "stack", content: [ { title: "Info", type: "component", componentName: this.view_map.get(QuestInfoView)!.name, isClosable: false, }, { title: "NPC Counts", type: "component", componentName: this.view_map.get(NpcCountsView)!.name, isClosable: false, }, ], }, { title: "Entity", type: "component", componentName: this.view_map.get(EntityInfoView)!.name, isClosable: false, }, ], }, { type: "stack", width: 9, content: [ { id: this.view_map.get(QuestEditorRendererView)!.name, title: "3D View", type: "component", componentName: this.view_map.get(QuestEditorRendererView)!.name, isClosable: false, }, { title: "Script", type: "component", componentName: this.view_map.get(AsmEditorView)!.name, isClosable: false, }, ...(this.gui_store.feature_active("vm") ? [ { title: "Debug Log", type: "component", componentName: this.view_map.get(LogView)!.name, isClosable: false, }, { title: "Registers", type: "component", componentName: this.view_map.get(RegistersView)!.name, isClosable: false, }, ] : []), ], }, { type: "stack", width: 2, content: [ { title: "NPCs", type: "component", componentName: this.view_map.get(NpcListView)!.name, isClosable: false, }, { title: "Objects", type: "component", componentName: this.view_map.get(ObjectListView)!.name, isClosable: false, }, ...(this.gui_store.feature_active("events") ? [ { title: "Events", type: "component", componentName: this.view_map.get(EventsView)!.name, isClosable: false, }, ] : []), ], }, ], }, ]; } }