• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2023 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {assertDefined} from 'common/assert_utils';
18import {PersistentStoreProxy} from 'common/persistent_store_proxy';
19import {FilterType, TreeUtils} from 'common/tree_utils';
20import {Point} from 'trace/flickerlib/common';
21import {Trace} from 'trace/trace';
22import {Traces} from 'trace/traces';
23import {TraceEntryFinder} from 'trace/trace_entry_finder';
24import {TracePosition} from 'trace/trace_position';
25import {TraceType} from 'trace/trace_type';
26import {Rectangle} from 'viewers/common/rectangle';
27import {TreeGenerator} from 'viewers/common/tree_generator';
28import {TreeTransformer} from 'viewers/common/tree_transformer';
29import {HierarchyTreeNode, PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
30import {UserOptions} from 'viewers/common/user_options';
31import {UiData} from './ui_data';
32
33export class Presenter {
34  private viewCaptureTrace: Trace<object>;
35
36  private selectedFrameData: any | undefined;
37  private previousFrameData: any | undefined;
38  private selectedHierarchyTree: HierarchyTreeNode | undefined;
39
40  private uiData: UiData | undefined;
41
42  private pinnedItems: HierarchyTreeNode[] = [];
43  private pinnedIds: string[] = [];
44
45  private highlightedItems: string[] = [];
46
47  private hierarchyFilter: FilterType = TreeUtils.makeNodeFilter('');
48  private propertiesFilter: FilterType = TreeUtils.makeNodeFilter('');
49
50  private hierarchyUserOptions: UserOptions = PersistentStoreProxy.new<UserOptions>(
51    'SfHierarchyOptions',
52    {
53      showDiff: {
54        name: 'Show diff', // TODO: PersistentStoreObject.Ignored("Show diff") or something like that to instruct to not store this info
55        enabled: false,
56      },
57      simplifyNames: {
58        name: 'Simplify names',
59        enabled: true,
60      },
61      onlyVisible: {
62        name: 'Only visible',
63        enabled: false,
64      },
65    },
66    this.storage
67  );
68
69  private propertiesUserOptions: UserOptions = PersistentStoreProxy.new<UserOptions>(
70    'SfPropertyOptions',
71    {
72      showDiff: {
73        name: 'Show diff',
74        enabled: false,
75      },
76      showDefaults: {
77        name: 'Show defaults',
78        enabled: false,
79        tooltip: `
80                If checked, shows the value of all properties.
81                Otherwise, hides all properties whose value is
82                the default for its data type.
83              `,
84      },
85    },
86    this.storage
87  );
88
89  constructor(
90    traces: Traces,
91    private readonly storage: Storage,
92    private readonly notifyUiDataCallback: (data: UiData) => void
93  ) {
94    this.viewCaptureTrace = assertDefined(traces.getTrace(TraceType.VIEW_CAPTURE));
95  }
96
97  async onTracePositionUpdate(position: TracePosition) {
98    const entry = TraceEntryFinder.findCorrespondingEntry(this.viewCaptureTrace, position);
99
100    let prevEntry: typeof entry;
101    if (entry && entry.getIndex() > 0) {
102      prevEntry = this.viewCaptureTrace.getEntry(entry.getIndex() - 1);
103    }
104
105    this.selectedFrameData = await entry?.getValue();
106    this.previousFrameData = await prevEntry?.getValue();
107
108    this.refreshUI();
109  }
110
111  private refreshUI() {
112    // this.pinnedItems is updated in generateTree, so don't inline
113    const tree = this.generateTree();
114    if (!this.selectedHierarchyTree && tree) {
115      this.selectedHierarchyTree = tree;
116    }
117
118    this.uiData = new UiData(
119      this.generateRectangles(),
120      tree,
121      this.hierarchyUserOptions,
122      this.propertiesUserOptions,
123      this.pinnedItems,
124      this.highlightedItems,
125      this.getTreeWithTransformedProperties(this.selectedHierarchyTree)
126    );
127    this.notifyUiDataCallback(this.uiData);
128  }
129
130  private generateRectangles(): Rectangle[] {
131    const rectangles: Rectangle[] = [];
132
133    function inner(node: any /* ViewNode */) {
134      const aRectangle: Rectangle = {
135        topLeft: new Point(node.boxPos.left, node.boxPos.top),
136        bottomRight: new Point(
137          node.boxPos.left + node.boxPos.width,
138          node.boxPos.top + node.boxPos.height
139        ),
140        label: '',
141        transform: null,
142        isVisible: node.isVisible,
143        isDisplay: false,
144        ref: {},
145        id: node.id,
146        displayId: 0,
147        isVirtual: false,
148        isClickable: true,
149        cornerRadius: 0,
150        depth: node.depth,
151      };
152      rectangles.push(aRectangle);
153      node.children.forEach((it: any) /* ViewNode */ => inner(it));
154    }
155    if (this.selectedFrameData?.node) {
156      inner(this.selectedFrameData.node);
157    }
158
159    return rectangles;
160  }
161
162  private generateTree(): HierarchyTreeNode | null {
163    if (!this.selectedFrameData?.node) {
164      return null;
165    }
166    const generator = new TreeGenerator(
167      this.selectedFrameData.node,
168      this.hierarchyFilter,
169      this.pinnedIds
170    )
171      .setIsOnlyVisibleView(this.hierarchyUserOptions['onlyVisible']?.enabled)
172      .setIsSimplifyNames(this.hierarchyUserOptions['simplifyNames']?.enabled)
173      .withUniqueNodeId();
174
175    this.pinnedItems = generator.getPinnedItems();
176
177    if (this.hierarchyUserOptions['showDiff'].enabled && this.previousFrameData?.node) {
178      return generator
179        .compareWith(this.previousFrameData.node)
180        .withModifiedCheck()
181        .generateFinalTreeWithDiff();
182    } else {
183      return generator.generateTree();
184    }
185  }
186
187  updatePinnedItems(pinnedItem: HierarchyTreeNode) {
188    const pinnedId = `${pinnedItem.id}`;
189    if (this.pinnedItems.map((item) => `${item.id}`).includes(pinnedId)) {
190      this.pinnedItems = this.pinnedItems.filter((pinned) => `${pinned.id}` !== pinnedId);
191    } else {
192      this.pinnedItems.push(pinnedItem);
193    }
194    this.updatePinnedIds(pinnedId);
195    this.uiData!!.pinnedItems = this.pinnedItems;
196    this.copyUiDataAndNotifyView();
197  }
198
199  updatePinnedIds(newId: string) {
200    if (this.pinnedIds.includes(newId)) {
201      this.pinnedIds = this.pinnedIds.filter((pinned) => pinned !== newId);
202    } else {
203      this.pinnedIds.push(newId);
204    }
205  }
206
207  updateHighlightedItems(id: string) {
208    if (this.highlightedItems.includes(id)) {
209      this.highlightedItems = this.highlightedItems.filter((hl) => hl !== id);
210    } else {
211      this.highlightedItems = [];
212      this.highlightedItems.push(id);
213    }
214    this.uiData!!.highlightedItems = this.highlightedItems;
215    this.copyUiDataAndNotifyView();
216  }
217
218  updateHierarchyTree(userOptions: any) {
219    this.hierarchyUserOptions = userOptions;
220    this.uiData!!.hierarchyUserOptions = this.hierarchyUserOptions;
221    this.uiData!!.tree = this.generateTree();
222    this.copyUiDataAndNotifyView();
223  }
224
225  filterHierarchyTree(filterString: string) {
226    this.hierarchyFilter = TreeUtils.makeNodeFilter(filterString);
227    this.uiData!!.tree = this.generateTree();
228    this.copyUiDataAndNotifyView();
229  }
230
231  updatePropertiesTree(userOptions: UserOptions) {
232    this.propertiesUserOptions = userOptions;
233    this.uiData!!.propertiesUserOptions = this.propertiesUserOptions;
234    this.updateSelectedTreeUiData();
235  }
236
237  filterPropertiesTree(filterString: string) {
238    this.propertiesFilter = TreeUtils.makeNodeFilter(filterString);
239    this.updateSelectedTreeUiData();
240  }
241
242  newPropertiesTree(selectedItem: HierarchyTreeNode) {
243    this.selectedHierarchyTree = selectedItem;
244    this.updateSelectedTreeUiData();
245  }
246
247  private updateSelectedTreeUiData() {
248    this.uiData!!.propertiesTree = this.getTreeWithTransformedProperties(
249      this.selectedHierarchyTree
250    );
251    this.copyUiDataAndNotifyView();
252  }
253
254  private getTreeWithTransformedProperties(selectedTree: any): PropertiesTreeNode | null {
255    if (!selectedTree) {
256      return null;
257    }
258
259    return new TreeTransformer(selectedTree, this.propertiesFilter)
260      .setOnlyProtoDump(false)
261      .setIsShowDiff(this.propertiesUserOptions['showDiff']?.enabled)
262      .setIsShowDefaults(this.propertiesUserOptions['showDefaults']?.enabled)
263      .setProperties(this.selectedFrameData?.node)
264      .setDiffProperties(this.previousFrameData?.node)
265      .transform();
266  }
267
268  private copyUiDataAndNotifyView() {
269    // Create a shallow copy of the data, otherwise the Angular OnPush change detection strategy
270    // won't detect the new input
271    const copy = Object.assign({}, this.uiData);
272    this.notifyUiDataCallback(copy);
273  }
274}
275