Implement collapsible TreeView

Test: Manually by trying to collapse trees
Change-Id: I2fecf357165357e03b347bb89631d9daac46c321
This commit is contained in:
Pablo Gamito
2020-05-18 09:46:00 +01:00
parent 92e970a7b9
commit 231e2c378a
3 changed files with 176 additions and 37 deletions

View File

@@ -29,7 +29,6 @@
<dataadb ref="adb" :store="store" @dataReady="onDataReady" @statusChange="setStatus" /> <dataadb ref="adb" :store="store" @dataReady="onDataReady" @statusChange="setStatus" />
<datainput ref="input" :store="store" @dataReady="onDataReady" @statusChange="setStatus" /> <datainput ref="input" :store="store" @dataReady="onDataReady" @statusChange="setStatus" />
</div> </div>
<dataview <dataview
v-for="file in files" v-for="file in files"
:key="file.filename" :key="file.filename"

View File

@@ -29,7 +29,16 @@
<md-checkbox v-model="store.flattened">Flat</md-checkbox> <md-checkbox v-model="store.flattened">Flat</md-checkbox>
<input id="filter" type="search" placeholder="Filter..." v-model="hierarchyPropertyFilterString" /> <input id="filter" type="search" placeholder="Filter..." v-model="hierarchyPropertyFilterString" />
</md-content> </md-content>
<tree-view class="data-card" :item="tree" @item-selected="itemSelected" :selected="hierarchySelected" :filter="hierarchyFilter" :flattened="store.flattened" ref="hierarchy" /> <tree-view
class="data-card"
:item="tree"
@item-selected="itemSelected"
:selected="hierarchySelected"
:filter="hierarchyFilter"
:flattened="store.flattened"
:items-clickable="true"
ref="hierarchy"
/>
</md-card> </md-card>
<md-card class="properties"> <md-card class="properties">
<md-content md-tag="md-toolbar" md-elevation="0" class="card-toolbar md-transparent md-dense"> <md-content md-tag="md-toolbar" md-elevation="0" class="card-toolbar md-transparent md-dense">

View File

