Refactored widget properties to simplify the interface.

This commit is contained in:
Daan Vanden Bosch 2019-08-28 00:50:38 +02:00
parent 3fd4d7c882
commit f100220176
33 changed files with 285 additions and 254 deletions

View File

@ -1,10 +1,10 @@
import { NavigationView } from "./NavigationView"; import { NavigationView } from "./NavigationView";
import { MainContentView } from "./MainContentView"; import { MainContentView } from "./MainContentView";
import { create_element } from "../../core/gui/dom"; import { create_element } from "../../core/gui/dom";
import { ResizableView } from "../../core/gui/ResizableView"; import { ResizableWidget } from "../../core/gui/ResizableWidget";
export class ApplicationView extends ResizableView { export class ApplicationView extends ResizableWidget {
element = create_element("div", { class: "application_ApplicationView" }); readonly element = create_element("div", { class: "application_ApplicationView" });
private menu_view = this.disposable(new NavigationView()); private menu_view = this.disposable(new NavigationView());
private main_content_view = this.disposable(new MainContentView()); private main_content_view = this.disposable(new MainContentView());

View File

@ -1,10 +1,10 @@
import { create_element } from "../../core/gui/dom"; import { create_element } from "../../core/gui/dom";
import { gui_store, GuiTool } from "../../core/stores/GuiStore"; import { gui_store, GuiTool } from "../../core/stores/GuiStore";
import { LazyView } from "../../core/gui/LazyView"; import { LazyWidget } from "../../core/gui/LazyWidget";
import { ResizableView } from "../../core/gui/ResizableView"; import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { ChangeEvent } from "../../core/observable/Observable"; import { ChangeEvent } from "../../core/observable/Observable";
const TOOLS: [GuiTool, () => Promise<ResizableView>][] = [ const TOOLS: [GuiTool, () => Promise<ResizableWidget>][] = [
[GuiTool.Viewer, async () => new (await import("../../viewer/gui/ViewerView")).ViewerView()], [GuiTool.Viewer, async () => new (await import("../../viewer/gui/ViewerView")).ViewerView()],
[ [
GuiTool.QuestEditor, GuiTool.QuestEditor,
@ -12,11 +12,11 @@ const TOOLS: [GuiTool, () => Promise<ResizableView>][] = [
], ],
]; ];
export class MainContentView extends ResizableView { export class MainContentView extends ResizableWidget {
element = create_element("div", { class: "application_MainContentView" }); readonly element = create_element("div", { class: "application_MainContentView" });
private tool_views = new Map( 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() { constructor() {

View File

@ -1,7 +1,7 @@
import { create_element } from "../../core/gui/dom"; import { create_element } from "../../core/gui/dom";
import "./NavigationView.css"; import "./NavigationView.css";
import { gui_store, GuiTool } from "../../core/stores/GuiStore"; import { gui_store, GuiTool } from "../../core/stores/GuiStore";
import { View } from "../../core/gui/View"; import { Widget } from "../../core/gui/Widget";
const TOOLS: [GuiTool, string][] = [ const TOOLS: [GuiTool, string][] = [
[GuiTool.Viewer, "Viewer"], [GuiTool.Viewer, "Viewer"],
@ -9,7 +9,7 @@ const TOOLS: [GuiTool, string][] = [
[GuiTool.HuntOptimizer, "Hunt Optimizer"], [GuiTool.HuntOptimizer, "Hunt Optimizer"],
]; ];
export class NavigationView extends View { export class NavigationView extends Widget {
readonly element = create_element("div", { class: "application_NavigationView" }); readonly element = create_element("div", { class: "application_NavigationView" });
readonly height = 30; 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"); element: HTMLElement = create_element("span");
private input: HTMLInputElement = create_element("input"); private input: HTMLInputElement = create_element("input");

View File

@ -3,15 +3,20 @@ import "./Button.css";
import { Observable } from "../observable/Observable"; import { Observable } from "../observable/Observable";
import { emitter } from "../observable"; import { emitter } from "../observable";
import { Control } from "./Control"; import { Control } from "./Control";
import { Emitter } from "../observable/Emitter";
import { ViewOptions } from "./Widget";
export class Button extends Control { export class Button extends Control {
readonly element: HTMLButtonElement = create_element("button", { class: "core_Button" }); readonly element: HTMLButtonElement = create_element("button", { class: "core_Button" });
private readonly _click = emitter<MouseEvent>(); readonly click: Observable<MouseEvent>;
readonly click: Observable<MouseEvent> = this._click;
constructor(text: string) { private readonly _click: Emitter<MouseEvent> = emitter<MouseEvent>();
super();
constructor(text: string, options?: ViewOptions) {
super(options);
this.click = this._click;
this.element.append(create_element("span", { class: "core_Button_inner", text })); 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 }); this.element.onclick = (e: MouseEvent) => this._click.emit({ value: e });
} }
protected set_enabled(enabled: boolean): void {
super.set_enabled(enabled);
this.element.disabled = !enabled;
}
} }

View File

@ -1,27 +1,36 @@
import { create_element } from "./dom"; import { create_element } from "./dom";
import { WritableProperty } from "../observable/WritableProperty"; import { WritableProperty } from "../observable/WritableProperty";
import { property } from "../observable"; import { LabelledControl, LabelledControlOptions } from "./LabelledControl";
import { LabelledControl } from "./LabelledControl"; import { WidgetProperty } from "../observable/WidgetProperty";
export type CheckBoxOptions = LabelledControlOptions;
export class CheckBox extends LabelledControl { export class CheckBox extends LabelledControl {
readonly element: HTMLInputElement = create_element("input", { class: "core_CheckBox" }); readonly element: HTMLInputElement = create_element("input", { class: "core_CheckBox" });
readonly checked: WritableProperty<boolean> = property(false);
readonly preferred_label_position = "right"; readonly preferred_label_position = "right";
constructor(checked: boolean = false, label?: string) { readonly checked: WritableProperty<boolean>;
super(label);
private readonly _checked: WidgetProperty<boolean>;
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.type = "checkbox";
this.element.onchange = () => (this.checked.val = this.element.checked); this.element.onchange = () => (this._checked.val = this.element.checked);
}
this.disposables( protected set_enabled(enabled: boolean): void {
this.checked.observe(({ value }) => (this.element.checked = value)), super.set_enabled(enabled);
this.element.disabled = !enabled;
}
this.enabled.observe(({ value }) => (this.element.disabled = !value)), protected set_checked(checked: boolean): void {
); this.element.checked = checked;
this.checked.val = checked;
} }
} }

View File

@ -1,7 +1,3 @@
import { View } from "./View"; import { Widget } from "./Widget";
import { WritableProperty } from "../observable/WritableProperty";
import { property } from "../observable";
export abstract class Control extends View { export abstract class Control extends Widget {}
readonly enabled: WritableProperty<boolean> = property(true);
}

View File

@ -4,22 +4,26 @@ import "./Button.css";
import { property } from "../observable"; import { property } from "../observable";
import { Property } from "../observable/Property"; import { Property } from "../observable/Property";
import { Control } from "./Control"; import { Control } from "./Control";
import { WritableProperty } from "../observable/WritableProperty";
export class FileButton extends Control { export class FileButton extends Control {
readonly element: HTMLLabelElement = create_element("label", { readonly element: HTMLLabelElement = create_element("label", {
class: "core_FileButton core_Button", class: "core_FileButton core_Button",
}); });
private readonly _files = property<File[]>([]); readonly files: Property<File[]>;
readonly files: Property<File[]> = this._files;
private input: HTMLInputElement = create_element("input", { private input: HTMLInputElement = create_element("input", {
class: "core_FileButton_input core_Button_inner", class: "core_FileButton_input core_Button_inner",
}); });
private readonly _files: WritableProperty<File[]> = property<File[]>([]);
constructor(text: string, accept: string = "") { constructor(text: string, accept: string = "") {
super(); super();
this.files = this._files;
this.input.type = "file"; this.input.type = "file";
this.input.accept = accept; this.input.accept = accept;
this.input.onchange = () => { this.input.onchange = () => {

View File

@ -1,9 +1,12 @@
/* eslint-disable no-dupe-class-members */ /* eslint-disable no-dupe-class-members */
import { LabelledControl } from "./LabelledControl"; import { LabelledControl, LabelledControlOptions } from "./LabelledControl";
import { create_element } from "./dom"; import { create_element } from "./dom";
import { WritableProperty } from "../observable/WritableProperty"; import { WritableProperty } from "../observable/WritableProperty";
import { is_any_property, Property } from "../observable/Property"; import { is_any_property, Property } from "../observable/Property";
import "./Input.css"; import "./Input.css";
import { WidgetProperty } from "../observable/WidgetProperty";
export type InputOptions = LabelledControlOptions;
export abstract class Input<T> extends LabelledControl { export abstract class Input<T> extends LabelledControl {
readonly element: HTMLElement; readonly element: HTMLElement;
@ -12,16 +15,20 @@ export abstract class Input<T> extends LabelledControl {
protected readonly input: HTMLInputElement; protected readonly input: HTMLInputElement;
private readonly _value: WidgetProperty<T>;
private ignore_input_change = false;
protected constructor( protected constructor(
value: WritableProperty<T>, value: T,
class_name: string, class_name: string,
input_type: string, input_type: string,
input_class_name: string, input_class_name: string,
label?: string, options?: InputOptions,
) { ) {
super(label); super(options);
this.value = value; this._value = new WidgetProperty<T>(this, value, this.set_value);
this.value = this._value;
this.element = create_element("span", { class: `${class_name} core_Input` }); this.element = create_element("span", { class: `${class_name} core_Input` });
@ -30,47 +37,26 @@ export abstract class Input<T> extends LabelledControl {
}); });
this.input.type = input_type; this.input.type = input_type;
this.input.onchange = () => { this.input.onchange = () => {
if (this.input_value_changed()) { this._value.val = this.get_value();
this.value.val = this.get_input_value();
}
}; };
this.set_input_value(value.val);
this.element.append(this.input); 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 { protected set_enabled(enabled: boolean): void {
this.value.set_val(value, options); super.set_enabled(enabled);
this.input.disabled = !enabled;
if (options.silent) {
this.set_input_value(value);
}
} }
protected input_value_changed(): boolean { protected abstract get_value(): T;
return true;
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<T>(attr: InputAttrsOfType<T>, value?: T | Property<T>): void; protected set_attr<T>(attr: InputAttrsOfType<T>, value?: T | Property<T>): void;
protected set_attr<T, U>( protected set_attr<T, U>(
attr: InputAttrsOfType<U>, attr: InputAttrsOfType<U>,

View File

@ -1,39 +1,34 @@
import { View } from "./View"; import { ViewOptions, Widget } from "./Widget";
import { create_element } from "./dom"; import { create_element } from "./dom";
import { WritableProperty } from "../observable/WritableProperty"; import { WritableProperty } from "../observable/WritableProperty";
import "./Label.css"; import "./Label.css";
import { property } from "../observable";
import { Property } from "../observable/Property"; import { Property } from "../observable/Property";
import { WidgetProperty } from "../observable/WidgetProperty";
export class Label extends View { export class Label extends Widget {
readonly element = create_element<HTMLLabelElement>("label", { class: "core_Label" }); readonly element = create_element<HTMLLabelElement>("label", { class: "core_Label" });
set for(id: string) { set for(id: string) {
this.element.htmlFor = id; this.element.htmlFor = id;
} }
readonly enabled: WritableProperty<boolean> = property(true); readonly text: WritableProperty<string>;
constructor(text: string | Property<string>, options: { enabled?: boolean } = {}) { private readonly _text = new WidgetProperty<string>(this, "", this.set_text);
super();
constructor(text: string | Property<string>, options?: ViewOptions) {
super(options);
this.text = this._text;
if (typeof text === "string") { if (typeof text === "string") {
this.element.append(text); this.set_text(text);
} else { } else {
this.element.append(text.val); this.disposable(this._text.bind_to(text));
this.disposable(text.observe(({ value }) => (this.element.textContent = value))); }
} }
this.disposables( protected set_text(text: string): void {
this.enabled.observe(({ value }) => { this.element.textContent = text;
if (value) {
this.element.classList.remove("disabled");
} else {
this.element.classList.add("disabled");
}
}),
);
if (options.enabled != undefined) this.enabled.val = options.enabled;
} }
} }

View File

@ -1,12 +1,14 @@
import { Label } from "./Label"; import { Label } from "./Label";
import { Control } from "./Control"; import { Control } from "./Control";
import { ViewOptions } from "./Widget";
export type LabelledControlOptions = ViewOptions & {
label?: string;
};
export abstract class LabelledControl extends Control { export abstract class LabelledControl extends Control {
abstract readonly preferred_label_position: "left" | "right" | "top" | "bottom"; abstract readonly preferred_label_position: "left" | "right" | "top" | "bottom";
private readonly _label_text: string;
private _label?: Label;
get label(): Label { get label(): Label {
if (!this._label) { if (!this._label) {
this._label = this.disposable(new Label(this._label_text)); this._label = this.disposable(new Label(this._label_text));
@ -21,10 +23,13 @@ export abstract class LabelledControl extends Control {
return this._label; return this._label;
} }
protected constructor(label: string | undefined) { private readonly _label_text: string;
super(); private _label?: Label;
this._label_text = label || ""; protected constructor(options?: LabelledControlOptions) {
super(options);
this._label_text = (options && options.label) || "";
} }
} }

View File

@ -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<View & Resizable>) {
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;
}
}

View File

@ -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<Widget & Resizable>) {
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;
}
}

View File

@ -1,17 +1,15 @@
import { property } from "../observable";
import { Property } from "../observable/Property"; import { Property } from "../observable/Property";
import { Input } from "./Input"; import { Input, InputOptions } from "./Input";
import "./NumberInput.css"; import "./NumberInput.css";
export class NumberInput extends Input<number> { export class NumberInput extends Input<number> {
readonly preferred_label_position = "left"; readonly preferred_label_position = "left";
private readonly rounding_factor: number; private readonly rounding_factor: number;
private rounded_value: number = 0;
constructor( constructor(
value: number = 0, value: number = 0,
options: { options: InputOptions & {
label?: string; label?: string;
min?: number | Property<number>; min?: number | Property<number>;
max?: number | Property<number>; max?: number | Property<number>;
@ -20,13 +18,7 @@ export class NumberInput extends Input<number> {
round_to?: number; round_to?: number;
} = {}, } = {},
) { ) {
super( super(value, "core_NumberInput", "number", "core_NumberInput_inner", options);
property(value),
"core_NumberInput",
"number",
"core_NumberInput_inner",
options.label,
);
const { min, max, step } = options; const { min, max, step } = options;
this.set_attr("min", min, String); this.set_attr("min", min, String);
@ -40,18 +32,18 @@ export class NumberInput extends Input<number> {
} }
this.element.style.width = `${options.width == undefined ? 54 : options.width}px`; this.element.style.width = `${options.width == undefined ? 54 : options.width}px`;
this.set_value(value);
} }
protected input_value_changed(): boolean { protected get_value(): number {
return this.input.valueAsNumber !== this.rounded_value;
}
protected get_input_value(): number {
return this.input.valueAsNumber; return this.input.valueAsNumber;
} }
protected set_input_value(value: number): void { protected set_value(value: number): void {
this.input.valueAsNumber = this.rounded_value = this.ignore_change(() => {
this.input.valueAsNumber =
Math.round(this.rounding_factor * value) / this.rounding_factor; Math.round(this.rounding_factor * value) / this.rounding_factor;
});
} }
} }

View File

@ -1,8 +1,8 @@
import { ResizableView } from "./ResizableView"; import { ResizableWidget } from "./ResizableWidget";
import { create_element } from "./dom"; import { create_element } from "./dom";
import { Renderer } from "../rendering/Renderer"; import { Renderer } from "../rendering/Renderer";
export class RendererView extends ResizableView { export class RendererWidget extends ResizableWidget {
readonly element = create_element("div"); readonly element = create_element("div");
constructor(private renderer: Renderer) { constructor(private renderer: Renderer) {

View File

@ -1,7 +1,7 @@
import { View } from "./View"; import { Widget } from "./Widget";
import { Resizable } from "./Resizable"; 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 width: number = 0;
protected height: number = 0; protected height: number = 0;

View File

@ -1,21 +1,21 @@
import { View } from "./View"; import { Widget } from "./Widget";
import { create_element } from "./dom"; import { create_element } from "./dom";
import { LazyView } from "./LazyView"; import { LazyWidget } from "./LazyWidget";
import { Resizable } from "./Resizable"; import { Resizable } from "./Resizable";
import { ResizableView } from "./ResizableView"; import { ResizableWidget } from "./ResizableWidget";
import "./TabContainer.css"; import "./TabContainer.css";
export type Tab = { export type Tab = {
title: string; title: string;
key: string; key: string;
create_view: () => Promise<View & Resizable>; create_view: () => Promise<Widget & Resizable>;
}; };
type TabInfo = Tab & { tab_element: HTMLSpanElement; lazy_view: LazyView }; type TabInfo = Tab & { tab_element: HTMLSpanElement; lazy_view: LazyWidget };
const BAR_HEIGHT = 28; const BAR_HEIGHT = 28;
export class TabContainer extends ResizableView { export class TabContainer extends ResizableWidget {
readonly element = create_element("div", { class: "core_TabContainer" }); readonly element = create_element("div", { class: "core_TabContainer" });
private tabs: TabInfo[] = []; private tabs: TabInfo[] = [];
@ -35,7 +35,7 @@ export class TabContainer extends ResizableView {
}); });
this.bar_element.append(tab_element); 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({ this.tabs.push({
...tab, ...tab,

View File

@ -1,8 +1,16 @@
import { LabelledControl } from "./LabelledControl"; import { LabelledControl, LabelledControlOptions } from "./LabelledControl";
import { el } from "./dom"; import { el } from "./dom";
import { property } from "../observable"; import { property } from "../observable";
import { WritableProperty } from "../observable/WritableProperty"; import { WritableProperty } from "../observable/WritableProperty";
import "./TextArea.css"; 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 { export class TextArea extends LabelledControl {
readonly element: HTMLElement = el.div({ class: "core_TextArea" }); readonly element: HTMLElement = el.div({ class: "core_TextArea" });
@ -15,17 +23,10 @@ export class TextArea extends LabelledControl {
class: "core_TextArea_inner", class: "core_TextArea_inner",
}); });
constructor( private readonly _value = new WidgetProperty<string>(this, "", this.set_value);
value = "",
options?: { constructor(value = "", options?: TextAreaOptions) {
label?: string; super(options);
max_length?: number;
font_family?: string;
rows?: number;
cols?: number;
},
) {
super(options && options.label);
if (options) { if (options) {
if (options.max_length != undefined) this.text_element.maxLength = options.max_length; 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; 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.text_element.onchange = () => (this._value.val = this.text_element.value);
this.disposables(this.value.observe(({ value }) => (this.text_element.value = value)));
this.element.append(this.text_element); this.element.append(this.text_element);
} }
protected set_value(value: string): void {
this.text_element.value = value;
}
} }

View File

@ -1,36 +1,29 @@
import { Input } from "./Input"; import { Input, InputOptions } from "./Input";
import { Property } from "../observable/Property"; import { Property } from "../observable/Property";
import { property } from "../observable";
export type TextInputOptions = InputOptions & {
max_length?: number | Property<number>;
};
export class TextInput extends Input<string> { export class TextInput extends Input<string> {
readonly preferred_label_position = "left"; readonly preferred_label_position = "left";
constructor( constructor(value = "", options?: TextInputOptions) {
value = "", super(value, "core_TextInput", "text", "core_TextInput_inner", options);
options?: {
label?: string;
max_length?: number | Property<number>;
},
) {
super(
property(value),
"core_TextInput",
"text",
"core_TextInput_inner",
options && options.label,
);
if (options) { if (options) {
const { max_length } = options; const { max_length } = options;
this.set_attr("maxLength", max_length); this.set_attr("maxLength", max_length);
} }
this.set_value(value);
} }
protected get_input_value(): string { protected get_value(): string {
return this.input.value; return this.input.value;
} }
protected set_input_value(value: string): void { protected set_value(value: string): void {
this.input.value = value; this.input.value = value;
} }
} }

View File

@ -1,13 +1,14 @@
import { View } from "./View"; import { Widget } from "./Widget";
import { create_element } from "./dom"; import { create_element } from "./dom";
import "./ToolBar.css"; import "./ToolBar.css";
import { LabelledControl } from "./LabelledControl"; import { LabelledControl } from "./LabelledControl";
export class ToolBar extends View { export class ToolBar extends Widget {
readonly element = create_element("div", { class: "core_ToolBar" }); readonly element = create_element("div", { class: "core_ToolBar" });
readonly height = 33; readonly height = 33;
constructor(...children: View[]) { constructor(...children: Widget[]) {
super(); super();
this.element.style.height = `${this.height}px`; this.element.style.height = `${this.height}px`;

View File

@ -3,9 +3,11 @@ import { Disposer } from "../observable/Disposer";
import { Observable } from "../observable/Observable"; import { Observable } from "../observable/Observable";
import { bind_hidden } from "./dom"; import { bind_hidden } from "./dom";
import { WritableProperty } from "../observable/WritableProperty"; 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; abstract readonly element: HTMLElement;
get id(): string { get id(): string {
@ -16,12 +18,18 @@ export abstract class View implements Disposable {
this.element.id = id; this.element.id = id;
} }
readonly visible: WritableProperty<boolean> = property(true); readonly visible: WritableProperty<boolean>;
readonly enabled: WritableProperty<boolean>;
protected disposed = false;
private disposer = new Disposer(); private disposer = new Disposer();
private _visible = new WidgetProperty<boolean>(this, true, this.set_visible);
private _enabled = new WidgetProperty<boolean>(this, true, this.set_enabled);
constructor() { constructor(_options?: ViewOptions) {
this.disposables(this.visible.observe(({ value }) => (this.element.hidden = !value))); this.visible = this._visible;
this.enabled = this._enabled;
} }
focus(): void { focus(): void {
@ -31,6 +39,19 @@ export abstract class View implements Disposable {
dispose(): void { dispose(): void {
this.element.remove(); this.element.remove();
this.disposer.dispose(); 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<boolean>): void { protected bind_hidden(element: HTMLElement, observable: Observable<boolean>): void {

View File

@ -0,0 +1,13 @@
import { SimpleProperty } from "./SimpleProperty";
import { Widget } from "../gui/Widget";
export class WidgetProperty<T> extends SimpleProperty<T> {
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);
}
}

View File

@ -1,4 +1,4 @@
import { ResizableView } from "../../core/gui/ResizableView"; import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { el } from "../../core/gui/dom"; import { el } from "../../core/gui/dom";
import { editor } from "monaco-editor"; import { editor } from "monaco-editor";
import { asm_editor_store } from "../stores/AsmEditorStore"; import { asm_editor_store } from "../stores/AsmEditorStore";
@ -25,7 +25,7 @@ editor.defineTheme("phantasmal-world", {
const DUMMY_MODEL = editor.createModel("", "psoasm"); const DUMMY_MODEL = editor.createModel("", "psoasm");
export class AsmEditorView extends ResizableView { export class AsmEditorView extends ResizableWidget {
readonly element = el.div(); readonly element = el.div();
private readonly editor: IStandaloneCodeEditor = this.disposable( private readonly editor: IStandaloneCodeEditor = this.disposable(

View File

@ -1,9 +1,9 @@
import { View } from "../../core/gui/View"; import { Widget } from "../../core/gui/Widget";
import { el } from "../../core/gui/dom"; import { el } from "../../core/gui/dom";
import { Label } from "../../core/gui/Label"; import { Label } from "../../core/gui/Label";
import "./DisabledView.css"; import "./DisabledView.css";
export class DisabledView extends View { export class DisabledView extends Widget {
readonly element = el.div({ class: "quest_editor_DisabledView" }); readonly element = el.div({ class: "quest_editor_DisabledView" });
private readonly label: Label; private readonly label: Label;

View File

@ -1,4 +1,4 @@
import { ResizableView } from "../../core/gui/ResizableView"; import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { el } from "../../core/gui/dom"; import { el } from "../../core/gui/dom";
import { DisabledView } from "./DisabledView"; import { DisabledView } from "./DisabledView";
import { quest_editor_store } from "../stores/QuestEditorStore"; 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 { Vec3 } from "../../core/data_formats/vector";
import { QuestEntityModel } from "../model/QuestEntityModel"; 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 }); readonly element = el.div({ class: "quest_editor_EntityInfoView", tab_index: -1 });
private readonly no_entity_view = new DisabledView("No entity selected."); private readonly no_entity_view = new DisabledView("No entity selected.");
@ -153,9 +153,9 @@ export class EntityInfoView extends ResizableView {
this.entity_disposer.add_all( this.entity_disposer.add_all(
pos.observe( pos.observe(
({ value: { x, y, z } }) => { ({ value: { x, y, z } }) => {
x_input.set_value(x, { silent: true }); x_input.value.set_val(x, { silent: true });
y_input.set_value(y, { silent: true }); y_input.value.set_val(y, { silent: true });
z_input.set_value(z, { silent: true }); z_input.value.set_val(z, { silent: true });
}, },
{ call_now: true }, { call_now: true },
), ),

View File

@ -1,4 +1,4 @@
import { ResizableView } from "../../core/gui/ResizableView"; import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { el } from "../../core/gui/dom"; import { el } from "../../core/gui/dom";
import { quest_editor_store } from "../stores/QuestEditorStore"; import { quest_editor_store } from "../stores/QuestEditorStore";
import { npc_data, NpcType } from "../../core/data_formats/parsing/quest/npc_types"; 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 "./NpcCountsView.css";
import { DisabledView } from "./DisabledView"; import { DisabledView } from "./DisabledView";
export class NpcCountsView extends ResizableView { export class NpcCountsView extends ResizableWidget {
readonly element = el.div({ class: "quest_editor_NpcCountsView" }); readonly element = el.div({ class: "quest_editor_NpcCountsView" });
private readonly table_element = el.table(); private readonly table_element = el.table();

View File

@ -1,4 +1,4 @@
import { ResizableView } from "../../core/gui/ResizableView"; import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { el } from "../../core/gui/dom"; import { el } from "../../core/gui/dom";
import { quest_editor_store } from "../stores/QuestEditorStore"; import { quest_editor_store } from "../stores/QuestEditorStore";
import { Episode } from "../../core/data_formats/parsing/quest/Episode"; import { Episode } from "../../core/data_formats/parsing/quest/Episode";
@ -9,7 +9,7 @@ import { TextArea } from "../../core/gui/TextArea";
import "./QuesInfoView.css"; import "./QuesInfoView.css";
import { DisabledView } from "./DisabledView"; 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 }); readonly element = el.div({ class: "quest_editor_QuesInfoView", tab_index: -1 });
private readonly table_element = el.table(); private readonly table_element = el.table();

View File

@ -1,4 +1,4 @@
import { ResizableView } from "../../core/gui/ResizableView"; import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { create_element } from "../../core/gui/dom"; import { create_element } from "../../core/gui/dom";
import { ToolBarView } from "./ToolBarView"; import { ToolBarView } from "./ToolBarView";
import GoldenLayout, { Container, ContentItem, ItemConfigType } from "golden-layout"; import GoldenLayout, { Container, ContentItem, ItemConfigType } from "golden-layout";
@ -15,7 +15,7 @@ import { EntityInfoView } from "./EntityInfoView";
const logger = Logger.get("quest_editor/gui/QuestEditorView"); const logger = Logger.get("quest_editor/gui/QuestEditorView");
// Don't change these values, as they are persisted in the user's browser. // Don't change these values, as they are persisted in the user's browser.
const VIEW_TO_NAME = new Map<new () => ResizableView, string>([ const VIEW_TO_NAME = new Map<new () => ResizableWidget, string>([
[QuesInfoView, "quest_info"], [QuesInfoView, "quest_info"],
[NpcCountsView, "npc_counts"], [NpcCountsView, "npc_counts"],
[QuestRendererView, "quest_renderer"], [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" }); readonly element = create_element("div", { class: "quest_editor_QuestEditorView" });
private readonly tool_bar_view = this.disposable(new ToolBarView()); 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_element = create_element("div", { class: "quest_editor_gl_container" });
private readonly layout: Promise<GoldenLayout>; private readonly layout: Promise<GoldenLayout>;
private readonly sub_views = new Map<string, ResizableView>(); private readonly sub_views = new Map<string, ResizableWidget>();
constructor() { constructor() {
super(); super();

View File

@ -1,14 +1,14 @@
import { ResizableView } from "../../core/gui/ResizableView"; import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { el } from "../../core/gui/dom"; import { el } from "../../core/gui/dom";
import { RendererView } from "../../core/gui/RendererView"; import { RendererWidget } from "../../core/gui/RendererWidget";
import { QuestRenderer } from "../rendering/QuestRenderer"; import { QuestRenderer } from "../rendering/QuestRenderer";
import { gui_store, GuiTool } from "../../core/stores/GuiStore"; import { gui_store, GuiTool } from "../../core/stores/GuiStore";
import { quest_editor_store } from "../stores/QuestEditorStore"; 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 }); 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() { constructor() {
super(); super();

View File

@ -1,11 +1,11 @@
import { View } from "../../core/gui/View"; import { Widget } from "../../core/gui/Widget";
import { ToolBar } from "../../core/gui/ToolBar"; import { ToolBar } from "../../core/gui/ToolBar";
import { FileButton } from "../../core/gui/FileButton"; import { FileButton } from "../../core/gui/FileButton";
import { Button } from "../../core/gui/Button"; import { Button } from "../../core/gui/Button";
import { quest_editor_store } from "../stores/QuestEditorStore"; import { quest_editor_store } from "../stores/QuestEditorStore";
import { undo_manager } from "../../core/undo/UndoManager"; 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 open_file_button = new FileButton("Open file...", ".qst");
private readonly save_as_button = new Button("Save as..."); private readonly save_as_button = new Button("Save as...");
private readonly undo_button = new Button("Undo"); private readonly undo_button = new Button("Undo");

View File

@ -1,12 +1,12 @@
import { create_element } from "../../core/gui/dom"; 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 { ToolBar } from "../../core/gui/ToolBar";
import "./Model3DView.css"; import "./Model3DView.css";
import { model_store } from "../stores/Model3DStore"; import { model_store } from "../stores/Model3DStore";
import { WritableProperty } from "../../core/observable/WritableProperty"; import { WritableProperty } from "../../core/observable/WritableProperty";
import { RendererView } from "../../core/gui/RendererView"; import { RendererWidget } from "../../core/gui/RendererWidget";
import { Model3DRenderer } from "../rendering/Model3DRenderer"; import { Model3DRenderer } from "../rendering/Model3DRenderer";
import { View } from "../../core/gui/View"; import { Widget } from "../../core/gui/Widget";
import { FileButton } from "../../core/gui/FileButton"; import { FileButton } from "../../core/gui/FileButton";
import { CheckBox } from "../../core/gui/CheckBox"; import { CheckBox } from "../../core/gui/CheckBox";
import { NumberInput } from "../../core/gui/NumberInput"; 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 MODEL_LIST_WIDTH = 100;
const ANIMATION_LIST_WIDTH = 140; const ANIMATION_LIST_WIDTH = 140;
export class Model3DView extends ResizableView { export class Model3DView extends ResizableWidget {
readonly element = create_element("div", { class: "viewer_Model3DView" }); readonly element = create_element("div", { class: "viewer_Model3DView" });
private tool_bar_view = this.disposable(new ToolBarView()); private tool_bar_view = this.disposable(new ToolBarView());
@ -28,7 +28,7 @@ export class Model3DView extends ResizableView {
private animation_list_view = this.disposable( private animation_list_view = this.disposable(
new ModelSelectListView(model_store.animations, model_store.current_animation), 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() { constructor() {
super(); 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 open_file_button = new FileButton("Open file...", ".nj, .njm, .xj, .xvm");
private readonly skeleton_checkbox = new CheckBox(false, "Show skeleton"); private readonly skeleton_checkbox = new CheckBox(false, { label: "Show skeleton" });
private readonly play_animation_checkbox = new CheckBox(true, "Play animation"); private readonly play_animation_checkbox = new CheckBox(true, { label: "Play animation" });
private readonly animation_frame_rate_input = new NumberInput(PSO_FRAME_RATE, { private readonly animation_frame_rate_input = new NumberInput(PSO_FRAME_RATE, {
label: "Frame rate:", label: "Frame rate:",
min: 1, min: 1,
@ -144,7 +144,7 @@ class ToolBarView extends View {
} }
} }
class ModelSelectListView<T extends { name: string }> extends ResizableView { class ModelSelectListView<T extends { name: string }> extends ResizableWidget {
element = create_element("ul", { class: "viewer_ModelSelectListView" }); element = create_element("ul", { class: "viewer_ModelSelectListView" });
set borders(borders: boolean) { set borders(borders: boolean) {

View File

@ -1,20 +1,20 @@
import { create_element } from "../../core/gui/dom"; 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 { FileButton } from "../../core/gui/FileButton";
import { ToolBar } from "../../core/gui/ToolBar"; import { ToolBar } from "../../core/gui/ToolBar";
import { texture_store } from "../stores/TextureStore"; import { texture_store } from "../stores/TextureStore";
import { RendererView } from "../../core/gui/RendererView"; import { RendererWidget } from "../../core/gui/RendererWidget";
import { TextureRenderer } from "../rendering/TextureRenderer"; import { TextureRenderer } from "../rendering/TextureRenderer";
import { gui_store, GuiTool } from "../../core/stores/GuiStore"; 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" }); readonly element = create_element("div", { class: "viewer_TextureView" });
private readonly open_file_button = new FileButton("Open file...", ".xvm"); private readonly open_file_button = new FileButton("Open file...", ".xvm");
private readonly tool_bar = this.disposable(new ToolBar(this.open_file_button)); 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() { constructor() {
super(); super();

View File

@ -1,7 +1,7 @@
import { TabContainer } from "../../core/gui/TabContainer"; 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( private tabs = this.disposable(
new TabContainer( new TabContainer(
{ {

View File

@ -57,9 +57,9 @@ export class Model3DStore implements Disposable {
readonly show_skeleton: WritableProperty<boolean> = property(false); readonly show_skeleton: WritableProperty<boolean> = property(false);
readonly current_animation: WritableProperty<CharacterClassAnimationModel | undefined> = property( readonly current_animation: WritableProperty<
undefined, CharacterClassAnimationModel | undefined
); > = property(undefined);
private readonly _current_nj_motion = property<NjMotion | undefined>(undefined); private readonly _current_nj_motion = property<NjMotion | undefined>(undefined);
readonly current_nj_motion: Property<NjMotion | undefined> = this._current_nj_motion; readonly current_nj_motion: Property<NjMotion | undefined> = this._current_nj_motion;