1<!-- Copyright (C) 2019 The Android Open Source Project
2
3     Licensed under the Apache License, Version 2.0 (the "License");
4     you may not use this file except in compliance with the License.
5     You may obtain a copy of the License at
6
7          http://www.apache.org/licenses/LICENSE-2.0
8
9     Unless required by applicable law or agreed to in writing, software
10     distributed under the License is distributed on an "AS IS" BASIS,
11     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12     See the License for the specific language governing permissions and
13     limitations under the License.
14-->
15<template>
16  <md-card-content class="container">
17    <div class="rects" v-if="hasScreenView">
18      <rects
19        :bounds="bounds"
20        :rects="rects"
21        :highlight="highlight"
22        @rect-click="onRectClick"
23      />
24    </div>
25
26    <div class="hierarchy">
27      <flat-card>
28        <md-content
29          md-tag="md-toolbar"
30          md-elevation="0"
31          class="card-toolbar md-transparent md-dense"
32        >
33          <h2 class="md-title" style="flex: 1;">Hierarchy</h2>
34          <md-checkbox
35            v-model="showHierarchyDiff"
36            v-if="diffVisualizationAvailable"
37          >
38            Show Diff
39          </md-checkbox>
40          <md-checkbox v-model="store.simplifyNames">
41            Simplify names
42          </md-checkbox>
43          <md-checkbox v-model="store.onlyVisible">Only visible</md-checkbox>
44          <md-checkbox v-model="store.flattened">Flat</md-checkbox>
45          <md-checkbox v-if="hasTagsOrErrors" v-model="store.flickerTraceView">Flicker</md-checkbox>
46          <md-field md-inline class="filter">
47            <label>Filter...</label>
48            <md-input
49              v-model="hierarchyPropertyFilterString"
50              v-on:focus="updateInputMode(true)"
51              v-on:blur="updateInputMode(false)"
52            />
53          </md-field>
54        </md-content>
55        <div class="tree-view-wrapper">
56          <tree-view
57            class="treeview"
58            :item="tree"
59            @item-selected="itemSelected"
60            :selected="hierarchySelected"
61            :filter="hierarchyFilter"
62            :flattened="store.flattened"
63            :onlyVisible="store.onlyVisible"
64            :flickerTraceView="store.flickerTraceView"
65            :presentTags="presentTags"
66            :presentErrors="presentErrors"
67            :items-clickable="true"
68            :useGlobalCollapsedState="true"
69            :simplify-names="store.simplifyNames"
70            ref="hierarchy"
71          />
72        </div>
73      </flat-card>
74    </div>
75
76    <div class="properties">
77      <flat-card>
78        <md-content
79          md-tag="md-toolbar"
80          md-elevation="0"
81          class="card-toolbar md-transparent md-dense"
82        >
83          <h2 class="md-title" style="flex: 1">Properties</h2>
84          <div>
85            <md-checkbox
86              v-model="displayDefaults"
87              @change="checkboxChange"
88            >
89              Show Defaults
90            </md-checkbox>
91            <md-tooltip md-direction="bottom">
92                If checked, shows the value of all properties.
93                Otherwise, hides all properties whose value is
94                the default for its data type.
95            </md-tooltip>
96          </div>
97          <md-checkbox
98            v-model="showPropertiesDiff"
99            v-if="diffVisualizationAvailable"
100          >
101            Show Diff
102          </md-checkbox>
103          <md-field md-inline class="filter">
104            <label>Filter...</label>
105            <md-input
106              v-model="propertyFilterString"
107              v-on:focus="updateInputMode(true)"
108              v-on:blur="updateInputMode(false)"
109            />
110          </md-field>
111        </md-content>
112        <div class="properties-content">
113          <div v-if="elementSummary" class="element-summary">
114            <div v-for="elem in elementSummary" v-bind:key="elem.key">
115              <!-- eslint-disable-next-line max-len -->
116              <span class="key">{{ elem.key }}:</span> <span class="value">{{ elem.value }}</span>
117            </div>
118          </div>
119          <div v-if="selectedTree" class="tree-view-wrapper">
120            <tree-view
121              class="treeview"
122              :item="selectedTree"
123              :filter="propertyFilter"
124              :collapseChildren="true"
125              :elementView="PropertiesTreeElement"
126            />
127          </div>
128          <div class="no-properties" v-else>
129            <i class="material-icons none-icon">
130              filter_none
131            </i>
132            <span>No element selected in the hierarchy.</span>
133          </div>
134        </div>
135      </flat-card>
136    </div>
137
138  </md-card-content>
139</template>
140<script>
141import TreeView from './TreeView.vue';
142import Rects from './Rects.vue';
143import FlatCard from './components/FlatCard.vue';
144import PropertiesTreeElement from './PropertiesTreeElement.vue';
145
146import {ObjectTransformer} from './transform.js';
147import {DiffGenerator, defaultModifiedCheck} from './utils/diff.js';
148import {TRACE_TYPES, DUMP_TYPES} from './decode.js';
149import {isPropertyMatch, stableIdCompatibilityFixup} from './utils/utils.js';
150import {CompatibleFeatures} from './utils/compatibility.js';
151import {getPropertiesForDisplay} from './flickerlib/mixin';
152import ObjectFormatter from './flickerlib/ObjectFormatter';
153
154function formatProto(obj) {
155  if (obj?.prettyPrint) {
156    return obj.prettyPrint();
157  }
158}
159
160function findEntryInTree(tree, id) {
161  if (tree.stableId === id) {
162    return tree;
163  }
164
165  if (!tree.children) {
166    return null;
167  }
168
169  for (const child of tree.children) {
170    const foundEntry = findEntryInTree(child, id);
171    if (foundEntry) {
172      return foundEntry;
173    }
174  }
175
176  return null;
177}
178
179export default {
180  name: 'traceview',
181  props: ['store', 'file', 'summarizer', 'presentTags', 'presentErrors'],
182  data() {
183    return {
184      propertyFilterString: '',
185      hierarchyPropertyFilterString: '',
186      selectedTree: null,
187      hierarchySelected: null,
188      lastSelectedStableId: null,
189      bounds: {},
190      rects: [],
191      item: null,
192      tree: null,
193      highlight: null,
194      showHierarchyDiff: false,
195      displayDefaults: false,
196      showPropertiesDiff: false,
197      PropertiesTreeElement,
198    };
199  },
200  methods: {
201    checkboxChange(checked) {
202      this.itemSelected(this.item);
203    },
204    itemSelected(item) {
205      this.hierarchySelected = item;
206      this.selectedTree = this.getTransformedProperties(item);
207      this.highlight = item.rect;
208      this.lastSelectedStableId = item.stableId;
209      this.$emit('focus');
210    },
211    getTransformedProperties(item) {
212      ObjectFormatter.displayDefaults = this.displayDefaults;
213      const transformer = new ObjectTransformer(
214          getPropertiesForDisplay(item),
215          item.name,
216          stableIdCompatibilityFixup(item),
217      ).setOptions({
218        skip: item.skip,
219        formatter: formatProto,
220      });
221
222      if (this.showPropertiesDiff && this.diffVisualizationAvailable) {
223        const prevItem = this.getItemFromPrevTree(item);
224        transformer.withDiff(getPropertiesForDisplay(prevItem));
225      }
226
227      return transformer.transform();
228    },
229    onRectClick(item) {
230      if (item) {
231        this.itemSelected(item);
232      }
233    },
234    generateTreeFromItem(item) {
235      if (!this.showHierarchyDiff || !this.diffVisualizationAvailable) {
236        return item;
237      }
238
239      const thisItem = this.item;
240      const prevItem = this.getDataWithOffset(-1);
241      return new DiffGenerator(thisItem)
242          .compareWith(prevItem)
243          .withUniqueNodeId((node) => {
244            return node.stableId;
245          })
246          .withModifiedCheck(defaultModifiedCheck)
247          .generateDiffTree();
248    },
249    setData(item) {
250      this.item = item;
251      this.tree = this.generateTreeFromItem(item);
252
253      const rects = item.rects; // .toArray()
254      this.rects = [...rects].reverse();
255      this.bounds = item.bounds;
256
257      this.hierarchySelected = null;
258      this.selectedTree = null;
259      this.highlight = null;
260
261      function findItem(item, stableId) {
262        if (item.stableId === stableId) {
263          return item;
264        }
265        if (Array.isArray(item.children)) {
266          for (const child of item.children) {
267            const found = findItem(child, stableId);
268            if (found) {
269              return found;
270            }
271          }
272        }
273        return null;
274      }
275
276      if (this.lastSelectedStableId) {
277        const found = findItem(item, this.lastSelectedStableId);
278        if (found) {
279          this.itemSelected(found);
280        }
281      }
282    },
283    arrowUp() {
284      return this.$refs.hierarchy.selectPrev();
285    },
286    arrowDown() {
287      return this.$refs.hierarchy.selectNext();
288    },
289    getDataWithOffset(offset) {
290      const index = this.file.selectedIndex + offset;
291
292      if (index < 0 || index >= this.file.data.length) {
293        return null;
294      }
295
296      return this.file.data[index];
297    },
298    getItemFromPrevTree(entry) {
299      if (!this.showPropertiesDiff || !this.hierarchySelected) {
300        return null;
301      }
302
303      const id = entry.stableId;
304      if (!id) {
305        throw new Error('Entry has no stableId...');
306      }
307
308      const prevTree = this.getDataWithOffset(-1);
309      if (!prevTree) {
310        console.warn('No previous entry');
311        return null;
312      }
313
314      const prevEntry = findEntryInTree(prevTree, id);
315      if (!prevEntry) {
316        console.warn('Didn\'t exist in last entry');
317        // TODO: Maybe handle this in some way.
318      }
319
320      return prevEntry;
321    },
322
323    /** Performs check for id match between entry and present tags/errors
324     * must be carried out for every present tag/error
325     */
326    matchItems(flickerItems, entryItem) {
327      var match = false;
328      flickerItems.forEach(flickerItem => {
329        if (isPropertyMatch(flickerItem, entryItem)) match = true;
330      });
331      return match;
332    },
333    /** Returns check for id match between entry and present tags/errors */
334    isEntryTagMatch(entryItem) {
335      return this.matchItems(this.presentTags, entryItem) || this.matchItems(this.presentErrors, entryItem);
336    },
337
338    /** determines whether left/right arrow keys should move cursor in input field */
339    updateInputMode(isInputMode) {
340      this.store.isInputMode = isInputMode;
341    },
342  },
343  created() {
344    this.setData(this.file.data[this.file.selectedIndex ?? 0]);
345  },
346  destroyed() {
347    this.store.flickerTraceView = false;
348  },
349  watch: {
350    selectedIndex() {
351      this.setData(this.file.data[this.file.selectedIndex ?? 0]);
352    },
353    showHierarchyDiff() {
354      this.tree = this.generateTreeFromItem(this.item);
355    },
356    showPropertiesDiff() {
357      if (this.hierarchySelected) {
358        this.selectedTree =
359            this.getTransformedProperties(this.hierarchySelected);
360      }
361    },
362  },
363  computed: {
364    diffVisualizationAvailable() {
365      return CompatibleFeatures.DiffVisualization && (
366        this.file.type == TRACE_TYPES.WINDOW_MANAGER ||
367          this.file.type == TRACE_TYPES.SURFACE_FLINGER
368      );
369    },
370    selectedIndex() {
371      return this.file.selectedIndex;
372    },
373    hierarchyFilter() {
374      const hierarchyPropertyFilter =
375          getFilter(this.hierarchyPropertyFilterString);
376      var fil = this.store.onlyVisible ? (c) => {
377        return c.isVisible && hierarchyPropertyFilter(c);
378      } : hierarchyPropertyFilter;
379      return this.store.flickerTraceView ? (c) => {
380        return this.isEntryTagMatch(c);
381      } : fil;
382    },
383    propertyFilter() {
384      return getFilter(this.propertyFilterString);
385    },
386    hasScreenView() {
387      return this.file.type == TRACE_TYPES.WINDOW_MANAGER ||
388          this.file.type == TRACE_TYPES.SURFACE_FLINGER ||
389          this.file.type == DUMP_TYPES.WINDOW_MANAGER ||
390          this.file.type == DUMP_TYPES.SURFACE_FLINGER;
391    },
392    elementSummary() {
393      if (!this.hierarchySelected || !this.summarizer) {
394        return null;
395      }
396
397      const summary = this.summarizer(this.hierarchySelected);
398
399      if (summary?.length === 0) {
400        return null;
401      }
402
403      return summary;
404    },
405    hasTagsOrErrors() {
406      return this.presentTags.length > 0 || this.presentErrors.length > 0;
407    },
408  },
409  components: {
410    'tree-view': TreeView,
411    'rects': Rects,
412    'flat-card': FlatCard,
413  },
414};
415
416function getFilter(filterString) {
417  const filterStrings = filterString.split(',');
418  const positive = [];
419  const negative = [];
420  filterStrings.forEach((f) => {
421    if (f.startsWith('!')) {
422      const str = f.substring(1);
423      negative.push((s) => s.indexOf(str) === -1);
424    } else {
425      const str = f;
426      positive.push((s) => s.indexOf(str) !== -1);
427    }
428  });
429  const filter = (item) => {
430    const apply = (f) => f(String(item.name));
431    return (positive.length === 0 || positive.some(apply)) &&
432          (negative.length === 0 || negative.every(apply));
433  };
434  return filter;
435}
436
437</script>
438<style scoped>
439.container {
440  display: flex;
441  flex-wrap: wrap;
442}
443
444.rects {
445  flex: none;
446  margin: 8px;
447}
448
449.hierarchy,
450.properties {
451  flex: 1;
452  margin: 8px;
453  min-width: 400px;
454  min-height: 50rem;
455}
456
457.rects,
458.hierarchy,
459.properties {
460  padding: 5px;
461}
462
463.flat-card {
464  display: flex;
465  flex-direction: column;
466  height: 100%;
467}
468
469.hierarchy>.tree-view,
470.properties>.tree-view {
471  margin: 16px;
472}
473
474.treeview {
475  overflow: auto;
476  white-space: pre-line;
477}
478
479.no-properties {
480  display: flex;
481  flex: 1;
482  flex-direction: column;
483  align-self: center;
484  align-items: center;
485  justify-content: center;
486  padding: 50px 25px;
487}
488
489.no-properties .none-icon {
490  font-size: 35px;
491  margin-bottom: 10px;
492}
493
494.no-properties span {
495  font-weight: 100;
496}
497
498.filter {
499  width: auto;
500}
501
502.element-summary {
503  padding: 1rem;
504  border-bottom: thin solid rgba(0,0,0,.12);
505}
506
507.element-summary .key {
508  font-weight: 500;
509}
510
511.element-summary .value {
512  color: rgba(0, 0, 0, 0.75);
513}
514
515.properties-content {
516  display: flex;
517  flex-direction: column;
518  flex: 1;
519}
520
521.tree-view-wrapper {
522  display: flex;
523  flex-direction: column;
524  flex: 1;
525}
526
527.treeview {
528  flex: 1 0 0;
529}
530</style>
531