From 5074ff9f4e3208c67b0188d4b76ccfc4079075c4 Mon Sep 17 00:00:00 2001 From: Jacob Fischer Date: Mon, 15 Apr 2019 17:51:27 -0500 Subject: [PATCH 1/3] Add inspect tab back with rudamentary features --- src/core/ui/index.ts | 2 +- src/core/ui/tree-view/index.ts | 1 + src/core/ui/tree-view/tree-view.scss | 56 +++------ src/core/ui/tree-view/tree-view.ts | 106 +++++++++++++++++- src/viseur/gui/info-pane/tabs/index.ts | 2 + .../tabs/inspect-tab/inspect-tab.hbs | 9 ++ .../tabs/inspect-tab/inspect-tab.scss | 45 ++++++++ .../info-pane/tabs/inspect-tab/inspect-tab.ts | 35 ++++++ .../tabs/inspect-tab/inspect-tree.ts | 21 ++++ 9 files changed, 233 insertions(+), 44 deletions(-) create mode 100644 src/core/ui/tree-view/index.ts create mode 100644 src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.hbs create mode 100644 src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.scss create mode 100644 src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.ts create mode 100644 src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tree.ts diff --git a/src/core/ui/index.ts b/src/core/ui/index.ts index 11b7294..fcb54e4 100644 --- a/src/core/ui/index.ts +++ b/src/core/ui/index.ts @@ -3,6 +3,6 @@ export * from "./context-menu"; export * from "./inputs"; export * from "./modal"; export * from "./tabular"; -// export * from "./tree-view"; export * from "./disableable-element"; export * from "./pretty-polygons"; +export * from "./tree-view"; diff --git a/src/core/ui/tree-view/index.ts b/src/core/ui/tree-view/index.ts new file mode 100644 index 0000000..4514f03 --- /dev/null +++ b/src/core/ui/tree-view/index.ts @@ -0,0 +1 @@ +export * from "./tree-view"; diff --git a/src/core/ui/tree-view/tree-view.scss b/src/core/ui/tree-view/tree-view.scss index c77033f..dbf8e1b 100644 --- a/src/core/ui/tree-view/tree-view.scss +++ b/src/core/ui/tree-view/tree-view.scss @@ -3,52 +3,28 @@ @import "src/core/fonts"; @import "src/core/colors"; -li.tree-view-node { - & > .node-children { - &.hidden { - display: none; - } - - &:empty { - &::after { - content: "Empty"; - margin-left: 1.5em; - color: $dark-gray; - font-style: italic; - } - } +.tree-view { + list-style-type: none; + margin-left: 0; - & > li { - margin-left: 1em; - } + & ul { + list-style-type: none; + margin-left: 1em; } - & > header { - & .node-key::after { - content: " = "; - } + & li.tree-view-node > header { + color: $black; + font-size: 1em; + font-weight: normal; + @extend .font-monospace; - &:before { - @include icon(square); - font-size: 0.75em; + @include dark-mode { + color: $white; } - &.expandable { - cursor: pointer; - - &:before { - @include icon(plus-square); - font-size: 0.75em; - } - } - - &.expanded { - cursor: pointer; - - &:before { - @include icon(minus-square); - font-size: 0.75em; - } + .node-key:after { + color: $dark-gray; + content: " ="; } } } diff --git a/src/core/ui/tree-view/tree-view.ts b/src/core/ui/tree-view/tree-view.ts index c67294f..327a288 100644 --- a/src/core/ui/tree-view/tree-view.ts +++ b/src/core/ui/tree-view/tree-view.ts @@ -1,9 +1,48 @@ +import { escape } from "lodash"; +import { partial } from "src/core/partial"; +import { isObject } from "src/utils"; import { BaseElement, IBaseElementArgs } from "../base-element"; +import * as treeViewNodeHbs from "./tree-view-node.hbs"; import * as treeViewHbs from "./tree-view.hbs"; -import "./treeView.scss"; +import "./tree-view.scss"; + +/** Primitive types tree view can display */ +type TreeablePrimitives = string | number | boolean | null | undefined; + +/** types a tree view can display */ +export type Treeable = TreeablePrimitives | ITreeableObject; + +/** Treeable key value object */ +interface ITreeableObject { + [key: string]: Treeable | undefined; +} + +/** A node used t display a key/value pair */ +export interface ITreeViewNode { + /** main li element */ + $element: JQuery; + + /** The header for the element */ + $header: JQuery; + + /** The key element container */ + $key: JQuery; + + /** The value element container */ + $value: JQuery; + + /** The container for children */ + $children: JQuery; +} /** a multi-level tree of expandable lists */ export class TreeView extends BaseElement { + /** The object we are currently displaying. */ + private displaying: Treeable = {}; + + // /** cache of items to re-use */ + // private itemCache = new InsureMap(); + /** * Creates a new TreeView component. * @@ -18,7 +57,68 @@ export class TreeView extends BaseElement { * * @param tree - The object to display. */ - public display(tree: object): void { - // TODO: do + public display(tree: ITreeableObject): void { + this.displaying = tree; + + this.element.empty(); + this.deepDisplay("root", this.displaying, this.element); + } + + /** + * Gets the string to display as the value for a given node. + * + * @param node - the node's JQuery elements + * @param value - The node to get the value for. + * @returns Whatever string to display. + */ + protected formatNodeValue(node: ITreeViewNode, value: Treeable): void { + let formatted = String(value); + if (Array.isArray(value)) { + formatted = `Array[${value.length}]`; + } + else if (typeof value === "object") { + formatted = "Object"; + } + + node.$value.html(escape(formatted)); // tslint:disable-line:no-inner-html - safe with lodash escape + } + + /** + * Displays some tree objects first keys in a parent. + * + * @param levelKey - The key level of the display + * @param tree - Treeable object to display + * @param $parent - The parent element for these keys. + */ + private deepDisplay(levelKey: string, tree: ITreeableObject, $parent: JQuery): void { + for (const key of Object.keys(tree).sort()) { + const fullKey = `${levelKey}.${key}`; + const value = tree[key]; + const $element = partial(treeViewNodeHbs, { key }, $parent); + const node = { + $element, + $header: $element.find("header"), + $key: $element.find(".node-key"), + $value: $element.find(".node-value"), + $children: $element.find(".node-children"), + }; + + this.formatNodeValue(node, value); + node.$element.appendTo($parent); + node.$element.attr("data-inspect-key", fullKey); + + if (isObject(value)) { + const onClick = () => { + this.deepDisplay(fullKey, value, node.$children); + + node.$header.one("click", () => { + node.$children.empty(); + node.$header.one("click", onClick); + }); + }; + + node.$header.one("click", onClick); + } + } } } diff --git a/src/viseur/gui/info-pane/tabs/index.ts b/src/viseur/gui/info-pane/tabs/index.ts index ffdf46b..bdecd6a 100644 --- a/src/viseur/gui/info-pane/tabs/index.ts +++ b/src/viseur/gui/info-pane/tabs/index.ts @@ -2,11 +2,13 @@ import { Immutable } from "@cadre/ts-utils"; import { Tab } from "src/core/ui/tabular"; import { FileTab } from "./file-tab/file-tab"; import { HelpTab } from "./help-tab/help-tab"; +import { InspectTab } from "./inspect-tab/inspect-tab"; import { SettingsTab } from "./settings-tab/settings-tab"; /** these are all the tabs for the InfoPane, in order */ export const TABS: Immutable> = [ FileTab, + InspectTab, SettingsTab, HelpTab, ]; diff --git a/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.hbs b/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.hbs new file mode 100644 index 0000000..275b4ed --- /dev/null +++ b/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.hbs @@ -0,0 +1,9 @@ +
+
+ Load a gamelog to inspect it. +
+
+
Inspect Gamelog
+
+
+
diff --git a/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.scss b/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.scss new file mode 100644 index 0000000..075b174 --- /dev/null +++ b/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.scss @@ -0,0 +1,45 @@ +:not(.gamelog-loaded) .inspect-tab { + & .inspect-no-gamelog { + display: block; + } + + & .inspect-gamelog-loaded { + display: none; + } +} + +.gamelog-loaded .inspect-tab { + & .inspect-no-gamelog { + display: none; + } + + & .inspect-gamelog-loaded { + display: block; + } +} + +.inspect-tab li.tree-view-node { + &[data-inspect-type="array"] > header > .node-value { + color: orange; + } + + &[data-inspect-type="object"] > header > .node-value { + color: teal; + } + + &[data-inspect-type="string"] > header > .node-value { + color: green; + + &:before, &:after { + content: "\""; + } + } + + &[data-inspect-type="number"] > header > .node-value { + color: deeppink; + } + + &[data-inspect-type="boolean"] > header > .node-value { + color: darkblue; + } +} diff --git a/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.ts b/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.ts new file mode 100644 index 0000000..a58b7d0 --- /dev/null +++ b/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.ts @@ -0,0 +1,35 @@ +import { ITabArgs, Tab } from "src/core/ui"; +import * as inspectTabHbs from "./inspect-tab.hbs"; +import "./inspect-tab.scss"; +import { InspectTreeView } from "./inspect-tree"; + +/** + * The "Inspect" tab on the InfoPane + */ +export class InspectTab extends Tab { + /** + * Creates the Inspect Tab. + * + * @param args - The arguments to create the tab. + */ + constructor(args: ITabArgs) { + super({ + contentTemplate: inspectTabHbs, + title: "Inspect", + ...args, + }); + + const treeView = new InspectTreeView({ + parent: this.element.find(".inspect-tree-root"), + }); + + args.viseur.events.stateChanged.on(({ game }) => { + treeView.display({ + game: game as {}, // TODO: something sane + settings: args.viseur.rawGamelog + ? args.viseur.rawGamelog.settings + : null, + }); + }); + } +} diff --git a/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tree.ts b/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tree.ts new file mode 100644 index 0000000..f8d5ddd --- /dev/null +++ b/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tree.ts @@ -0,0 +1,21 @@ +import { ITreeViewNode, Treeable, TreeView } from "src/core/ui/tree-view"; + +/** A tree view for inspecting game states */ +export class InspectTreeView extends TreeView { + /** + * Gets the string to display as the value for a given node. + * + * @param node - the node's JQuery elements + * @param value - The node to get the value for. + * @returns Whatever string to display. + */ + protected formatNodeValue(node: ITreeViewNode, value: Treeable): void { + const type = Array.isArray(value) + ? "array" + : typeof value; + + node.$element.attr("data-inspect-type", type); + + super.formatNodeValue(node, value); + } +} From 75e1ddc1c6d766a73f59f7e470b3e5ef0a1bac80 Mon Sep 17 00:00:00 2001 From: Jacob Fischer Date: Mon, 15 Apr 2019 19:16:40 -0500 Subject: [PATCH 2/3] Inspect tab: better formatting and displays --- src/core/ui/tree-view/tree-view.scss | 8 +++ src/core/ui/tree-view/tree-view.ts | 5 +- .../tabs/inspect-tab/inspect-tab.scss | 13 +++- .../info-pane/tabs/inspect-tab/inspect-tab.ts | 34 ++++++--- .../tabs/inspect-tab/inspect-tree.ts | 69 +++++++++++++++++-- 5 files changed, 113 insertions(+), 16 deletions(-) diff --git a/src/core/ui/tree-view/tree-view.scss b/src/core/ui/tree-view/tree-view.scss index dbf8e1b..342b68a 100644 --- a/src/core/ui/tree-view/tree-view.scss +++ b/src/core/ui/tree-view/tree-view.scss @@ -26,5 +26,13 @@ color: $dark-gray; content: " ="; } + + &.inspect-expandable { + cursor: zoom-in; + + &.expanded { + cursor: zoom-out; + } + } } } diff --git a/src/core/ui/tree-view/tree-view.ts b/src/core/ui/tree-view/tree-view.ts index 327a288..23a1404 100644 --- a/src/core/ui/tree-view/tree-view.ts +++ b/src/core/ui/tree-view/tree-view.ts @@ -76,7 +76,7 @@ export class TreeView extends BaseElement { if (Array.isArray(value)) { formatted = `Array[${value.length}]`; } - else if (typeof value === "object") { + else if (isObject(value)) { formatted = "Object"; } @@ -108,12 +108,15 @@ export class TreeView extends BaseElement { node.$element.attr("data-inspect-key", fullKey); if (isObject(value)) { + node.$header.addClass("inspect-expandable"); const onClick = () => { this.deepDisplay(fullKey, value, node.$children); + node.$header.addClass("expanded"); node.$header.one("click", () => { node.$children.empty(); node.$header.one("click", onClick); + node.$header.removeClass("expanded"); }); }; diff --git a/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.scss b/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.scss index 075b174..780727a 100644 --- a/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.scss +++ b/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.scss @@ -23,8 +23,8 @@ color: orange; } - &[data-inspect-type="object"] > header > .node-value { - color: teal; + &[data-inspect-type="map"] > header > .node-value { + color: red; } &[data-inspect-type="string"] > header > .node-value { @@ -42,4 +42,13 @@ &[data-inspect-type="boolean"] > header > .node-value { color: darkblue; } + + &[data-inspect-type="null"] > header > .node-value { + color: blueviolet; + font-style: italic; + } + + &[data-inspect-type="game-object"] > header > .node-value { + color: teal; + } } diff --git a/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.ts b/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.ts index a58b7d0..aade471 100644 --- a/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.ts +++ b/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.ts @@ -1,4 +1,5 @@ import { ITabArgs, Tab } from "src/core/ui"; +import { IViseurGameState } from "src/viseur/game"; import * as inspectTabHbs from "./inspect-tab.hbs"; import "./inspect-tab.scss"; import { InspectTreeView } from "./inspect-tree"; @@ -7,6 +8,12 @@ import { InspectTreeView } from "./inspect-tree"; * The "Inspect" tab on the InfoPane */ export class InspectTab extends Tab { + /** Our treeview we basically are. */ + private readonly treeView: InspectTreeView; + + /** The settings to display, they never change once set. */ + private settings = {}; + /** * Creates the Inspect Tab. * @@ -19,17 +26,28 @@ export class InspectTab extends Tab { ...args, }); - const treeView = new InspectTreeView({ + this.treeView = new InspectTreeView({ parent: this.element.find(".inspect-tree-root"), }); - args.viseur.events.stateChanged.on(({ game }) => { - treeView.display({ - game: game as {}, // TODO: something sane - settings: args.viseur.rawGamelog - ? args.viseur.rawGamelog.settings - : null, - }); + args.viseur.events.ready.once(({ gamelog }) => { + this.settings = gamelog.settings; + this.treeView.setGameName(gamelog.gameName); + this.refreshTree(args.viseur.getCurrentState()); + + args.viseur.events.stateChanged.on((state) => this.refreshTree(state)); + }); + } + + /** + * Refreshes the tree to display a new state. + * + * @param state - The new game states to use to re-build the tree. + */ + private refreshTree(state: IViseurGameState): void { + this.treeView.display({ + game: state.game as {}, // TODO: something sane + settings: this.settings, }); } } diff --git a/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tree.ts b/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tree.ts index f8d5ddd..874a7e7 100644 --- a/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tree.ts +++ b/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tree.ts @@ -1,7 +1,34 @@ -import { ITreeViewNode, Treeable, TreeView } from "src/core/ui/tree-view"; +import { IBaseGame, IBaseGameObject, IBasePlayer } from "@cadre/ts-utils/cadre"; +import { ITreeViewNode, Treeable, TreeView } from "src/core/ui"; +import { isObject } from "src/utils"; +import { IBaseTile } from "src/viseur/game"; + +function isGameObject(val: unknown): val is IBaseGameObject { + return isObject(val) + && typeof val.id === "string" + && typeof val.gameObjectName === "string"; +} + +function isGame(val: unknown): val is IBaseGame { + return isObject(val) + && isObject(val.gameObjects) + && Array.isArray(val.players); +} /** A tree view for inspecting game states */ export class InspectTreeView extends TreeView { + /** The name of the game we are inspecting */ + private gameName = "???"; + + /** + * Sets the game name for the tree. + * + * @param gameName - The name of the game to use. + */ + public setGameName(gameName: string): void { + this.gameName = gameName; + } + /** * Gets the string to display as the value for a given node. * @@ -10,12 +37,44 @@ export class InspectTreeView extends TreeView { * @returns Whatever string to display. */ protected formatNodeValue(node: ITreeViewNode, value: Treeable): void { - const type = Array.isArray(value) - ? "array" - : typeof value; + let type = "???"; + let displayValue = value; + if (Array.isArray(value)) { + type = "array"; + } + else if (value === null) { + type = "null"; + } + else if (isGameObject(value)) { + type = "game-object"; + + const gameObject = value as unknown as IBaseGameObject; // sketchy, but above check should it valid... + switch (gameObject.gameObjectName) { + case "Player": + displayValue = `Player "${(gameObject as IBasePlayer).name}"`; + break; + case "Tile": + displayValue = `Tile (${(gameObject as IBaseTile).x}, ${(gameObject as IBaseTile).y})`; + break; + default: + displayValue = gameObject.gameObjectName; + } + displayValue += ` #${gameObject.id}`; + } + else if (isGame(value)) { + type = "game-object"; + displayValue = `${this.gameName} Game`; + } + else if (isObject(value)) { + type = "map"; + displayValue = `Map[${Object.keys(value).length}]`; + } + else { + type = typeof value; + } node.$element.attr("data-inspect-type", type); - super.formatNodeValue(node, value); + super.formatNodeValue(node, displayValue); } } From 6b3cd989eabd396f6c17c36344ed7482b7e6d98d Mon Sep 17 00:00:00 2001 From: Jacob Fischer Date: Thu, 18 Apr 2019 22:12:14 -0500 Subject: [PATCH 3/3] Update inspect trees to support doing back in history and empty objects --- src/core/ui/tabular/tabular.ts | 9 + src/core/ui/tree-view/tree-view-node.hbs | 1 + src/core/ui/tree-view/tree-view.scss | 34 ++- src/core/ui/tree-view/tree-view.ts | 222 ++++++++++++++---- .../tabs/inspect-tab/inspect-tab.scss | 48 +++- .../info-pane/tabs/inspect-tab/inspect-tab.ts | 42 ++-- .../tabs/inspect-tab/inspect-tree.ts | 17 +- 7 files changed, 290 insertions(+), 83 deletions(-) diff --git a/src/core/ui/tabular/tabular.ts b/src/core/ui/tabular/tabular.ts index 78d4848..03531e1 100644 --- a/src/core/ui/tabular/tabular.ts +++ b/src/core/ui/tabular/tabular.ts @@ -116,6 +116,15 @@ export class Tabular extends BaseElement { } } + /** + * Gets the currently active tab. + * + * @returns The currently active tab. + */ + public getActiveTab(): Tab { + return this.activeTab; + } + /** * Fades a tab out, invoked when switching tabs. * diff --git a/src/core/ui/tree-view/tree-view-node.hbs b/src/core/ui/tree-view/tree-view-node.hbs index 9e53155..836739e 100644 --- a/src/core/ui/tree-view/tree-view-node.hbs +++ b/src/core/ui/tree-view/tree-view-node.hbs @@ -1,6 +1,7 @@
  • {{key}} + = {{value}}
      diff --git a/src/core/ui/tree-view/tree-view.scss b/src/core/ui/tree-view/tree-view.scss index 342b68a..bb1b372 100644 --- a/src/core/ui/tree-view/tree-view.scss +++ b/src/core/ui/tree-view/tree-view.scss @@ -10,6 +10,7 @@ & ul { list-style-type: none; margin-left: 1em; + margin-bottom: 0.25em; } & li.tree-view-node > header { @@ -22,16 +23,45 @@ color: $white; } - .node-key:after { + &:before { + @include icon(square); + color: $dark-gray; + } + + & .node-key-value-spacer { color: $dark-gray; - content: " ="; } &.inspect-expandable { cursor: zoom-in; + &:before { + @include icon(plus-square); + } + &.expanded { cursor: zoom-out; + + &:before { + @include icon(minus-square); + } + + & + ul:empty { + &:before, &:after { + color: $gray; + @extend .font-monospace; + } + + &:before { + @include icon(square-o); + } + + &:after { + content: "Empty"; + font-style: italic; + margin-left: 0.5em; + } + } } } } diff --git a/src/core/ui/tree-view/tree-view.ts b/src/core/ui/tree-view/tree-view.ts index 23a1404..73a6e26 100644 --- a/src/core/ui/tree-view/tree-view.ts +++ b/src/core/ui/tree-view/tree-view.ts @@ -17,11 +17,23 @@ interface ITreeableObject { [key: string]: Treeable | undefined; } -/** A node used t display a key/value pair */ -export interface ITreeViewNode { - /** main li element */ +/** The base empty node */ +interface IBaseNode { + /** Parent node. */ + parent?: ITreeViewNode; + + /** the main element */ $element: JQuery; + /** Our node's key */ + key: string | number; + + /** A flag used to determine if this should be popped */ + flag: boolean; +} + +/** A node used t display a key/value pair */ +export interface ITreeViewNode extends IBaseNode { /** The header for the element */ $header: JQuery; @@ -31,25 +43,42 @@ export interface ITreeViewNode { /** The value element container */ $value: JQuery; + /** A flag to indicate if this node is/was expanded */ + expanded: boolean; + /** The container for children */ $children: JQuery; + + /** Child nodes of this node */ + children: { [key: string]: undefined | ITreeViewNode }; } /** a multi-level tree of expandable lists */ export class TreeView extends BaseElement { - /** The object we are currently displaying. */ - private displaying: Treeable = {}; + /** The name of this treeview */ + public readonly name: string; + + /** The root node for this tree. */ + private readonly rootNode: ITreeViewNode; - // /** cache of items to re-use */ - // private itemCache = new InsureMap(); + /** Flag we flip to determine which nodes were not used */ + private currentUnusedFlag = true; /** * Creates a new TreeView component. * * @param args - The base input args. */ - constructor(args: IBaseElementArgs) { + constructor(args: IBaseElementArgs & { + /** The name of the root */ + name: string; + }) { super(args, treeViewHbs); + + this.name = args.name; + this.rootNode = this.createNewNode(args.name || "root"); + this.rootNode.$children.first().appendTo(this.element); + this.rootNode.expanded = true; } /** @@ -58,10 +87,10 @@ export class TreeView extends BaseElement { * @param tree - The object to display. */ public display(tree: ITreeableObject): void { - this.displaying = tree; + this.currentUnusedFlag = !this.currentUnusedFlag; // invert so all nodes have the opposite value - this.element.empty(); - this.deepDisplay("root", this.displaying, this.element); + this.deepDisplay(this.rootNode.key, tree, this.rootNode); + this.pruneUnusedNodes(this.rootNode); } /** @@ -84,43 +113,144 @@ export class TreeView extends BaseElement { } /** - * Displays some tree objects first keys in a parent. + * Creates a new node and inserts it as necessary. + * + * @param key - The key of this new node. + * @param parent - the parent node. If none then this is assume to be the root node. + * @returns The newly created node, inserted as necessary. + */ + protected createNewNode(key: string | number, parent?: ITreeViewNode): ITreeViewNode { + const $parent = parent + ? parent.$children + : this.element; + const $element = partial(treeViewNodeHbs, { key }, $parent); + + const node = { + $element, + $header: $element.find("header"), + $key: $element.find(".node-key"), + $value: $element.find(".node-value"), + $children: $element.find(".node-children"), + children: {}, + expanded: false, + key, + flag: this.currentUnusedFlag, + parent, + }; + + if (parent) { + parent.children[key] = node; + } + + return node; + } + + /** + * Gets the keys for a given object. + * + * @param tree - The tree to get keys for + * @returns An array of the keys, in order to display + */ + protected getKeysFor(tree: T): Array { + return Object.keys(tree).sort((a, b) => { + const aNum = Number(a); + const bNum = Number(b); + + if (!isNaN(aNum) && !isNaN(bNum)) { + return aNum - bNum; + } + + if (a === b) { + return 0; + } + else { + return a < b ? -1 : 1; + } + }); + } + + /** + * Deeply displays a given node + * @param key - The key of the treeable from it's parent Treeable + * @param treeable - The Treeable to display, not an object with they given key set + * @param parentNode - The parent node of the Treeable, if a child node with the given key is not found, this will + * have a new child added to it. + */ + private deepDisplay(key: string | number, treeable: Treeable, parentNode: ITreeViewNode): void { + const node = parentNode.children[key] || this.createNewNode(key, parentNode); + node.flag = this.currentUnusedFlag; + + if (node.children !== undefined) { + this.formatNodeValue(node, treeable); + + if (isObject(treeable)) { + node.$header + .addClass("inspect-expandable") + .off("click") + .on("click", () => this.onNodeClicked(node, treeable)); + + if (node.expanded) { + const keys = this.getKeysFor(treeable); + for (const childKey of keys) { + const childValue = treeable[childKey]; + + this.deepDisplay(childKey, childValue, node); + } + } + } + } + + // TODO: + // If this is NOT the current flag, then it was previously displayed + // so, deep display it, updating its flag + // also if this HAD children, then it was expanded, + // so make the above expandable already expanded + } + + /** + * Invoked when a node's header is clicked to expand/retract + * @param node - The node clicked + * @param tree - The value of that given node + */ + private onNodeClicked(node: ITreeViewNode, tree: Treeable): void { + node.expanded = !node.expanded; + node.$header.toggleClass("expanded", node.expanded); + + if (node.expanded && isObject(tree)) { + const keys = this.getKeysFor(tree); + for (const key of keys) { + this.deepDisplay(key, tree[key], node); + } + } + else { + node.$children.empty(); + node.children = {}; + } + } + + /** + * Prunes this node, removing it from the dom, if it is no longer displayed. * - * @param levelKey - The key level of the display - * @param tree - Treeable object to display - * @param $parent - The parent element for these keys. + * @param node - The node to recursively pruned. + * @returns True if removed, false otherwise */ - private deepDisplay(levelKey: string, tree: ITreeableObject, $parent: JQuery): void { - for (const key of Object.keys(tree).sort()) { - const fullKey = `${levelKey}.${key}`; - const value = tree[key]; - const $element = partial(treeViewNodeHbs, { key }, $parent); - const node = { - $element, - $header: $element.find("header"), - $key: $element.find(".node-key"), - $value: $element.find(".node-value"), - $children: $element.find(".node-children"), - }; - - this.formatNodeValue(node, value); - node.$element.appendTo($parent); - node.$element.attr("data-inspect-key", fullKey); - - if (isObject(value)) { - node.$header.addClass("inspect-expandable"); - const onClick = () => { - this.deepDisplay(fullKey, value, node.$children); - node.$header.addClass("expanded"); - - node.$header.one("click", () => { - node.$children.empty(); - node.$header.one("click", onClick); - node.$header.removeClass("expanded"); - }); - }; - - node.$header.one("click", onClick); + private pruneUnusedNodes(node: ITreeViewNode): void { + if (node.flag !== this.currentUnusedFlag) { + node.$element.detach(); + if (node.parent) { + delete node.parent.children[node.key]; + } + } + + if (node.children) { + const keys = Object.keys(node.children); + for (const key of keys) { + const child = node.children[key]; + if (!child) { + throw new Error(`tree view ${key} should never have undefined children!`); + } + + this.pruneUnusedNodes(child); } } } diff --git a/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.scss b/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.scss index 780727a..dbaaf46 100644 --- a/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.scss +++ b/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.scss @@ -1,3 +1,15 @@ +@mixin inspect-value($type) { + &[data-inspect-type="#{$type}"] > header > .node-value { + @content; + } +} + +@mixin inspect-key($type) { + &[data-inspect-type="#{$type}"] > ul > li > header > .node-key { + @content; + } +} + :not(.gamelog-loaded) .inspect-tab { & .inspect-no-gamelog { display: block; @@ -18,16 +30,28 @@ } } +.inspect-tree-root > ul > ul { + margin-left: 0; +} + .inspect-tab li.tree-view-node { - &[data-inspect-type="array"] > header > .node-value { + @include inspect-value("array") { color: orange; } - &[data-inspect-type="map"] > header > .node-value { + @include inspect-key("array") { + color: deeppink; + } + + @include inspect-value("number") { + color: deeppink; + } + + @include inspect-value("map") { color: red; } - &[data-inspect-type="string"] > header > .node-value { + @include inspect-key("map") { color: green; &:before, &:after { @@ -35,20 +59,28 @@ } } - &[data-inspect-type="number"] > header > .node-value { - color: deeppink; + @include inspect-value("string") { + color: green; + + &:before, &:after { + content: "\""; + } } - &[data-inspect-type="boolean"] > header > .node-value { + @include inspect-value("boolean") { color: darkblue; } - &[data-inspect-type="null"] > header > .node-value { + @include inspect-value("null") { color: blueviolet; font-style: italic; } - &[data-inspect-type="game-object"] > header > .node-value { + @include inspect-value("game-object") { color: teal; } + + @include inspect-value("root") { + color: dodgerblue; + } } diff --git a/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.ts b/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.ts index aade471..7114b51 100644 --- a/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.ts +++ b/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tab.ts @@ -1,4 +1,5 @@ import { ITabArgs, Tab } from "src/core/ui"; +import { Viseur } from "src/viseur"; import { IViseurGameState } from "src/viseur/game"; import * as inspectTabHbs from "./inspect-tab.hbs"; import "./inspect-tab.scss"; @@ -8,11 +9,14 @@ import { InspectTreeView } from "./inspect-tree"; * The "Inspect" tab on the InfoPane */ export class InspectTab extends Tab { - /** Our treeview we basically are. */ - private readonly treeView: InspectTreeView; + /** Main treeview for the game. */ + private readonly gameTreeView: InspectTreeView; - /** The settings to display, they never change once set. */ - private settings = {}; + /** Three view for the settings. Never updated once set */ + private readonly settingsTreeView: InspectTreeView; + + /** The viseur instance. */ + private readonly viseur: Viseur; /** * Creates the Inspect Tab. @@ -26,17 +30,22 @@ export class InspectTab extends Tab { ...args, }); - this.treeView = new InspectTreeView({ - parent: this.element.find(".inspect-tree-root"), - }); + const parent = this.element.find(".inspect-tree-root"); + this.gameTreeView = new InspectTreeView({ parent, name: "game" }); + this.settingsTreeView = new InspectTreeView({ parent, name: "settings" }); + + this.viseur = args.viseur; + this.viseur.events.ready.once(({ gamelog }) => { + this.settingsTreeView.setGameName(gamelog.gameName); + this.settingsTreeView.display(gamelog.settings); - args.viseur.events.ready.once(({ gamelog }) => { - this.settings = gamelog.settings; - this.treeView.setGameName(gamelog.gameName); - this.refreshTree(args.viseur.getCurrentState()); + this.gameTreeView.setGameName(gamelog.gameName); + this.refreshTree(this.viseur.getCurrentState()); - args.viseur.events.stateChanged.on((state) => this.refreshTree(state)); + this.viseur.events.stateChanged.on((state) => this.refreshTree(state)); }); + + this.tabular.events.tabChanged.on(() => this.refreshTree(this.viseur.getCurrentState())); } /** @@ -45,9 +54,10 @@ export class InspectTab extends Tab { * @param state - The new game states to use to re-build the tree. */ private refreshTree(state: IViseurGameState): void { - this.treeView.display({ - game: state.game as {}, // TODO: something sane - settings: this.settings, - }); + if (this.tabular.getActiveTab() !== this) { + return; + } + + this.gameTreeView.display(state.game as {}); // TODO: sketchy cast } } diff --git a/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tree.ts b/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tree.ts index 874a7e7..183a579 100644 --- a/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tree.ts +++ b/src/viseur/gui/info-pane/tabs/inspect-tab/inspect-tree.ts @@ -1,4 +1,5 @@ -import { IBaseGame, IBaseGameObject, IBasePlayer } from "@cadre/ts-utils/cadre"; +import { IBaseGameObject, IBasePlayer } from "@cadre/ts-utils/cadre"; +import { capitalize } from "lodash"; import { ITreeViewNode, Treeable, TreeView } from "src/core/ui"; import { isObject } from "src/utils"; import { IBaseTile } from "src/viseur/game"; @@ -9,12 +10,6 @@ function isGameObject(val: unknown): val is IBaseGameObject { && typeof val.gameObjectName === "string"; } -function isGame(val: unknown): val is IBaseGame { - return isObject(val) - && isObject(val.gameObjects) - && Array.isArray(val.players); -} - /** A tree view for inspecting game states */ export class InspectTreeView extends TreeView { /** The name of the game we are inspecting */ @@ -45,6 +40,10 @@ export class InspectTreeView extends TreeView { else if (value === null) { type = "null"; } + else if (node.parent && !node.parent.parent) { + type = "root"; + displayValue = `${this.gameName} ${capitalize(this.name)}`; + } else if (isGameObject(value)) { type = "game-object"; @@ -61,10 +60,6 @@ export class InspectTreeView extends TreeView { } displayValue += ` #${gameObject.id}`; } - else if (isGame(value)) { - type = "game-object"; - displayValue = `${this.gameName} Game`; - } else if (isObject(value)) { type = "map"; displayValue = `Map[${Object.keys(value).length}]`;