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