• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2022 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 */
16import {
17  ChangeDetectionStrategy,
18  Component,
19  ElementRef,
20  EventEmitter,
21  Inject,
22  Input,
23  Output,
24} from '@angular/core';
25import {PersistentStore} from 'common/persistent_store';
26import {TraceType} from 'trace/trace_type';
27import {HierarchyTreeNode, UiTreeNode, UiTreeUtils} from 'viewers/common/ui_tree_utils';
28import {nodeStyles, treeNodeDataViewStyles} from 'viewers/components/styles/node.styles';
29
30@Component({
31  selector: 'tree-view',
32  changeDetection: ChangeDetectionStrategy.OnPush,
33  template: `
34    <tree-node
35      *ngIf="item && showNode(item)"
36      class="node"
37      [class.leaf]="isLeaf(this.item)"
38      [class.selected]="isHighlighted(item, highlightedItems)"
39      [class.clickable]="isClickable()"
40      [class.hover]="nodeHover"
41      [class.childHover]="childHover"
42      [isAlwaysCollapsed]="isAlwaysCollapsed"
43      [class]="diffClass(item)"
44      [style]="nodeOffsetStyle()"
45      [item]="item"
46      [flattened]="isFlattened"
47      [isLeaf]="isLeaf(this.item)"
48      [isCollapsed]="isAlwaysCollapsed ?? isCollapsed()"
49      [hasChildren]="hasChildren()"
50      [isPinned]="isPinned()"
51      (toggleTreeChange)="toggleTree()"
52      (click)="onNodeClick($event)"
53      (expandTreeChange)="expandTree()"
54      (pinNodeChange)="propagateNewPinnedItem($event)"></tree-node>
55
56    <div
57      *ngIf="hasChildren()"
58      class="children"
59      [class.flattened]="isFlattened"
60      [hidden]="!isCollapsed()">
61      <tree-view
62        *ngFor="let child of children(); trackBy: childTrackById"
63        class="childrenTree"
64        [item]="child"
65        [store]="store"
66        [showNode]="showNode"
67        [isLeaf]="isLeaf"
68        [dependencies]="dependencies"
69        [isFlattened]="isFlattened"
70        [useGlobalCollapsedState]="useGlobalCollapsedState"
71        [initialDepth]="initialDepth + 1"
72        [highlightedItems]="highlightedItems"
73        [pinnedItems]="pinnedItems"
74        (highlightedItemChange)="propagateNewHighlightedItem($event)"
75        (pinnedItemChange)="propagateNewPinnedItem($event)"
76        (selectedTreeChange)="propagateNewSelectedTree($event)"
77        [itemsClickable]="itemsClickable"
78        (hoverStart)="childHover = true"
79        (hoverEnd)="childHover = false"></tree-view>
80    </div>
81  `,
82  styles: [nodeStyles, treeNodeDataViewStyles],
83})
84export class TreeComponent {
85  diffClass = UiTreeUtils.diffClass;
86  isHighlighted = UiTreeUtils.isHighlighted;
87
88  // TODO (b/263779536): this array is passed down from viewers/presenters and is used to generate
89  //  an identifier supposed to be unique for each viewer. Let's just use a proper identifier
90  //  instead. Each viewer/presenter could pass down a random magic number, an UUID, ...
91  @Input() dependencies: TraceType[] = [];
92
93  @Input() item?: UiTreeNode;
94  @Input() store!: PersistentStore;
95  @Input() isFlattened? = false;
96  @Input() initialDepth = 0;
97  @Input() highlightedItems: string[] = [];
98  @Input() pinnedItems?: HierarchyTreeNode[] = [];
99  @Input() itemsClickable?: boolean;
100  @Input() useGlobalCollapsedState?: boolean;
101  @Input() isAlwaysCollapsed?: boolean;
102  @Input() showNode = (item: UiTreeNode) => true;
103  @Input() isLeaf = (item?: UiTreeNode) => {
104    return !item || !item.children || item.children.length === 0;
105  };
106
107  @Output() highlightedItemChange = new EventEmitter<string>();
108  @Output() selectedTreeChange = new EventEmitter<UiTreeNode>();
109  @Output() pinnedItemChange = new EventEmitter<UiTreeNode>();
110  @Output() hoverStart = new EventEmitter<void>();
111  @Output() hoverEnd = new EventEmitter<void>();
112
113  isCollapsedByDefault = true;
114  localCollapsedState = this.isCollapsedByDefault;
115  nodeHover = false;
116  childHover = false;
117  readonly levelOffset = 24;
118  nodeElement: HTMLElement;
119
120  childTrackById(index: number, child: UiTreeNode): string {
121    if (child.stableId !== undefined) {
122      return child.stableId;
123    }
124    if (!(child instanceof HierarchyTreeNode) && typeof child.propertyKey === 'string') {
125      return child.propertyKey;
126    }
127
128    throw Error('Missing stable id or property key on node');
129  }
130
131  constructor(@Inject(ElementRef) public elementRef: ElementRef) {
132    this.nodeElement = elementRef.nativeElement.querySelector('.node');
133    this.nodeElement?.addEventListener('mousedown', this.nodeMouseDownEventListener);
134    this.nodeElement?.addEventListener('mouseenter', this.nodeMouseEnterEventListener);
135    this.nodeElement?.addEventListener('mouseleave', this.nodeMouseLeaveEventListener);
136  }
137
138  ngOnInit() {
139    if (this.isCollapsedByDefault) {
140      this.setCollapseValue(this.isCollapsedByDefault);
141    }
142  }
143
144  ngOnChanges() {
145    if (
146      this.item instanceof HierarchyTreeNode &&
147      UiTreeUtils.isHighlighted(this.item, this.highlightedItems)
148    ) {
149      this.selectedTreeChange.emit(this.item);
150    }
151  }
152
153  ngOnDestroy() {
154    this.nodeElement?.removeEventListener('mousedown', this.nodeMouseDownEventListener);
155    this.nodeElement?.removeEventListener('mouseenter', this.nodeMouseEnterEventListener);
156    this.nodeElement?.removeEventListener('mouseleave', this.nodeMouseLeaveEventListener);
157  }
158
159  onNodeClick(event: MouseEvent) {
160    event.preventDefault();
161    if (window.getSelection()?.type === 'range') {
162      return;
163    }
164
165    const isDoubleClick = event.detail % 2 === 0;
166    if (!this.isLeaf(this.item) && isDoubleClick) {
167      event.preventDefault();
168      this.toggleTree();
169    } else {
170      this.updateHighlightedItems();
171    }
172  }
173
174  nodeOffsetStyle() {
175    const offset = this.levelOffset * this.initialDepth + 'px';
176
177    return {
178      marginLeft: '-' + offset,
179      paddingLeft: offset,
180    };
181  }
182
183  private updateHighlightedItems() {
184    if (this.item?.stableId) {
185      this.highlightedItemChange.emit(`${this.item.stableId}`);
186    }
187  }
188
189  isPinned() {
190    if (this.item instanceof HierarchyTreeNode) {
191      return this.pinnedItems?.map((item) => `${item.stableId}`).includes(`${this.item.stableId}`);
192    }
193    return false;
194  }
195
196  propagateNewHighlightedItem(newId: string) {
197    this.highlightedItemChange.emit(newId);
198  }
199
200  propagateNewPinnedItem(newPinnedItem: UiTreeNode) {
201    this.pinnedItemChange.emit(newPinnedItem);
202  }
203
204  propagateNewSelectedTree(newTree: UiTreeNode) {
205    this.selectedTreeChange.emit(newTree);
206  }
207
208  isClickable() {
209    return !this.isLeaf(this.item) || this.itemsClickable;
210  }
211
212  toggleTree() {
213    this.setCollapseValue(!this.isCollapsed());
214  }
215
216  expandTree() {
217    this.setCollapseValue(true);
218  }
219
220  isCollapsed() {
221    if (this.isAlwaysCollapsed || this.isLeaf(this.item)) {
222      return true;
223    }
224
225    if (this.useGlobalCollapsedState) {
226      return (
227        this.store.get(`collapsedState.item.${this.dependencies}.${this.item?.stableId}`) ===
228          'true' ?? this.isCollapsedByDefault
229      );
230    }
231    return this.localCollapsedState;
232  }
233
234  children(): UiTreeNode[] {
235    return this.item?.children ?? [];
236  }
237
238  hasChildren() {
239    if (!this.item) {
240      return false;
241    }
242    const isParentEntryInFlatView =
243      UiTreeUtils.isParentNode(this.item.kind ?? '') && this.isFlattened;
244    return (!this.isFlattened || isParentEntryInFlatView) && !this.isLeaf(this.item);
245  }
246
247  private setCollapseValue(isCollapsed: boolean) {
248    if (this.useGlobalCollapsedState) {
249      this.store.add(
250        `collapsedState.item.${this.dependencies}.${this.item?.stableId}`,
251        `${isCollapsed}`
252      );
253    } else {
254      this.localCollapsedState = isCollapsed;
255    }
256  }
257
258  private nodeMouseDownEventListener = (event: MouseEvent) => {
259    if (event.detail > 1) {
260      event.preventDefault();
261      return false;
262    }
263    return true;
264  };
265
266  private nodeMouseEnterEventListener = () => {
267    this.nodeHover = true;
268    this.hoverStart.emit();
269  };
270
271  private nodeMouseLeaveEventListener = () => {
272    this.nodeHover = false;
273    this.hoverEnd.emit();
274  };
275}
276