From f1002201760a6b95a3a27e969c5f27b301f12776 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Wed, 28 Aug 2019 00:50:38 +0200 Subject: [PATCH] Refactored widget properties to simplify the interface. --- src/application/gui/ApplicationView.ts | 6 +- src/application/gui/MainContentView.ts | 12 ++-- src/application/gui/NavigationView.ts | 6 +- src/core/gui/Button.ts | 18 ++++-- src/core/gui/CheckBox.ts | 35 +++++++---- src/core/gui/Control.ts | 8 +-- src/core/gui/FileButton.ts | 8 ++- src/core/gui/Input.ts | 60 +++++++------------ src/core/gui/Label.ts | 35 +++++------ src/core/gui/LabelledControl.ts | 17 ++++-- src/core/gui/LazyView.ts | 41 ------------- src/core/gui/LazyWidget.ts | 43 +++++++++++++ src/core/gui/NumberInput.ts | 30 ++++------ .../{RendererView.ts => RendererWidget.ts} | 4 +- .../{ResizableView.ts => ResizableWidget.ts} | 4 +- src/core/gui/TabContainer.ts | 14 ++--- src/core/gui/TextArea.ts | 36 ++++++----- src/core/gui/TextInput.ts | 29 ++++----- src/core/gui/ToolBar.ts | 7 ++- src/core/gui/{View.ts => Widget.ts} | 31 ++++++++-- src/core/observable/WidgetProperty.ts | 13 ++++ src/quest_editor/gui/AsmEditorView.ts | 4 +- src/quest_editor/gui/DisabledView.ts | 4 +- src/quest_editor/gui/EntityInfoView.ts | 10 ++-- src/quest_editor/gui/NpcCountsView.ts | 4 +- src/quest_editor/gui/QuesInfoView.ts | 4 +- src/quest_editor/gui/QuestEditorView.ts | 8 +-- src/quest_editor/gui/QuestRendererView.ts | 8 +-- src/quest_editor/gui/ToolBarView.ts | 4 +- src/viewer/gui/Model3DView.ts | 18 +++--- src/viewer/gui/TextureView.ts | 8 +-- src/viewer/gui/ViewerView.ts | 4 +- src/viewer/stores/Model3DStore.ts | 6 +- 33 files changed, 285 insertions(+), 254 deletions(-) delete mode 100644 src/core/gui/LazyView.ts create mode 100644 src/core/gui/LazyWidget.ts rename src/core/gui/{RendererView.ts => RendererWidget.ts} (85%) rename src/core/gui/{ResizableView.ts => ResizableWidget.ts} (75%) rename src/core/gui/{View.ts => Widget.ts} (56%) create mode 100644 src/core/observable/WidgetProperty.ts diff --git a/src/application/gui/ApplicationView.ts b/src/application/gui/ApplicationView.ts index e2fa658e..647f95f2 100644 --- a/src/application/gui/ApplicationView.ts +++ b/src/application/gui/ApplicationView.ts @@ -1,10 +1,10 @@ import { NavigationView } from "./NavigationView"; import { MainContentView } from "./MainContentView"; import { create_element } from "../../core/gui/dom"; -import { ResizableView } from "../../core/gui/ResizableView"; +import { ResizableWidget } from "../../core/gui/ResizableWidget"; -export class ApplicationView extends ResizableView { - element = create_element("div", { class: "application_ApplicationView" }); +export class ApplicationView extends ResizableWidget { + readonly element = create_element("div", { class: "application_ApplicationView" }); private menu_view = this.disposable(new NavigationView()); private main_content_view = this.disposable(new MainContentView()); diff --git a/src/application/gui/MainContentView.ts b/src/application/gui/MainContentView.ts index 4a1992c2..e908d887 100644 --- a/src/application/gui/MainContentView.ts +++ b/src/application/gui/MainContentView.ts @@ -1,10 +1,10 @@ import { create_element } from "../../core/gui/dom"; import { gui_store, GuiTool } from "../../core/stores/GuiStore"; -import { LazyView } from "../../core/gui/LazyView"; -import { ResizableView } from "../../core/gui/ResizableView"; +import { LazyWidget } from "../../core/gui/LazyWidget"; +import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { ChangeEvent } from "../../core/observable/Observable"; -const TOOLS: [GuiTool, () => Promise][] = [ +const TOOLS: [GuiTool, () => Promise][] = [ [GuiTool.Viewer, async () => new (await import("../../viewer/gui/ViewerView")).ViewerView()], [ GuiTool.QuestEditor, @@ -12,11 +12,11 @@ const TOOLS: [GuiTool, () => Promise][] = [ ], ]; -export class MainContentView extends ResizableView { - element = create_element("div", { class: "application_MainContentView" }); +export class MainContentView extends ResizableWidget { + readonly element = create_element("div", { class: "application_MainContentView" }); private tool_views = new Map( - TOOLS.map(([tool, create_view]) => [tool, this.disposable(new LazyView(create_view))]), + TOOLS.map(([tool, create_view]) => [tool, this.disposable(new LazyWidget(create_view))]), ); constructor() { diff --git a/src/application/gui/NavigationView.ts b/src/application/gui/NavigationView.ts index 4c245368..1bc6287b 100644 --- a/src/application/gui/NavigationView.ts +++ b/src/application/gui/NavigationView.ts @@ -1,7 +1,7 @@ import { create_element } from "../../core/gui/dom"; import "./NavigationView.css"; import { gui_store, GuiTool } from "../../core/stores/GuiStore"; -import { View } from "../../core/gui/View"; +import { Widget } from "../../core/gui/Widget"; const TOOLS: [GuiTool, string][] = [ [GuiTool.Viewer, "Viewer"], @@ -9,7 +9,7 @@ const TOOLS: [GuiTool, string][] = [ [GuiTool.HuntOptimizer, "Hunt Optimizer"], ]; -export class NavigationView extends View { +export class NavigationView extends Widget { readonly element = create_element("div", { class: "application_NavigationView" }); readonly height = 30; @@ -44,7 +44,7 @@ export class NavigationView extends View { }; } -class ToolButton extends View { +class ToolButton extends Widget { element: HTMLElement = create_element("span"); private input: HTMLInputElement = create_element("input"); diff --git a/src/core/gui/Button.ts b/src/core/gui/Button.ts index 80153671..8517aa64 100644 --- a/src/core/gui/Button.ts +++ b/src/core/gui/Button.ts @@ -3,15 +3,20 @@ import "./Button.css"; import { Observable } from "../observable/Observable"; import { emitter } from "../observable"; import { Control } from "./Control"; +import { Emitter } from "../observable/Emitter"; +import { ViewOptions } from "./Widget"; export class Button extends Control { readonly element: HTMLButtonElement = create_element("button", { class: "core_Button" }); - private readonly _click = emitter(); - readonly click: Observable = this._click; + readonly click: Observable; - constructor(text: string) { - super(); + private readonly _click: Emitter = emitter(); + + constructor(text: string, options?: ViewOptions) { + super(options); + + this.click = this._click; this.element.append(create_element("span", { class: "core_Button_inner", text })); @@ -19,4 +24,9 @@ export class Button extends Control { this.element.onclick = (e: MouseEvent) => this._click.emit({ value: e }); } + + protected set_enabled(enabled: boolean): void { + super.set_enabled(enabled); + this.element.disabled = !enabled; + } } diff --git a/src/core/gui/CheckBox.ts b/src/core/gui/CheckBox.ts index 14e745a4..593d0fdb 100644 --- a/src/core/gui/CheckBox.ts +++ b/src/core/gui/CheckBox.ts @@ -1,27 +1,36 @@ import { create_element } from "./dom"; import { WritableProperty } from "../observable/WritableProperty"; -import { property } from "../observable"; -import { LabelledControl } from "./LabelledControl"; +import { LabelledControl, LabelledControlOptions } from "./LabelledControl"; +import { WidgetProperty } from "../observable/WidgetProperty"; + +export type CheckBoxOptions = LabelledControlOptions; export class CheckBox extends LabelledControl { readonly element: HTMLInputElement = create_element("input", { class: "core_CheckBox" }); - readonly checked: WritableProperty = property(false); - readonly preferred_label_position = "right"; - constructor(checked: boolean = false, label?: string) { - super(label); + readonly checked: WritableProperty; + + private readonly _checked: WidgetProperty; + + constructor(checked: boolean = false, options?: CheckBoxOptions) { + super(options); + + this._checked = new WidgetProperty(this, checked, this.set_checked); + this.checked = this._checked; + this.set_checked(checked); this.element.type = "checkbox"; - this.element.onchange = () => (this.checked.val = this.element.checked); + this.element.onchange = () => (this._checked.val = this.element.checked); + } - this.disposables( - this.checked.observe(({ value }) => (this.element.checked = value)), + protected set_enabled(enabled: boolean): void { + super.set_enabled(enabled); + this.element.disabled = !enabled; + } - this.enabled.observe(({ value }) => (this.element.disabled = !value)), - ); - - this.checked.val = checked; + protected set_checked(checked: boolean): void { + this.element.checked = checked; } } diff --git a/src/core/gui/Control.ts b/src/core/gui/Control.ts index 82cda0df..a46d7bdd 100644 --- a/src/core/gui/Control.ts +++ b/src/core/gui/Control.ts @@ -1,7 +1,3 @@ -import { View } from "./View"; -import { WritableProperty } from "../observable/WritableProperty"; -import { property } from "../observable"; +import { Widget } from "./Widget"; -export abstract class Control extends View { - readonly enabled: WritableProperty = property(true); -} +export abstract class Control extends Widget {} diff --git a/src/core/gui/FileButton.ts b/src/core/gui/FileButton.ts index 1ba306ef..4a52e1cc 100644 --- a/src/core/gui/FileButton.ts +++ b/src/core/gui/FileButton.ts @@ -4,22 +4,26 @@ import "./Button.css"; import { property } from "../observable"; import { Property } from "../observable/Property"; import { Control } from "./Control"; +import { WritableProperty } from "../observable/WritableProperty"; export class FileButton extends Control { readonly element: HTMLLabelElement = create_element("label", { class: "core_FileButton core_Button", }); - private readonly _files = property([]); - readonly files: Property = this._files; + readonly files: Property; private input: HTMLInputElement = create_element("input", { class: "core_FileButton_input core_Button_inner", }); + private readonly _files: WritableProperty = property([]); + constructor(text: string, accept: string = "") { super(); + this.files = this._files; + this.input.type = "file"; this.input.accept = accept; this.input.onchange = () => { diff --git a/src/core/gui/Input.ts b/src/core/gui/Input.ts index 2fd86fa2..2215c43c 100644 --- a/src/core/gui/Input.ts +++ b/src/core/gui/Input.ts @@ -1,9 +1,12 @@ /* eslint-disable no-dupe-class-members */ -import { LabelledControl } from "./LabelledControl"; +import { LabelledControl, LabelledControlOptions } from "./LabelledControl"; import { create_element } from "./dom"; import { WritableProperty } from "../observable/WritableProperty"; import { is_any_property, Property } from "../observable/Property"; import "./Input.css"; +import { WidgetProperty } from "../observable/WidgetProperty"; + +export type InputOptions = LabelledControlOptions; export abstract class Input extends LabelledControl { readonly element: HTMLElement; @@ -12,16 +15,20 @@ export abstract class Input extends LabelledControl { protected readonly input: HTMLInputElement; + private readonly _value: WidgetProperty; + private ignore_input_change = false; + protected constructor( - value: WritableProperty, + value: T, class_name: string, input_type: string, input_class_name: string, - label?: string, + options?: InputOptions, ) { - super(label); + super(options); - this.value = value; + this._value = new WidgetProperty(this, value, this.set_value); + this.value = this._value; this.element = create_element("span", { class: `${class_name} core_Input` }); @@ -30,47 +37,26 @@ export abstract class Input extends LabelledControl { }); this.input.type = input_type; this.input.onchange = () => { - if (this.input_value_changed()) { - this.value.val = this.get_input_value(); - } + this._value.val = this.get_value(); }; - this.set_input_value(value.val); this.element.append(this.input); - - this.disposables( - this.value.observe(({ value }) => { - this.set_input_value(value); - }), - - this.enabled.observe(({ value }) => { - this.input.disabled = !value; - - if (value) { - this.element.classList.remove("disabled"); - } else { - this.element.classList.add("disabled"); - } - }), - ); } - set_value(value: T, options: { silent?: boolean } = {}): void { - this.value.set_val(value, options); - - if (options.silent) { - this.set_input_value(value); - } + protected set_enabled(enabled: boolean): void { + super.set_enabled(enabled); + this.input.disabled = !enabled; } - protected input_value_changed(): boolean { - return true; + protected abstract get_value(): T; + + protected abstract set_value(value: T): void; + + protected ignore_change(f: () => void): void { + this.ignore_input_change = true; + f(); } - protected abstract get_input_value(): T; - - protected abstract set_input_value(value: T): void; - protected set_attr(attr: InputAttrsOfType, value?: T | Property): void; protected set_attr( attr: InputAttrsOfType, diff --git a/src/core/gui/Label.ts b/src/core/gui/Label.ts index 8797b6c9..22281e77 100644 --- a/src/core/gui/Label.ts +++ b/src/core/gui/Label.ts @@ -1,39 +1,34 @@ -import { View } from "./View"; +import { ViewOptions, Widget } from "./Widget"; import { create_element } from "./dom"; import { WritableProperty } from "../observable/WritableProperty"; import "./Label.css"; -import { property } from "../observable"; import { Property } from "../observable/Property"; +import { WidgetProperty } from "../observable/WidgetProperty"; -export class Label extends View { +export class Label extends Widget { readonly element = create_element("label", { class: "core_Label" }); set for(id: string) { this.element.htmlFor = id; } - readonly enabled: WritableProperty = property(true); + readonly text: WritableProperty; - constructor(text: string | Property, options: { enabled?: boolean } = {}) { - super(); + private readonly _text = new WidgetProperty(this, "", this.set_text); + + constructor(text: string | Property, options?: ViewOptions) { + super(options); + + this.text = this._text; if (typeof text === "string") { - this.element.append(text); + this.set_text(text); } else { - this.element.append(text.val); - this.disposable(text.observe(({ value }) => (this.element.textContent = value))); + this.disposable(this._text.bind_to(text)); } + } - this.disposables( - this.enabled.observe(({ value }) => { - if (value) { - this.element.classList.remove("disabled"); - } else { - this.element.classList.add("disabled"); - } - }), - ); - - if (options.enabled != undefined) this.enabled.val = options.enabled; + protected set_text(text: string): void { + this.element.textContent = text; } } diff --git a/src/core/gui/LabelledControl.ts b/src/core/gui/LabelledControl.ts index 71cb6da4..3f748c68 100644 --- a/src/core/gui/LabelledControl.ts +++ b/src/core/gui/LabelledControl.ts @@ -1,12 +1,14 @@ import { Label } from "./Label"; import { Control } from "./Control"; +import { ViewOptions } from "./Widget"; + +export type LabelledControlOptions = ViewOptions & { + label?: string; +}; export abstract class LabelledControl extends Control { abstract readonly preferred_label_position: "left" | "right" | "top" | "bottom"; - private readonly _label_text: string; - private _label?: Label; - get label(): Label { if (!this._label) { this._label = this.disposable(new Label(this._label_text)); @@ -21,10 +23,13 @@ export abstract class LabelledControl extends Control { return this._label; } - protected constructor(label: string | undefined) { - super(); + private readonly _label_text: string; + private _label?: Label; - this._label_text = label || ""; + protected constructor(options?: LabelledControlOptions) { + super(options); + + this._label_text = (options && options.label) || ""; } } diff --git a/src/core/gui/LazyView.ts b/src/core/gui/LazyView.ts deleted file mode 100644 index 35730b48..00000000 --- a/src/core/gui/LazyView.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { View } from "./View"; -import { create_element } from "./dom"; -import { Resizable } from "./Resizable"; -import { ResizableView } from "./ResizableView"; - -export class LazyView extends ResizableView { - readonly element = create_element("div", { class: "core_LazyView" }); - - private initialized = false; - private view: View & Resizable | undefined; - - constructor(private create_view: () => Promise) { - super(); - - this.visible.val = false; - - this.disposables( - this.visible.observe(({ value }) => { - if (value && !this.initialized) { - this.initialized = true; - - this.create_view().then(view => { - this.view = this.disposable(view); - this.view.resize(this.width, this.height); - this.element.append(view.element); - }); - } - }), - ); - } - - 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/LazyWidget.ts b/src/core/gui/LazyWidget.ts new file mode 100644 index 00000000..c5a12043 --- /dev/null +++ b/src/core/gui/LazyWidget.ts @@ -0,0 +1,43 @@ +import { Widget } from "./Widget"; +import { create_element } from "./dom"; +import { Resizable } from "./Resizable"; +import { ResizableWidget } from "./ResizableWidget"; + +export class LazyWidget extends ResizableWidget { + readonly element = create_element("div", { class: "core_LazyView" }); + + private initialized = false; + private view: Widget & Resizable | undefined; + + constructor(private create_view: () => Promise) { + super(); + + this.visible.val = false; + } + + protected set_visible(visible: boolean): void { + super.set_visible(visible); + + if (visible && !this.initialized) { + this.initialized = true; + + this.create_view().then(view => { + if (!this.disposed) { + this.view = this.disposable(view); + this.view.resize(this.width, this.height); + this.element.append(view.element); + } + }); + } + } + + 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/NumberInput.ts b/src/core/gui/NumberInput.ts index 8eefefab..7aacb52c 100644 --- a/src/core/gui/NumberInput.ts +++ b/src/core/gui/NumberInput.ts @@ -1,17 +1,15 @@ -import { property } from "../observable"; import { Property } from "../observable/Property"; -import { Input } from "./Input"; +import { Input, InputOptions } from "./Input"; import "./NumberInput.css"; export class NumberInput extends Input { readonly preferred_label_position = "left"; private readonly rounding_factor: number; - private rounded_value: number = 0; constructor( value: number = 0, - options: { + options: InputOptions & { label?: string; min?: number | Property; max?: number | Property; @@ -20,13 +18,7 @@ export class NumberInput extends Input { round_to?: number; } = {}, ) { - super( - property(value), - "core_NumberInput", - "number", - "core_NumberInput_inner", - options.label, - ); + super(value, "core_NumberInput", "number", "core_NumberInput_inner", options); const { min, max, step } = options; this.set_attr("min", min, String); @@ -40,18 +32,18 @@ export class NumberInput extends Input { } this.element.style.width = `${options.width == undefined ? 54 : options.width}px`; + + this.set_value(value); } - protected input_value_changed(): boolean { - return this.input.valueAsNumber !== this.rounded_value; - } - - protected get_input_value(): number { + protected get_value(): number { return this.input.valueAsNumber; } - protected set_input_value(value: number): void { - this.input.valueAsNumber = this.rounded_value = - Math.round(this.rounding_factor * value) / this.rounding_factor; + protected set_value(value: number): void { + this.ignore_change(() => { + this.input.valueAsNumber = + Math.round(this.rounding_factor * value) / this.rounding_factor; + }); } } diff --git a/src/core/gui/RendererView.ts b/src/core/gui/RendererWidget.ts similarity index 85% rename from src/core/gui/RendererView.ts rename to src/core/gui/RendererWidget.ts index ee68c3bb..f39398fb 100644 --- a/src/core/gui/RendererView.ts +++ b/src/core/gui/RendererWidget.ts @@ -1,8 +1,8 @@ -import { ResizableView } from "./ResizableView"; +import { ResizableWidget } from "./ResizableWidget"; import { create_element } from "./dom"; import { Renderer } from "../rendering/Renderer"; -export class RendererView extends ResizableView { +export class RendererWidget extends ResizableWidget { readonly element = create_element("div"); constructor(private renderer: Renderer) { diff --git a/src/core/gui/ResizableView.ts b/src/core/gui/ResizableWidget.ts similarity index 75% rename from src/core/gui/ResizableView.ts rename to src/core/gui/ResizableWidget.ts index c8187401..1221a8e2 100644 --- a/src/core/gui/ResizableView.ts +++ b/src/core/gui/ResizableWidget.ts @@ -1,7 +1,7 @@ -import { View } from "./View"; +import { Widget } from "./Widget"; import { Resizable } from "./Resizable"; -export abstract class ResizableView extends View implements Resizable { +export abstract class ResizableWidget extends Widget implements Resizable { protected width: number = 0; protected height: number = 0; diff --git a/src/core/gui/TabContainer.ts b/src/core/gui/TabContainer.ts index 1db0e1da..37884320 100644 --- a/src/core/gui/TabContainer.ts +++ b/src/core/gui/TabContainer.ts @@ -1,21 +1,21 @@ -import { View } from "./View"; +import { Widget } from "./Widget"; import { create_element } from "./dom"; -import { LazyView } from "./LazyView"; +import { LazyWidget } from "./LazyWidget"; import { Resizable } from "./Resizable"; -import { ResizableView } from "./ResizableView"; +import { ResizableWidget } from "./ResizableWidget"; import "./TabContainer.css"; export type Tab = { title: string; key: string; - create_view: () => Promise; + create_view: () => Promise; }; -type TabInfo = Tab & { tab_element: HTMLSpanElement; lazy_view: LazyView }; +type TabInfo = Tab & { tab_element: HTMLSpanElement; lazy_view: LazyWidget }; const BAR_HEIGHT = 28; -export class TabContainer extends ResizableView { +export class TabContainer extends ResizableWidget { readonly element = create_element("div", { class: "core_TabContainer" }); private tabs: TabInfo[] = []; @@ -35,7 +35,7 @@ export class TabContainer extends ResizableView { }); this.bar_element.append(tab_element); - const lazy_view = new LazyView(tab.create_view); + const lazy_view = new LazyWidget(tab.create_view); this.tabs.push({ ...tab, diff --git a/src/core/gui/TextArea.ts b/src/core/gui/TextArea.ts index c2f19510..2b0c5e2d 100644 --- a/src/core/gui/TextArea.ts +++ b/src/core/gui/TextArea.ts @@ -1,8 +1,16 @@ -import { LabelledControl } from "./LabelledControl"; +import { LabelledControl, LabelledControlOptions } from "./LabelledControl"; import { el } from "./dom"; import { property } from "../observable"; import { WritableProperty } from "../observable/WritableProperty"; import "./TextArea.css"; +import { WidgetProperty } from "../observable/WidgetProperty"; + +export type TextAreaOptions = LabelledControlOptions & { + max_length?: number; + font_family?: string; + rows?: number; + cols?: number; +}; export class TextArea extends LabelledControl { readonly element: HTMLElement = el.div({ class: "core_TextArea" }); @@ -15,17 +23,10 @@ export class TextArea extends LabelledControl { class: "core_TextArea_inner", }); - constructor( - value = "", - options?: { - label?: string; - max_length?: number; - font_family?: string; - rows?: number; - cols?: number; - }, - ) { - super(options && options.label); + private readonly _value = new WidgetProperty(this, "", this.set_value); + + constructor(value = "", options?: TextAreaOptions) { + super(options); if (options) { if (options.max_length != undefined) this.text_element.maxLength = options.max_length; @@ -35,12 +36,15 @@ export class TextArea extends LabelledControl { if (options.cols != undefined) this.text_element.cols = options.cols; } - this.value = property(value); + this.value = this._value; + this.set_value(value); - this.text_element.onchange = () => (this.value.val = this.text_element.value); - - this.disposables(this.value.observe(({ value }) => (this.text_element.value = value))); + this.text_element.onchange = () => (this._value.val = this.text_element.value); this.element.append(this.text_element); } + + protected set_value(value: string): void { + this.text_element.value = value; + } } diff --git a/src/core/gui/TextInput.ts b/src/core/gui/TextInput.ts index 74e0e953..39f285b9 100644 --- a/src/core/gui/TextInput.ts +++ b/src/core/gui/TextInput.ts @@ -1,36 +1,29 @@ -import { Input } from "./Input"; +import { Input, InputOptions } from "./Input"; import { Property } from "../observable/Property"; -import { property } from "../observable"; + +export type TextInputOptions = InputOptions & { + max_length?: number | Property; +}; export class TextInput extends Input { readonly preferred_label_position = "left"; - constructor( - value = "", - options?: { - label?: string; - max_length?: number | Property; - }, - ) { - super( - property(value), - "core_TextInput", - "text", - "core_TextInput_inner", - options && options.label, - ); + constructor(value = "", options?: TextInputOptions) { + super(value, "core_TextInput", "text", "core_TextInput_inner", options); if (options) { const { max_length } = options; this.set_attr("maxLength", max_length); } + + this.set_value(value); } - protected get_input_value(): string { + protected get_value(): string { return this.input.value; } - protected set_input_value(value: string): void { + protected set_value(value: string): void { this.input.value = value; } } diff --git a/src/core/gui/ToolBar.ts b/src/core/gui/ToolBar.ts index ac48917f..61cfe539 100644 --- a/src/core/gui/ToolBar.ts +++ b/src/core/gui/ToolBar.ts @@ -1,13 +1,14 @@ -import { View } from "./View"; +import { Widget } from "./Widget"; import { create_element } from "./dom"; import "./ToolBar.css"; import { LabelledControl } from "./LabelledControl"; -export class ToolBar extends View { +export class ToolBar extends Widget { readonly element = create_element("div", { class: "core_ToolBar" }); + readonly height = 33; - constructor(...children: View[]) { + constructor(...children: Widget[]) { super(); this.element.style.height = `${this.height}px`; diff --git a/src/core/gui/View.ts b/src/core/gui/Widget.ts similarity index 56% rename from src/core/gui/View.ts rename to src/core/gui/Widget.ts index 4af5d1e0..9906ef36 100644 --- a/src/core/gui/View.ts +++ b/src/core/gui/Widget.ts @@ -3,9 +3,11 @@ import { Disposer } from "../observable/Disposer"; import { Observable } from "../observable/Observable"; import { bind_hidden } from "./dom"; import { WritableProperty } from "../observable/WritableProperty"; -import { property } from "../observable"; +import { WidgetProperty } from "../observable/WidgetProperty"; -export abstract class View implements Disposable { +export type ViewOptions = {}; + +export abstract class Widget implements Disposable { abstract readonly element: HTMLElement; get id(): string { @@ -16,12 +18,18 @@ export abstract class View implements Disposable { this.element.id = id; } - readonly visible: WritableProperty = property(true); + readonly visible: WritableProperty; + readonly enabled: WritableProperty; + + protected disposed = false; private disposer = new Disposer(); + private _visible = new WidgetProperty(this, true, this.set_visible); + private _enabled = new WidgetProperty(this, true, this.set_enabled); - constructor() { - this.disposables(this.visible.observe(({ value }) => (this.element.hidden = !value))); + constructor(_options?: ViewOptions) { + this.visible = this._visible; + this.enabled = this._enabled; } focus(): void { @@ -31,6 +39,19 @@ export abstract class View implements Disposable { dispose(): void { this.element.remove(); this.disposer.dispose(); + this.disposed = true; + } + + protected set_visible(visible: boolean): void { + this.element.hidden = !visible; + } + + protected set_enabled(enabled: boolean): void { + if (enabled) { + this.element.classList.remove("disabled"); + } else { + this.element.classList.add("disabled"); + } } protected bind_hidden(element: HTMLElement, observable: Observable): void { diff --git a/src/core/observable/WidgetProperty.ts b/src/core/observable/WidgetProperty.ts new file mode 100644 index 00000000..5c47391a --- /dev/null +++ b/src/core/observable/WidgetProperty.ts @@ -0,0 +1,13 @@ +import { SimpleProperty } from "./SimpleProperty"; +import { Widget } from "../gui/Widget"; + +export class WidgetProperty extends SimpleProperty { + constructor(private widget: Widget, val: T, private set_value: (this: Widget, val: T) => void) { + super(val); + } + + set_val(val: T, options?: { silent?: boolean }): void { + this.set_value.call(this.widget, val); + super.set_val(val, options); + } +} diff --git a/src/quest_editor/gui/AsmEditorView.ts b/src/quest_editor/gui/AsmEditorView.ts index a24ed22d..d4548ddf 100644 --- a/src/quest_editor/gui/AsmEditorView.ts +++ b/src/quest_editor/gui/AsmEditorView.ts @@ -1,4 +1,4 @@ -import { ResizableView } from "../../core/gui/ResizableView"; +import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { el } from "../../core/gui/dom"; import { editor } from "monaco-editor"; import { asm_editor_store } from "../stores/AsmEditorStore"; @@ -25,7 +25,7 @@ editor.defineTheme("phantasmal-world", { const DUMMY_MODEL = editor.createModel("", "psoasm"); -export class AsmEditorView extends ResizableView { +export class AsmEditorView extends ResizableWidget { readonly element = el.div(); private readonly editor: IStandaloneCodeEditor = this.disposable( diff --git a/src/quest_editor/gui/DisabledView.ts b/src/quest_editor/gui/DisabledView.ts index 1c7746ae..ce256356 100644 --- a/src/quest_editor/gui/DisabledView.ts +++ b/src/quest_editor/gui/DisabledView.ts @@ -1,9 +1,9 @@ -import { View } from "../../core/gui/View"; +import { Widget } from "../../core/gui/Widget"; import { el } from "../../core/gui/dom"; import { Label } from "../../core/gui/Label"; import "./DisabledView.css"; -export class DisabledView extends View { +export class DisabledView extends Widget { readonly element = el.div({ class: "quest_editor_DisabledView" }); private readonly label: Label; diff --git a/src/quest_editor/gui/EntityInfoView.ts b/src/quest_editor/gui/EntityInfoView.ts index 246b1de4..9b84f439 100644 --- a/src/quest_editor/gui/EntityInfoView.ts +++ b/src/quest_editor/gui/EntityInfoView.ts @@ -1,4 +1,4 @@ -import { ResizableView } from "../../core/gui/ResizableView"; +import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { el } from "../../core/gui/dom"; import { DisabledView } from "./DisabledView"; import { quest_editor_store } from "../stores/QuestEditorStore"; @@ -11,7 +11,7 @@ import { Property } from "../../core/observable/Property"; import { Vec3 } from "../../core/data_formats/vector"; import { QuestEntityModel } from "../model/QuestEntityModel"; -export class EntityInfoView extends ResizableView { +export class EntityInfoView extends ResizableWidget { readonly element = el.div({ class: "quest_editor_EntityInfoView", tab_index: -1 }); private readonly no_entity_view = new DisabledView("No entity selected."); @@ -153,9 +153,9 @@ export class EntityInfoView extends ResizableView { this.entity_disposer.add_all( pos.observe( ({ value: { x, y, z } }) => { - x_input.set_value(x, { silent: true }); - y_input.set_value(y, { silent: true }); - z_input.set_value(z, { silent: true }); + x_input.value.set_val(x, { silent: true }); + y_input.value.set_val(y, { silent: true }); + z_input.value.set_val(z, { silent: true }); }, { call_now: true }, ), diff --git a/src/quest_editor/gui/NpcCountsView.ts b/src/quest_editor/gui/NpcCountsView.ts index 2811c4da..067eb0e6 100644 --- a/src/quest_editor/gui/NpcCountsView.ts +++ b/src/quest_editor/gui/NpcCountsView.ts @@ -1,4 +1,4 @@ -import { ResizableView } from "../../core/gui/ResizableView"; +import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { el } from "../../core/gui/dom"; import { quest_editor_store } from "../stores/QuestEditorStore"; import { npc_data, NpcType } from "../../core/data_formats/parsing/quest/npc_types"; @@ -6,7 +6,7 @@ import { QuestModel } from "../model/QuestModel"; import "./NpcCountsView.css"; import { DisabledView } from "./DisabledView"; -export class NpcCountsView extends ResizableView { +export class NpcCountsView extends ResizableWidget { readonly element = el.div({ class: "quest_editor_NpcCountsView" }); private readonly table_element = el.table(); diff --git a/src/quest_editor/gui/QuesInfoView.ts b/src/quest_editor/gui/QuesInfoView.ts index 8ec1230a..036aeea6 100644 --- a/src/quest_editor/gui/QuesInfoView.ts +++ b/src/quest_editor/gui/QuesInfoView.ts @@ -1,4 +1,4 @@ -import { ResizableView } from "../../core/gui/ResizableView"; +import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { el } from "../../core/gui/dom"; import { quest_editor_store } from "../stores/QuestEditorStore"; import { Episode } from "../../core/data_formats/parsing/quest/Episode"; @@ -9,7 +9,7 @@ import { TextArea } from "../../core/gui/TextArea"; import "./QuesInfoView.css"; import { DisabledView } from "./DisabledView"; -export class QuesInfoView extends ResizableView { +export class QuesInfoView extends ResizableWidget { readonly element = el.div({ class: "quest_editor_QuesInfoView", tab_index: -1 }); private readonly table_element = el.table(); diff --git a/src/quest_editor/gui/QuestEditorView.ts b/src/quest_editor/gui/QuestEditorView.ts index 28dd1517..f3109e71 100644 --- a/src/quest_editor/gui/QuestEditorView.ts +++ b/src/quest_editor/gui/QuestEditorView.ts @@ -1,4 +1,4 @@ -import { ResizableView } from "../../core/gui/ResizableView"; +import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { create_element } from "../../core/gui/dom"; import { ToolBarView } from "./ToolBarView"; import GoldenLayout, { Container, ContentItem, ItemConfigType } from "golden-layout"; @@ -15,7 +15,7 @@ import { EntityInfoView } from "./EntityInfoView"; const logger = Logger.get("quest_editor/gui/QuestEditorView"); // Don't change these values, as they are persisted in the user's browser. -const VIEW_TO_NAME = new Map ResizableView, string>([ +const VIEW_TO_NAME = new Map ResizableWidget, string>([ [QuesInfoView, "quest_info"], [NpcCountsView, "npc_counts"], [QuestRendererView, "quest_renderer"], @@ -91,7 +91,7 @@ const DEFAULT_LAYOUT_CONTENT: ItemConfigType[] = [ }, ]; -export class QuestEditorView extends ResizableView { +export class QuestEditorView extends ResizableWidget { readonly element = create_element("div", { class: "quest_editor_QuestEditorView" }); private readonly tool_bar_view = this.disposable(new ToolBarView()); @@ -99,7 +99,7 @@ export class QuestEditorView extends ResizableView { private readonly layout_element = create_element("div", { class: "quest_editor_gl_container" }); private readonly layout: Promise; - private readonly sub_views = new Map(); + private readonly sub_views = new Map(); constructor() { super(); diff --git a/src/quest_editor/gui/QuestRendererView.ts b/src/quest_editor/gui/QuestRendererView.ts index 8d79a848..c77d386e 100644 --- a/src/quest_editor/gui/QuestRendererView.ts +++ b/src/quest_editor/gui/QuestRendererView.ts @@ -1,14 +1,14 @@ -import { ResizableView } from "../../core/gui/ResizableView"; +import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { el } from "../../core/gui/dom"; -import { RendererView } from "../../core/gui/RendererView"; +import { RendererWidget } from "../../core/gui/RendererWidget"; import { QuestRenderer } from "../rendering/QuestRenderer"; import { gui_store, GuiTool } from "../../core/stores/GuiStore"; import { quest_editor_store } from "../stores/QuestEditorStore"; -export class QuestRendererView extends ResizableView { +export class QuestRendererView extends ResizableWidget { readonly element = el.div({ class: "quest_editor_QuestRendererView", tab_index: -1 }); - private renderer_view = this.disposable(new RendererView(new QuestRenderer())); + private renderer_view = this.disposable(new RendererWidget(new QuestRenderer())); constructor() { super(); diff --git a/src/quest_editor/gui/ToolBarView.ts b/src/quest_editor/gui/ToolBarView.ts index a27319d4..89558820 100644 --- a/src/quest_editor/gui/ToolBarView.ts +++ b/src/quest_editor/gui/ToolBarView.ts @@ -1,11 +1,11 @@ -import { View } from "../../core/gui/View"; +import { Widget } from "../../core/gui/Widget"; import { ToolBar } from "../../core/gui/ToolBar"; import { FileButton } from "../../core/gui/FileButton"; import { Button } from "../../core/gui/Button"; import { quest_editor_store } from "../stores/QuestEditorStore"; import { undo_manager } from "../../core/undo/UndoManager"; -export class ToolBarView extends View { +export class ToolBarView extends Widget { private readonly open_file_button = new FileButton("Open file...", ".qst"); private readonly save_as_button = new Button("Save as..."); private readonly undo_button = new Button("Undo"); diff --git a/src/viewer/gui/Model3DView.ts b/src/viewer/gui/Model3DView.ts index 16d2f059..0f61f861 100644 --- a/src/viewer/gui/Model3DView.ts +++ b/src/viewer/gui/Model3DView.ts @@ -1,12 +1,12 @@ import { create_element } from "../../core/gui/dom"; -import { ResizableView } from "../../core/gui/ResizableView"; +import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { ToolBar } from "../../core/gui/ToolBar"; import "./Model3DView.css"; import { model_store } from "../stores/Model3DStore"; import { WritableProperty } from "../../core/observable/WritableProperty"; -import { RendererView } from "../../core/gui/RendererView"; +import { RendererWidget } from "../../core/gui/RendererWidget"; import { Model3DRenderer } from "../rendering/Model3DRenderer"; -import { View } from "../../core/gui/View"; +import { Widget } from "../../core/gui/Widget"; import { FileButton } from "../../core/gui/FileButton"; import { CheckBox } from "../../core/gui/CheckBox"; import { NumberInput } from "../../core/gui/NumberInput"; @@ -17,7 +17,7 @@ import { PSO_FRAME_RATE } from "../../core/rendering/conversion/ninja_animation" const MODEL_LIST_WIDTH = 100; const ANIMATION_LIST_WIDTH = 140; -export class Model3DView extends ResizableView { +export class Model3DView extends ResizableWidget { readonly element = create_element("div", { class: "viewer_Model3DView" }); private tool_bar_view = this.disposable(new ToolBarView()); @@ -28,7 +28,7 @@ export class Model3DView extends ResizableView { private animation_list_view = this.disposable( new ModelSelectListView(model_store.animations, model_store.current_animation), ); - private renderer_view = this.disposable(new RendererView(new Model3DRenderer())); + private renderer_view = this.disposable(new RendererWidget(new Model3DRenderer())); constructor() { super(); @@ -74,10 +74,10 @@ export class Model3DView extends ResizableView { } } -class ToolBarView extends View { +class ToolBarView extends Widget { private readonly open_file_button = new FileButton("Open file...", ".nj, .njm, .xj, .xvm"); - private readonly skeleton_checkbox = new CheckBox(false, "Show skeleton"); - private readonly play_animation_checkbox = new CheckBox(true, "Play animation"); + private readonly skeleton_checkbox = new CheckBox(false, { label: "Show skeleton" }); + private readonly play_animation_checkbox = new CheckBox(true, { label: "Play animation" }); private readonly animation_frame_rate_input = new NumberInput(PSO_FRAME_RATE, { label: "Frame rate:", min: 1, @@ -144,7 +144,7 @@ class ToolBarView extends View { } } -class ModelSelectListView extends ResizableView { +class ModelSelectListView extends ResizableWidget { element = create_element("ul", { class: "viewer_ModelSelectListView" }); set borders(borders: boolean) { diff --git a/src/viewer/gui/TextureView.ts b/src/viewer/gui/TextureView.ts index 3c086669..a943940e 100644 --- a/src/viewer/gui/TextureView.ts +++ b/src/viewer/gui/TextureView.ts @@ -1,20 +1,20 @@ import { create_element } from "../../core/gui/dom"; -import { ResizableView } from "../../core/gui/ResizableView"; +import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { FileButton } from "../../core/gui/FileButton"; import { ToolBar } from "../../core/gui/ToolBar"; import { texture_store } from "../stores/TextureStore"; -import { RendererView } from "../../core/gui/RendererView"; +import { RendererWidget } from "../../core/gui/RendererWidget"; import { TextureRenderer } from "../rendering/TextureRenderer"; import { gui_store, GuiTool } from "../../core/stores/GuiStore"; -export class TextureView extends ResizableView { +export class TextureView extends ResizableWidget { readonly element = create_element("div", { class: "viewer_TextureView" }); private readonly open_file_button = new FileButton("Open file...", ".xvm"); private readonly tool_bar = this.disposable(new ToolBar(this.open_file_button)); - private readonly renderer_view = this.disposable(new RendererView(new TextureRenderer())); + private readonly renderer_view = this.disposable(new RendererWidget(new TextureRenderer())); constructor() { super(); diff --git a/src/viewer/gui/ViewerView.ts b/src/viewer/gui/ViewerView.ts index 635d51d9..9d23cb02 100644 --- a/src/viewer/gui/ViewerView.ts +++ b/src/viewer/gui/ViewerView.ts @@ -1,7 +1,7 @@ import { TabContainer } from "../../core/gui/TabContainer"; -import { ResizableView } from "../../core/gui/ResizableView"; +import { ResizableWidget } from "../../core/gui/ResizableWidget"; -export class ViewerView extends ResizableView { +export class ViewerView extends ResizableWidget { private tabs = this.disposable( new TabContainer( { diff --git a/src/viewer/stores/Model3DStore.ts b/src/viewer/stores/Model3DStore.ts index 74da63a8..fbacf66d 100644 --- a/src/viewer/stores/Model3DStore.ts +++ b/src/viewer/stores/Model3DStore.ts @@ -57,9 +57,9 @@ export class Model3DStore implements Disposable { readonly show_skeleton: WritableProperty = property(false); - readonly current_animation: WritableProperty = property( - undefined, - ); + readonly current_animation: WritableProperty< + CharacterClassAnimationModel | undefined + > = property(undefined); private readonly _current_nj_motion = property(undefined); readonly current_nj_motion: Property = this._current_nj_motion;