Merge branch 'master' into script-editor-settings

This commit is contained in:
jtuu 2019-09-15 21:44:31 +03:00
commit f1e6a31f0e
43 changed files with 297 additions and 210 deletions

View File

@ -7,13 +7,17 @@ export class ApplicationView extends ResizableWidget {
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());
readonly element = el.div(
{ class: "application_ApplicationView" },
this.menu_view.element,
this.main_content_view.element,
);
constructor() { constructor() {
super(el.div({ class: "application_ApplicationView" })); super();
this.element.id = "root"; this.element.id = "root";
this.element.append(this.menu_view.element, this.main_content_view.element);
this.finalize_construction(ApplicationView.prototype); this.finalize_construction(ApplicationView.prototype);
} }

View File

@ -18,12 +18,14 @@ const TOOLS: [GuiTool, () => Promise<ResizableWidget>][] = [
]; ];
export class MainContentView extends ResizableWidget { export class MainContentView extends ResizableWidget {
readonly element = el.div({ class: "application_MainContentView" });
private tool_views = new Map( private tool_views = new Map(
TOOLS.map(([tool, create_view]) => [tool, this.disposable(new LazyWidget(create_view))]), TOOLS.map(([tool, create_view]) => [tool, this.disposable(new LazyWidget(create_view))]),
); );
constructor() { constructor() {
super(el.div({ class: "application_MainContentView" })); super();
for (const tool_view of this.tool_views.values()) { for (const tool_view of this.tool_views.values()) {
this.element.append(tool_view.element); this.element.append(tool_view.element);

View File

@ -4,11 +4,13 @@ import { GuiTool } from "../../core/stores/GuiStore";
import "./NavigationButton.css"; import "./NavigationButton.css";
export class NavigationButton extends Widget { export class NavigationButton extends Widget {
readonly element = el.span({ class: "application_NavigationButton" });
private input: HTMLInputElement = create_element("input"); private input: HTMLInputElement = create_element("input");
private label: HTMLLabelElement = create_element("label"); private label: HTMLLabelElement = create_element("label");
constructor(tool: GuiTool, text: string) { constructor(tool: GuiTool, text: string) {
super(el.span({ class: "application_NavigationButton" })); super();
const tool_str = GuiTool[tool]; const tool_str = GuiTool[tool];

View File

@ -13,25 +13,10 @@ const TOOLS: [GuiTool, string][] = [
]; ];
export class NavigationView extends Widget { export class NavigationView extends Widget {
readonly height = 30; private readonly buttons = new Map<GuiTool, NavigationButton>(
private buttons = new Map<GuiTool, NavigationButton>(
TOOLS.map(([value, text]) => [value, this.disposable(new NavigationButton(value, text))]), TOOLS.map(([value, text]) => [value, this.disposable(new NavigationButton(value, text))]),
); );
private readonly server_select = this.disposable(
constructor() {
super(el.div({ class: "application_NavigationView" }));
this.element.style.height = `${this.height}px`;
this.element.onmousedown = this.mousedown;
for (const button of this.buttons.values()) {
this.element.append(button.element);
}
this.element.append(el.div({ class: "application_NavigationView_spacer" }));
const server_select = this.disposable(
new Select(property(["Ephinea"]), server => server, { new Select(property(["Ephinea"]), server => server, {
label: "Server:", label: "Server:",
enabled: false, enabled: false,
@ -40,12 +25,21 @@ export class NavigationView extends Widget {
}), }),
); );
this.element.append( readonly element = el.div(
{ class: "application_NavigationView" },
...[...this.buttons.values()].map(button => button.element),
el.div({ class: "application_NavigationView_spacer" }),
this.server_select.element,
el.span( el.span(
{ class: "application_NavigationView_server" }, { class: "application_NavigationView_server" },
server_select.label!.element, this.server_select.label!.element,
server_select.element, this.server_select.element,
), ),
el.a( el.a(
{ {
class: "application_NavigationView_github", class: "application_NavigationView_github",
@ -56,6 +50,14 @@ export class NavigationView extends Widget {
), ),
); );
readonly height = 30;
constructor() {
super();
this.element.style.height = `${this.height}px`;
this.element.onmousedown = this.mousedown;
this.mark_tool_button(gui_store.tool.val); this.mark_tool_button(gui_store.tool.val);
this.disposable(gui_store.tool.observe(({ value }) => this.mark_tool_button(value))); this.disposable(gui_store.tool.observe(({ value }) => this.mark_tool_button(value)));

View File

@ -14,7 +14,8 @@ export type ButtonOptions = WidgetOptions & {
icon_right?: Icon; icon_right?: Icon;
}; };
export class Button extends Control<HTMLButtonElement> { export class Button extends Control {
readonly element = el.button({ class: "core_Button" });
readonly mousedown: Observable<MouseEvent>; readonly mousedown: Observable<MouseEvent>;
readonly mouseup: Observable<MouseEvent>; readonly mouseup: Observable<MouseEvent>;
readonly click: Observable<MouseEvent>; readonly click: Observable<MouseEvent>;
@ -27,9 +28,9 @@ export class Button extends Control<HTMLButtonElement> {
private readonly center_element: HTMLSpanElement; private readonly center_element: HTMLSpanElement;
constructor(text: string | Property<string>, options?: ButtonOptions) { constructor(text: string | Property<string>, options?: ButtonOptions) {
const inner_element = el.span({ class: "core_Button_inner" }); super(options);
super(el.button({ class: "core_Button" }, inner_element), options); const inner_element = el.span({ class: "core_Button_inner" });
this.center_element = el.span({ class: "core_Button_center" }); this.center_element = el.span({ class: "core_Button_center" });
@ -64,6 +65,8 @@ export class Button extends Control<HTMLButtonElement> {
this.text.bind_to(text); this.text.bind_to(text);
} }
this.element.append(inner_element);
this.finalize_construction(Button.prototype); this.finalize_construction(Button.prototype);
} }

View File

@ -5,7 +5,9 @@ import { WidgetProperty } from "../observable/property/WidgetProperty";
export type CheckBoxOptions = LabelledControlOptions; export type CheckBoxOptions = LabelledControlOptions;
export class CheckBox extends LabelledControl<HTMLInputElement> { export class CheckBox extends LabelledControl {
readonly element = create_element<HTMLInputElement>("input", { class: "core_CheckBox" });
readonly preferred_label_position = "right"; readonly preferred_label_position = "right";
readonly checked: WritableProperty<boolean>; readonly checked: WritableProperty<boolean>;
@ -13,7 +15,7 @@ export class CheckBox extends LabelledControl<HTMLInputElement> {
private readonly _checked: WidgetProperty<boolean>; private readonly _checked: WidgetProperty<boolean>;
constructor(checked: boolean = false, options?: CheckBoxOptions) { constructor(checked: boolean = false, options?: CheckBoxOptions) {
super(create_element("input", { class: "core_CheckBox" }), options); super(options);
this._checked = new WidgetProperty(this, checked, this.set_checked); this._checked = new WidgetProperty(this, checked, this.set_checked);
this.checked = this._checked; this.checked = this._checked;

View File

@ -16,6 +16,8 @@ export type ComboBoxOptions<T> = LabelledControlOptions & {
}; };
export class ComboBox<T> extends LabelledControl { export class ComboBox<T> extends LabelledControl {
readonly element = el.span({ class: "core_ComboBox core_Input" });
readonly preferred_label_position = "left"; readonly preferred_label_position = "left";
readonly selected: WritableProperty<T | undefined>; readonly selected: WritableProperty<T | undefined>;
@ -26,7 +28,7 @@ export class ComboBox<T> extends LabelledControl {
private readonly _selected: WidgetProperty<T | undefined>; private readonly _selected: WidgetProperty<T | undefined>;
constructor(options: ComboBoxOptions<T>) { constructor(options: ComboBoxOptions<T>) {
super(el.span({ class: "core_ComboBox core_Input" }), options); super(options);
this.to_label = options.to_label; this.to_label = options.to_label;

View File

@ -2,4 +2,4 @@ import { Widget, WidgetOptions } from "./Widget";
export type ControlOptions = WidgetOptions; export type ControlOptions = WidgetOptions;
export abstract class Control<E extends HTMLElement = HTMLElement> extends Widget<E> {} export abstract class Control extends Widget {}

View File

@ -11,6 +11,8 @@ import { emitter } from "../observable";
export type DropDownOptions = ButtonOptions; export type DropDownOptions = ButtonOptions;
export class DropDown<T> extends Control { export class DropDown<T> extends Control {
readonly element = el.div({ class: "core_DropDown" });
readonly chosen: Observable<T>; readonly chosen: Observable<T>;
private readonly button: Button; private readonly button: Button;
@ -24,17 +26,15 @@ export class DropDown<T> extends Control {
to_label: (element: T) => string, to_label: (element: T) => string,
options?: DropDownOptions, options?: DropDownOptions,
) { ) {
const element = el.div({ class: "core_DropDown" }); super(options);
const button = new Button(text, {
this.button = this.disposable(
new Button(text, {
icon_left: options && options.icon_left, icon_left: options && options.icon_left,
icon_right: Icon.TriangleDown, icon_right: Icon.TriangleDown,
}); }),
const menu = new Menu<T>(items, to_label, element); );
this.menu = this.disposable(new Menu<T>(items, to_label, this.element));
super(element, options);
this.button = this.disposable(button);
this.menu = this.disposable(menu);
this.element.append(this.button.element, this.menu.element); this.element.append(this.button.element, this.menu.element);
this._chosen = emitter(); this._chosen = emitter();
@ -43,11 +43,11 @@ export class DropDown<T> extends Control {
this.just_opened = false; this.just_opened = false;
this.disposables( this.disposables(
disposable_listener(button.element, "mousedown", () => this.button_mousedown(), { disposable_listener(this.button.element, "mousedown", () => this.button_mousedown(), {
capture: true, capture: true,
}), }),
button.mouseup.observe(() => this.button_mouseup()), this.button.mouseup.observe(() => this.button_mouseup()),
this.menu.selected.observe(({ value }) => { this.menu.selected.observe(({ value }) => {
if (value) { if (value) {

View File

@ -11,7 +11,11 @@ export type FileButtonOptions = ControlOptions & {
icon_left?: Icon; icon_left?: Icon;
}; };
export class FileButton extends Control<HTMLElement> { export class FileButton extends Control {
readonly element = create_element("label", {
class: "core_FileButton core_Button",
});
readonly files: Property<File[]>; readonly files: Property<File[]>;
private input: HTMLInputElement = create_element("input", { private input: HTMLInputElement = create_element("input", {
@ -21,12 +25,7 @@ export class FileButton extends Control<HTMLElement> {
private readonly _files: WritableProperty<File[]> = property<File[]>([]); private readonly _files: WritableProperty<File[]> = property<File[]>([]);
constructor(text: string, options?: FileButtonOptions) { constructor(text: string, options?: FileButtonOptions) {
super( super(options);
create_element("label", {
class: "core_FileButton core_Button",
}),
options,
);
this.files = this._files; this.files = this._files;

View File

@ -8,7 +8,9 @@ import { WidgetProperty } from "../observable/property/WidgetProperty";
export type InputOptions = LabelledControlOptions; export type InputOptions = LabelledControlOptions;
export abstract class Input<T> extends LabelledControl<HTMLElement> { export abstract class Input<T> extends LabelledControl {
readonly element: HTMLElement;
readonly value: WritableProperty<T>; readonly value: WritableProperty<T>;
protected readonly input_element: HTMLInputElement; protected readonly input_element: HTMLInputElement;
@ -22,7 +24,9 @@ export abstract class Input<T> extends LabelledControl<HTMLElement> {
input_class_name: string, input_class_name: string,
options?: InputOptions, options?: InputOptions,
) { ) {
super(el.span({ class: `${class_name} core_Input` }), options); super(options);
this.element = el.span({ class: `${class_name} core_Input` });
this._value = new WidgetProperty<T>(this, value, this.set_value); this._value = new WidgetProperty<T>(this, value, this.set_value);
this.value = this._value; this.value = this._value;

View File

@ -5,7 +5,9 @@ import "./Label.css";
import { Property } from "../observable/property/Property"; import { Property } from "../observable/property/Property";
import { WidgetProperty } from "../observable/property/WidgetProperty"; import { WidgetProperty } from "../observable/property/WidgetProperty";
export class Label extends Widget<HTMLLabelElement> { export class Label extends Widget {
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;
} }
@ -15,7 +17,7 @@ export class Label extends Widget<HTMLLabelElement> {
private readonly _text = new WidgetProperty<string>(this, "", this.set_text); private readonly _text = new WidgetProperty<string>(this, "", this.set_text);
constructor(text: string | Property<string>, options?: WidgetOptions) { constructor(text: string | Property<string>, options?: WidgetOptions) {
super(create_element("label", { class: "core_Label" }), options); super(options);
this.text = this._text; this.text = this._text;

View File

@ -8,7 +8,7 @@ export type LabelledControlOptions = WidgetOptions & {
export type LabelPosition = "left" | "right" | "top" | "bottom"; export type LabelPosition = "left" | "right" | "top" | "bottom";
export abstract class LabelledControl<E extends HTMLElement = HTMLElement> extends Control<E> { export abstract class LabelledControl extends Control {
abstract readonly preferred_label_position: LabelPosition; abstract readonly preferred_label_position: LabelPosition;
get label(): Label | undefined { get label(): Label | undefined {
@ -28,8 +28,8 @@ export abstract class LabelledControl<E extends HTMLElement = HTMLElement> exten
private readonly _label_text?: string; private readonly _label_text?: string;
private _label?: Label; private _label?: Label;
protected constructor(element: E, options?: LabelledControlOptions) { protected constructor(options?: LabelledControlOptions) {
super(element, options); super(options);
this._label_text = options && options.label; this._label_text = options && options.label;
} }

View File

@ -4,11 +4,13 @@ import { Resizable } from "./Resizable";
import { ResizableWidget } from "./ResizableWidget"; import { ResizableWidget } from "./ResizableWidget";
export class LazyWidget extends ResizableWidget { export class LazyWidget extends ResizableWidget {
readonly element = el.div({ class: "core_LazyView" });
private initialized = false; private initialized = false;
private view: Widget & Resizable | undefined; private view: Widget & Resizable | undefined;
constructor(private create_view: () => Promise<Widget & Resizable>) { constructor(private create_view: () => Promise<Widget & Resizable>) {
super(el.div({ class: "core_LazyView" })); super();
this.visible.val = false; this.visible.val = false;
} }

View File

@ -7,6 +7,7 @@ import { WidgetProperty } from "../observable/property/WidgetProperty";
import "./Menu.css"; import "./Menu.css";
export class Menu<T> extends Widget { export class Menu<T> extends Widget {
readonly element = el.div({ class: "core_Menu", tab_index: -1 });
readonly selected: WritableProperty<T | undefined>; readonly selected: WritableProperty<T | undefined>;
private readonly to_label: (element: T) => string; private readonly to_label: (element: T) => string;
@ -22,7 +23,7 @@ export class Menu<T> extends Widget {
to_label: (element: T) => string, to_label: (element: T) => string,
related_element: HTMLElement, related_element: HTMLElement,
) { ) {
super(el.div({ class: "core_Menu", tab_index: -1 })); super();
this.visible.val = false; this.visible.val = false;

View File

@ -1,10 +1,12 @@
import { ResizableWidget } from "./ResizableWidget"; import { ResizableWidget } from "./ResizableWidget";
import { create_element } from "./dom"; import { el } from "./dom";
import { Renderer } from "../rendering/Renderer"; import { Renderer } from "../rendering/Renderer";
export class RendererWidget extends ResizableWidget { export class RendererWidget extends ResizableWidget {
readonly element = el.div();
constructor(private renderer: Renderer) { constructor(private renderer: Renderer) {
super(create_element("div")); super();
this.element.append(renderer.dom_element); this.element.append(renderer.dom_element);

View File

@ -1,8 +1,7 @@
import { Widget } from "./Widget"; import { Widget } from "./Widget";
import { Resizable } from "./Resizable"; import { Resizable } from "./Resizable";
export abstract class ResizableWidget<E extends HTMLElement = HTMLElement> extends Widget<E> export abstract class ResizableWidget extends Widget implements Resizable {
implements Resizable {
protected width: number = 0; protected width: number = 0;
protected height: number = 0; protected height: number = 0;

View File

@ -12,6 +12,8 @@ export type SelectOptions<T> = LabelledControlOptions & {
}; };
export class Select<T> extends LabelledControl { export class Select<T> extends LabelledControl {
readonly element = el.div({ class: "core_Select" });
readonly preferred_label_position: LabelPosition; readonly preferred_label_position: LabelPosition;
readonly selected: WritableProperty<T | undefined>; readonly selected: WritableProperty<T | undefined>;
@ -27,19 +29,17 @@ export class Select<T> extends LabelledControl {
to_label: (element: T) => string, to_label: (element: T) => string,
options?: SelectOptions<T>, options?: SelectOptions<T>,
) { ) {
const element = el.div({ class: "core_Select" }); super(options);
const button = new Button(" ", {
icon_right: Icon.TriangleDown,
});
const menu = new Menu<T>(items, to_label, element);
super(element, options);
this.preferred_label_position = "left"; this.preferred_label_position = "left";
this.to_label = to_label; this.to_label = to_label;
this.button = this.disposable(button); this.button = this.disposable(
this.menu = this.disposable(menu); new Button(" ", {
icon_right: Icon.TriangleDown,
}),
);
this.menu = this.disposable(new Menu<T>(items, to_label, this.element));
this.element.append(this.button.element, this.menu.element); this.element.append(this.button.element, this.menu.element);
this._selected = new WidgetProperty<T | undefined>(this, undefined, this.set_selected); this._selected = new WidgetProperty<T | undefined>(this, undefined, this.set_selected);
@ -48,9 +48,9 @@ export class Select<T> extends LabelledControl {
this.just_opened = false; this.just_opened = false;
this.disposables( this.disposables(
disposable_listener(button.element, "mousedown", e => this.button_mousedown(e)), disposable_listener(this.button.element, "mousedown", e => this.button_mousedown(e)),
button.mouseup.observe(() => this.button_mouseup()), this.button.mouseup.observe(() => this.button_mouseup()),
this.menu.selected.observe(({ value }) => this.menu.selected.observe(({ value }) =>
this._selected.set_val(value, { silent: false }), this._selected.set_val(value, { silent: false }),

View File

@ -20,12 +20,14 @@ type TabInfo = Tab & { tab_element: HTMLSpanElement; lazy_view: LazyWidget };
const BAR_HEIGHT = 28; const BAR_HEIGHT = 28;
export class TabContainer extends ResizableWidget { export class TabContainer extends ResizableWidget {
readonly element = el.div({ class: "core_TabContainer" });
private tabs: TabInfo[] = []; private tabs: TabInfo[] = [];
private bar_element = el.div({ class: "core_TabContainer_Bar" }); private bar_element = el.div({ class: "core_TabContainer_Bar" });
private panes_element = el.div({ class: "core_TabContainer_Panes" }); private panes_element = el.div({ class: "core_TabContainer_Panes" });
constructor(options: TabContainerOptions) { constructor(options: TabContainerOptions) {
super(el.div({ class: "core_TabContainer" }), options); super(options);
this.bar_element.onmousedown = this.bar_mousedown; this.bar_element.onmousedown = this.bar_mousedown;

View File

@ -38,7 +38,9 @@ export type TableOptions<T> = WidgetOptions & {
sort?(sort_columns: { column: Column<T>; direction: SortDirection }[]): void; sort?(sort_columns: { column: Column<T>; direction: SortDirection }[]): void;
}; };
export class Table<T> extends Widget<HTMLTableElement> { export class Table<T> extends Widget {
readonly element = el.table({ class: "core_Table" });
private readonly table_disposer = this.disposable(new Disposer()); private readonly table_disposer = this.disposable(new Disposer());
private readonly tbody_element = el.tbody(); private readonly tbody_element = el.tbody();
private readonly footer_row_element?: HTMLTableRowElement; private readonly footer_row_element?: HTMLTableRowElement;
@ -46,7 +48,7 @@ export class Table<T> extends Widget<HTMLTableElement> {
private readonly columns: Column<T>[]; private readonly columns: Column<T>[];
constructor(options: TableOptions<T>) { constructor(options: TableOptions<T>) {
super(el.table({ class: "core_Table" }), options); super(options);
this.values = options.values; this.values = options.values;
this.columns = options.columns; this.columns = options.columns;

View File

@ -12,6 +12,8 @@ export type TextAreaOptions = LabelledControlOptions & {
}; };
export class TextArea extends LabelledControl { export class TextArea extends LabelledControl {
readonly element = el.div({ class: "core_TextArea" });
readonly preferred_label_position = "left"; readonly preferred_label_position = "left";
readonly value: WritableProperty<string>; readonly value: WritableProperty<string>;
@ -23,7 +25,7 @@ export class TextArea extends LabelledControl {
private readonly _value = new WidgetProperty<string>(this, "", this.set_value); private readonly _value = new WidgetProperty<string>(this, "", this.set_value);
constructor(value = "", options?: TextAreaOptions) { constructor(value = "", options?: TextAreaOptions) {
super(el.div({ class: "core_TextArea" }), options); super(options);
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;

View File

@ -8,10 +8,11 @@ export type ToolBarOptions = WidgetOptions & {
}; };
export class ToolBar extends Widget { export class ToolBar extends Widget {
readonly element = create_element("div", { class: "core_ToolBar" });
readonly height = 33; readonly height = 33;
constructor(options?: ToolBarOptions) { constructor(options?: ToolBarOptions) {
super(create_element("div", { class: "core_ToolBar" }), options); super(options);
this.element.style.height = `${this.height}px`; this.element.style.height = `${this.height}px`;

View File

@ -15,8 +15,8 @@ export type WidgetOptions = {
tooltip?: string | Property<string>; tooltip?: string | Property<string>;
}; };
export abstract class Widget<E extends HTMLElement = HTMLElement> implements Disposable { export abstract class Widget implements Disposable {
readonly element: E; abstract readonly element: HTMLElement;
get id(): string { get id(): string {
return this.element.id; return this.element.id;
@ -51,18 +51,13 @@ export abstract class Widget<E extends HTMLElement = HTMLElement> implements Dis
private readonly options: WidgetOptions; private readonly options: WidgetOptions;
private construction_finalized = false; private construction_finalized = false;
protected constructor(element: E, options?: WidgetOptions) { protected constructor(options?: WidgetOptions) {
this.element = element;
this.visible = this._visible; this.visible = this._visible;
this.enabled = this._enabled; this.enabled = this._enabled;
this.tooltip = this._tooltip; this.tooltip = this._tooltip;
this.options = options || {}; this.options = options || {};
if (this.options.class) {
this.element.classList.add(this.options.class);
}
setTimeout(() => { setTimeout(() => {
if (!this.construction_finalized) { if (!this.construction_finalized) {
logger.warn( logger.warn(
@ -87,7 +82,9 @@ export abstract class Widget<E extends HTMLElement = HTMLElement> implements Dis
protected finalize_construction(proto: any): void { protected finalize_construction(proto: any): void {
if (Object.getPrototypeOf(this) !== proto) return; if (Object.getPrototypeOf(this) !== proto) return;
this.construction_finalized = true; if (this.options.class) {
this.element.classList.add(this.options.class);
}
if (typeof this.options.enabled === "boolean") { if (typeof this.options.enabled === "boolean") {
this.enabled.val = this.options.enabled; this.enabled.val = this.options.enabled;
@ -100,6 +97,8 @@ export abstract class Widget<E extends HTMLElement = HTMLElement> implements Dis
} else if (this.options.tooltip) { } else if (this.options.tooltip) {
this.tooltip.bind_to(this.options.tooltip); this.tooltip.bind_to(this.options.tooltip);
} }
this.construction_finalized = true;
} }
protected set_visible(visible: boolean): void { protected set_visible(visible: boolean): void {

View File

@ -25,7 +25,7 @@ class GuiStore implements Disposable {
private readonly hash_disposer = this.tool.observe(({ value: tool }) => { private readonly hash_disposer = this.tool.observe(({ value: tool }) => {
window.location.hash = `#/${gui_tool_to_string(tool)}`; window.location.hash = `#/${gui_tool_to_string(tool)}`;
}); });
private readonly global_keydown_handlers = new Map<string, () => void>(); private readonly global_keydown_handlers = new Map<string, (e: KeyboardEvent) => void>();
constructor() { constructor() {
const tool = window.location.hash.slice(2); const tool = window.location.hash.slice(2);
@ -43,7 +43,7 @@ class GuiStore implements Disposable {
window.removeEventListener("keydown", this.dispatch_global_keydown); window.removeEventListener("keydown", this.dispatch_global_keydown);
} }
on_global_keydown(tool: GuiTool, binding: string, handler: () => void): Disposable { on_global_keydown(tool: GuiTool, binding: string, handler: (e: KeyboardEvent) => void): Disposable {
const key = this.handler_key(tool, binding); const key = this.handler_key(tool, binding);
this.global_keydown_handlers.set(key, handler); this.global_keydown_handlers.set(key, handler);
@ -67,7 +67,7 @@ class GuiStore implements Disposable {
if (handler) { if (handler) {
e.preventDefault(); e.preventDefault();
handler(); handler(e);
} }
}; };

View File

@ -3,9 +3,7 @@ import { ResizableWidget } from "../../core/gui/ResizableWidget";
import "./HelpView.css"; import "./HelpView.css";
export class HelpView extends ResizableWidget { export class HelpView extends ResizableWidget {
constructor() { readonly element = el.div(
super(
el.div(
{ class: "hunt_optimizer_HelpView" }, { class: "hunt_optimizer_HelpView" },
el.p({ el.p({
text: text:
@ -20,9 +18,10 @@ export class HelpView extends ResizableWidget {
text: text:
"The optimal result is calculated using linear optimization. The optimizer takes into account rare enemies and the fact that pan arms can be split in two.", "The optimal result is calculated using linear optimization. The optimizer takes into account rare enemies and the fact that pan arms can be split in two.",
}), }),
),
); );
constructor() {
super();
this.finalize_construction(HelpView.prototype); this.finalize_construction(HelpView.prototype);
} }
} }

View File

@ -16,12 +16,14 @@ import { SortDirection, Table } from "../../core/gui/Table";
import { list_property } from "../../core/observable"; import { list_property } from "../../core/observable";
export class MethodsForEpisodeView extends ResizableWidget { export class MethodsForEpisodeView extends ResizableWidget {
readonly element = el.div({ class: "hunt_optimizer_MethodsForEpisodeView" });
private readonly episode: Episode; private readonly episode: Episode;
private readonly enemy_types: NpcType[]; private readonly enemy_types: NpcType[];
private hunt_methods_observer?: Disposable; private hunt_methods_observer?: Disposable;
constructor(episode: Episode) { constructor(episode: Episode) {
super(el.div({ class: "hunt_optimizer_MethodsForEpisodeView" })); super();
this.episode = episode; this.episode = episode;

View File

@ -11,16 +11,16 @@ import "./OptimizationResultView.css";
import { Duration } from "luxon"; import { Duration } from "luxon";
export class OptimizationResultView extends Widget { export class OptimizationResultView extends Widget {
readonly element = el.div(
{ class: "hunt_optimizer_OptimizationResultView" },
el.h2({ text: "Ideal Combination of Methods" }),
);
private results_observer?: Disposable; private results_observer?: Disposable;
private table?: Table<OptimalMethodModel>; private table?: Table<OptimalMethodModel>;
constructor() { constructor() {
super( super();
el.div(
{ class: "hunt_optimizer_OptimizationResultView" },
el.h2({ text: "Ideal Combination of Methods" }),
),
);
this.disposable( this.disposable(
hunt_optimizer_stores.observe_current( hunt_optimizer_stores.observe_current(

View File

@ -5,8 +5,10 @@ import "./OptimizerView.css";
import { OptimizationResultView } from "./OptimizationResultView"; import { OptimizationResultView } from "./OptimizationResultView";
export class OptimizerView extends ResizableWidget { export class OptimizerView extends ResizableWidget {
readonly element = el.div({ class: "hunt_optimizer_OptimizerView" });
constructor() { constructor() {
super(el.div({ class: "hunt_optimizer_OptimizerView" })); super();
this.element.append( this.element.append(
this.disposable(new WantedItemsView()).element, this.disposable(new WantedItemsView()).element,

View File

@ -15,12 +15,14 @@ import { list_property } from "../../core/observable";
import { ItemType } from "../../core/model/items"; import { ItemType } from "../../core/model/items";
export class WantedItemsView extends Widget { export class WantedItemsView extends Widget {
readonly element = el.div({ class: "hunt_optimizer_WantedItemsView" });
private readonly tbody_element = el.tbody(); private readonly tbody_element = el.tbody();
private readonly table_disposer = this.disposable(new Disposer()); private readonly table_disposer = this.disposable(new Disposer());
private readonly store_disposer = this.disposable(new Disposer()); private readonly store_disposer = this.disposable(new Disposer());
constructor() { constructor() {
super(el.div({ class: "hunt_optimizer_WantedItemsView" })); super();
const huntable_items = list_property<ItemType>(); const huntable_items = list_property<ItemType>();
const filtered_huntable_items = list_property<ItemType>(); const filtered_huntable_items = list_property<ItemType>();

View File

@ -12,26 +12,21 @@ Logger.useDefaults({
defaultLevel: (Logger as any)[process.env["LOG_LEVEL"] || "OFF"], defaultLevel: (Logger as any)[process.env["LOG_LEVEL"] || "OFF"],
}); });
// Disable native undo/redo.
document.addEventListener("keydown", e => {
const kbe = e as KeyboardEvent;
if (kbe.ctrlKey && !kbe.altKey && kbe.key.toUpperCase() === "Z") {
kbe.preventDefault();
}
});
// This doesn't work in FireFox:
document.addEventListener("beforeinput", e => {
const ie = e as any;
if (ie.inputType === "historyUndo" || ie.inputType === "historyRedo") {
e.preventDefault();
}
});
function initialize(): Disposable { function initialize(): Disposable {
// Disable native undo/redo.
document.addEventListener("beforeinput", before_input);
// Work-around for FireFox:
document.addEventListener("keydown", keydown);
// Disable native drag-and-drop.
document.addEventListener("dragenter", dragenter);
document.addEventListener("dragover", dragover);
document.addEventListener("drop", drop);
// Initialize view.
const application_view = new ApplicationView(); const application_view = new ApplicationView();
// Resize the view on window resize.
const resize = throttle( const resize = throttle(
() => { () => {
application_view.resize(window.innerWidth, window.innerHeight); application_view.resize(window.innerWidth, window.innerHeight);
@ -44,12 +39,50 @@ function initialize(): Disposable {
document.body.append(application_view.element); document.body.append(application_view.element);
window.addEventListener("resize", resize); window.addEventListener("resize", resize);
// Dispose view and global event listeners when necessary.
return { return {
dispose(): void { dispose(): void {
window.removeEventListener("beforeinput", before_input);
window.removeEventListener("keydown", keydown);
window.removeEventListener("resize", resize); window.removeEventListener("resize", resize);
window.removeEventListener("dragenter", dragenter);
window.removeEventListener("dragover", dragover);
window.removeEventListener("drop", drop);
application_view.dispose(); application_view.dispose();
}, },
}; };
} }
function before_input(e: Event): void {
const ie = e as any;
if (ie.inputType === "historyUndo" || ie.inputType === "historyRedo") {
e.preventDefault();
}
}
function keydown(e: Event): void {
const kbe = e as KeyboardEvent;
if (kbe.ctrlKey && !kbe.altKey && kbe.key.toUpperCase() === "Z") {
kbe.preventDefault();
}
}
function dragenter(e: DragEvent): void {
e.preventDefault();
if (e.dataTransfer) {
e.dataTransfer.dropEffect = "none";
}
}
function dragover(e: DragEvent): void {
dragenter(e);
}
function drop(e: DragEvent): void {
dragenter(e);
}
initialize(); initialize();

View File

@ -1,6 +1,6 @@
import { ResizableWidget } from "../../core/gui/ResizableWidget"; 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, KeyCode, KeyMod } from "monaco-editor";
import { asm_editor_store } from "../stores/AsmEditorStore"; import { asm_editor_store } from "../stores/AsmEditorStore";
import IStandaloneCodeEditor = editor.IStandaloneCodeEditor; import IStandaloneCodeEditor = editor.IStandaloneCodeEditor;
import { AsmEditorToolBar } from "./AsmEditorToolBar"; import { AsmEditorToolBar } from "./AsmEditorToolBar";
@ -28,11 +28,12 @@ const DUMMY_MODEL = editor.createModel("", "psoasm");
export class AsmEditorView extends ResizableWidget { export class AsmEditorView extends ResizableWidget {
private readonly tool_bar_view = this.disposable(new AsmEditorToolBar()); private readonly tool_bar_view = this.disposable(new AsmEditorToolBar());
readonly element = el.div();
private readonly editor: IStandaloneCodeEditor; private readonly editor: IStandaloneCodeEditor;
constructor() { constructor() {
super(el.div()); super();
this.element.append(this.tool_bar_view.element); this.element.append(this.tool_bar_view.element);
@ -50,6 +51,9 @@ export class AsmEditorView extends ResizableWidget {
}), }),
); );
this.editor.addCommand(KeyMod.CtrlCmd | KeyCode.KEY_Z, () => {});
this.editor.addCommand(KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z, () => {});
this.disposables( this.disposables(
asm_editor_store.did_undo.observe(({ value: source }) => { asm_editor_store.did_undo.observe(({ value: source }) => {
this.editor.trigger(source, "undo", undefined); this.editor.trigger(source, "undo", undefined);

View File

@ -4,10 +4,12 @@ import { Label } from "../../core/gui/Label";
import "./DisabledView.css"; import "./DisabledView.css";
export class DisabledView extends Widget { export class DisabledView extends Widget {
readonly element = el.div({ class: "quest_editor_DisabledView" });
private readonly label: Label; private readonly label: Label;
constructor(text: string) { constructor(text: string) {
super(el.div({ class: "quest_editor_DisabledView" })); super();
this.label = this.disposable(new Label(text, { enabled: false })); this.label = this.disposable(new Label(text, { enabled: false }));

View File

@ -12,6 +12,8 @@ import { Vec3 } from "../../core/data_formats/vector";
import { QuestEntityModel } from "../model/QuestEntityModel"; import { QuestEntityModel } from "../model/QuestEntityModel";
export class EntityInfoView extends ResizableWidget { 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."); private readonly no_entity_view = new DisabledView("No entity selected.");
private readonly table_element = el.table(); private readonly table_element = el.table();
@ -41,7 +43,7 @@ export class EntityInfoView extends ResizableWidget {
private readonly entity_disposer = new Disposer(); private readonly entity_disposer = new Disposer();
constructor() { constructor() {
super(el.div({ class: "quest_editor_EntityInfoView", tab_index: -1 })); super();
const entity = quest_editor_store.selected_entity; const entity = quest_editor_store.selected_entity;
const no_entity = entity.map(e => e == undefined); const no_entity = entity.map(e => e == undefined);

View File

@ -7,12 +7,14 @@ import "./NpcCountsView.css";
import { DisabledView } from "./DisabledView"; import { DisabledView } from "./DisabledView";
export class NpcCountsView extends ResizableWidget { export class NpcCountsView extends ResizableWidget {
readonly element = el.div({ class: "quest_editor_NpcCountsView" });
private readonly table_element = el.table(); private readonly table_element = el.table();
private readonly no_quest_view = new DisabledView("No quest loaded."); private readonly no_quest_view = new DisabledView("No quest loaded.");
constructor() { constructor() {
super(el.div({ class: "quest_editor_NpcCountsView" })); super();
this.element.append(this.table_element, this.no_quest_view.element); this.element.append(this.table_element, this.no_quest_view.element);

View File

@ -1,26 +0,0 @@
.quest_editor_QuesInfoView {
box-sizing: border-box;
padding: 3px;
overflow: auto;
outline: none;
}
.quest_editor_QuesInfoView table {
width: 100%;
}
.quest_editor_QuesInfoView th {
text-align: left;
}
.quest_editor_QuesInfoView .core_TextInput {
width: 100%;
}
.quest_editor_QuesInfoView .core_TextArea {
width: 100%;
}
.quest_editor_QuesInfoView textarea {
width: 100%;
}

View File

@ -11,7 +11,6 @@ import { DropDown } from "../../core/gui/DropDown";
import { Episode } from "../../core/data_formats/parsing/quest/Episode"; import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { area_store } from "../stores/AreaStore"; import { area_store } from "../stores/AreaStore";
import { gui_store, GuiTool } from "../../core/stores/GuiStore"; import { gui_store, GuiTool } from "../../core/stores/GuiStore";
import { asm_editor_store } from "../stores/AsmEditorStore";
export class QuestEditorToolBar extends ToolBar { export class QuestEditorToolBar extends ToolBar {
constructor() { constructor() {
@ -120,17 +119,11 @@ export class QuestEditorToolBar extends ToolBar {
), ),
gui_store.on_global_keydown(GuiTool.QuestEditor, "Ctrl-Z", () => { gui_store.on_global_keydown(GuiTool.QuestEditor, "Ctrl-Z", () => {
// Let Monaco handle its own key bindings.
if (undo_manager.current.val !== asm_editor_store.undo) {
undo_manager.undo(); undo_manager.undo();
}
}), }),
gui_store.on_global_keydown(GuiTool.QuestEditor, "Ctrl-Shift-Z", () => { gui_store.on_global_keydown(GuiTool.QuestEditor, "Ctrl-Shift-Z", () => {
// Let Monaco handle its own key bindings.
if (undo_manager.current.val !== asm_editor_store.undo) {
undo_manager.redo(); undo_manager.redo();
}
}), }),
); );

View File

@ -3,7 +3,7 @@ import { create_element, el } from "../../core/gui/dom";
import { QuestEditorToolBar } from "./QuestEditorToolBar"; import { QuestEditorToolBar } from "./QuestEditorToolBar";
import GoldenLayout, { Container, ContentItem, ItemConfigType } from "golden-layout"; import GoldenLayout, { Container, ContentItem, ItemConfigType } from "golden-layout";
import { quest_editor_ui_persister } from "../persistence/QuestEditorUiPersister"; import { quest_editor_ui_persister } from "../persistence/QuestEditorUiPersister";
import { QuesInfoView } from "./QuesInfoView"; import { QuestInfoView } from "./QuestInfoView";
import "golden-layout/src/css/goldenlayout-base.css"; import "golden-layout/src/css/goldenlayout-base.css";
import "../../core/gui/golden_layout_theme.css"; import "../../core/gui/golden_layout_theme.css";
import { NpcCountsView } from "./NpcCountsView"; import { NpcCountsView } from "./NpcCountsView";
@ -18,7 +18,7 @@ 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 () => ResizableWidget, string>([ const VIEW_TO_NAME = new Map<new () => ResizableWidget, string>([
[QuesInfoView, "quest_info"], [QuestInfoView, "quest_info"],
[NpcCountsView, "npc_counts"], [NpcCountsView, "npc_counts"],
[QuestRendererView, "quest_renderer"], [QuestRendererView, "quest_renderer"],
[AsmEditorView, "asm_editor"], [AsmEditorView, "asm_editor"],
@ -53,7 +53,7 @@ const DEFAULT_LAYOUT_CONTENT: ItemConfigType[] = [
{ {
title: "Info", title: "Info",
type: "component", type: "component",
componentName: VIEW_TO_NAME.get(QuesInfoView), componentName: VIEW_TO_NAME.get(QuestInfoView),
isClosable: false, isClosable: false,
}, },
{ {
@ -94,6 +94,8 @@ const DEFAULT_LAYOUT_CONTENT: ItemConfigType[] = [
]; ];
export class QuestEditorView extends ResizableWidget { export class QuestEditorView extends ResizableWidget {
readonly element = el.div({ class: "quest_editor_QuestEditorView" });
private readonly tool_bar_view = this.disposable(new QuestEditorToolBar()); private readonly tool_bar_view = this.disposable(new QuestEditorToolBar());
private readonly layout_element = create_element("div", { class: "quest_editor_gl_container" }); private readonly layout_element = create_element("div", { class: "quest_editor_gl_container" });
@ -102,7 +104,7 @@ export class QuestEditorView extends ResizableWidget {
private readonly sub_views = new Map<string, ResizableWidget>(); private readonly sub_views = new Map<string, ResizableWidget>();
constructor() { constructor() {
super(el.div({ class: "quest_editor_QuestEditorView" })); super();
this.element.append(this.tool_bar_view.element, this.layout_element); this.element.append(this.tool_bar_view.element, this.layout_element);

View File

@ -0,0 +1,26 @@
.quest_editor_QuestInfoView {
box-sizing: border-box;
padding: 3px;
overflow: auto;
outline: none;
}
.quest_editor_QuestInfoView table {
width: 100%;
}
.quest_editor_QuestInfoView th {
text-align: left;
}
.quest_editor_QuestInfoView .core_TextInput {
width: 100%;
}
.quest_editor_QuestInfoView .core_TextArea {
width: 100%;
}
.quest_editor_QuestInfoView textarea {
width: 100%;
}

View File

@ -6,10 +6,12 @@ import { NumberInput } from "../../core/gui/NumberInput";
import { Disposer } from "../../core/observable/Disposer"; import { Disposer } from "../../core/observable/Disposer";
import { TextInput } from "../../core/gui/TextInput"; import { TextInput } from "../../core/gui/TextInput";
import { TextArea } from "../../core/gui/TextArea"; import { TextArea } from "../../core/gui/TextArea";
import "./QuesInfoView.css"; import "./QuestInfoView.css";
import { DisabledView } from "./DisabledView"; import { DisabledView } from "./DisabledView";
export class QuesInfoView extends ResizableWidget { export class QuestInfoView extends ResizableWidget {
readonly element = el.div({ class: "quest_editor_QuestInfoView", tab_index: -1 });
private readonly table_element = el.table(); private readonly table_element = el.table();
private readonly episode_element: HTMLElement; private readonly episode_element: HTMLElement;
private readonly id_input = this.disposable(new NumberInput()); private readonly id_input = this.disposable(new NumberInput());
@ -40,7 +42,7 @@ export class QuesInfoView extends ResizableWidget {
private readonly quest_disposer = this.disposable(new Disposer()); private readonly quest_disposer = this.disposable(new Disposer());
constructor() { constructor() {
super(el.div({ class: "quest_editor_QuesInfoView", tab_index: -1 })); super();
const quest = quest_editor_store.current_quest; const quest = quest_editor_store.current_quest;
const no_quest = quest.map(q => q == undefined); const no_quest = quest.map(q => q == undefined);
@ -91,6 +93,6 @@ export class QuesInfoView extends ResizableWidget {
}), }),
); );
this.finalize_construction(QuesInfoView.prototype); this.finalize_construction(QuestInfoView.prototype);
} }
} }

View File

@ -6,10 +6,12 @@ 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 ResizableWidget { export class QuestRendererView extends ResizableWidget {
readonly element = el.div({ class: "quest_editor_QuestRendererView", tab_index: -1 });
private renderer_view = this.disposable(new RendererWidget(new QuestRenderer())); private renderer_view = this.disposable(new RendererWidget(new QuestRenderer()));
constructor() { constructor() {
super(el.div({ class: "quest_editor_QuestRendererView", tab_index: -1 })); super();
this.element.append(this.renderer_view.element); this.element.append(this.renderer_view.element);

View File

@ -8,6 +8,8 @@ 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 ResizableWidget { export class TextureView extends ResizableWidget {
readonly element = el.div({ class: "viewer_TextureView" });
private readonly open_file_button = new FileButton("Open file...", { private readonly open_file_button = new FileButton("Open file...", {
icon_left: Icon.File, icon_left: Icon.File,
accept: ".xvm", accept: ".xvm",
@ -18,7 +20,7 @@ export class TextureView extends ResizableWidget {
private readonly renderer_view = this.disposable(new RendererWidget(new TextureRenderer())); private readonly renderer_view = this.disposable(new RendererWidget(new TextureRenderer()));
constructor() { constructor() {
super(el.div({ class: "viewer_TextureView" })); super();
this.element.append(this.tool_bar.element, this.renderer_view.element); this.element.append(this.tool_bar.element, this.renderer_view.element);

View File

@ -4,6 +4,8 @@ import { WritableProperty } from "../../../core/observable/property/WritableProp
import "./Model3DSelectListView.css"; import "./Model3DSelectListView.css";
export class Model3DSelectListView<T extends { name: string }> extends ResizableWidget { export class Model3DSelectListView<T extends { name: string }> extends ResizableWidget {
readonly element = create_element("ul", { class: "viewer_Model3DSelectListView" });
set borders(borders: boolean) { set borders(borders: boolean) {
if (borders) { if (borders) {
this.element.style.borderLeft = "var(--border)"; this.element.style.borderLeft = "var(--border)";
@ -18,7 +20,7 @@ export class Model3DSelectListView<T extends { name: string }> extends Resizable
private selected_element?: HTMLLIElement; private selected_element?: HTMLLIElement;
constructor(private models: T[], private selected: WritableProperty<T | undefined>) { constructor(private models: T[], private selected: WritableProperty<T | undefined>) {
super(create_element("ul", { class: "viewer_Model3DSelectListView" })); super();
this.element.onclick = this.list_click; this.element.onclick = this.list_click;

View File

@ -14,13 +14,15 @@ const MODEL_LIST_WIDTH = 100;
const ANIMATION_LIST_WIDTH = 140; const ANIMATION_LIST_WIDTH = 140;
export class Model3DView extends ResizableWidget { export class Model3DView extends ResizableWidget {
readonly element = el.div({ class: "viewer_Model3DView" });
private tool_bar_view: Model3DToolBar; private tool_bar_view: Model3DToolBar;
private model_list_view: Model3DSelectListView<CharacterClassModel>; private model_list_view: Model3DSelectListView<CharacterClassModel>;
private animation_list_view: Model3DSelectListView<CharacterClassAnimationModel>; private animation_list_view: Model3DSelectListView<CharacterClassAnimationModel>;
private renderer_view: RendererWidget; private renderer_view: RendererWidget;
constructor() { constructor() {
super(el.div({ class: "viewer_Model3DView" })); super();
this.tool_bar_view = this.disposable(new Model3DToolBar()); this.tool_bar_view = this.disposable(new Model3DToolBar());
this.model_list_view = this.disposable( this.model_list_view = this.disposable(
@ -33,13 +35,15 @@ export class Model3DView extends ResizableWidget {
this.animation_list_view.borders = true; this.animation_list_view.borders = true;
const container_element = el.div({ class: "viewer_Model3DView_container" }); this.element.append(
container_element.append( this.tool_bar_view.element,
el.div(
{ class: "viewer_Model3DView_container" },
this.model_list_view.element, this.model_list_view.element,
this.animation_list_view.element, this.animation_list_view.element,
this.renderer_view.element, this.renderer_view.element,
),
); );
this.element.append(this.tool_bar_view.element, container_element);
model_store.current_model.val = model_store.models[5]; model_store.current_model.val = model_store.models[5];