| <!-- Copyright (C) 2019 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> |
| <md-card-content class="container"> |
| <div class="rects" v-if="hasScreenView"> |
| <rects |
| :bounds="bounds" |
| :rects="rects" |
| :displays="displays" |
| :highlight="highlight" |
| @rect-click="onRectClick" |
| /> |
| </div> |
| |
| <div class="hierarchy"> |
| <flat-card |
| v-bind:class ="imeAdditionalProperties ? 'height-reduced' : ''"> |
| <md-content |
| md-tag="md-toolbar" |
| md-elevation="0" |
| class="card-toolbar md-transparent md-dense" |
| > |
| <h2 class="md-title" style="flex: 1;">Hierarchy</h2> |
| <md-checkbox |
| v-model="showHierarchyDiff" |
| v-if="diffVisualizationAvailable" |
| > |
| Show Diff |
| </md-checkbox> |
| <md-checkbox v-model="store.simplifyNames"> |
| Simplify names |
| </md-checkbox> |
| <md-checkbox v-model="store.onlyVisible">Only visible</md-checkbox> |
| <md-checkbox v-model="store.flattened">Flat</md-checkbox> |
| <md-checkbox v-if="hasTagsOrErrors" v-model="store.flickerTraceView"> |
| Flicker</md-checkbox> |
| <md-field md-inline class="filter"> |
| <label>Filter...</label> |
| <md-input |
| v-model="hierarchyPropertyFilterString" |
| v-on:focus="updateInputMode(true)" |
| v-on:blur="updateInputMode(false)" |
| /> |
| </md-field> |
| </md-content> |
| <div class="hierarchy-content"> |
| <properties-table-view |
| v-if="propertiesForTableView" |
| :tableEntries="propertiesForTableView" |
| /> |
| <div class="tree-view-wrapper"> |
| <tree-view |
| class="treeview" |
| :item="tree" |
| @item-selected="itemSelected" |
| :selected="hierarchySelected" |
| :filter="hierarchyFilter" |
| :flattened="store.flattened" |
| :onlyVisible="store.onlyVisible" |
| :flickerTraceView="store.flickerTraceView" |
| :presentTags="presentTags" |
| :presentErrors="presentErrors" |
| :items-clickable="true" |
| :useGlobalCollapsedState="true" |
| :simplify-names="store.simplifyNames" |
| ref="hierarchy" |
| /> |
| </div> |
| </div> |
| </flat-card> |
| |
| <div v-if="imeAdditionalProperties" class="ime-additional-properties"> |
| <flat-card> |
| <md-content |
| md-tag="md-toolbar" |
| md-elevation="0" |
| class="card-toolbar md-transparent md-dense" |
| > |
| <h2 class="md-title" style="flex: 1;">WM & SF Properties</h2> |
| </md-content> |
| <div> |
| <ime-additional-properties |
| :entry="this.item" |
| :isImeManagerService="this.isImeManagerService" |
| :onSelectItem="itemSelected" |
| /> |
| </div> |
| </flat-card> |
| </div> |
| </div> |
| |
| <div class="properties"> |
| <flat-card> |
| <md-content |
| md-tag="md-toolbar" |
| md-elevation="0" |
| class="card-toolbar md-transparent md-dense" |
| > |
| <h2 class="md-title" style="flex: 1">Properties</h2> |
| <div> |
| <md-checkbox |
| v-model="displayDefaults" |
| @change="checkboxChange" |
| > |
| Show Defaults |
| </md-checkbox> |
| <md-tooltip md-direction="bottom"> |
| If checked, shows the value of all properties. |
| Otherwise, hides all properties whose value is |
| the default for its data type. |
| </md-tooltip> |
| </div> |
| <md-checkbox |
| v-model="showPropertiesDiff" |
| v-if="diffVisualizationAvailable" |
| > |
| Show Diff |
| </md-checkbox> |
| <md-field md-inline class="filter"> |
| <label>Filter...</label> |
| <md-input |
| v-model="propertyFilterString" |
| v-on:focus="updateInputMode(true)" |
| v-on:blur="updateInputMode(false)" |
| /> |
| </md-field> |
| </md-content> |
| <div class="properties-content"> |
| <div v-if="elementSummary && !propertyGroups" class="element-summary"> |
| <div v-for="elem in elementSummary" v-bind:key="elem.key"> |
| <span class="key">{{ elem.key }}:</span> |
| <span class="value">{{ elem.value }}</span> |
| </div> |
| </div> |
| <div v-if="selectedTree && propertyGroups" class="element-summary"> |
| <sf-property-groups |
| :layer="this.hierarchySelected" |
| :visibilityReason="elementSummary" |
| /> |
| </div> |
| <div v-if="selectedTree" class="tree-view-wrapper"> |
| <tree-view |
| class="treeview" |
| :item="selectedTree" |
| :filter="propertyFilter" |
| :collapseChildren="true" |
| :elementView="PropertiesTreeElement" |
| /> |
| </div> |
| <div class="no-properties" v-else> |
| <i class="material-icons none-icon"> |
| filter_none |
| </i> |
| <span>No element selected in the hierarchy.</span> |
| </div> |
| </div> |
| </flat-card> |
| </div> |
| |
| </md-card-content> |
| </template> |
| <script> |
| import TreeView from './TreeView.vue'; |
| import Rects from './Rects.vue'; |
| import FlatCard from './components/FlatCard.vue'; |
| import PropertiesTreeElement from './PropertiesTreeElement.vue'; |
| import SurfaceFlingerPropertyGroups from '@/SurfaceFlingerPropertyGroups.vue'; |
| import PropertiesTableView from './PropertiesTableView'; |
| import ImeAdditionalProperties from '@/ImeAdditionalProperties'; |
| |
| import {ObjectTransformer} from './transform.js'; |
| import {DiffGenerator, defaultModifiedCheck} from './utils/diff.js'; |
| import {TRACE_TYPES, DUMP_TYPES} from './decode.js'; |
| import { |
| isPropertyMatch, stableIdCompatibilityFixup, getFilter, |
| } from './utils/utils.js'; |
| import {CompatibleFeatures} from './utils/compatibility.js'; |
| import {getPropertiesForDisplay} from './flickerlib/mixin'; |
| import ObjectFormatter from './flickerlib/ObjectFormatter'; |
| |
| function formatProto(obj) { |
| if (obj?.prettyPrint) { |
| return obj.prettyPrint(); |
| } |
| } |
| |
| function findEntryInTree(tree, id) { |
| if (tree.stableId === id) { |
| return tree; |
| } |
| |
| if (!tree.children) { |
| return null; |
| } |
| |
| for (const child of tree.children) { |
| const foundEntry = findEntryInTree(child, id); |
| if (foundEntry) { |
| return foundEntry; |
| } |
| } |
| |
| return null; |
| } |
| |
| export default { |
| name: 'traceview', |
| props: ['store', 'file', 'summarizer', 'presentTags', 'presentErrors', |
| 'propertyGroups', 'imeAdditionalProperties'], |
| data() { |
| return { |
| propertyFilterString: '', |
| hierarchyPropertyFilterString: '', |
| selectedTree: null, |
| hierarchySelected: null, |
| lastSelectedStableId: null, |
| bounds: {}, |
| rects: [], |
| displays: [], |
| item: null, |
| tree: null, |
| highlight: null, |
| showHierarchyDiff: false, |
| displayDefaults: false, |
| showPropertiesDiff: false, |
| PropertiesTreeElement, |
| isImeManagerService: false, |
| }; |
| }, |
| methods: { |
| checkboxChange(checked) { |
| this.itemSelected(this.item); |
| }, |
| itemSelected(item) { |
| this.hierarchySelected = item; |
| this.selectedTree = this.getTransformedProperties(item); |
| this.highlight = item.rect; |
| this.lastSelectedStableId = item.stableId; |
| // Record analytics event |
| if (item.type || item.kind || item.stableId) { |
| this.recordOpenedEntryEvent(item.type ?? item.kind ?? item.stableId); |
| } |
| this.$emit('focus'); |
| }, |
| getTransformedProperties(item) { |
| ObjectFormatter.displayDefaults = this.displayDefaults; |
| // There are 2 types of object whose properties can appear in the property |
| // list: Flicker objects (WM/SF traces) and dictionaries |
| // (IME/Accessibilty/Transactions). |
| // While flicker objects have their properties directly in the main |
| // object, those created by a call to the transform function have their |
| // properties inside an obj property. This makes both cases work |
| // TODO(209452852) Refactor both flicker and winscope-native objects to |
| // implement a common display interface that can be better handled |
| const target = item.obj ?? item; |
| const transformer = new ObjectTransformer( |
| getPropertiesForDisplay(target), |
| item.name, |
| stableIdCompatibilityFixup(item), |
| ).setOptions({ |
| skip: item.skip, |
| formatter: formatProto, |
| }); |
| |
| if (this.showPropertiesDiff && this.diffVisualizationAvailable) { |
| const prevItem = this.getItemFromPrevTree(item); |
| transformer.withDiff(getPropertiesForDisplay(prevItem)); |
| } |
| |
| return transformer.transform(); |
| }, |
| onRectClick(item) { |
| if (item) { |
| this.itemSelected(item); |
| } |
| }, |
| generateTreeFromItem(item) { |
| if (!this.showHierarchyDiff || !this.diffVisualizationAvailable) { |
| return item; |
| } |
| |
| const thisItem = this.item; |
| const prevItem = this.getDataWithOffset(-1); |
| return new DiffGenerator(thisItem) |
| .compareWith(prevItem) |
| .withUniqueNodeId((node) => { |
| return node.stableId; |
| }) |
| .withModifiedCheck(defaultModifiedCheck) |
| .generateDiffTree(); |
| }, |
| setData(item) { |
| this.item = item; |
| this.tree = this.generateTreeFromItem(item); |
| |
| const rects = item.rects; // .toArray() |
| this.rects = [...rects].reverse(); |
| this.bounds = item.bounds; |
| |
| // only update displays if item is SF trace and displays present |
| if (item.stableId==='LayerTraceEntry') { |
| this.displays = item.displays; |
| } else { |
| this.displays = []; |
| } |
| |
| this.hierarchySelected = null; |
| this.selectedTree = null; |
| this.highlight = null; |
| |
| function findItem(item, stableId) { |
| if (item.stableId === stableId) { |
| return item; |
| } |
| if (Array.isArray(item.children)) { |
| for (const child of item.children) { |
| const found = findItem(child, stableId); |
| if (found) { |
| return found; |
| } |
| } |
| } |
| return null; |
| } |
| |
| if (this.lastSelectedStableId) { |
| const found = findItem(item, this.lastSelectedStableId); |
| if (found) { |
| this.itemSelected(found); |
| } |
| } |
| |
| this.isImeManagerService = |
| this.file.type === TRACE_TYPES.IME_MANAGERSERVICE; |
| }, |
| arrowUp() { |
| return this.$refs.hierarchy.selectPrev(); |
| }, |
| arrowDown() { |
| return this.$refs.hierarchy.selectNext(); |
| }, |
| getDataWithOffset(offset) { |
| const index = this.file.selectedIndex + offset; |
| |
| if (index < 0 || index >= this.file.data.length) { |
| return null; |
| } |
| |
| return this.file.data[index]; |
| }, |
| getItemFromPrevTree(entry) { |
| if (!this.showPropertiesDiff || !this.hierarchySelected) { |
| return null; |
| } |
| |
| const id = entry.stableId; |
| if (!id) { |
| throw new Error('Entry has no stableId...'); |
| } |
| |
| const prevTree = this.getDataWithOffset(-1); |
| if (!prevTree) { |
| console.warn('No previous entry'); |
| return null; |
| } |
| |
| const prevEntry = findEntryInTree(prevTree, id); |
| if (!prevEntry) { |
| console.warn('Didn\'t exist in last entry'); |
| // TODO: Maybe handle this in some way. |
| } |
| |
| return prevEntry; |
| }, |
| |
| /** Performs check for id match between entry and present tags/errors |
| * must be carried out for every present tag/error |
| */ |
| matchItems(flickerItems, entryItem) { |
| let match = false; |
| flickerItems.forEach((flickerItem) => { |
| if (isPropertyMatch(flickerItem, entryItem)) match = true; |
| }); |
| return match; |
| }, |
| /** Returns check for id match between entry and present tags/errors */ |
| isEntryTagMatch(entryItem) { |
| return this.matchItems(this.presentTags, entryItem) || |
| this.matchItems(this.presentErrors, entryItem); |
| }, |
| |
| /** determines whether left/right arrow keys should move cursor in input field */ |
| updateInputMode(isInputMode) { |
| this.store.isInputMode = isInputMode; |
| }, |
| }, |
| created() { |
| const item = this.file.data[this.file.selectedIndex ?? 0]; |
| if (item) { |
| // Record analytics event |
| if (item.type || item.kind || item.stableId) { |
| this.recordOpenTraceEvent(item.type ?? item.kind ?? item.stableId); |
| } |
| this.setData(item); |
| } else { |
| console.log('Item passed into TraceView is null or undefined: ', item); |
| } |
| }, |
| destroyed() { |
| this.store.flickerTraceView = false; |
| }, |
| watch: { |
| selectedIndex() { |
| this.setData(this.file.data[this.file.selectedIndex ?? 0]); |
| }, |
| showHierarchyDiff() { |
| this.tree = this.generateTreeFromItem(this.item); |
| }, |
| showPropertiesDiff() { |
| if (this.hierarchySelected) { |
| this.selectedTree = |
| this.getTransformedProperties(this.hierarchySelected); |
| } |
| }, |
| }, |
| computed: { |
| diffVisualizationAvailable() { |
| return CompatibleFeatures.DiffVisualization && ( |
| this.file.type == TRACE_TYPES.WINDOW_MANAGER || |
| this.file.type == TRACE_TYPES.SURFACE_FLINGER |
| ); |
| }, |
| selectedIndex() { |
| return this.file.selectedIndex; |
| }, |
| hierarchyFilter() { |
| const hierarchyPropertyFilter = |
| getFilter(this.hierarchyPropertyFilterString); |
| const fil = this.store.onlyVisible ? (c) => { |
| return c.isVisible && hierarchyPropertyFilter(c); |
| } : hierarchyPropertyFilter; |
| return this.store.flickerTraceView ? (c) => { |
| return this.isEntryTagMatch(c); |
| } : fil; |
| }, |
| propertyFilter() { |
| return getFilter(this.propertyFilterString); |
| }, |
| hasScreenView() { |
| return this.file.type == TRACE_TYPES.WINDOW_MANAGER || |
| this.file.type == TRACE_TYPES.SURFACE_FLINGER || |
| this.file.type == DUMP_TYPES.WINDOW_MANAGER || |
| this.file.type == DUMP_TYPES.SURFACE_FLINGER; |
| }, |
| elementSummary() { |
| if (!this.hierarchySelected || !this.summarizer) { |
| return null; |
| } |
| |
| const summary = this.summarizer(this.hierarchySelected); |
| |
| if (summary?.length === 0) { |
| return null; |
| } |
| |
| return summary; |
| }, |
| hasTagsOrErrors() { |
| return this.presentTags.length > 0 || this.presentErrors.length > 0; |
| }, |
| propertiesForTableView() { |
| if (this.file.type == TRACE_TYPES.IME_CLIENTS) { |
| if (!this.item?.obj?.client) { |
| console.log('ImeTrace Clients: Client is null'); |
| } |
| return { |
| 'inputMethodId': this.item?.obj?.client?.inputMethodManager?.curId, |
| 'packageName': this.item?.obj?.client?.editorInfo?.packageName, |
| }; |
| } else if (this.file.type == TRACE_TYPES.IME_SERVICE) { |
| if (!this.item?.obj?.inputMethodService) { |
| console.log('ImeTrace InputMethodService: ' + |
| 'inputMethodService is null'); |
| } |
| return { |
| 'windowVisible': this.item?.obj?.inputMethodService?.windowVisible, |
| 'decorViewVisible': |
| this.item?.obj?.inputMethodService?.decorViewVisible, |
| 'packageName': |
| this.item?.obj?.inputMethodService?.inputEditorInfo?.packageName, |
| }; |
| } else if (this.file.type == TRACE_TYPES.IME_MANAGERSERVICE) { |
| if (!this.item?.obj?.inputMethodManagerService) { |
| console.log('ImeTrace InputMethodManagerService: ' + |
| 'inputMethodManagerService is null'); |
| } |
| return { |
| 'inputMethodId': |
| this.item?.obj?.inputMethodManagerService?.curMethodId, |
| 'curFocusedWindow': |
| this.item?.obj?.inputMethodManagerService?.curFocusedWindowName, |
| 'lastImeTargetWindow': |
| this.item?.obj?.inputMethodManagerService?.lastImeTargetWindowName, |
| 'inputShown': this.item?.obj?.inputMethodManagerService?.inputShown, |
| }; |
| } else { |
| return null; |
| } |
| }, |
| }, |
| components: { |
| 'tree-view': TreeView, |
| 'rects': Rects, |
| 'flat-card': FlatCard, |
| 'sf-property-groups': SurfaceFlingerPropertyGroups, |
| 'properties-table-view': PropertiesTableView, |
| 'ime-additional-properties': ImeAdditionalProperties, |
| }, |
| }; |
| |
| </script> |
| <style scoped> |
| .container { |
| display: flex; |
| flex-wrap: wrap; |
| } |
| |
| .rects { |
| flex: none; |
| margin: 8px; |
| } |
| |
| .hierarchy, |
| .properties { |
| flex: 1; |
| margin: 8px; |
| min-width: 400px; |
| min-height: 70rem; |
| max-height: 70rem; |
| } |
| |
| .rects, |
| .hierarchy, |
| .properties { |
| padding: 5px; |
| } |
| |
| .flat-card { |
| display: flex; |
| flex-direction: column; |
| height: 100%; |
| overflow: auto; |
| } |
| |
| .hierarchy>.tree-view, |
| .properties>.tree-view { |
| margin: 16px; |
| } |
| |
| .treeview { |
| overflow: auto; |
| white-space: pre-line; |
| flex: 1 0 0; |
| } |
| |
| .no-properties { |
| display: flex; |
| flex: 1; |
| flex-direction: column; |
| align-self: center; |
| align-items: center; |
| justify-content: center; |
| padding: 50px 25px; |
| } |
| |
| .no-properties .none-icon { |
| font-size: 35px; |
| margin-bottom: 10px; |
| } |
| |
| .no-properties span { |
| font-weight: 100; |
| } |
| |
| .filter { |
| width: auto; |
| } |
| |
| .element-summary { |
| padding: 1rem; |
| border-bottom: thin solid rgba(0,0,0,.12); |
| } |
| |
| .element-summary .key { |
| font-weight: 500; |
| } |
| |
| .element-summary .value { |
| color: rgba(0, 0, 0, 0.75); |
| } |
| |
| .hierarchy-content, |
| .properties-content { |
| display: flex; |
| flex-direction: column; |
| flex: 1; |
| } |
| |
| .tree-view-wrapper { |
| display: flex; |
| flex-direction: column; |
| flex: 1; |
| } |
| |
| .height-reduced { |
| height: 55%; |
| } |
| |
| .ime-additional-properties { |
| padding-top: 10px; |
| display: flex; |
| flex-direction: column; |
| flex: 1; |
| height: 45%; |
| overflow: auto; |
| } |
| </style> |