@@ -14,18 +14,35 @@
--> -->
<template> <template>
<div class="tree-view"> <div class="tree-view">
<div @click="clicked" :class="computedClass" class="node"> <div class="node"
<span class="kind">{{item.kind}}</span> :class="{ leaf: isLeaf, selected: isSelected, clickable: isClickable }"
<span v-if="item.kind && item.name">-</span> :style="nodeOffsetStyle" @click="clicked"
<span>{{item.name}}</span> >
<div <button class="toggle-tree-btn" @click="toggleTree" v-if="!isLeaf" v-on:click.stop>
v-for="c in item.chips" <i aria-hidden="true" class="md-icon md-theme-default material-icons">
v-bind:key="c.long" {{isCollapsed ? "chevron_right" : "expand_more"}}
:title="c.long" </i>
:class="chipClassForChip(c)" </button>
>{{c.short}}</div> <div class="description">
<span class="kind">{{item.kind}}</span>
<span v-if="item.kind && item.name">-</span>
<span>{{item.name}}</span>
<div
v-for="c in item.chips"
v-bind:key="c.long"
:title="c.long"
:class="chipClassForChip(c)"
>
{{c.short}}
</div>
</div>
<div v-show="isCollapsed">
<button class="expand-tree-btn" :class="{ 'child-selected': isCollapsed && childIsSelected }" v-if="children" @click="expandTree" v-on:click.stop>
<i aria-hidden="true" class="md-icon md-theme-default material-icons">more_horiz</i>
</button>
</div>
</div> </div>
<div class="children" v-if="children"> <div class="children" v-if="children" v-show="!isCollapsed">
<tree-view <tree-view
v-for="(c,i) in children" v-for="(c,i) in children"
:item="c" :item="c"
@@ -37,6 +54,8 @@
:flattened="flattened" :flattened="flattened"
:force-flattened="applyingFlattened" :force-flattened="applyingFlattened"
v-show="filterMatches(c)" v-show="filterMatches(c)"
:items-clickable="itemsClickable"
:initial-depth="depth + 1"
ref="children" ref="children"
/> />
</div> </div>
@@ -55,6 +74,8 @@ var ServiceMessage = protoDefs.lookupType(
"com.android.server.wm.WindowManagerServiceDumpProto" "com.android.server.wm.WindowManagerServiceDumpProto"
); );
const levelOffset = 24; /* in px, must be kept in sync with css, maybe find a better solution... */
export default { export default {
name: "tree-view", name: "tree-view",
props: [ props: [
@@ -63,50 +84,108 @@ export default {
"chipClass", "chipClass",
"filter", "filter",
"flattened", "flattened",
"force-flattened" "force-flattened",
"items-clickable",
"initial-depth"
], ],
data: function() { data: function() {
return { return {
isChildSelected: false isChildSelected: false,
isCollapsed: false,
clickTimeout: null,
}; };
}, },
methods: { methods: {
selectNext(found, parent) { toggleTree() {
if (found && this.filterMatches(this.item)) { this.isCollapsed = !this.isCollapsed;
this.clicked(); },
expandTree() {
this.isCollapsed = false;
},
selectNext(found, inCollapsedTree) {
// Check if this is the next visible item
if (found && this.filterMatches(this.item) && !inCollapsedTree) {
this.select();
return false; return false;
} }
if (this.isCurrentSelected()) {
// Set traversal state variables
if (this.isSelected) {
found = true; found = true;
} }
if (this.isCollapsed) {
inCollapsedTree = true;
}
// Travers children trees recursively in reverse to find currently
// selected item and select the next visible one
if (this.$refs.children) { if (this.$refs.children) {
for (var c of this.$refs.children) { for (var c of this.$refs.children) {
found = c.selectNext(found); found = c.selectNext(found, inCollapsedTree);
} }
} }
return found; return found;
}, },
selectPrev(found) { selectPrev(found, inCollapsedTree) {
// Set inCollapseTree flag to make sure elements in collapsed trees are not selected.
const isRootCollapse = !inCollapsedTree && this.isCollapsed;
if (isRootCollapse) {
inCollapsedTree = true;
}
// Travers children trees recursively in reverse to find currently
// selected item and select the previous visible one
if (this.$refs.children) { if (this.$refs.children) {
for (var c of [...this.$refs.children].reverse()) { for (var c of [...this.$refs.children].reverse()) {
found = c.selectPrev(found); found = c.selectPrev(found, inCollapsedTree);
} }
} }
if (found && this.filterMatches(this.item)) {
this.clicked(); // Unset inCollapseTree flag as we are no longer in a collapsed tree.
if (isRootCollapse) {
inCollapsedTree = false;
}
// Check if this is the previous visible item
if (found && this.filterMatches(this.item) && !inCollapsedTree) {
this.select();
return false; return false;
} }
if (this.isCurrentSelected()) {
// Set found flag so that the next visited visible item can be selected.
if (this.isSelected) {
found = true; found = true;
} }
return found; return found;
}, },
childItemSelected(item) { childItemSelected(item) {
this.isChildSelected = true; this.isChildSelected = true;
this.$emit("item-selected", item); this.$emit("item-selected", item);
}, },
select() {
this.$emit('item-selected', this.item);
},
clicked() { clicked() {
this.$emit("item-selected", this.item); if (!this.clickTimeout) {
this.clickTimeout = setTimeout(() => {
// Single click
this.clickTimeout = null;
if (this.itemsClickable) {
this.select();
}
}, 200);
} else {
// Double click
clearTimeout(this.clickTimeout);
this.clickTimeout = null;
if (!this.isLeaf) {
this.toggleTree();
}
}
}, },
chipClassForChip(c) { chipClassForChip(c) {
return [ return [
@@ -121,7 +200,7 @@ export default {
if (this.filter) { if (this.filter) {
var thisMatches = this.filter(c); var thisMatches = this.filter(c);
const childMatches = (child) => this.filterMatches(child); const childMatches = (child) => this.filterMatches(child);
return thisMatches || (!this.applyingFlattened && return thisMatches || (!this.applyingFlattened &&
c.children && c.children.some(childMatches)); c.children && c.children.some(childMatches));
} }
return true; return true;
@@ -140,8 +219,19 @@ export default {
}, },
}, },
computed: { computed: {
computedClass() { isSelected() {
return (this.item == this.selected) ? 'selected' : '' return this.selected === this.item;
},
childIsSelected() {
if (this.$refs.children) {
for (var c of this.$refs.children) {
if (c.isSelected || c.childIsSelected) {
return true;
}
}
}
return false;
}, },
chipClassOrDefault() { chipClassOrDefault() {
return this.chipClass || "tree-view-chip"; return this.chipClass || "tree-view-chip";
@@ -152,6 +242,23 @@ export default {
children() { children() {
return this.applyingFlattened ? this.item.flattened : this.item.children; return this.applyingFlattened ? this.item.flattened : this.item.children;
}, },
isLeaf() {
return !this.children || this.children.length == 0;
},
isClickable() {
return !this.isLeaf || this.itemsClickable;
},
depth() {
return this.initialDepth || 0;
},
nodeOffsetStyle() {
const offest = levelOffset * (this.depth + this.isLeaf) + 'px';
return {
marginLeft: '-' + offest,
paddingLeft: offest,
}
}
} }
}; };
</script> </script>
@@ -162,18 +269,17 @@ export default {
.tree-view { .tree-view {
display: block; display: block;
border-left: 1px solid rgb(238, 238, 238);
}
.tree-view.tree-view {
margin: 0;
padding: 1px 8px;
} }
.tree-view .node { .tree-view .node {
display: inline-block; display: flex;
padding: 2px; padding: 2px;
align-items: flex-start;
}
.tree-view .node.clickable {
cursor: pointer; cursor: pointer;
user-select: none;
} }
.tree-view .node:hover:not(.selected) { .tree-view .node:hover:not(.selected) {
@@ -181,10 +287,17 @@ export default {
} }
.children { .children {
margin-left: 8px; /* Aligns border with collapse arrows */
margin-left: 12px;
padding-left: 12px;
border-left: 1px solid rgb(238, 238, 238);
margin-top: 0px; margin-top: 0px;
} }
.tree-view .node:hover + .children {
border-left: 1px solid rgb(200, 200, 200);
}
.kind { .kind {
color: #333; color: #333;
font-weight: bold; font-weight: bold;
@@ -234,4 +347,22 @@ export default {
color: black; color: black;
} }
.toggle-tree-btn, .expand-tree-btn {
background: none;
color: inherit;
border: none;
padding: 0;
font: inherit;
cursor: pointer;
outline: inherit;
}
.expand-tree-btn {
margin-left: 5px;
}
.expand-tree-btn.child-selected {
color: #3f51b5;
}
</style> </style>