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