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 {PersistentStore} from 'common/persistent_store';
25import {Analytics} from 'logging/analytics';
26import {TraceType} from 'trace/trace_type';
27import {RectShowState} from 'viewers/common/rect_show_state';
28import {TableProperties} from 'viewers/common/table_properties';
29import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node';
30import {UiTreeUtils} from 'viewers/common/ui_tree_utils';
31import {UserOptions} from 'viewers/common/user_options';
32import {ViewerEvents} from 'viewers/common/viewer_events';
33import {nodeStyles} from 'viewers/components/styles/node.styles';
34import {searchBoxStyle} from './styles/search_box.styles';
35import {viewerCardInnerStyle} from './styles/viewer_card.styles';
36
37@Component({
38  selector: 'hierarchy-view',
39  template: `
40    <div class="view-header">
41      <div class="title-section">
42        <collapsible-section-title
43          class="hierarchy-title"
44          title="HIERARCHY"
45          (collapseButtonClicked)="collapseButtonClicked.emit()"></collapsible-section-title>
46        <mat-form-field class="search-box" (keydown.enter)="$event.target.blur()">
47          <mat-label>Search</mat-label>
48          <input
49            matInput
50            [(ngModel)]="filterString"
51            (ngModelChange)="onFilterChange()"
52            name="filter" />
53        </mat-form-field>
54      </div>
55      <user-options
56        class="view-controls"
57        [userOptions]="userOptions"
58        [eventType]="ViewerEvents.HierarchyUserOptionsChange"
59        [traceType]="dependencies[0]"
60        [logCallback]="Analytics.Navigation.logHierarchySettingsChanged">
61      </user-options>
62      <properties-table
63        *ngIf="tableProperties"
64        class="properties-table"
65        [properties]="tableProperties"></properties-table>
66      <div *ngIf="pinnedItems.length > 0" class="pinned-items">
67        <tree-node
68          *ngFor="let pinnedItem of pinnedItems"
69          class="node"
70          [class]="pinnedItem.getDiff()"
71          [class.selected]="isHighlighted(pinnedItem, highlightedItem)"
72          [class.clickable]="true"
73          [node]="pinnedItem"
74          [isPinned]="true"
75          [isInPinnedSection]="true"
76          [isSelected]="isHighlighted(pinnedItem, highlightedItem)"
77          (pinNodeChange)="onPinnedItemChange($event)"
78          (click)="onPinnedNodeClick($event, pinnedItem)"></tree-node>
79      </div>
80    </div>
81    <mat-divider></mat-divider>
82    <div class="hierarchy-content tree-wrapper">
83      <tree-view
84        *ngIf="tree"
85        [isFlattened]="isFlattened()"
86        [node]="tree"
87        [useStoredExpandedState]="true"
88        [itemsClickable]="true"
89        [highlightedItem]="highlightedItem"
90        [pinnedItems]="pinnedItems"
91        [rectIdToShowState]="rectIdToShowState"
92        (highlightedChange)="onHighlightedItemChange($event)"
93        (pinnedItemChange)="onPinnedItemChange($event)"
94        (selectedTreeChange)="onSelectedTreeChange($event)"></tree-view>
95
96      <div class="subtrees">
97        <tree-view
98          *ngFor="let subtree of subtrees; trackBy: trackById"
99          class="subtree"
100          [node]="subtree"
101          [isFlattened]="isFlattened()"
102          [useStoredExpandedState]="true"
103          [highlightedItem]="highlightedItem"
104          [pinnedItems]="pinnedItems"
105          [itemsClickable]="true"
106          [rectIdToShowState]="rectIdToShowState"
107          (highlightedChange)="onHighlightedItemChange($event)"
108          (pinnedItemChange)="onPinnedItemChange($event)"
109          (selectedTreeChange)="onSelectedTreeChange($event)"></tree-view>
110      </div>
111    </div>
112  `,
113  styles: [
114    `
115      .view-header {
116        display: flex;
117        flex-direction: column;
118      }
119
120      .properties-table {
121        padding-top: 5px;
122      }
123
124      .hierarchy-content {
125        height: 100%;
126        overflow: auto;
127        padding: 0px 12px;
128      }
129
130      .pinned-items {
131        width: 100%;
132        box-sizing: border-box;
133        border: 2px solid #ffd58b;
134      }
135
136      tree-view {
137        overflow: auto;
138      }
139    `,
140    nodeStyles,
141    searchBoxStyle,
142    viewerCardInnerStyle,
143  ],
144})
145export class HierarchyComponent {
146  filterString = '';
147  isHighlighted = UiTreeUtils.isHighlighted;
148  ViewerEvents = ViewerEvents;
149  Analytics = Analytics;
150
151  @Input() tree: UiHierarchyTreeNode | undefined;
152  @Input() subtrees: UiHierarchyTreeNode[] = [];
153  @Input() tableProperties: TableProperties | undefined;
154  @Input() dependencies: TraceType[] = [];
155  @Input() highlightedItem = '';
156  @Input() pinnedItems: UiHierarchyTreeNode[] = [];
157  @Input() store: PersistentStore | undefined;
158  @Input() userOptions: UserOptions = {};
159  @Input() rectIdToShowState?: Map<string, RectShowState>;
160
161  @Output() collapseButtonClicked = new EventEmitter();
162
163  constructor(@Inject(ElementRef) private elementRef: ElementRef) {}
164
165  trackById(index: number, child: UiHierarchyTreeNode): string {
166    return child.id;
167  }
168
169  isFlattened() {
170    return this.userOptions['flat']?.enabled;
171  }
172
173  onPinnedNodeClick(event: MouseEvent, pinnedItem: UiHierarchyTreeNode) {
174    event.preventDefault();
175    if (window.getSelection()?.type === 'range') {
176      return;
177    }
178    this.onHighlightedItemChange(pinnedItem);
179  }
180
181  onFilterChange() {
182    const event = new CustomEvent(ViewerEvents.HierarchyFilterChange, {
183      bubbles: true,
184      detail: {filterString: this.filterString},
185    });
186    this.elementRef.nativeElement.dispatchEvent(event);
187  }
188
189  onHighlightedItemChange(node: UiHierarchyTreeNode) {
190    const event = new CustomEvent(ViewerEvents.HighlightedNodeChange, {
191      bubbles: true,
192      detail: {node},
193    });
194    this.elementRef.nativeElement.dispatchEvent(event);
195  }
196
197  onPinnedItemChange(item: UiHierarchyTreeNode) {
198    const event = new CustomEvent(ViewerEvents.HierarchyPinnedChange, {
199      bubbles: true,
200      detail: {pinnedItem: item},
201    });
202    this.elementRef.nativeElement.dispatchEvent(event);
203  }
204}
205