• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2024 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 {
18  Component,
19  ElementRef,
20  EventEmitter,
21  HostListener,
22  Inject,
23  Input,
24  Output,
25} from '@angular/core';
26import {Color} from 'app/colors';
27import {InMemoryStorage} from 'common/store/in_memory_storage';
28import {PersistentStore} from 'common/store/persistent_store';
29import {Analytics} from 'logging/analytics';
30import {UserWarning} from 'messaging/user_warning';
31import {TraceType} from 'trace/trace_type';
32import {RectShowState} from 'viewers/common/rect_show_state';
33import {TableProperties} from 'viewers/common/table_properties';
34import {TextFilter} from 'viewers/common/text_filter';
35import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node';
36import {UiTreeUtils} from 'viewers/common/ui_tree_utils';
37import {UserOptions} from 'viewers/common/user_options';
38import {ViewerEvents} from 'viewers/common/viewer_events';
39import {nodeStyles} from 'viewers/components/styles/node.styles';
40import {viewerCardInnerStyle} from './styles/viewer_card.styles';
41
42@Component({
43  selector: 'hierarchy-view',
44  template: `
45    <div class="view-header">
46      <div class="title-section">
47        <collapsible-section-title
48          class="hierarchy-title"
49          title="HIERARCHY"
50          (collapseButtonClicked)="collapseButtonClicked.emit()"></collapsible-section-title>
51        <search-box
52          formFieldClass="applied-field"
53          [textFilter]="textFilter"
54          (filterChange)="onFilterChange($event)"></search-box>
55      </div>
56      <user-options
57        class="view-controls"
58        [userOptions]="userOptions"
59        [eventType]="ViewerEvents.HierarchyUserOptionsChange"
60        [traceType]="dependencies[0]"
61        [logCallback]="Analytics.Navigation.logHierarchySettingsChanged">
62      </user-options>
63      <ng-container *ngIf="getWarnings().length > 0">
64        <span
65          *ngFor="let warning of getWarnings()"
66          class="mat-body-1 warning"
67          [matTooltip]="warning.getMessage()"
68          [matTooltipDisabled]="disableTooltip(warningEl)">
69          <mat-icon class="warning-icon"> warning </mat-icon>
70          <span class="warning-message" #warningEl>{{warning.getMessage()}}</span>
71        </span>
72      </ng-container>
73      <properties-table
74        *ngIf="tableProperties"
75        class="properties-table"
76        [properties]="tableProperties"></properties-table>
77      <div
78        *ngIf="pinnedItems.length > 0"
79        class="pinned-items"
80        [style.padding]="getPinnedItemsPadding()">
81        <tree-node
82          *ngFor="let pinnedItem of pinnedItems"
83          class="node full-opacity"
84          [class]="pinnedItem.getDiff()"
85          [class.selected]="isHighlighted(pinnedItem, highlightedItem)"
86          [class.clickable]="true"
87          [node]="pinnedItem"
88          [isPinned]="true"
89          [isInPinnedSection]="true"
90          [isSelected]="isHighlighted(pinnedItem, highlightedItem)"
91          (pinNodeChange)="onPinnedItemChange($event)"
92          (click)="onPinnedNodeClick($event, pinnedItem)"></tree-node>
93      </div>
94    </div>
95    <mat-divider></mat-divider>
96    <span class="mat-body-1 placeholder-text" *ngIf="showPlaceholderText()"> {{ placeholderText + ' Try changing timeline position.' }} </span>
97    <div class="hierarchy-content tree-wrapper">
98      <div class="trees">
99        <tree-view
100          *ngFor="let tree of trees; trackBy: trackById"
101          class="tree"
102          [node]="tree"
103          [isFlattened]="isFlattened()"
104          [useStoredExpandedState]="true"
105          [highlightedItem]="highlightedItem"
106          [pinnedItems]="pinnedItems"
107          [itemsClickable]="true"
108          [rectIdToShowState]="rectIdToShowState"
109          [store]="treeStorage"
110          (highlightedChange)="onHighlightedItemChange($event)"
111          (pinnedItemChange)="onPinnedItemChange($event)"
112          (selectedTreeChange)="onSelectedTreeChange($event)"></tree-view>
113      </div>
114    </div>
115  `,
116  styles: [
117    `
118      .view-header {
119        display: flex;
120        flex-direction: column;
121      }
122
123      .properties-table {
124        padding-top: 5px;
125      }
126
127      .hierarchy-content {
128        height: 100%;
129        overflow: auto;
130        padding: 0px 12px;
131      }
132
133      .pinned-items {
134        width: 100%;
135        box-sizing: border-box;
136        border: 2px solid ${Color.PINNED_ITEM_BORDER};
137      }
138
139      tree-view {
140        overflow: auto;
141      }
142    `,
143    nodeStyles,
144    viewerCardInnerStyle,
145  ],
146})
147export class HierarchyComponent {
148  isHighlighted = UiTreeUtils.isHighlighted;
149  ViewerEvents = ViewerEvents;
150  Analytics = Analytics;
151  treeStorage = new InMemoryStorage();
152
153  @Input() trees: UiHierarchyTreeNode[] = [];
154  @Input() tableProperties: TableProperties | undefined;
155  @Input() dependencies: TraceType[] = [];
156  @Input() highlightedItem = '';
157  @Input() pinnedItems: UiHierarchyTreeNode[] = [];
158  @Input() store: PersistentStore | undefined;
159  @Input() userOptions: UserOptions = {};
160  @Input() rectIdToShowState?: Map<string, RectShowState>;
161  @Input() placeholderText = 'No entry found.';
162  @Input() textFilter: TextFilter | undefined;
163
164  @Output() collapseButtonClicked = new EventEmitter();
165
166  constructor(
167    @Inject(ElementRef) private elementRef: ElementRef<HTMLElement>,
168  ) {}
169
170  trackById(index: number, child: UiHierarchyTreeNode): string {
171    return child.id;
172  }
173
174  isFlattened(): boolean {
175    return this.userOptions['flat']?.enabled;
176  }
177
178  showPlaceholderText(): boolean {
179    return this.trees.length === 0 && !!this.placeholderText;
180  }
181
182  getWarnings(): UserWarning[] {
183    return this.trees.flatMap((tree) => tree.getWarnings());
184  }
185
186  onPinnedNodeClick(event: MouseEvent, pinnedItem: UiHierarchyTreeNode) {
187    event.preventDefault();
188    if (window.getSelection()?.type === 'range') {
189      return;
190    }
191    this.onHighlightedItemChange(pinnedItem);
192  }
193
194  onFilterChange(detail: TextFilter) {
195    const event = new CustomEvent(ViewerEvents.HierarchyFilterChange, {
196      bubbles: true,
197      detail,
198    });
199    this.elementRef.nativeElement.dispatchEvent(event);
200  }
201
202  onHighlightedItemChange(node: UiHierarchyTreeNode) {
203    const event = new CustomEvent(ViewerEvents.HighlightedNodeChange, {
204      bubbles: true,
205      detail: {node},
206    });
207    this.elementRef.nativeElement.dispatchEvent(event);
208  }
209
210  onPinnedItemChange(item: UiHierarchyTreeNode) {
211    const event = new CustomEvent(ViewerEvents.HierarchyPinnedChange, {
212      bubbles: true,
213      detail: {pinnedItem: item},
214    });
215    this.elementRef.nativeElement.dispatchEvent(event);
216  }
217
218  disableTooltip(el: HTMLElement) {
219    return el.scrollWidth === el.clientWidth;
220  }
221
222  getPinnedItemsPadding() {
223    const addGutter = (this.rectIdToShowState?.size ?? 0) > 0;
224    return `0px 10.5px 0px ${addGutter ? 22.5 : 10.5}px`;
225  }
226
227  @HostListener('document:keydown', ['$event'])
228  async handleKeyboardEvent(event: KeyboardEvent) {
229    const domRect = this.elementRef.nativeElement.getBoundingClientRect();
230    const componentVisible = domRect.height > 0 && domRect.width > 0;
231    if (
232      componentVisible &&
233      (event.key === 'ArrowDown' || event.key === 'ArrowUp')
234    ) {
235      event.preventDefault();
236      const details = {bubbles: true, detail: this.treeStorage};
237      if (event.key === 'ArrowDown') {
238        const arrowEvent = new CustomEvent(
239          ViewerEvents.ArrowDownPress,
240          details,
241        );
242        this.elementRef.nativeElement.dispatchEvent(arrowEvent);
243      } else {
244        const arrowEvent = new CustomEvent(ViewerEvents.ArrowUpPress, details);
245        this.elementRef.nativeElement.dispatchEvent(arrowEvent);
246      }
247    }
248  }
249}
250