From c8ff72726e1ee56959a454e7b472d16ff193ac60 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Sat, 14 Sep 2019 11:59:50 +0200 Subject: [PATCH] The OptimizationResultView is now completely ported to the new GUI system. --- src/core/gui/Menu.ts | 9 ++- src/core/gui/Table.css | 7 +- src/core/gui/Table.ts | 73 +++++++++++++++++-- src/core/gui/dom.ts | 3 + .../gui/MethodsForEpisodeView.ts | 4 +- .../gui/OptimizationResultView.ts | 66 +++++++++++++---- 6 files changed, 139 insertions(+), 23 deletions(-) diff --git a/src/core/gui/Menu.ts b/src/core/gui/Menu.ts index 49444d7a..a1db270d 100644 --- a/src/core/gui/Menu.ts +++ b/src/core/gui/Menu.ts @@ -61,12 +61,14 @@ export class Menu extends Widget { } hover_next(): void { + this.visible.val = true; this.hover_item( this.hovered_index != undefined ? (this.hovered_index + 1) % this.items.val.length : 0, ); } hover_prev(): void { + this.visible.val = true; this.hover_item(this.hovered_index ? this.hovered_index - 1 : this.items.val.length - 1); } @@ -78,8 +80,11 @@ export class Menu extends Widget { protected set_visible(visible: boolean): void { super.set_visible(visible); - this.hover_item(); - this.inner_element.scrollTo(0, 0); + + if (this.visible.val != visible) { + this.hover_item(); + this.inner_element.scrollTo(0, 0); + } } protected set_selected(): void { diff --git a/src/core/gui/Table.css b/src/core/gui/Table.css index ed8a4698..5e5efb4d 100644 --- a/src/core/gui/Table.css +++ b/src/core/gui/Table.css @@ -52,7 +52,12 @@ white-space: nowrap; } -.core_Table tbody th { +.core_Table tbody th, +.core_Table tfoot th { + text-align: left; +} + +.core_Table th.fixed { position: sticky; text-align: left; } diff --git a/src/core/gui/Table.ts b/src/core/gui/Table.ts index 23cf828c..0f8ff7ad 100644 --- a/src/core/gui/Table.ts +++ b/src/core/gui/Table.ts @@ -13,12 +13,16 @@ const logger = Logger.get("core/gui/Table"); export type Column = { title: string; - sticky?: boolean; + fixed?: boolean; width: number; input?: boolean; text_align?: string; tooltip?: (value: T) => string; render_cell(value: T, disposer: Disposer): string | HTMLElement; + footer?: { + render_cell(): string; + tooltip?(): string; + }; }; export type TableOptions = WidgetOptions & { @@ -29,6 +33,7 @@ export type TableOptions = WidgetOptions & { export class Table extends Widget { private readonly table_disposer = this.disposable(new Disposer()); private readonly tbody_element = el.tbody(); + private readonly footer_row_element?: HTMLTableRowElement; private readonly values: ListProperty; private readonly columns: Column[]; @@ -42,12 +47,13 @@ export class Table extends Widget { const header_tr_element = el.tr(); let left = 0; + let has_footer = false; header_tr_element.append( ...this.columns.map(column => { const th = el.th({}, el.span({ text: column.title })); - if (column.sticky) { + if (column.fixed) { th.style.position = "sticky"; th.style.left = `${left}px`; left += column.width; @@ -55,6 +61,10 @@ export class Table extends Widget { th.style.width = `${column.width}px`; + if (column.footer) { + has_footer = true; + } + return th; }), ); @@ -63,6 +73,12 @@ export class Table extends Widget { this.tbody_element = el.tbody(); this.element.append(thead_element, this.tbody_element); + if (has_footer) { + this.footer_row_element = el.tr(); + this.element.append(el.tfoot({}, this.footer_row_element)); + this.create_footer(); + } + this.disposables(this.values.observe_list(this.update_table)); this.splice_rows(0, this.values.length.val, this.values.val); @@ -71,6 +87,7 @@ export class Table extends Widget { private update_table = (change: ListPropertyChangeEvent): void => { if (change.type === ListChangeType.ListChange) { this.splice_rows(change.index, change.removed.length, change.inserted); + this.update_footer(); } else if (change.type === ListChangeType.ValueChange) { // TODO: update rows } @@ -104,7 +121,7 @@ export class Table extends Widget { return el.tr( {}, ...this.columns.map((column, i) => { - const cell = column.sticky ? el.th() : el.td(); + const cell = column.fixed ? el.th() : el.td(); try { const content = column.render_cell(value, disposer); @@ -113,12 +130,13 @@ export class Table extends Widget { if (column.input) cell.classList.add("input"); - if (column.sticky) { + if (column.fixed) { + cell.classList.add("fixed"); cell.style.left = `${left}px`; left += column.width || 0; } - if (column.width != undefined) cell.style.width = `${column.width}px`; + cell.style.width = `${column.width}px`; if (column.text_align) cell.style.textAlign = column.text_align; @@ -131,4 +149,49 @@ export class Table extends Widget { }), ); }; + + private create_footer(): void { + const footer_cells: HTMLTableHeaderCellElement[] = []; + let left = 0; + + for (let i = 0; i < this.columns.length; i++) { + const column = this.columns[i]; + const cell = el.th(); + + cell.style.width = `${column.width}px`; + + if (column.fixed) { + cell.classList.add("fixed"); + cell.style.left = `${left}px`; + left += column.width || 0; + } + + if (column.footer) { + cell.textContent = column.footer.render_cell(); + cell.title = column.footer.tooltip ? column.footer.tooltip() : ""; + } + + if (column.text_align) cell.style.textAlign = column.text_align; + + footer_cells.push(cell); + } + + this.footer_row_element!.append(...footer_cells); + } + + private update_footer(): void { + if (!this.footer_row_element) return; + + const col_count = this.columns.length; + + for (let i = 0; i < col_count; i++) { + const column = this.columns[i]; + + if (column.footer) { + const cell = this.footer_row_element.children[i] as HTMLTableHeaderCellElement; + cell.textContent = column.footer.render_cell(); + cell.title = column.footer.tooltip ? column.footer.tooltip() : ""; + } + } + } } diff --git a/src/core/gui/dom.ts b/src/core/gui/dom.ts index 208a3138..ce2052e2 100644 --- a/src/core/gui/dom.ts +++ b/src/core/gui/dom.ts @@ -61,6 +61,9 @@ export const el = { tbody: (attributes?: {}, ...children: HTMLElement[]): HTMLTableSectionElement => create_element("tbody", attributes, ...children), + tfoot: (attributes?: {}, ...children: HTMLElement[]): HTMLTableSectionElement => + create_element("tfoot", attributes, ...children), + tr: (attributes?: {}, ...children: HTMLElement[]): HTMLTableRowElement => create_element("tr", attributes, ...children), diff --git a/src/hunt_optimizer/gui/MethodsForEpisodeView.ts b/src/hunt_optimizer/gui/MethodsForEpisodeView.ts index e36e9596..6633d964 100644 --- a/src/hunt_optimizer/gui/MethodsForEpisodeView.ts +++ b/src/hunt_optimizer/gui/MethodsForEpisodeView.ts @@ -36,7 +36,7 @@ export class MethodsForEpisodeView extends ResizableWidget { columns: [ { title: "Method", - sticky: true, + fixed: true, width: 250, render_cell(method: HuntMethodModel) { return method.name; @@ -44,7 +44,7 @@ export class MethodsForEpisodeView extends ResizableWidget { }, { title: "Time", - sticky: true, + fixed: true, width: 60, input: true, render_cell(method: HuntMethodModel, disposer: Disposer) { diff --git a/src/hunt_optimizer/gui/OptimizationResultView.ts b/src/hunt_optimizer/gui/OptimizationResultView.ts index 02304966..a5046728 100644 --- a/src/hunt_optimizer/gui/OptimizationResultView.ts +++ b/src/hunt_optimizer/gui/OptimizationResultView.ts @@ -8,6 +8,7 @@ import { OptimalMethodModel, OptimalResultModel } from "../model"; import { Difficulty } from "../../core/model"; import { Episode } from "../../core/data_formats/parsing/quest/Episode"; import "./OptimizationResultView.css"; +import { Duration } from "luxon"; export class OptimizationResultView extends Widget { private results_observer?: Disposable; @@ -57,18 +58,33 @@ export class OptimizationResultView extends Widget { this.table.dispose(); } + let total_runs = 0; + let total_time = Duration.fromMillis(0); + + if (result) { + for (const method of result.optimal_methods) { + total_runs += method.runs; + total_time = total_time.plus(method.total_time); + } + } + const columns: Column[] = [ { title: "Difficulty", - sticky: true, + fixed: true, width: 80, render_cell(value: OptimalMethodModel) { return Difficulty[value.difficulty]; }, + footer: { + render_cell() { + return "Totals:"; + }, + }, }, { title: "Method", - sticky: true, + fixed: true, width: 250, render_cell(value: OptimalMethodModel) { return value.method_name; @@ -76,7 +92,7 @@ export class OptimizationResultView extends Widget { }, { title: "Ep.", - sticky: true, + fixed: true, width: 40, render_cell(value: OptimalMethodModel) { return Episode[value.method_episode]; @@ -84,7 +100,7 @@ export class OptimizationResultView extends Widget { }, { title: "Section ID", - sticky: true, + fixed: true, width: 90, render_cell(value: OptimalMethodModel) { const element = el.span( @@ -107,45 +123,69 @@ export class OptimizationResultView extends Widget { title: "Runs", width: 60, text_align: "right", + render_cell(value: OptimalMethodModel) { + return value.runs.toFixed(1); + }, tooltip(value: OptimalMethodModel) { return value.runs.toString(); }, - render_cell(value: OptimalMethodModel) { - return value.runs.toFixed(1); + footer: { + render_cell() { + return total_runs.toFixed(1); + }, + tooltip() { + return total_runs.toString(); + }, }, }, { title: "Total Hours", width: 60, text_align: "right", + render_cell(value: OptimalMethodModel) { + return value.total_time.as("hours").toFixed(1); + }, tooltip(value: OptimalMethodModel) { return value.total_time.as("hours").toString(); }, - render_cell(value: OptimalMethodModel) { - return value.total_time.as("hours").toFixed(1); + footer: { + render_cell() { + return total_time.as("hours").toFixed(1); + }, + tooltip() { + return total_time.as("hours").toString(); + }, }, }, ]; if (result) { for (const item of result.wanted_items) { - let totalCount = 0; + let total_count = 0; for (const method of result.optimal_methods) { - totalCount += method.item_counts.get(item) || 0; + total_count += method.item_counts.get(item) || 0; } columns.push({ title: item.name, width: 80, text_align: "right", + render_cell(value: OptimalMethodModel) { + const count = value.item_counts.get(item); + return count ? count.toFixed(2) : ""; + }, tooltip(value: OptimalMethodModel) { const count = value.item_counts.get(item); return count ? count.toString() : ""; }, - render_cell(value: OptimalMethodModel) { - const count = value.item_counts.get(item); - return count ? count.toFixed(2) : ""; + footer: { + render_cell() { + return total_count.toFixed(2); + }, + tooltip() { + return total_count.toString(); + }, }, }); }