diff --git a/FEATURES.md b/FEATURES.md index 97ff3c2a..9ad27a3c 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -74,14 +74,19 @@ Features that are in ***bold italics*** are planned and not yet implemented. ## Events -- ***Event graph*** -- ***Delete event*** +- Event graph +- Add events +- Delete event - ***Delete coupled NPCs if requested*** -- ***Edit event section*** +- ***Add parent-child relationship*** +- ***Remove parent-child relationship*** +- Edit event delay ### Event Actions -- ***Add/Delete*** +- Add/Delete +- Lock/unlock doors +- ***Spawn NPCs*** ## Script Object Code @@ -132,7 +137,7 @@ Features that are in ***bold italics*** are planned and not yet implemented. - Start debugging by clicking "Debug" or pressing F5 - Stop debugging by clicking "Stop" or pressing Shift-F5 -- Step with "Step over", "Step into" and "Step out" +- Step with "Step over" (F8), "Step into" (F7) and "Step out" (Shift-F8) - Continue to next breakpoint with "Continue" (F6) - Set breakpoints in the script editor - Register viewer @@ -146,8 +151,9 @@ Features that are in ***bold italics*** are planned and not yet implemented. ## Bugs -- [3D View](#3d-view): Random Type Box 1 and Fixed Type Box objects aren't rendered correctly -- [3D View](#3d-view): Some objects are only partially loaded (they consist of several separate models) +- [3D View](#3d-view): Some objects are only partially loaded (they consist of several separate models), e.g.: + - Random Type Box 1 + - Fixed Type Box - Forest Switch - Laser Fence - Forest Laser @@ -155,3 +161,4 @@ Features that are in ***bold italics*** are planned and not yet implemented. - Energy Barrier - Teleporter - [Load Quest](#load-quest): Can't parse quest 125 White Day +- [Script Assembly Editor](#script-assembly-editor): Go to definition doesn't work in RT (#231) diff --git a/src/application/gui/ApplicationView.ts b/src/application/gui/ApplicationView.ts index ff97f58e..b4f771ac 100644 --- a/src/application/gui/ApplicationView.ts +++ b/src/application/gui/ApplicationView.ts @@ -1,24 +1,26 @@ import { NavigationView } from "./NavigationView"; import { MainContentView } from "./MainContentView"; -import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { GuiStore, GuiTool } from "../../core/stores/GuiStore"; import { div } from "../../core/gui/dom"; import "./ApplicationView.css"; +import { ResizableView } from "../../core/gui/ResizableView"; +import { Widget } from "../../core/gui/Widget"; +import { Resizable } from "../../core/gui/Resizable"; /** * The top-level view which contains all other views. */ -export class ApplicationView extends ResizableWidget { +export class ApplicationView extends ResizableView { private menu_view: NavigationView; private main_content_view: MainContentView; readonly element: HTMLElement; - constructor(gui_store: GuiStore, tool_views: [GuiTool, () => Promise][]) { + constructor(gui_store: GuiStore, tool_views: [GuiTool, () => Promise][]) { super(); - this.menu_view = this.disposable(new NavigationView(gui_store)); - this.main_content_view = this.disposable(new MainContentView(gui_store, tool_views)); + this.menu_view = this.add(new NavigationView(gui_store)); + this.main_content_view = this.add(new MainContentView(gui_store, tool_views)); this.element = div( { className: "application_ApplicationView" }, diff --git a/src/application/gui/MainContentView.ts b/src/application/gui/MainContentView.ts index 6cd0f630..708548eb 100644 --- a/src/application/gui/MainContentView.ts +++ b/src/application/gui/MainContentView.ts @@ -1,32 +1,30 @@ import { GuiStore, GuiTool } from "../../core/stores/GuiStore"; import { LazyWidget } from "../../core/gui/LazyWidget"; -import { ResizableWidget } from "../../core/gui/ResizableWidget"; -import { ChangeEvent } from "../../core/observable/Observable"; import { div } from "../../core/gui/dom"; +import { ResizableView } from "../../core/gui/ResizableView"; +import { Widget } from "../../core/gui/Widget"; +import { Resizable } from "../../core/gui/Resizable"; + +export class MainContentView extends ResizableView { + private tool_views: Map; + private current_tool_view?: LazyWidget; -export class MainContentView extends ResizableWidget { readonly element = div({ className: "application_MainContentView" }); - private tool_views: Map; - - constructor(gui_store: GuiStore, tool_views: [GuiTool, () => Promise][]) { + constructor(gui_store: GuiStore, tool_views: [GuiTool, () => Promise][]) { super(); this.tool_views = new Map( - tool_views.map(([tool, create_view]) => [ - tool, - this.disposable(new LazyWidget(create_view)), - ]), + tool_views.map(([tool, create_view]) => [tool, this.add(new LazyWidget(create_view))]), ); for (const tool_view of this.tool_views.values()) { this.element.append(tool_view.element); } - const tool_view = this.tool_views.get(gui_store.tool.val); - if (tool_view) tool_view.visible.val = true; - - this.disposable(gui_store.tool.observe(this.tool_changed)); + this.disposables( + gui_store.tool.observe(({ value }) => this.set_current_tool(value), { call_now: true }), + ); this.finalize_construction(); } @@ -41,12 +39,17 @@ export class MainContentView extends ResizableWidget { return this; } - private tool_changed = ({ value: new_tool }: ChangeEvent): void => { - for (const tool of this.tool_views.values()) { - tool.visible.val = false; + private set_current_tool(tool: GuiTool): void { + if (this.current_tool_view) { + this.current_tool_view.visible.val = false; + this.current_tool_view.deactivate(); } - const new_view = this.tool_views.get(new_tool); - if (new_view) new_view.visible.val = true; - }; + this.current_tool_view = this.tool_views.get(tool); + + if (this.current_tool_view) { + this.current_tool_view.visible.val = true; + this.current_tool_view.activate(); + } + } } diff --git a/src/application/gui/NavigationButton.ts b/src/application/gui/NavigationButton.ts index a26c1f98..75c9f94f 100644 --- a/src/application/gui/NavigationButton.ts +++ b/src/application/gui/NavigationButton.ts @@ -1,9 +1,9 @@ -import { Widget } from "../../core/gui/Widget"; import { GuiTool } from "../../core/stores/GuiStore"; import "./NavigationButton.css"; import { input, label, span } from "../../core/gui/dom"; +import { Control } from "../../core/gui/Control"; -export class NavigationButton extends Widget { +export class NavigationButton extends Control { readonly element = span({ className: "application_NavigationButton" }); private input: HTMLInputElement = input(); diff --git a/src/application/gui/NavigationView.ts b/src/application/gui/NavigationView.ts index 67b145e6..abbca850 100644 --- a/src/application/gui/NavigationView.ts +++ b/src/application/gui/NavigationView.ts @@ -1,9 +1,9 @@ import { a, div, icon, Icon, span } from "../../core/gui/dom"; import "./NavigationView.css"; import { GuiStore, GuiTool } from "../../core/stores/GuiStore"; -import { Widget } from "../../core/gui/Widget"; import { NavigationButton } from "./NavigationButton"; import { Select } from "../../core/gui/Select"; +import { View } from "../../core/gui/View"; const TOOLS: [GuiTool, string][] = [ [GuiTool.Viewer, "Viewer"], @@ -11,11 +11,11 @@ const TOOLS: [GuiTool, string][] = [ [GuiTool.HuntOptimizer, "Hunt Optimizer"], ]; -export class NavigationView extends Widget { +export class NavigationView extends View { private readonly buttons = new Map( - TOOLS.map(([value, text]) => [value, this.disposable(new NavigationButton(value, text))]), + TOOLS.map(([value, text]) => [value, this.add(new NavigationButton(value, text))]), ); - private readonly server_select = this.disposable( + private readonly server_select = this.add( new Select({ label: "Server:", items: ["Ephinea"], @@ -57,15 +57,16 @@ export class NavigationView extends Widget { this.element.style.height = `${this.height}px`; this.element.onmousedown = this.mousedown; - this.mark_tool_button(gui_store.tool.val); - this.disposable(gui_store.tool.observe(({ value }) => this.mark_tool_button(value))); + this.disposables( + gui_store.tool.observe(({ value }) => this.mark_tool_button(value), { call_now: true }), + ); this.finalize_construction(); } private mousedown = (e: MouseEvent): void => { if (e.target instanceof HTMLLabelElement && e.target.control instanceof HTMLInputElement) { - this.gui_store.tool.val = (GuiTool as any)[e.target.control.value]; + this.gui_store.set_tool((GuiTool as any)[e.target.control.value]); } }; diff --git a/src/application/index.ts b/src/application/index.ts index a4804fb9..8dfb5b35 100644 --- a/src/application/index.ts +++ b/src/application/index.ts @@ -90,6 +90,8 @@ export function initialize_application( resize(); document.body.append(application_view.element); + application_view.activate(); + disposer.add(disposable_listener(window, "resize", resize)); return { diff --git a/src/core/gui/ComboBox.ts b/src/core/gui/ComboBox.ts index 6ef4bc8c..b22a9706 100644 --- a/src/core/gui/ComboBox.ts +++ b/src/core/gui/ComboBox.ts @@ -1,5 +1,5 @@ import { LabelledControl, LabelledControlOptions } from "./LabelledControl"; -import { Icon, icon, input, span } from "./dom"; +import { bind_attr, Icon, icon, input, span } from "./dom"; import "./ComboBox.css"; import "./Input.css"; import { Menu } from "./Menu"; @@ -90,14 +90,7 @@ export class ComboBox extends LabelledControl { }; const down_arrow_element = icon(Icon.TriangleDown); - this.bind_hidden(down_arrow_element, this.menu.visible); - const up_arrow_element = icon(Icon.TriangleUp); - this.bind_hidden( - up_arrow_element, - this.menu.visible.map(v => !v), - ); - const button_element = span( { className: "core_ComboBox_button" }, down_arrow_element, @@ -128,6 +121,14 @@ export class ComboBox extends LabelledControl { this.selected.set_val(value, { silent: false }); this.input_element.focus(); }), + + bind_attr( + up_arrow_element, + "hidden", + this.menu.visible.map(v => !v), + ), + + bind_attr(down_arrow_element, "hidden", this.menu.visible), ); this.finalize_construction(); diff --git a/src/core/gui/Control.ts b/src/core/gui/Control.ts index 013930d4..2b9e5f96 100644 --- a/src/core/gui/Control.ts +++ b/src/core/gui/Control.ts @@ -6,4 +6,6 @@ export type ControlOptions = WidgetOptions; * Represents all widgets that allow for user interaction such as buttons, text inputs, combo boxes, * etc. */ -export abstract class Control extends Widget {} +export abstract class Control extends Widget { + readonly children: readonly Widget[] = []; +} diff --git a/src/core/gui/ErrorView.css b/src/core/gui/ErrorView.css deleted file mode 100644 index 97850593..00000000 --- a/src/core/gui/ErrorView.css +++ /dev/null @@ -1,4 +0,0 @@ -.core_ErrorView { - box-sizing: border-box; - padding: 10%; -} diff --git a/src/core/gui/ErrorView.ts b/src/core/gui/ErrorView.ts deleted file mode 100644 index f5301789..00000000 --- a/src/core/gui/ErrorView.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ResizableWidget } from "./ResizableWidget"; -import { UnavailableView } from "../../quest_editor/gui/UnavailableView"; -import "./ErrorView.css"; -import { div } from "./dom"; - -export class ErrorView extends ResizableWidget { - readonly element: HTMLElement; - - constructor(message: string) { - super(); - - this.element = div( - { className: "core_ErrorView" }, - this.disposable(new UnavailableView(message)).element, - ); - - this.finalize_construction(); - } -} diff --git a/src/core/gui/ErrorWidget.css b/src/core/gui/ErrorWidget.css new file mode 100644 index 00000000..3d0e2449 --- /dev/null +++ b/src/core/gui/ErrorWidget.css @@ -0,0 +1,10 @@ +.core_ErrorWidget { + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + padding: 10%; + text-align: center; +} diff --git a/src/core/gui/ErrorWidget.ts b/src/core/gui/ErrorWidget.ts new file mode 100644 index 00000000..0a0bd2f6 --- /dev/null +++ b/src/core/gui/ErrorWidget.ts @@ -0,0 +1,22 @@ +import { ResizableWidget } from "./ResizableWidget"; +import "./ErrorWidget.css"; +import { div } from "./dom"; +import { Widget } from "./Widget"; +import { Label } from "./Label"; + +export class ErrorWidget extends ResizableWidget { + private readonly label: Label; + + readonly element = div({ className: "core_ErrorWidget" }); + readonly children: readonly Widget[] = []; + + constructor(message: string) { + super(); + + this.label = this.disposable(new Label(message, { enabled: false })); + + this.element.append(this.label.element); + + this.finalize_construction(); + } +} diff --git a/src/core/gui/Label.ts b/src/core/gui/Label.ts index 1bf23f7f..b8885681 100644 --- a/src/core/gui/Label.ts +++ b/src/core/gui/Label.ts @@ -6,21 +6,20 @@ import { WidgetProperty } from "../observable/property/WidgetProperty"; import { label } from "./dom"; export class Label extends Widget { + private readonly _text = new WidgetProperty(this, "", this.set_text); + readonly element = label({ className: "core_Label" }); + readonly children: readonly Widget[] = []; set for(id: string) { this.element.htmlFor = id; } - readonly text: WritableProperty; - - private readonly _text = new WidgetProperty(this, "", this.set_text); + readonly text: WritableProperty = this._text; constructor(text: string | Property, options?: WidgetOptions) { super(options); - this.text = this._text; - if (typeof text === "string") { this.set_text(text); } else { diff --git a/src/core/gui/LazyWidget.ts b/src/core/gui/LazyWidget.ts index 397e02eb..d9f31d8f 100644 --- a/src/core/gui/LazyWidget.ts +++ b/src/core/gui/LazyWidget.ts @@ -1,20 +1,34 @@ -import { Widget } from "./Widget"; -import { Resizable } from "./Resizable"; import { ResizableWidget } from "./ResizableWidget"; import { div } from "./dom"; +import { Widget } from "./Widget"; +import { Resizable } from "./Resizable"; export class LazyWidget extends ResizableWidget { - readonly element = div({ className: "core_LazyView" }); - private initialized = false; private view: (Widget & Resizable) | undefined; + readonly element = div({ className: "core_LazyView" }); + + get children(): readonly (Widget & Resizable)[] { + return this.view ? [this.view] : []; + } + constructor(private create_view: () => Promise) { super(); this.visible.val = false; } + resize(width: number, height: number): this { + super.resize(width, height); + + if (this.view) { + this.view.resize(width, height); + } + + return this; + } + protected set_visible(visible: boolean): void { super.set_visible(visible); @@ -28,20 +42,11 @@ export class LazyWidget extends ResizableWidget { this.view = this.disposable(view); this.view.resize(this.width, this.height); this.element.append(view.element); + this.view.activate(); } }); } this.finalize_construction(); } - - resize(width: number, height: number): this { - super.resize(width, height); - - if (this.view) { - this.view.resize(width, height); - } - - return this; - } } diff --git a/src/core/gui/Menu.ts b/src/core/gui/Menu.ts index fca68b57..cd799eb0 100644 --- a/src/core/gui/Menu.ts +++ b/src/core/gui/Menu.ts @@ -14,6 +14,7 @@ export type MenuOptions = { export class Menu extends Widget { readonly element = div({ className: "core_Menu", tabIndex: -1 }); + readonly children: readonly Widget[] = []; readonly selected: WritableProperty; private readonly to_label: (item: T) => string; diff --git a/src/core/gui/RendererWidget.ts b/src/core/gui/RendererWidget.ts index 0f359f27..a3be5f99 100644 --- a/src/core/gui/RendererWidget.ts +++ b/src/core/gui/RendererWidget.ts @@ -1,9 +1,11 @@ import { ResizableWidget } from "./ResizableWidget"; import { Renderer } from "../rendering/Renderer"; import { div } from "./dom"; +import { Widget } from "./Widget"; export class RendererWidget extends ResizableWidget { readonly element = div({ className: "core_RendererWidget" }); + readonly children: readonly Widget[] = []; constructor(private renderer: Renderer) { super(); diff --git a/src/core/gui/Resizable.ts b/src/core/gui/Resizable.ts index 14d665d2..a1853a90 100644 --- a/src/core/gui/Resizable.ts +++ b/src/core/gui/Resizable.ts @@ -1,3 +1,3 @@ export interface Resizable { - resize(width: number, height: number): this; + resize(width: number, height: number): void; } diff --git a/src/core/gui/ResizableView.ts b/src/core/gui/ResizableView.ts new file mode 100644 index 00000000..00d7a219 --- /dev/null +++ b/src/core/gui/ResizableView.ts @@ -0,0 +1,14 @@ +import { View } from "./View"; +import { Resizable } from "./Resizable"; + +export abstract class ResizableView extends View implements Resizable { + protected width: number = 0; + protected height: number = 0; + + resize(width: number, height: number): void { + this.width = width; + this.height = height; + this.element.style.width = `${width}px`; + this.element.style.height = `${height}px`; + } +} diff --git a/src/core/gui/ResizableWidget.ts b/src/core/gui/ResizableWidget.ts index 1221a8e2..43aaecfe 100644 --- a/src/core/gui/ResizableWidget.ts +++ b/src/core/gui/ResizableWidget.ts @@ -5,11 +5,10 @@ export abstract class ResizableWidget extends Widget implements Resizable { protected width: number = 0; protected height: number = 0; - resize(width: number, height: number): this { + resize(width: number, height: number): void { this.width = width; this.height = height; this.element.style.width = `${width}px`; this.element.style.height = `${height}px`; - return this; } } diff --git a/src/core/gui/TabContainer.ts b/src/core/gui/TabContainer.ts index 2afe2afd..e626022a 100644 --- a/src/core/gui/TabContainer.ts +++ b/src/core/gui/TabContainer.ts @@ -1,13 +1,15 @@ import { Widget, WidgetOptions } from "./Widget"; import { LazyWidget } from "./LazyWidget"; -import { Resizable } from "./Resizable"; import { ResizableWidget } from "./ResizableWidget"; import "./TabContainer.css"; import { div, span } from "./dom"; +import { GuiStore } from "../stores/GuiStore"; +import { Resizable } from "./Resizable"; export type Tab = { title: string; key: string; + path?: string; create_view: () => Promise; }; @@ -20,13 +22,18 @@ type TabInfo = Tab & { tab_element: HTMLSpanElement; lazy_view: LazyWidget }; const BAR_HEIGHT = 28; export class TabContainer extends ResizableWidget { - readonly element = div({ className: "core_TabContainer" }); - private tabs: TabInfo[] = []; private bar_element = div({ className: "core_TabContainer_Bar" }); private panes_element = div({ className: "core_TabContainer_Panes" }); + private active_tab?: TabInfo; - constructor(options: TabContainerOptions) { + readonly element = div({ className: "core_TabContainer" }); + + get children(): readonly Widget[] { + return this.tabs.flatMap(tab => tab.lazy_view.children); + } + + constructor(private readonly gui_store: GuiStore, options: TabContainerOptions) { super(options); this.bar_element.onmousedown = this.bar_mousedown; @@ -43,19 +50,16 @@ export class TabContainer extends ResizableWidget { const lazy_view = this.disposable(new LazyWidget(tab.create_view)); - this.tabs.push({ + const tab_info: TabInfo = { ...tab, tab_element, lazy_view, - }); + }; + this.tabs.push(tab_info); this.panes_element.append(lazy_view.element); } - if (this.tabs.length) { - this.activate(this.tabs[0].key); - } - this.element.append(this.bar_element, this.panes_element); this.finalize_construction(); @@ -79,24 +83,61 @@ export class TabContainer extends ResizableWidget { return this; } + activate(): void { + if (this.active_tab) { + this.activate_tab(this.active_tab); + } else { + let active_tab: TabInfo | undefined; + + for (const tab_info of this.tabs) { + if ( + tab_info.path != undefined && + this.gui_store.path.val.startsWith(tab_info.path) + ) { + active_tab = tab_info; + } + } + + if (active_tab) { + this.activate_tab(active_tab); + } else if (this.tabs.length) { + this.activate_tab(this.tabs[0]); + } + } + } + private bar_mousedown = (e: MouseEvent): void => { if (e.target instanceof HTMLElement) { const key = e.target.dataset["key"]; - if (key) this.activate(key); + if (key) this.activate_key(key); } }; - private activate(key: string): void { + private activate_key(key: string): void { for (const tab of this.tabs) { - const active = tab.key === key; + if (tab.key === key) { + this.activate_tab(tab); + break; + } + } + } - if (active) { - tab.tab_element.classList.add("active"); - } else { - tab.tab_element.classList.remove("active"); + private activate_tab(tab: TabInfo): void { + if (this.active_tab !== tab) { + if (this.active_tab) { + this.active_tab.tab_element.classList.remove("active"); + this.active_tab.lazy_view.visible.val = false; + this.active_tab.lazy_view.deactivate(); } - tab.lazy_view.visible.val = active; + this.active_tab = tab; + tab.tab_element.classList.add("active"); + tab.lazy_view.visible.val = true; + } + + if (tab.path != undefined) { + this.gui_store.set_path_prefix(tab.path); + tab.lazy_view.activate(); } } } diff --git a/src/core/gui/Table.ts b/src/core/gui/Table.ts index b5094691..88e4eadf 100644 --- a/src/core/gui/Table.ts +++ b/src/core/gui/Table.ts @@ -36,13 +36,14 @@ export type TableOptions = WidgetOptions & { }; export class Table extends Widget { - readonly element = table({ className: "core_Table" }); - private readonly tbody_element = tbody(); private readonly footer_row_element?: HTMLTableRowElement; private readonly values: ListProperty; private readonly columns: Column[]; + readonly element = table({ className: "core_Table" }); + readonly children: readonly Widget[] = []; + constructor(options: TableOptions) { super(options); diff --git a/src/core/gui/ToolBar.ts b/src/core/gui/ToolBar.ts index e65e7315..c4b4e7a9 100644 --- a/src/core/gui/ToolBar.ts +++ b/src/core/gui/ToolBar.ts @@ -4,10 +4,9 @@ import { LabelledControl } from "./LabelledControl"; import { div } from "./dom"; export class ToolBar extends Widget { - private readonly children: readonly Widget[]; - readonly element = div({ className: "core_ToolBar" }); readonly height = 33; + readonly children: readonly Widget[]; constructor(options?: WidgetOptions, ...children: Widget[]) { // noinspection SuspiciousTypeOfGuard diff --git a/src/core/gui/View.ts b/src/core/gui/View.ts new file mode 100644 index 00000000..274b6922 --- /dev/null +++ b/src/core/gui/View.ts @@ -0,0 +1,32 @@ +import { Widget } from "./Widget"; +import { array_remove } from "../util"; + +export abstract class View extends Widget { + private readonly _children: Widget[] = []; + + get children(): readonly Widget[] { + return this._children; + } + + dispose(): void { + this._children.splice(0); + super.dispose(); + } + + /** + * Adds a child widget to the {@link _children} array and makes sure it is disposed when this + * widget is disposed. + */ + protected add(child: T): T { + this._children.push(child); + return this.disposable(child); + } + + /** + * Removes a child widget from the {@link _children} array and disposes it. + */ + protected remove(child: Widget): void { + array_remove(this._children, child); + this.remove_disposable(child); + } +} diff --git a/src/core/gui/Widget.ts b/src/core/gui/Widget.ts index 50fcf6e2..034eac9c 100644 --- a/src/core/gui/Widget.ts +++ b/src/core/gui/Widget.ts @@ -1,7 +1,5 @@ import { Disposable } from "../observable/Disposable"; import { Disposer } from "../observable/Disposer"; -import { Observable } from "../observable/Observable"; -import { bind_hidden } from "./dom"; import { WritableProperty } from "../observable/property/WritableProperty"; import { WidgetProperty } from "../observable/property/WidgetProperty"; import { Property } from "../observable/property/Property"; @@ -21,6 +19,9 @@ export type WidgetOptions = { */ export abstract class Widget implements Disposable { private readonly disposer = new Disposer(); + + private _active = false; + private readonly _visible: WidgetProperty = new WidgetProperty( this, true, @@ -49,12 +50,32 @@ export abstract class Widget implements Disposable { this.element.id = id; } + /** + * An active widget might, for example, run an animation loop. + */ + get active(): boolean { + return this._active; + } + + abstract readonly children: readonly Widget[]; + get disposed(): boolean { return this.disposer.disposed; } + /** + * An invisible widget typically sets the hidden attribute on its {@link element}. + */ readonly visible: WritableProperty = this._visible; + /** + * A disabled widget typically sets the disabled attribute on its {@link element} and adds the + * `disabled` class to it. + */ readonly enabled: WritableProperty = this._enabled; + /** + * The {@link tooltip} property typically corresponds to the `tooltip` attribute of its + * {@link element}. + */ readonly tooltip: WritableProperty = this._tooltip; protected constructor(options: WidgetOptions = {}) { @@ -71,15 +92,48 @@ export abstract class Widget implements Disposable { }, 0); } + /** + * Activate this widget. This call will also be propagated to the relevant children. + */ + activate(): void { + this._active = true; + + for (const child of this.children) { + child.activate(); + } + } + + /** + * Deactivate this widget. This call will also be propagated to the relevant children. + */ + deactivate(): void { + this._active = false; + + for (const child of this.children) { + child.deactivate(); + } + } + + /** + * Move focus to this widget. + */ focus(): void { this.element.focus(); } + /** + * Removes the widget's {@link element} from the DOM and disposes all its held disposables. + */ dispose(): void { this.element.remove(); this.disposer.dispose(); } + /** + * Every concrete subclass of {@link Widget} should call this method at the end of its + * constructor. When this method is called, we can refer to abstract properties that are + * provided by subclasses. + */ protected finalize_construction(): void { if (Object.getPrototypeOf(this) !== this.constructor.prototype) return; @@ -123,10 +177,6 @@ export abstract class Widget implements Disposable { this.element.title = tooltip; } - protected bind_hidden(element: HTMLElement, observable: Observable): void { - this.disposable(bind_hidden(element, observable)); - } - protected disposable(disposable: T): T { return this.disposer.add(disposable); } @@ -134,4 +184,8 @@ export abstract class Widget implements Disposable { protected disposables(...disposables: Disposable[]): void { this.disposer.add_all(...disposables); } + + protected remove_disposable(disposable: Disposable): void { + this.disposer.remove(disposable); + } } diff --git a/src/core/gui/dom.ts b/src/core/gui/dom.ts index 5e52bac3..c711091f 100644 --- a/src/core/gui/dom.ts +++ b/src/core/gui/dom.ts @@ -192,10 +192,6 @@ export function bind_attr( return observable.observe(({ value }) => (element[attribute] = value)); } -export function bind_hidden(element: HTMLElement, observable: Observable): Disposable { - return bind_attr(element, "hidden", observable); -} - export enum Icon { ArrowDown, Eye, diff --git a/src/core/observable/Disposer.ts b/src/core/observable/Disposer.ts index 4ddba732..763c716e 100644 --- a/src/core/observable/Disposer.ts +++ b/src/core/observable/Disposer.ts @@ -1,5 +1,6 @@ import { Disposable } from "./Disposable"; import { LogManager } from "../Logger"; +import { array_remove } from "../util"; const logger = LogManager.get("core/observable/Disposer"); @@ -60,6 +61,14 @@ export class Disposer implements Disposable { return this; } + /** + * Removes and disposes the given disposable. + */ + remove(disposable: Disposable): void { + array_remove(this.disposables, disposable); + disposable.dispose(); + } + /** * Disposes all held disposables. */ diff --git a/src/core/observable/index.ts b/src/core/observable/index.ts index 0c1d472a..f8440a28 100644 --- a/src/core/observable/index.ts +++ b/src/core/observable/index.ts @@ -10,6 +10,8 @@ import { Observable } from "./Observable"; import { FlatMappedProperty } from "./property/FlatMappedProperty"; import { ListProperty } from "./property/list/ListProperty"; import { FlatMappedListProperty } from "./property/list/FlatMappedListProperty"; +import { Disposable } from "./Disposable"; +import { Disposer } from "./Disposer"; export function emitter(): Emitter { return new SimpleEmitter(); @@ -34,6 +36,43 @@ export function sub(left: Property, right: number): Property { return left.map(l => l - right); } +export function observe(observer: (prop_1: P1) => void, prop_1: Property): Disposable; +export function observe( + observer: (prop_1: P1, prop_2: P2) => void, + prop_1: Property, + prop_2: Property, +): Disposable; +export function observe( + observer: (prop_1: P1, prop_2: P2, prop_3: P3) => void, + prop_1: Property, + prop_2: Property, + prop_3: Property, +): Disposable; +export function observe( + observer: (prop_1: P1, prop_2: P2, prop_3: P3, prop_4: P4) => void, + prop_1: Property, + prop_2: Property, + prop_3: Property, + prop_4: Property, +): Disposable; +export function observe( + observer: (prop_1: P1, prop_2: P2, prop_3: P3, prop_4: P4, prop_5: P5) => void, + prop_1: Property, + prop_2: Property, + prop_3: Property, + prop_4: Property, + prop_5: Property, +): Disposable; +export function observe( + observer: (...props: any[]) => void, + ...props: Property[] +): Disposable { + const observer_function = (prop: Property): Disposable => + prop.observe(() => observer(...props.map(p => p.val))); + + return new Disposer(...props.map(observer_function)); +} + export function map(transform: (prop_1: P1) => R, prop_1: Property): Property; export function map( transform: (prop_1: P1, prop_2: P2) => R, diff --git a/src/core/rendering/Renderer.ts b/src/core/rendering/Renderer.ts index b4181ad4..1e3b81ac 100644 --- a/src/core/rendering/Renderer.ts +++ b/src/core/rendering/Renderer.ts @@ -72,8 +72,10 @@ export abstract class Renderer implements Disposable { } start_rendering(): void { - this.schedule_render(); - this.animation_frame_handle = requestAnimationFrame(this.call_render); + if (this.animation_frame_handle == undefined) { + this.schedule_render(); + this.animation_frame_handle = requestAnimationFrame(this.call_render); + } } stop_rendering(): void { diff --git a/src/core/stores/GuiStore.ts b/src/core/stores/GuiStore.ts index 18fb4636..c7d33296 100644 --- a/src/core/stores/GuiStore.ts +++ b/src/core/stores/GuiStore.ts @@ -20,18 +20,25 @@ const GUI_TOOL_TO_STRING = new Map([ const STRING_TO_GUI_TOOL = new Map([...GUI_TOOL_TO_STRING.entries()].map(([k, v]) => [v, k])); export class GuiStore extends Store { + private readonly _tool: WritableProperty = property(GuiTool.Viewer); + private readonly _path: WritableProperty = property(""); private readonly _server: WritableProperty = property(Server.Ephinea); private readonly global_keydown_handlers = new Map void>(); private readonly features: Set = new Set(); - readonly tool: WritableProperty = property(GuiTool.Viewer); + readonly tool: Property = this._tool; + readonly path: Property = this._path; readonly server: Property = this._server; constructor() { super(); const url = window.location.hash.slice(2); - const [tool_str, params_str] = url.split("?"); + const [full_path, params_str] = url.split("?"); + + const first_slash_idx = full_path.indexOf("/"); + const tool_str = first_slash_idx === -1 ? full_path : full_path.slice(0, first_slash_idx); + const path = first_slash_idx === -1 ? "" : full_path.slice(first_slash_idx); if (params_str) { const features = params_str @@ -46,21 +53,35 @@ export class GuiStore extends Store { } } - this.disposables( - this.tool.observe(({ value: tool }) => { - let hash = `#/${gui_tool_to_string(tool)}`; + this.disposables(disposable_listener(window, "keydown", this.dispatch_global_keydown)); - if (this.features.size) { - hash += "?features=" + [...this.features].join(","); - } + this.set_tool(string_to_gui_tool(tool_str) ?? GuiTool.Viewer, path); + } - window.location.hash = hash; - }), + set_tool(tool: GuiTool, path: string = ""): void { + this._path.val = path; + this._tool.val = tool; + this.update_location(); + } - disposable_listener(window, "keydown", this.dispatch_global_keydown), - ); + /** + * Updates the path to `path_prefix` if the current path doesn't start with `path_prefix`. + */ + set_path_prefix(path_prefix: string): void { + if (!this.path.val.startsWith(path_prefix)) { + this._path.val = path_prefix; + this.update_location(); + } + } - this.tool.val = string_to_gui_tool(tool_str) || GuiTool.Viewer; + private update_location(): void { + let hash = `#/${gui_tool_to_string(this.tool.val)}${this.path.val}`; + + if (this.features.size) { + hash += "?features=" + [...this.features].join(","); + } + + window.location.hash = hash; } on_global_keydown( diff --git a/src/core/util.ts b/src/core/util.ts index 5810529f..4fdc323c 100644 --- a/src/core/util.ts +++ b/src/core/util.ts @@ -14,6 +14,26 @@ export function arrays_equal( return true; } +/** + * Removes 0 or more elements from `array`. + * + * @returns The number of removed elements. + */ +export function array_remove(array: T[], ...elements: T[]): number { + let count = 0; + + for (const element of elements) { + const index = array.indexOf(element); + + if (index !== -1) { + array.splice(index, 1); + count++; + } + } + + return count; +} + /** * @param min - The minimum value, inclusive. * @param max - The maximum value, exclusive. diff --git a/src/hunt_optimizer/gui/HelpView.ts b/src/hunt_optimizer/gui/HelpView.ts index 2d45f9e5..11e1cda1 100644 --- a/src/hunt_optimizer/gui/HelpView.ts +++ b/src/hunt_optimizer/gui/HelpView.ts @@ -1,8 +1,8 @@ -import { ResizableWidget } from "../../core/gui/ResizableWidget"; import "./HelpView.css"; import { div, p } from "../../core/gui/dom"; +import { ResizableView } from "../../core/gui/ResizableView"; -export class HelpView extends ResizableWidget { +export class HelpView extends ResizableView { readonly element = div( { className: "hunt_optimizer_HelpView" }, p( diff --git a/src/hunt_optimizer/gui/HuntOptimizerView.ts b/src/hunt_optimizer/gui/HuntOptimizerView.ts index 2bb42fd1..d795d4af 100644 --- a/src/hunt_optimizer/gui/HuntOptimizerView.ts +++ b/src/hunt_optimizer/gui/HuntOptimizerView.ts @@ -2,41 +2,65 @@ import { TabContainer } from "../../core/gui/TabContainer"; import { ServerMap } from "../../core/stores/ServerMap"; import { HuntOptimizerStore } from "../stores/HuntOptimizerStore"; import { HuntMethodStore } from "../stores/HuntMethodStore"; +import { GuiStore } from "../../core/stores/GuiStore"; +import { ResizableView } from "../../core/gui/ResizableView"; + +export class HuntOptimizerView extends ResizableView { + private readonly tab_container: TabContainer; + + get element(): HTMLElement { + return this.tab_container.element; + } -export class HuntOptimizerView extends TabContainer { constructor( + gui_store: GuiStore, hunt_optimizer_stores: ServerMap, hunt_method_stores: ServerMap, ) { - super({ - class: "hunt_optimizer_HuntOptimizerView", - tabs: [ - { - title: "Optimize", - key: "optimize", - create_view: async function() { - return new (await import("./OptimizerView")).OptimizerView( - hunt_optimizer_stores, - ); + super(); + + this.tab_container = this.add( + new TabContainer(gui_store, { + class: "hunt_optimizer_HuntOptimizerView", + tabs: [ + { + title: "Optimize", + key: "optimize", + path: "/optimize", + create_view: async function() { + return new (await import("./OptimizerView")).OptimizerView( + hunt_optimizer_stores, + ); + }, }, - }, - { - title: "Methods", - key: "methods", - create_view: async function() { - return new (await import("./MethodsView")).MethodsView(hunt_method_stores); + { + title: "Methods", + key: "methods", + path: "/methods", + create_view: async function() { + return new (await import("./MethodsView")).MethodsView( + gui_store, + hunt_method_stores, + ); + }, }, - }, - { - title: "Help", - key: "help", - create_view: async function() { - return new (await import("./HelpView")).HelpView(); + { + title: "Help", + key: "help", + path: "/help", + create_view: async function() { + return new (await import("./HelpView")).HelpView(); + }, }, - }, - ], - }); + ], + }), + ); this.finalize_construction(); } + + resize(width: number, height: number): void { + super.resize(width, height); + this.tab_container.resize(width, height); + } } diff --git a/src/hunt_optimizer/gui/MethodsForEpisodeView.ts b/src/hunt_optimizer/gui/MethodsForEpisodeView.ts index 9ac65157..4755ee7c 100644 --- a/src/hunt_optimizer/gui/MethodsForEpisodeView.ts +++ b/src/hunt_optimizer/gui/MethodsForEpisodeView.ts @@ -1,4 +1,3 @@ -import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { Episode } from "../../core/data_formats/parsing/quest/Episode"; import { HuntMethodModel } from "../model/HuntMethodModel"; import { @@ -16,10 +15,11 @@ import { ServerMap } from "../../core/stores/ServerMap"; import { HuntMethodStore } from "../stores/HuntMethodStore"; import { LogManager } from "../../core/Logger"; import { div } from "../../core/gui/dom"; +import { ResizableView } from "../../core/gui/ResizableView"; const logger = LogManager.get("hunt_optimizer/gui/MethodsForEpisodeView"); -export class MethodsForEpisodeView extends ResizableWidget { +export class MethodsForEpisodeView extends ResizableView { readonly element = div({ className: "hunt_optimizer_MethodsForEpisodeView" }); private readonly episode: Episode; @@ -35,7 +35,7 @@ export class MethodsForEpisodeView extends ResizableWidget { const hunt_methods = list_property(); - const table = this.disposable( + const table = this.add( new Table({ class: "hunt_optimizer_MethodsForEpisodeView_table", values: hunt_methods, @@ -123,7 +123,7 @@ export class MethodsForEpisodeView extends ResizableWidget { this.element.append(table.element); - this.disposable( + this.disposables( hunt_method_stores.current.observe( async ({ value }) => { try { diff --git a/src/hunt_optimizer/gui/MethodsView.ts b/src/hunt_optimizer/gui/MethodsView.ts index ea8f211b..ed782d7f 100644 --- a/src/hunt_optimizer/gui/MethodsView.ts +++ b/src/hunt_optimizer/gui/MethodsView.ts @@ -3,15 +3,17 @@ import { Episode } from "../../core/data_formats/parsing/quest/Episode"; import { MethodsForEpisodeView } from "./MethodsForEpisodeView"; import { ServerMap } from "../../core/stores/ServerMap"; import { HuntMethodStore } from "../stores/HuntMethodStore"; +import { GuiStore } from "../../core/stores/GuiStore"; export class MethodsView extends TabContainer { - constructor(hunt_method_stores: ServerMap) { - super({ + constructor(gui_store: GuiStore, hunt_method_stores: ServerMap) { + super(gui_store, { class: "hunt_optimizer_MethodsView", tabs: [ { title: "Episode I", key: "episode_1", + path: "/methods/episode_1", create_view: async function() { return new MethodsForEpisodeView(hunt_method_stores, Episode.I); }, @@ -19,6 +21,7 @@ export class MethodsView extends TabContainer { { title: "Episode II", key: "episode_2", + path: "/methods/episode_2", create_view: async function() { return new MethodsForEpisodeView(hunt_method_stores, Episode.II); }, @@ -26,6 +29,7 @@ export class MethodsView extends TabContainer { { title: "Episode IV", key: "episode_4", + path: "/methods/episode_4", create_view: async function() { return new MethodsForEpisodeView(hunt_method_stores, Episode.IV); }, diff --git a/src/hunt_optimizer/gui/OptimizationResultView.ts b/src/hunt_optimizer/gui/OptimizationResultView.ts index 50ef9291..413aa701 100644 --- a/src/hunt_optimizer/gui/OptimizationResultView.ts +++ b/src/hunt_optimizer/gui/OptimizationResultView.ts @@ -1,4 +1,3 @@ -import { Widget } from "../../core/gui/Widget"; import { div, h2, section_id_icon, span } from "../../core/gui/dom"; import { Column, Table } from "../../core/gui/Table"; import { Disposable } from "../../core/observable/Disposable"; @@ -11,10 +10,11 @@ import { Duration } from "luxon"; import { ServerMap } from "../../core/stores/ServerMap"; import { HuntOptimizerStore } from "../stores/HuntOptimizerStore"; import { LogManager } from "../../core/Logger"; +import { View } from "../../core/gui/View"; const logger = LogManager.get("hunt_optimizer/gui/OptimizationResultView"); -export class OptimizationResultView extends Widget { +export class OptimizationResultView extends View { readonly element = div( { className: "hunt_optimizer_OptimizationResultView" }, h2("Ideal Combination of Methods"), @@ -34,14 +34,16 @@ export class OptimizationResultView extends Widget { if (this.disposed) return; if (this.results_observer) { - this.results_observer.dispose(); + this.remove_disposable(this.results_observer); } - this.results_observer = hunt_optimizer_store.result.observe( - ({ value }) => this.update_table(value), - { - call_now: true, - }, + this.results_observer = this.disposable( + hunt_optimizer_store.result.observe( + ({ value }) => this.update_table(value), + { + call_now: true, + }, + ), ); } catch (e) { logger.error("Couldn't load hunt optimizer store.", e); @@ -54,21 +56,9 @@ export class OptimizationResultView extends Widget { this.finalize_construction(); } - dispose(): void { - super.dispose(); - - if (this.results_observer) { - this.results_observer.dispose(); - } - - if (this.table) { - this.table.dispose(); - } - } - private update_table(result?: OptimalResultModel): void { if (this.table) { - this.table.dispose(); + this.remove(this.table); } let total_runs = 0; @@ -203,11 +193,15 @@ export class OptimizationResultView extends Widget { } } - this.table = new Table({ - class: "hunt_optimizer_OptimizationResultView_table", - values: result ? list_property(undefined, ...result.optimal_methods) : list_property(), - columns, - }); + this.table = this.add( + new Table({ + class: "hunt_optimizer_OptimizationResultView_table", + values: result + ? list_property(undefined, ...result.optimal_methods) + : list_property(), + columns, + }), + ); this.element.append(this.table.element); } diff --git a/src/hunt_optimizer/gui/OptimizerView.ts b/src/hunt_optimizer/gui/OptimizerView.ts index 3483661e..ace633aa 100644 --- a/src/hunt_optimizer/gui/OptimizerView.ts +++ b/src/hunt_optimizer/gui/OptimizerView.ts @@ -1,20 +1,20 @@ -import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { WantedItemsView } from "./WantedItemsView"; import "./OptimizerView.css"; import { OptimizationResultView } from "./OptimizationResultView"; import { ServerMap } from "../../core/stores/ServerMap"; import { HuntOptimizerStore } from "../stores/HuntOptimizerStore"; import { div } from "../../core/gui/dom"; +import { ResizableView } from "../../core/gui/ResizableView"; -export class OptimizerView extends ResizableWidget { +export class OptimizerView extends ResizableView { readonly element = div({ className: "hunt_optimizer_OptimizerView" }); constructor(hunt_optimizer_stores: ServerMap) { super(); this.element.append( - this.disposable(new WantedItemsView(hunt_optimizer_stores)).element, - this.disposable(new OptimizationResultView(hunt_optimizer_stores)).element, + this.add(new WantedItemsView(hunt_optimizer_stores)).element, + this.add(new OptimizationResultView(hunt_optimizer_stores)).element, ); this.finalize_construction(); diff --git a/src/hunt_optimizer/gui/WantedItemsView.ts b/src/hunt_optimizer/gui/WantedItemsView.ts index a3c13875..ab068042 100644 --- a/src/hunt_optimizer/gui/WantedItemsView.ts +++ b/src/hunt_optimizer/gui/WantedItemsView.ts @@ -2,7 +2,6 @@ import { bind_children_to, div, h2, Icon, table, tbody, td, tr } from "../../cor import "./WantedItemsView.css"; import { Button } from "../../core/gui/Button"; import { Disposer } from "../../core/observable/Disposer"; -import { Widget } from "../../core/gui/Widget"; import { WantedItemModel } from "../model"; import { NumberInput } from "../../core/gui/NumberInput"; import { ComboBox } from "../../core/gui/ComboBox"; @@ -12,22 +11,23 @@ import { Disposable } from "../../core/observable/Disposable"; import { ServerMap } from "../../core/stores/ServerMap"; import { HuntOptimizerStore } from "../stores/HuntOptimizerStore"; import { LogManager } from "../../core/Logger"; +import { View } from "../../core/gui/View"; const logger = LogManager.get("hunt_optimizer/gui/WantedItemsView"); -export class WantedItemsView extends Widget { - readonly element = div({ className: "hunt_optimizer_WantedItemsView" }); - +export class WantedItemsView extends View { private readonly tbody_element = tbody(); private readonly store_disposer = this.disposable(new Disposer()); + readonly element = div({ className: "hunt_optimizer_WantedItemsView" }); + constructor(private readonly hunt_optimizer_stores: ServerMap) { super(); const huntable_items = list_property(); const filtered_huntable_items = list_property(); - const combo_box = this.disposable( + const combo_box = this.add( new ComboBox({ items: filtered_huntable_items, to_label: item_type => item_type.name, diff --git a/src/hunt_optimizer/index.ts b/src/hunt_optimizer/index.ts index 2f35260e..bb140982 100644 --- a/src/hunt_optimizer/index.ts +++ b/src/hunt_optimizer/index.ts @@ -32,7 +32,9 @@ export function initialize_hunt_optimizer( ), ); - const view = disposer.add(new HuntOptimizerView(hunt_optimizer_stores, hunt_method_stores)); + const view = disposer.add( + new HuntOptimizerView(gui_store, hunt_optimizer_stores, hunt_method_stores), + ); return { view, diff --git a/src/quest_editor/gui/AsmEditorView.ts b/src/quest_editor/gui/AsmEditorView.ts index 81c09b71..4921454d 100644 --- a/src/quest_editor/gui/AsmEditorView.ts +++ b/src/quest_editor/gui/AsmEditorView.ts @@ -1,4 +1,3 @@ -import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { editor, KeyCode, KeyMod, Range } from "monaco-editor"; import { AsmEditorToolBar } from "./AsmEditorToolBar"; import { EditorHistory } from "./EditorHistory"; @@ -8,6 +7,7 @@ import { GuiStore } from "../../core/stores/GuiStore"; import { AsmEditorStore } from "../stores/AsmEditorStore"; import { QuestRunner } from "../QuestRunner"; import { div } from "../../core/gui/dom"; +import { ResizableView } from "../../core/gui/ResizableView"; import IStandaloneCodeEditor = editor.IStandaloneCodeEditor; editor.defineTheme("phantasmal-world", { @@ -31,7 +31,7 @@ editor.defineTheme("phantasmal-world", { const DUMMY_MODEL = editor.createModel("", "psoasm"); -export class AsmEditorView extends ResizableWidget { +export class AsmEditorView extends ResizableView { private readonly tool_bar_view: AsmEditorToolBar; private readonly editor: IStandaloneCodeEditor; private readonly history: EditorHistory; @@ -48,7 +48,7 @@ export class AsmEditorView extends ResizableWidget { ) { super(); - this.tool_bar_view = this.disposable(new AsmEditorToolBar(asm_editor_store)); + this.tool_bar_view = this.add(new AsmEditorToolBar(asm_editor_store)); this.element.append(this.tool_bar_view.element); diff --git a/src/quest_editor/gui/EntityInfoView.ts b/src/quest_editor/gui/EntityInfoView.ts index 5c5acfa1..359a2a36 100644 --- a/src/quest_editor/gui/EntityInfoView.ts +++ b/src/quest_editor/gui/EntityInfoView.ts @@ -1,12 +1,12 @@ -import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { bind_attr, div, table, td, th, tr } from "../../core/gui/dom"; import { UnavailableView } from "./UnavailableView"; import "./EntityInfoView.css"; import { NumberInput } from "../../core/gui/NumberInput"; import { rad_to_deg } from "../../core/math"; import { EntityInfoController } from "../controllers/EntityInfoController"; +import { ResizableView } from "../../core/gui/ResizableView"; -export class EntityInfoView extends ResizableWidget { +export class EntityInfoView extends ResizableView { readonly element = div({ className: "quest_editor_EntityInfoView", tabIndex: -1 }); private readonly no_entity_view = new UnavailableView("No entity selected."); @@ -18,12 +18,12 @@ export class EntityInfoView extends ResizableWidget { private readonly section_id_element: HTMLTableCellElement; private readonly wave_element: HTMLTableCellElement; private readonly wave_row_element: HTMLTableRowElement; - private readonly pos_x_element = this.disposable(new NumberInput(0, { round_to: 3 })); - private readonly pos_y_element = this.disposable(new NumberInput(0, { round_to: 3 })); - private readonly pos_z_element = this.disposable(new NumberInput(0, { round_to: 3 })); - private readonly rot_x_element = this.disposable(new NumberInput(0, { round_to: 3 })); - private readonly rot_y_element = this.disposable(new NumberInput(0, { round_to: 3 })); - private readonly rot_z_element = this.disposable(new NumberInput(0, { round_to: 3 })); + private readonly pos_x_element = this.add(new NumberInput(0, { round_to: 3 })); + private readonly pos_y_element = this.add(new NumberInput(0, { round_to: 3 })); + private readonly pos_z_element = this.add(new NumberInput(0, { round_to: 3 })); + private readonly rot_x_element = this.add(new NumberInput(0, { round_to: 3 })); + private readonly rot_y_element = this.add(new NumberInput(0, { round_to: 3 })); + private readonly rot_z_element = this.add(new NumberInput(0, { round_to: 3 })); constructor(private readonly ctrl: EntityInfoController) { super(); diff --git a/src/quest_editor/gui/EntityListView.ts b/src/quest_editor/gui/EntityListView.ts index 03fe36e0..7d5c688b 100644 --- a/src/quest_editor/gui/EntityListView.ts +++ b/src/quest_editor/gui/EntityListView.ts @@ -1,4 +1,3 @@ -import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { bind_children_to, div, img, span } from "../../core/gui/dom"; import "./EntityListView.css"; import { entity_data, EntityType } from "../../core/data_formats/parsing/quest/entities"; @@ -7,8 +6,9 @@ import { WritableListProperty } from "../../core/observable/property/list/Writab import { list_property } from "../../core/observable"; import { QuestEditorStore } from "../stores/QuestEditorStore"; import { EntityImageRenderer } from "../rendering/EntityImageRenderer"; +import { ResizableView } from "../../core/gui/ResizableView"; -export abstract class EntityListView extends ResizableWidget { +export abstract class EntityListView extends ResizableView { readonly element: HTMLElement; protected readonly entities: WritableListProperty = list_property(); diff --git a/src/quest_editor/gui/EventSubGraphView.ts b/src/quest_editor/gui/EventSubGraphView.ts index ab8ca088..543f70b2 100644 --- a/src/quest_editor/gui/EventSubGraphView.ts +++ b/src/quest_editor/gui/EventSubGraphView.ts @@ -1,4 +1,3 @@ -import { Widget } from "../../core/gui/Widget"; import { bind_children_to, div } from "../../core/gui/dom"; import { QuestEventDagModel, QuestEventDagModelChangeType } from "../model/QuestEventDagModel"; import { QuestEventModel } from "../model/QuestEventModel"; @@ -14,13 +13,14 @@ import { } from "../../core/observable/property/list/ListProperty"; import { WritableProperty } from "../../core/observable/property/WritableProperty"; import { LogManager } from "../../core/Logger"; +import { View } from "../../core/gui/View"; const logger = LogManager.get("quest_editor/gui/EventSubGraphView"); const EDGE_HORIZONTAL_SPACING = 8; const EDGE_VERTICAL_SPACING = 20; -export class EventSubGraphView extends Widget { +export class EventSubGraphView extends View { /** * Maps event IDs to GUI data. */ diff --git a/src/quest_editor/gui/EventView.ts b/src/quest_editor/gui/EventView.ts index 70e3545a..ef23d65b 100644 --- a/src/quest_editor/gui/EventView.ts +++ b/src/quest_editor/gui/EventView.ts @@ -1,4 +1,3 @@ -import { Widget } from "../../core/gui/Widget"; import { bind_attr, bind_children_to, @@ -23,8 +22,9 @@ import { Disposer } from "../../core/observable/Disposer"; import { property } from "../../core/observable"; import { DropDown } from "../../core/gui/DropDown"; import { Button } from "../../core/gui/Button"; +import { View } from "../../core/gui/View"; -export class EventView extends Widget { +export class EventView extends View { private readonly inputs_enabled = property(true); private readonly delay_input: NumberInput; @@ -34,11 +34,11 @@ export class EventView extends Widget { super(); const wave_node = document.createTextNode(event.wave.id.val.toString()); - this.delay_input = this.disposable( + this.delay_input = this.add( new NumberInput(event.delay.val, { min: 0, step: 1, enabled: this.inputs_enabled }), ); const action_table = table({ className: "quest_editor_EventView_actions" }); - const add_action_dropdown: DropDown = this.disposable( + const add_action_dropdown: DropDown = this.add( new DropDown({ text: "Add action", items: QuestEventActionTypes, diff --git a/src/quest_editor/gui/EventsView.ts b/src/quest_editor/gui/EventsView.ts index f58859f2..6fcf8bcf 100644 --- a/src/quest_editor/gui/EventsView.ts +++ b/src/quest_editor/gui/EventsView.ts @@ -1,4 +1,3 @@ -import { ResizableWidget } from "../../core/gui/ResizableWidget"; import "./EventsView.css"; import { EventsController } from "../controllers/EventsController"; import { UnavailableView } from "./UnavailableView"; @@ -13,8 +12,9 @@ import { ListProperty, } from "../../core/observable/property/list/ListProperty"; import { QuestEventModel } from "../model/QuestEventModel"; +import { ResizableView } from "../../core/gui/ResizableView"; -export class EventsView extends ResizableWidget { +export class EventsView extends ResizableView { private readonly sub_graph_views: Map< ListProperty, EventSubGraphView @@ -37,9 +37,8 @@ export class EventsView extends ResizableWidget { { className: "quest_editor_EventsView", tabIndex: -1 }, (this.container_element = div( { className: "quest_editor_EventsView_container" }, - this.disposable( - new ToolBar((this.add_event_button = new Button({ text: "Add event" }))), - ).element, + this.add(new ToolBar((this.add_event_button = new Button({ text: "Add event" })))) + .element, (this.dag_container_element = div({ className: "quest_editor_EventsView_sub_graph_container", })), diff --git a/src/quest_editor/gui/LogView.ts b/src/quest_editor/gui/LogView.ts index c0295e0a..0ab0da2c 100644 --- a/src/quest_editor/gui/LogView.ts +++ b/src/quest_editor/gui/LogView.ts @@ -1,14 +1,14 @@ import { bind_children_to, div } from "../../core/gui/dom"; -import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { ToolBar } from "../../core/gui/ToolBar"; import "./LogView.css"; import { log_store } from "../stores/LogStore"; import { Select } from "../../core/gui/Select"; import { LogEntry, LogLevel, LogLevels, time_to_string } from "../../core/Logger"; +import { ResizableView } from "../../core/gui/ResizableView"; const AUTOSCROLL_TRESHOLD = 5; -export class LogView extends ResizableWidget { +export class LogView extends ResizableView { readonly element = div({ className: "quest_editor_LogView", tabIndex: -1 }); // container is needed to get a scrollbar in the right place @@ -26,7 +26,7 @@ export class LogView extends ResizableWidget { this.list_container = div({ className: "quest_editor_LogView_list_container" }); this.list_element = div({ className: "quest_editor_LogView_message_list" }); - this.level_filter = this.disposable( + this.level_filter = this.add( new Select({ class: "quest_editor_LogView_level_filter", label: "Level:", @@ -35,7 +35,7 @@ export class LogView extends ResizableWidget { }), ); - this.settings_bar = this.disposable( + this.settings_bar = this.add( new ToolBar({ class: "quest_editor_LogView_settings" }, this.level_filter), ); diff --git a/src/quest_editor/gui/NpcCountsView.ts b/src/quest_editor/gui/NpcCountsView.ts index e03e34d9..6fd65c99 100644 --- a/src/quest_editor/gui/NpcCountsView.ts +++ b/src/quest_editor/gui/NpcCountsView.ts @@ -1,15 +1,15 @@ -import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { bind_attr, div, table, td, th, tr } from "../../core/gui/dom"; import "./NpcCountsView.css"; import { UnavailableView } from "./UnavailableView"; import { NameWithCount, NpcCountsController } from "../controllers/NpcCountsController"; +import { ResizableView } from "../../core/gui/ResizableView"; -export class NpcCountsView extends ResizableWidget { +export class NpcCountsView extends ResizableView { readonly element = div({ className: "quest_editor_NpcCountsView" }); private readonly table_element = table(); - private readonly unavailable_view = new UnavailableView("No quest loaded."); + private readonly unavailable_view = this.add(new UnavailableView("No quest loaded.")); constructor(ctrl: NpcCountsController) { super(); diff --git a/src/quest_editor/gui/QuestEditorRendererView.ts b/src/quest_editor/gui/QuestEditorRendererView.ts index 1e82612e..a2d91163 100644 --- a/src/quest_editor/gui/QuestEditorRendererView.ts +++ b/src/quest_editor/gui/QuestEditorRendererView.ts @@ -3,7 +3,6 @@ import { QuestEditorStore } from "../stores/QuestEditorStore"; import { QuestEditor3DModelManager } from "../rendering/QuestEditor3DModelManager"; import { QuestRendererView } from "./QuestRendererView"; import { QuestEntityControls } from "../rendering/QuestEntityControls"; -import { GuiStore } from "../../core/stores/GuiStore"; import { AreaAssetLoader } from "../loading/AreaAssetLoader"; import { EntityAssetLoader } from "../loading/EntityAssetLoader"; import { DisposableThreeRenderer } from "../../core/rendering/Renderer"; @@ -12,14 +11,12 @@ export class QuestEditorRendererView extends QuestRendererView { private readonly entity_controls: QuestEntityControls; constructor( - gui_store: GuiStore, quest_editor_store: QuestEditorStore, area_asset_loader: AreaAssetLoader, entity_asset_loader: EntityAssetLoader, three_renderer: DisposableThreeRenderer, ) { super( - gui_store, quest_editor_store, "quest_editor_QuestEditorRendererView", new QuestRenderer( diff --git a/src/quest_editor/gui/QuestEditorView.ts b/src/quest_editor/gui/QuestEditorView.ts index 5497e6e1..abfdc8df 100644 --- a/src/quest_editor/gui/QuestEditorView.ts +++ b/src/quest_editor/gui/QuestEditorView.ts @@ -1,4 +1,3 @@ -import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { QuestEditorToolBar } from "./QuestEditorToolBar"; import GoldenLayout, { Container, ContentItem, ItemConfigType } from "golden-layout"; import { QuestInfoView } from "./QuestInfoView"; @@ -18,8 +17,11 @@ import { QuestRunnerRendererView } from "./QuestRunnerRendererView"; import { QuestEditorStore } from "../stores/QuestEditorStore"; import { QuestEditorUiPersister } from "../persistence/QuestEditorUiPersister"; import { LogManager } from "../../core/Logger"; -import { ErrorView } from "../../core/gui/ErrorView"; +import { ErrorWidget } from "../../core/gui/ErrorWidget"; import { div } from "../../core/gui/dom"; +import { ResizableView } from "../../core/gui/ResizableView"; +import { Widget } from "../../core/gui/Widget"; +import { Resizable } from "../../core/gui/Resizable"; const logger = LogManager.get("quest_editor/gui/QuestEditorView"); @@ -40,22 +42,22 @@ const DEFAULT_LAYOUT_CONFIG = { }, }; -export class QuestEditorView extends ResizableWidget { +export class QuestEditorView extends ResizableView { readonly element = div({ className: "quest_editor_QuestEditorView" }); /** * Maps views to names and creation functions. */ private readonly view_map: Map< - new (...args: never) => ResizableWidget, - { name: string; create(): ResizableWidget } + new (...args: never) => Widget & Resizable, + { name: string; create(): Widget & Resizable } >; private readonly layout_element = div({ className: "quest_editor_gl_container" }); private readonly layout: Promise; private loaded_layout: GoldenLayout | undefined; - private readonly sub_views = new Map(); + private readonly sub_views = new Map(); constructor( private readonly gui_store: GuiStore, @@ -77,8 +79,8 @@ export class QuestEditorView extends ResizableWidget { // 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 } + new (...args: never) => Widget & Resizable, + { name: string; create(): Widget & Resizable } >([ [ QuestInfoView, @@ -185,6 +187,22 @@ export class QuestEditorView extends ResizableWidget { this.finalize_construction(); } + activate(): void { + super.activate(); + + for (const sub_view of this.sub_views.values()) { + sub_view.activate(); + } + } + + deactivate(): void { + for (const sub_view of this.sub_views.values()) { + sub_view.deactivate(); + } + + super.deactivate(); + } + resize(width: number, height: number): this { super.resize(width, height); @@ -239,21 +257,25 @@ export class QuestEditorView extends ResizableWidget { private attempt_gl_init(config: GoldenLayout.Config): GoldenLayout { const layout = new GoldenLayout(config, this.layout_element); - const sub_views = this.sub_views; + const self = this; // eslint-disable-line @typescript-eslint/no-this-alias 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) { - let view: ResizableWidget; + let view: Widget & Resizable; try { view = create(); + + if (self.active) { + view.activate(); + } } catch (e) { logger.error(`Couldn't instantiate "${name}".`, e); - view = new ErrorView("Something went wrong while creating this window."); + view = new ErrorWidget("Something went wrong while creating this window."); } container.on("close", () => view.dispose()); @@ -265,7 +287,7 @@ export class QuestEditorView extends ResizableWidget { view.resize(container.width, container.height); - sub_views.set(name, view); + self.sub_views.set(name, view); container.getElement().append(view.element); }); } diff --git a/src/quest_editor/gui/QuestInfoView.ts b/src/quest_editor/gui/QuestInfoView.ts index f3f4dc8e..a1278df0 100644 --- a/src/quest_editor/gui/QuestInfoView.ts +++ b/src/quest_editor/gui/QuestInfoView.ts @@ -1,4 +1,3 @@ -import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { Episode } from "../../core/data_formats/parsing/quest/Episode"; import { NumberInput } from "../../core/gui/NumberInput"; import { Disposer } from "../../core/observable/Disposer"; @@ -7,20 +6,21 @@ import { TextArea } from "../../core/gui/TextArea"; import "./QuestInfoView.css"; import { UnavailableView } from "./UnavailableView"; import { QuestInfoController } from "../controllers/QuestInfoController"; -import { div, table, td, th, tr } from "../../core/gui/dom"; +import { bind_attr, div, table, td, th, tr } from "../../core/gui/dom"; +import { ResizableView } from "../../core/gui/ResizableView"; -export class QuestInfoView extends ResizableWidget { +export class QuestInfoView extends ResizableView { readonly element = div({ className: "quest_editor_QuestInfoView", tabIndex: -1 }); private readonly table_element = table(); private readonly episode_element: HTMLElement; - private readonly id_input = this.disposable(new NumberInput(0, { min: 0, step: 1 })); - private readonly name_input = this.disposable( + private readonly id_input = this.add(new NumberInput(0, { min: 0, step: 1 })); + private readonly name_input = this.add( new TextInput("", { max_length: 32, }), ); - private readonly short_description_input = this.disposable( + private readonly short_description_input = this.add( new TextArea("", { max_length: 128, font_family: '"Courier New", monospace', @@ -28,7 +28,7 @@ export class QuestInfoView extends ResizableWidget { rows: 5, }), ); - private readonly long_description_input = this.disposable( + private readonly long_description_input = this.add( new TextArea("", { max_length: 288, font_family: '"Courier New", monospace', @@ -37,7 +37,7 @@ export class QuestInfoView extends ResizableWidget { }), ); - private readonly unavailable_view = new UnavailableView("No quest loaded."); + private readonly unavailable_view = this.add(new UnavailableView("No quest loaded.")); private readonly quest_disposer = this.disposable(new Disposer()); @@ -56,8 +56,6 @@ export class QuestInfoView extends ResizableWidget { tr(td({ colSpan: 2 }, this.long_description_input.element)), ); - this.bind_hidden(this.table_element, ctrl.unavailable); - this.element.append(this.table_element, this.unavailable_view.element); this.element.addEventListener("focus", ctrl.focused, true); @@ -65,6 +63,8 @@ export class QuestInfoView extends ResizableWidget { this.disposables( this.unavailable_view.visible.bind_to(ctrl.unavailable), + bind_attr(this.table_element, "hidden", ctrl.unavailable), + quest.observe(({ value: q }) => { this.quest_disposer.dispose_all(); diff --git a/src/quest_editor/gui/QuestRendererView.ts b/src/quest_editor/gui/QuestRendererView.ts index 7d9afd97..3628927a 100644 --- a/src/quest_editor/gui/QuestRendererView.ts +++ b/src/quest_editor/gui/QuestRendererView.ts @@ -1,11 +1,10 @@ -import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { RendererWidget } from "../../core/gui/RendererWidget"; import { QuestRenderer } from "../rendering/QuestRenderer"; -import { GuiStore, GuiTool } from "../../core/stores/GuiStore"; import { QuestEditorStore } from "../stores/QuestEditorStore"; import { div } from "../../core/gui/dom"; +import { ResizableView } from "../../core/gui/ResizableView"; -export abstract class QuestRendererView extends ResizableWidget { +export abstract class QuestRendererView extends ResizableView { private readonly renderer_view: RendererWidget; protected readonly renderer: QuestRenderer; @@ -13,7 +12,6 @@ export abstract class QuestRendererView extends ResizableWidget { readonly element: HTMLElement; protected constructor( - gui_store: GuiStore, quest_editor_store: QuestEditorStore, className: string, renderer: QuestRenderer, @@ -22,25 +20,26 @@ export abstract class QuestRendererView extends ResizableWidget { this.element = div({ className: className, tabIndex: -1 }); this.renderer = renderer; - this.renderer_view = this.disposable(new RendererWidget(this.renderer)); + this.renderer_view = this.add(new RendererWidget(this.renderer)); this.element.append(this.renderer_view.element); - this.renderer_view.start_rendering(); this.disposables( - gui_store.tool.observe(({ value: tool }) => { - if (tool === GuiTool.QuestEditor) { - this.renderer_view.start_rendering(); - } else { - this.renderer_view.stop_rendering(); - } - }), - quest_editor_store.debug.observe(({ value }) => (this.renderer.debug = value)), ); this.finalize_construction(); } + activate(): void { + this.renderer_view.start_rendering(); + super.activate(); + } + + deactivate(): void { + super.deactivate(); + this.renderer_view.stop_rendering(); + } + resize(width: number, height: number): this { super.resize(width, height); diff --git a/src/quest_editor/gui/QuestRunnerRendererView.ts b/src/quest_editor/gui/QuestRunnerRendererView.ts index f7f5475a..b85d55c6 100644 --- a/src/quest_editor/gui/QuestRunnerRendererView.ts +++ b/src/quest_editor/gui/QuestRunnerRendererView.ts @@ -1,7 +1,6 @@ import { QuestRenderer } from "../rendering/QuestRenderer"; import { QuestRunner3DModelManager } from "../rendering/QuestRunner3DModelManager"; import { QuestRendererView } from "./QuestRendererView"; -import { GuiStore } from "../../core/stores/GuiStore"; import { QuestEditorStore } from "../stores/QuestEditorStore"; import { AreaAssetLoader } from "../loading/AreaAssetLoader"; import { EntityAssetLoader } from "../loading/EntityAssetLoader"; @@ -9,14 +8,12 @@ import { DisposableThreeRenderer } from "../../core/rendering/Renderer"; export class QuestRunnerRendererView extends QuestRendererView { constructor( - gui_store: GuiStore, quest_editor_store: QuestEditorStore, area_asset_loader: AreaAssetLoader, entity_asset_loader: EntityAssetLoader, three_renderer: DisposableThreeRenderer, ) { super( - gui_store, quest_editor_store, "quest_editor_QuestRunnerRendererView", new QuestRenderer( diff --git a/src/quest_editor/gui/RegistersView.ts b/src/quest_editor/gui/RegistersView.ts index 099d8ad1..fb7f02cc 100644 --- a/src/quest_editor/gui/RegistersView.ts +++ b/src/quest_editor/gui/RegistersView.ts @@ -1,4 +1,3 @@ -import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { REGISTER_COUNT } from "../scripting/vm/VirtualMachine"; import { TextInput } from "../../core/gui/TextInput"; import { ToolBar } from "../../core/gui/ToolBar"; @@ -8,6 +7,7 @@ import "./RegistersView.css"; import { Select } from "../../core/gui/Select"; import { QuestRunner } from "../QuestRunner"; import { div } from "../../core/gui/dom"; +import { ResizableView } from "../../core/gui/ResizableView"; enum RegisterDisplayType { Signed, @@ -19,8 +19,8 @@ enum RegisterDisplayType { type RegisterGetterFunction = (register: number) => number; -export class RegistersView extends ResizableWidget { - private readonly type_select = this.disposable( +export class RegistersView extends ResizableView { + private readonly type_select = this.add( new Select({ label: "Display type:", tooltip: "Select which data type register values should be displayed as.", @@ -38,16 +38,14 @@ export class RegistersView extends ResizableWidget { RegisterDisplayType.Signed, ); - private readonly hex_checkbox = this.disposable( + private readonly hex_checkbox = this.add( new CheckBox(false, { label: "Hex", tooltip: "Display register values in hexadecimal.", }), ); - private readonly settings_bar = this.disposable( - new ToolBar(this.type_select, this.hex_checkbox), - ); + private readonly settings_bar = this.add(new ToolBar(this.type_select, this.hex_checkbox)); private readonly register_els: TextInput[]; private readonly list_element = div({ className: "quest_editor_RegistersView_list" }); @@ -70,7 +68,7 @@ export class RegistersView extends ResizableWidget { // create register elements const register_els: TextInput[] = Array(REGISTER_COUNT); for (let i = 0; i < REGISTER_COUNT; i++) { - const value_el = this.disposable( + const value_el = this.add( new TextInput("", { class: "quest_editor_RegistersView_value", label: `r${i}:`, diff --git a/src/quest_editor/gui/UnavailableView.ts b/src/quest_editor/gui/UnavailableView.ts index 0fc350ae..6e7f0ae1 100644 --- a/src/quest_editor/gui/UnavailableView.ts +++ b/src/quest_editor/gui/UnavailableView.ts @@ -1,12 +1,12 @@ -import { Widget } from "../../core/gui/Widget"; import { Label } from "../../core/gui/Label"; import "./UnavailableView.css"; import { div } from "../../core/gui/dom"; +import { View } from "../../core/gui/View"; /** * Used to show that a view exists but is unavailable at the moment. */ -export class UnavailableView extends Widget { +export class UnavailableView extends View { readonly element = div({ className: "quest_editor_UnavailableView" }); private readonly label: Label; diff --git a/src/quest_editor/index.ts b/src/quest_editor/index.ts index 2f2ec011..0eccaf03 100644 --- a/src/quest_editor/index.ts +++ b/src/quest_editor/index.ts @@ -69,7 +69,6 @@ export function initialize_quest_editor( () => new NpcCountsView(disposer.add(new NpcCountsController(quest_editor_store))), () => new QuestEditorRendererView( - gui_store, quest_editor_store, area_asset_loader, entity_asset_loader, @@ -82,7 +81,6 @@ export function initialize_quest_editor( () => new EventsView(disposer.add(new EventsController(quest_editor_store))), () => new QuestRunnerRendererView( - gui_store, quest_editor_store, area_asset_loader, entity_asset_loader, diff --git a/src/viewer/gui/TextureView.ts b/src/viewer/gui/TextureView.ts index 8c9345c6..03d0138b 100644 --- a/src/viewer/gui/TextureView.ts +++ b/src/viewer/gui/TextureView.ts @@ -1,14 +1,13 @@ import { div, Icon } from "../../core/gui/dom"; -import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { FileButton } from "../../core/gui/FileButton"; import { ToolBar } from "../../core/gui/ToolBar"; import { RendererWidget } from "../../core/gui/RendererWidget"; import { TextureRenderer } from "../rendering/TextureRenderer"; -import { GuiStore, GuiTool } from "../../core/stores/GuiStore"; import { TextureStore } from "../stores/TextureStore"; import { DisposableThreeRenderer } from "../../core/rendering/Renderer"; +import { ResizableView } from "../../core/gui/ResizableView"; -export class TextureView extends ResizableWidget { +export class TextureView extends ResizableView { readonly element = div({ className: "viewer_TextureView" }); private readonly open_file_button = new FileButton("Open file...", { @@ -16,44 +15,38 @@ export class TextureView extends ResizableWidget { accept: ".afs, .xvm", }); - private readonly tool_bar = this.disposable(new ToolBar(this.open_file_button)); + private readonly tool_bar = this.add(new ToolBar(this.open_file_button)); private readonly renderer_view: RendererWidget; - constructor( - gui_store: GuiStore, - texture_store: TextureStore, - three_renderer: DisposableThreeRenderer, - ) { + constructor(texture_store: TextureStore, three_renderer: DisposableThreeRenderer) { super(); - this.renderer_view = this.disposable( + this.renderer_view = this.add( new RendererWidget(new TextureRenderer(three_renderer, texture_store)), ); this.element.append(this.tool_bar.element, this.renderer_view.element); - this.disposable( + this.disposables( this.open_file_button.files.observe(({ value: files }) => { if (files.length) texture_store.load_file(files[0]); }), ); - this.renderer_view.start_rendering(); - - this.disposables( - gui_store.tool.observe(({ value: tool }) => { - if (tool === GuiTool.Viewer) { - this.renderer_view.start_rendering(); - } else { - this.renderer_view.stop_rendering(); - } - }), - ); - this.finalize_construction(); } + activate(): void { + this.renderer_view.start_rendering(); + super.activate(); + } + + deactivate(): void { + super.deactivate(); + this.renderer_view.stop_rendering(); + } + resize(width: number, height: number): this { super.resize(width, height); diff --git a/src/viewer/gui/ViewerView.ts b/src/viewer/gui/ViewerView.ts index 3f660f67..843b7833 100644 --- a/src/viewer/gui/ViewerView.ts +++ b/src/viewer/gui/ViewerView.ts @@ -1,28 +1,47 @@ import { TabContainer } from "../../core/gui/TabContainer"; import { Model3DView } from "./model_3d/Model3DView"; import { TextureView } from "./TextureView"; +import { ResizableView } from "../../core/gui/ResizableView"; +import { GuiStore } from "../../core/stores/GuiStore"; + +export class ViewerView extends ResizableView { + private readonly tab_container: TabContainer; + + get element(): HTMLElement { + return this.tab_container.element; + } -export class ViewerView extends TabContainer { constructor( + gui_store: GuiStore, create_model_3d_view: () => Promise, create_texture_view: () => Promise, ) { - super({ - class: "viewer_ViewerView", - tabs: [ - { - title: "Models", - key: "model", - create_view: create_model_3d_view, - }, - { - title: "Textures", - key: "texture", - create_view: create_texture_view, - }, - ], - }); + super(); + + this.tab_container = this.add( + new TabContainer(gui_store, { + class: "viewer_ViewerView", + tabs: [ + { + title: "Models", + key: "models", + path: "/models", + create_view: create_model_3d_view, + }, + { + title: "Textures", + key: "textures", + path: "/textures", + create_view: create_texture_view, + }, + ], + }), + ); this.finalize_construction(); } + + resize(width: number, height: number): void { + this.tab_container.resize(width, height); + } } diff --git a/src/viewer/gui/model_3d/Model3DSelectListView.ts b/src/viewer/gui/model_3d/Model3DSelectListView.ts index e6fbc8b2..d9f1ed93 100644 --- a/src/viewer/gui/model_3d/Model3DSelectListView.ts +++ b/src/viewer/gui/model_3d/Model3DSelectListView.ts @@ -1,9 +1,9 @@ -import { ResizableWidget } from "../../../core/gui/ResizableWidget"; import "./Model3DSelectListView.css"; import { Property } from "../../../core/observable/property/Property"; import { li, ul } from "../../../core/gui/dom"; +import { ResizableView } from "../../../core/gui/ResizableView"; -export class Model3DSelectListView extends ResizableWidget { +export class Model3DSelectListView extends ResizableView { readonly element = ul({ className: "viewer_Model3DSelectListView" }); set borders(borders: boolean) { @@ -32,7 +32,7 @@ export class Model3DSelectListView extends Resizable this.element.append(li({ data: { index: index.toString() } }, model.name)); }); - this.disposable( + this.disposables( selected.observe( ({ value: model }) => { if (this.selected_element) { diff --git a/src/viewer/gui/model_3d/Model3DToolBar.ts b/src/viewer/gui/model_3d/Model3DToolBarView.ts similarity index 82% rename from src/viewer/gui/model_3d/Model3DToolBar.ts rename to src/viewer/gui/model_3d/Model3DToolBarView.ts index 11ec936d..861f1b56 100644 --- a/src/viewer/gui/model_3d/Model3DToolBar.ts +++ b/src/viewer/gui/model_3d/Model3DToolBarView.ts @@ -6,9 +6,22 @@ import { PSO_FRAME_RATE } from "../../../core/rendering/conversion/ninja_animati import { Label } from "../../../core/gui/Label"; import { Icon } from "../../../core/gui/dom"; import { Model3DStore } from "../../stores/Model3DStore"; +import { View } from "../../../core/gui/View"; + +export class Model3DToolBarView extends View { + private readonly toolbar: ToolBar; + + get element(): HTMLElement { + return this.toolbar.element; + } + + get height(): number { + return this.toolbar.height; + } -export class Model3DToolBar extends ToolBar { constructor(model_3d_store: Model3DStore) { + super(); + const open_file_button = new FileButton("Open file...", { icon_left: Icon.File, accept: ".afs, .nj, .njm, .xj, .xvm", @@ -31,13 +44,15 @@ export class Model3DToolBar extends ToolBar { model_3d_store.animation_frame_count.map(count => `/ ${count}`), ); - super( - open_file_button, - skeleton_checkbox, - play_animation_checkbox, - animation_frame_rate_input, - animation_frame_input, - animation_frame_count_label, + this.toolbar = this.add( + new ToolBar( + open_file_button, + skeleton_checkbox, + play_animation_checkbox, + animation_frame_rate_input, + animation_frame_input, + animation_frame_count_label, + ), ); // Always-enabled controls. diff --git a/src/viewer/gui/model_3d/Model3DView.ts b/src/viewer/gui/model_3d/Model3DView.ts index 2b8beaa3..f98e28df 100644 --- a/src/viewer/gui/model_3d/Model3DView.ts +++ b/src/viewer/gui/model_3d/Model3DView.ts @@ -1,50 +1,50 @@ -import { ResizableWidget } from "../../../core/gui/ResizableWidget"; import "./Model3DView.css"; -import { GuiStore, GuiTool } from "../../../core/stores/GuiStore"; import { RendererWidget } from "../../../core/gui/RendererWidget"; import { Model3DRenderer } from "../../rendering/Model3DRenderer"; -import { Model3DToolBar } from "./Model3DToolBar"; +import { Model3DToolBarView } from "./Model3DToolBarView"; import { Model3DSelectListView } from "./Model3DSelectListView"; import { CharacterClassModel } from "../../model/CharacterClassModel"; import { CharacterClassAnimationModel } from "../../model/CharacterClassAnimationModel"; import { Model3DStore } from "../../stores/Model3DStore"; import { DisposableThreeRenderer } from "../../../core/rendering/Renderer"; import { div } from "../../../core/gui/dom"; +import { ResizableView } from "../../../core/gui/ResizableView"; +import { GuiStore } from "../../../core/stores/GuiStore"; const MODEL_LIST_WIDTH = 100; const ANIMATION_LIST_WIDTH = 140; -export class Model3DView extends ResizableWidget { +export class Model3DView extends ResizableView { readonly element = div({ className: "viewer_Model3DView" }); - private tool_bar_view: Model3DToolBar; + private tool_bar_view: Model3DToolBarView; private model_list_view: Model3DSelectListView; private animation_list_view: Model3DSelectListView; private renderer_view: RendererWidget; constructor( - gui_store: GuiStore, + private readonly gui_store: GuiStore, model_3d_store: Model3DStore, three_renderer: DisposableThreeRenderer, ) { super(); - this.tool_bar_view = this.disposable(new Model3DToolBar(model_3d_store)); - this.model_list_view = this.disposable( + this.tool_bar_view = this.add(new Model3DToolBarView(model_3d_store)); + this.model_list_view = this.add( new Model3DSelectListView( model_3d_store.models, model_3d_store.current_model, model_3d_store.set_current_model, ), ); - this.animation_list_view = this.disposable( + this.animation_list_view = this.add( new Model3DSelectListView( model_3d_store.animations, model_3d_store.current_animation, model_3d_store.set_current_animation, ), ); - this.renderer_view = this.disposable( + this.renderer_view = this.add( new RendererWidget(new Model3DRenderer(three_renderer, model_3d_store)), ); @@ -60,21 +60,19 @@ export class Model3DView extends ResizableWidget { ), ); - this.renderer_view.start_rendering(); - - this.disposable( - gui_store.tool.observe(({ value: tool }) => { - if (tool === GuiTool.Viewer) { - this.renderer_view.start_rendering(); - } else { - this.renderer_view.stop_rendering(); - } - }), - ); - this.finalize_construction(); } + activate(): void { + this.renderer_view.start_rendering(); + super.activate(); + } + + deactivate(): void { + super.deactivate(); + this.renderer_view.stop_rendering(); + } + resize(width: number, height: number): this { super.resize(width, height); diff --git a/src/viewer/index.ts b/src/viewer/index.ts index 58098734..2afbf33a 100644 --- a/src/viewer/index.ts +++ b/src/viewer/index.ts @@ -13,6 +13,7 @@ export function initialize_viewer( const disposer = new Disposer(); const view = new ViewerView( + gui_store, async () => { const { Model3DStore } = await import("./stores/Model3DStore"); const { Model3DView } = await import("./gui/model_3d/Model3DView"); @@ -42,7 +43,7 @@ export function initialize_viewer( disposer.add(store); } - return new TextureView(gui_store, store, create_three_renderer()); + return new TextureView(store, create_three_renderer()); }, ); diff --git a/version.txt b/version.txt index 920a1396..c739b42c 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -43 +44 diff --git a/webpack.prod.js b/webpack.prod.js index d0d1ed49..7f476fc6 100644 --- a/webpack.prod.js +++ b/webpack.prod.js @@ -15,17 +15,16 @@ module.exports = merge(common, { moduleIds: "hashed", runtimeChunk: "single", splitChunks: { + chunks: "all", cacheGroups: { styles: { name: "style", test: /\.css$/, - chunks: "all", enforce: true, }, vendor: { test: /node_modules/, name: "vendors", - chunks: "all", }, }, },