diff --git a/src/core/gui/Dialog.ts b/src/core/gui/Dialog.ts index 0436aaa8..215c4094 100644 --- a/src/core/gui/Dialog.ts +++ b/src/core/gui/Dialog.ts @@ -8,29 +8,52 @@ import "./Dialog.css"; const DIALOG_WIDTH = 500; const DIALOG_MAX_HEIGHT = 500; +/** + * A popup window with a title, description, body and dismiss button. + */ export class Dialog extends ResizableWidget { private x = 0; private y = 0; private prev_mouse_x = 0; private prev_mouse_y = 0; private readonly overlay_element: HTMLElement; + private readonly header_element = h1(); + private readonly description_element = div({ className: "core_Dialog_description" }); + private readonly content_element = div({ className: "core_Dialog_body" }); + private readonly dismiss_button = this.disposable(new Button({ text: "Dismiss" })); + private readonly footer_element = div( + { className: "core_Dialog_footer" }, + this.dismiss_button.element, + ); - readonly element: HTMLElement; + readonly element: HTMLElement = section( + { className: "core_Dialog", tabIndex: 0 }, + this.header_element, + this.description_element, + this.content_element, + this.footer_element, + ); readonly children: readonly Widget[] = []; - readonly dismiss_button = this.disposable(new Button({ text: "Dismiss" })); - constructor(title: string, description: string, content: Node | string) { + set title(title: string) { + this.header_element.textContent = title; + } + + set description(description: string) { + this.description_element.textContent = description; + } + + set content(content: Node | string) { + this.content_element.textContent = ""; + this.content_element.append(content); + } + + constructor(title: string = "", description: string = "", content: Node | string = "") { super(); - let header_element: HTMLElement; - - this.element = section( - { className: "core_Dialog", tabIndex: 0 }, - (header_element = h1(title)), - div({ className: "core_Dialog_description" }, description), - div({ className: "core_Dialog_body" }, content), - div({ className: "core_Dialog_footer" }, this.dismiss_button.element), - ); + this.title = title; + this.description = description; + this.content = content; this.element.style.width = `${DIALOG_WIDTH}px`; this.element.style.maxHeight = `${DIALOG_MAX_HEIGHT}px`; @@ -41,13 +64,12 @@ export class Dialog extends ResizableWidget { ); this.element.addEventListener("keydown", this.keydown); - header_element.addEventListener("mousedown", this.mousedown); + this.header_element.addEventListener("mousedown", this.mousedown); this.overlay_element = div({ className: "core_Dialog_modal_overlay", tabIndex: -1 }); this.overlay_element.addEventListener("focus", () => this.element.focus()); - document.body.append(this.overlay_element); - this.disposables(this.dismiss_button.onclick.observe(() => this.dispose())); + this.disposables(this.dismiss_button.onclick.observe(() => this.hide())); this.finalize_construction(); } @@ -57,6 +79,17 @@ export class Dialog extends ResizableWidget { this.overlay_element.remove(); } + show(): void { + document.body.append(this.overlay_element); + document.body.append(this.element); + this.focus(); + } + + hide(): void { + this.overlay_element.remove(); + this.element.remove(); + } + set_position(x: number, y: number): void { this.x = x; this.y = y; @@ -88,34 +121,35 @@ export class Dialog extends ResizableWidget { private keydown = (evt: KeyboardEvent): void => { if (evt.key === "Escape") { - this.dispose(); + this.hide(); } }; } /** - * Shows a dialog window if `result` failed or succeeded with problems. + * Shows the details of a result in a dialog window if the result failed or succeeded with problems. * + * @param dialog * @param result * @param problems_message - Message to show if problems occurred when result is successful. * @param error_message - Message to show if result failed. */ -export function show_result_dialog( +export function show_result_in_dialog( + dialog: Dialog, result: Result, problems_message: string, error_message: string, ): void { - let dialog: Dialog | undefined; + dialog.content = create_result_body(result); if (!result.success) { - dialog = new Dialog("Error", error_message, create_result_body(result)); + dialog.title = "Error"; + dialog.description = error_message; + dialog.show(); } else if (result.problems.length) { - dialog = new Dialog("Problems", problems_message, create_result_body(result)); - } - - if (dialog) { - document.body.append(dialog.element); - dialog.focus(); + dialog.title = "Problems"; + dialog.description = problems_message; + dialog.show(); } } diff --git a/src/viewer/controllers/TextureController.ts b/src/viewer/controllers/TextureController.ts index 9c2cfec8..102c2040 100644 --- a/src/viewer/controllers/TextureController.ts +++ b/src/viewer/controllers/TextureController.ts @@ -12,7 +12,6 @@ import { ListProperty } from "../../core/observable/property/list/ListProperty"; import { prs_decompress } from "../../core/data_formats/compression/prs/decompress"; import { failure, Result, result_builder } from "../../core/Result"; import { Severity } from "../../core/Severity"; -import { show_result_dialog } from "../../core/gui/Dialog"; const logger = LogManager.get("viewer/controllers/TextureController"); @@ -20,7 +19,7 @@ export class TextureController extends Controller { private readonly _textures: WritableListProperty = list_property(); readonly textures: ListProperty = this._textures; - load_file = async (file: File): Promise => { + load_file = async (file: File): Promise> => { let result: Result; try { @@ -75,10 +74,6 @@ export class TextureController extends Controller { result = failure(); } - show_result_dialog( - result, - `Encountered some problems while opening "${file.name}".`, - `Couldn't open "${file.name}".`, - ); + return result; }; } diff --git a/src/viewer/controllers/model/ModelToolBarController.ts b/src/viewer/controllers/model/ModelToolBarController.ts index 571f963e..015ab162 100644 --- a/src/viewer/controllers/model/ModelToolBarController.ts +++ b/src/viewer/controllers/model/ModelToolBarController.ts @@ -12,7 +12,6 @@ import { LogManager } from "../../../core/Logger"; import { prs_decompress } from "../../../core/data_formats/compression/prs/decompress"; import { failure, Result, result_builder, success } from "../../../core/Result"; import { Severity } from "../../../core/Severity"; -import { show_result_dialog } from "../../../core/gui/Dialog"; const logger = LogManager.get("viewer/controllers/model/ModelToolBarController"); @@ -53,7 +52,7 @@ export class ModelToolBarController extends Controller { this.store.set_animation_frame(frame); }; - load_file = async (file: File): Promise => { + load_file = async (file: File): Promise> => { let result: Result; try { @@ -136,10 +135,6 @@ export class ModelToolBarController extends Controller { result = failure(); } - show_result_dialog( - result, - `Encountered some problems while opening "${file.name}".`, - `Couldn't open "${file.name}".`, - ); + return result; }; } diff --git a/src/viewer/gui/TextureView.ts b/src/viewer/gui/TextureView.ts index 0b00f7a8..1a3ad8ea 100644 --- a/src/viewer/gui/TextureView.ts +++ b/src/viewer/gui/TextureView.ts @@ -5,6 +5,7 @@ import { RendererWidget } from "../../core/gui/RendererWidget"; import { TextureRenderer } from "../rendering/TextureRenderer"; import { ResizableView } from "../../core/gui/ResizableView"; import { TextureController } from "../controllers/TextureController"; +import { Dialog, show_result_in_dialog } from "../../core/gui/Dialog"; export class TextureView extends ResizableView { readonly element = div({ className: "viewer_TextureView" }); @@ -26,9 +27,21 @@ export class TextureView extends ResizableView { this.element.append(this.tool_bar.element, this.renderer_view.element); + const dialog = this.disposable(new Dialog()); + this.disposables( - this.open_file_button.files.observe(({ value: files }) => { - if (files.length) ctrl.load_file(files[0]); + this.open_file_button.files.observe(async ({ value: files }) => { + if (files.length) { + const file = files[0]; + const result = await ctrl.load_file(file); + + show_result_in_dialog( + dialog, + result, + `Encountered some problems while opening "${file.name}".`, + `Couldn't open "${file.name}".`, + ); + } }), ); diff --git a/src/viewer/gui/model/ModelToolBarView.ts b/src/viewer/gui/model/ModelToolBarView.ts index 3a9ebcb7..402c7405 100644 --- a/src/viewer/gui/model/ModelToolBarView.ts +++ b/src/viewer/gui/model/ModelToolBarView.ts @@ -7,6 +7,7 @@ import { Label } from "../../../core/gui/Label"; import { Icon } from "../../../core/gui/dom"; import { View } from "../../../core/gui/View"; import { ModelToolBarController } from "../../controllers/model/ModelToolBarController"; +import { Dialog, show_result_in_dialog } from "../../../core/gui/Dialog"; export class ModelToolBarView extends View { private readonly toolbar: ToolBar; @@ -54,10 +55,21 @@ export class ModelToolBarView extends View { ), ); + const dialog = this.disposable(new Dialog()); + // Always-enabled controls. this.disposables( - open_file_button.files.observe(({ value: files }) => { - if (files.length) ctrl.load_file(files[0]); + open_file_button.files.observe(async ({ value: files }) => { + if (files.length) { + const file = files[0]; + const result = await ctrl.load_file(file); + show_result_in_dialog( + dialog, + result, + `Encountered some problems while opening "${file.name}".`, + `Couldn't open "${file.name}".`, + ); + } }), skeleton_checkbox.checked.observe(({ value }) => ctrl.set_show_skeleton(value)),