447 lines
11 KiB
Vue
447 lines
11 KiB
Vue
<!-- Copyright (C) 2017 The Android Open Source Project
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
-->
|
|
<template>
|
|
<div class="tree-view" v-if="item">
|
|
<div class="node"
|
|
:class="{ leaf: isLeaf, selected: isSelected, clickable: isClickable, diffClass }"
|
|
:style="nodeOffsetStyle"
|
|
@click="clicked"
|
|
ref="node"
|
|
>
|
|
<button class="toggle-tree-btn" @click="toggleTree" v-if="!isLeaf" v-on:click.stop>
|
|
<i aria-hidden="true" class="md-icon md-theme-default material-icons">
|
|
{{isCollapsed ? "chevron_right" : "expand_more"}}
|
|
</i>
|
|
</button>
|
|
<div class="padding" v-else>
|
|
<i aria-hidden="true" class="md-icon md-theme-default material-icons">
|
|
arrow_right
|
|
</i>
|
|
</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 class="children" v-if="children" v-show="!isCollapsed">
|
|
<tree-view
|
|
v-for="(c,i) in children"
|
|
:item="c"
|
|
@item-selected="childItemSelected"
|
|
:selected="selected"
|
|
:key="i"
|
|
:chip-class="chipClass"
|
|
:filter="childFilter(c)"
|
|
:flattened="flattened"
|
|
:force-flattened="applyingFlattened"
|
|
v-show="filterMatches(c)"
|
|
:items-clickable="itemsClickable"
|
|
:initial-depth="depth + 1"
|
|
:collapse="collapseChildren"
|
|
:collapseChildren="collapseChildren"
|
|
:useGlobalCollapsedState="useGlobalCollapsedState"
|
|
ref="children"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import jsonProtoDefs from "frameworks/base/core/proto/android/server/windowmanagertrace.proto";
|
|
import protobuf from "protobufjs";
|
|
|
|
import { DiffType } from "./utils/diff.js";
|
|
|
|
var protoDefs = protobuf.Root.fromJSON(jsonProtoDefs);
|
|
var TraceMessage = protoDefs.lookupType(
|
|
"com.android.server.wm.WindowManagerTraceFileProto"
|
|
);
|
|
var ServiceMessage = protoDefs.lookupType(
|
|
"com.android.server.wm.WindowManagerServiceDumpProto"
|
|
);
|
|
|
|
const levelOffset = 24; /* in px, must be kept in sync with css, maybe find a better solution... */
|
|
|
|
export default {
|
|
name: "tree-view",
|
|
props: [
|
|
"item",
|
|
"selected",
|
|
"chipClass",
|
|
"filter",
|
|
"flattened",
|
|
"force-flattened",
|
|
"items-clickable",
|
|
"initial-depth",
|
|
"collapse",
|
|
"collapseChildren",
|
|
// Allows collapse state to be tracked by Vuex so that collapse state of
|
|
// items with same stableId can remain consisten accross time and easily
|
|
// toggled from anywhere in the app.
|
|
// Should be true if you are using the same TreeView to display multiple
|
|
// trees throughout the component's lifetime to make sure same nodes are
|
|
// toggled when switching back and forth between trees.
|
|
// If true, requires all nodes in tree to have a stableId.
|
|
"useGlobalCollapsedState",
|
|
],
|
|
data() {
|
|
const isCollapsedByDefault = this.collapse ?? false;
|
|
|
|
return {
|
|
isChildSelected: false,
|
|
clickTimeout: null,
|
|
isCollapsedByDefault,
|
|
localCollapsedState: isCollapsedByDefault,
|
|
diffSymbol: {
|
|
[DiffType.NONE]: "",
|
|
[DiffType.ADDED]: "+",
|
|
[DiffType.DELETED]: "-",
|
|
[DiffType.MODIFIED]: ".",
|
|
[DiffType.MOVED]: ".",
|
|
},
|
|
};
|
|
},
|
|
methods: {
|
|
setCollapseValue(isCollapsed) {
|
|
if (this.useGlobalCollapsedState) {
|
|
this.$store.commit('setCollapsedState', {
|
|
item: this.item,
|
|
isCollapsed,
|
|
});
|
|
} else {
|
|
this.localCollapsedState = isCollapsed;
|
|
}
|
|
},
|
|
toggleTree() {
|
|
this.setCollapseValue(!this.isCollapsed);
|
|
},
|
|
expandTree() {
|
|
this.setCollapseValue(false);
|
|
},
|
|
selectNext(found, inCollapsedTree) {
|
|
// Check if this is the next visible item
|
|
if (found && this.filterMatches(this.item) && !inCollapsedTree) {
|
|
this.select();
|
|
return false;
|
|
}
|
|
|
|
// Set traversal state variables
|
|
if (this.isSelected) {
|
|
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) {
|
|
for (var c of this.$refs.children) {
|
|
found = c.selectNext(found, inCollapsedTree);
|
|
}
|
|
}
|
|
|
|
return 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) {
|
|
for (var c of [...this.$refs.children].reverse()) {
|
|
found = c.selectPrev(found, inCollapsedTree);
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Set found flag so that the next visited visible item can be selected.
|
|
if (this.isSelected) {
|
|
found = true;
|
|
}
|
|
|
|
return found;
|
|
},
|
|
childItemSelected(item) {
|
|
this.isChildSelected = true;
|
|
this.$emit("item-selected", item);
|
|
},
|
|
select() {
|
|
this.$emit('item-selected', this.item);
|
|
},
|
|
clicked(e) {
|
|
if (window.getSelection().type === "range") {
|
|
// Ignore click if is selection
|
|
return;
|
|
}
|
|
|
|
if (!this.isLeaf && e.detail % 2 === 0) {
|
|
// Double click collaspable node
|
|
this.toggleTree();
|
|
} else {
|
|
this.select();
|
|
}
|
|
},
|
|
chipClassForChip(c) {
|
|
return [
|
|
"tree-view-internal-chip",
|
|
this.chipClassOrDefault,
|
|
this.chipClassOrDefault + "-" + (c.class || "default")
|
|
];
|
|
},
|
|
filterMatches(c) {
|
|
// If a filter is set, consider the item matches if the current item or any of its
|
|
// children matches.
|
|
if (this.filter) {
|
|
var thisMatches = this.filter(c);
|
|
const childMatches = (child) => this.filterMatches(child);
|
|
return thisMatches || (!this.applyingFlattened &&
|
|
c.children && c.children.some(childMatches));
|
|
}
|
|
return true;
|
|
},
|
|
childFilter(c) {
|
|
if (this.filter) {
|
|
if (this.filter(c)) {
|
|
// Filter matched c, don't apply further filtering on c's children.
|
|
return undefined;
|
|
}
|
|
}
|
|
return this.filter;
|
|
},
|
|
isCurrentSelected() {
|
|
return this.selected === this.item;
|
|
},
|
|
},
|
|
computed: {
|
|
isCollapsed() {
|
|
if (this.item.children.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
if (this.useGlobalCollapsedState) {
|
|
return this.$store.getters.collapsedStateStoreFor(this.item) ??
|
|
this.isCollapsedByDefault;
|
|
}
|
|
|
|
return this.localCollapsedState;
|
|
},
|
|
isSelected() {
|
|
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;
|
|
},
|
|
diffClass() {
|
|
return this.item.diff ? this.item.diff.type : ''
|
|
},
|
|
chipClassOrDefault() {
|
|
return this.chipClass || "tree-view-chip";
|
|
},
|
|
applyingFlattened() {
|
|
return (this.flattened && this.item.flattened) || this.forceFlattened;
|
|
},
|
|
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,
|
|
}
|
|
}
|
|
},
|
|
mounted() {
|
|
// Prevent highlighting on multiclick of node element
|
|
this.$refs.node?.addEventListener('mousedown', (e) => {
|
|
if (e.detail > 1) {
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
},
|
|
|
|
};
|
|
</script>
|
|
<style>
|
|
.data-card > .tree-view {
|
|
border: none;
|
|
}
|
|
|
|
.tree-view {
|
|
display: block;
|
|
}
|
|
|
|
.tree-view .node {
|
|
display: flex;
|
|
padding: 2px;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.tree-view .node.clickable {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.tree-view .node:hover:not(.selected) {
|
|
background: #f1f1f1;
|
|
}
|
|
|
|
.tree-view .node:not(.selected).added {
|
|
background: chartreuse;
|
|
}
|
|
|
|
.tree-view .node:not(.selected).deleted {
|
|
background: coral;
|
|
}
|
|
|
|
.tree-view .node:not(.selected).modified {
|
|
background: cyan;
|
|
}
|
|
|
|
.tree-view .node:not(.selected).addedMove,
|
|
.tree-view .node:not(.selected).deletedMove {
|
|
background: slateblue;
|
|
}
|
|
|
|
.children {
|
|
/* Aligns border with collapse arrows */
|
|
margin-left: 12px;
|
|
padding-left: 12px;
|
|
border-left: 1px solid rgb(238, 238, 238);
|
|
margin-top: 0px;
|
|
}
|
|
|
|
.tree-view .node:hover + .children {
|
|
border-left: 1px solid rgb(200, 200, 200);
|
|
}
|
|
|
|
.kind {
|
|
color: #333;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.selected {
|
|
background-color: #365179;
|
|
color: white;
|
|
}
|
|
|
|
.childSelected {
|
|
border-left: 1px solid rgb(233, 22, 22)
|
|
}
|
|
|
|
.selected .kind {
|
|
color: #e9e9e9;
|
|
}
|
|
|
|
.tree-view-internal-chip {
|
|
display: inline-block;
|
|
}
|
|
|
|
.tree-view-chip {
|
|
padding: 0 10px;
|
|
border-radius: 10px;
|
|
background-color: #aaa;
|
|
color: black;
|
|
}
|
|
|
|
.tree-view-chip.tree-view-chip-warn {
|
|
background-color: #ffaa6b;
|
|
color: black;
|
|
}
|
|
|
|
.tree-view-chip.tree-view-chip-error {
|
|
background-color: #ff6b6b;
|
|
color: black;
|
|
}
|
|
|
|
.tree-view-chip.tree-view-chip-gpu {
|
|
background-color: #00c853;
|
|
color: black;
|
|
}
|
|
|
|
.tree-view-chip.tree-view-chip-hwc {
|
|
background-color: #448aff;
|
|
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>
|