/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Inject, Input, Output, } from '@angular/core'; import {PersistentStore} from 'common/persistent_store'; import {TraceType} from 'trace/trace_type'; import {HierarchyTreeNode, UiTreeNode, UiTreeUtils} from 'viewers/common/ui_tree_utils'; import {nodeStyles, treeNodeDataViewStyles} from 'viewers/components/styles/node.styles'; @Component({ selector: 'tree-view', changeDetection: ChangeDetectionStrategy.OnPush, template: `
`, styles: [nodeStyles, treeNodeDataViewStyles], }) export class TreeComponent { diffClass = UiTreeUtils.diffClass; isHighlighted = UiTreeUtils.isHighlighted; // TODO (b/263779536): this array is passed down from viewers/presenters and is used to generate // an identifier supposed to be unique for each viewer. Let's just use a proper identifier // instead. Each viewer/presenter could pass down a random magic number, an UUID, ... @Input() dependencies: TraceType[] = []; @Input() item?: UiTreeNode; @Input() store!: PersistentStore; @Input() isFlattened? = false; @Input() initialDepth = 0; @Input() highlightedItems: string[] = []; @Input() pinnedItems?: HierarchyTreeNode[] = []; @Input() itemsClickable?: boolean; @Input() useGlobalCollapsedState?: boolean; @Input() isAlwaysCollapsed?: boolean; @Input() showNode = (item: UiTreeNode) => true; @Input() isLeaf = (item?: UiTreeNode) => { return !item || !item.children || item.children.length === 0; }; @Output() highlightedItemChange = new EventEmitter(); @Output() selectedTreeChange = new EventEmitter(); @Output() pinnedItemChange = new EventEmitter(); @Output() hoverStart = new EventEmitter(); @Output() hoverEnd = new EventEmitter(); isCollapsedByDefault = true; localCollapsedState = this.isCollapsedByDefault; nodeHover = false; childHover = false; readonly levelOffset = 24; nodeElement: HTMLElement; childTrackById(index: number, child: UiTreeNode): string { if (child.stableId !== undefined) { return child.stableId; } if (!(child instanceof HierarchyTreeNode) && typeof child.propertyKey === 'string') { return child.propertyKey; } throw Error('Missing stable id or property key on node'); } constructor(@Inject(ElementRef) public elementRef: ElementRef) { this.nodeElement = elementRef.nativeElement.querySelector('.node'); this.nodeElement?.addEventListener('mousedown', this.nodeMouseDownEventListener); this.nodeElement?.addEventListener('mouseenter', this.nodeMouseEnterEventListener); this.nodeElement?.addEventListener('mouseleave', this.nodeMouseLeaveEventListener); } ngOnInit() { if (this.isCollapsedByDefault) { this.setCollapseValue(this.isCollapsedByDefault); } } ngOnChanges() { if ( this.item instanceof HierarchyTreeNode && UiTreeUtils.isHighlighted(this.item, this.highlightedItems) ) { this.selectedTreeChange.emit(this.item); } } ngOnDestroy() { this.nodeElement?.removeEventListener('mousedown', this.nodeMouseDownEventListener); this.nodeElement?.removeEventListener('mouseenter', this.nodeMouseEnterEventListener); this.nodeElement?.removeEventListener('mouseleave', this.nodeMouseLeaveEventListener); } onNodeClick(event: MouseEvent) { event.preventDefault(); if (window.getSelection()?.type === 'range') { return; } const isDoubleClick = event.detail % 2 === 0; if (!this.isLeaf(this.item) && isDoubleClick) { event.preventDefault(); this.toggleTree(); } else { this.updateHighlightedItems(); } } nodeOffsetStyle() { const offset = this.levelOffset * this.initialDepth + 'px'; return { marginLeft: '-' + offset, paddingLeft: offset, }; } private updateHighlightedItems() { if (this.item?.stableId) { this.highlightedItemChange.emit(`${this.item.stableId}`); } } isPinned() { if (this.item instanceof HierarchyTreeNode) { return this.pinnedItems?.map((item) => `${item.stableId}`).includes(`${this.item.stableId}`); } return false; } propagateNewHighlightedItem(newId: string) { this.highlightedItemChange.emit(newId); } propagateNewPinnedItem(newPinnedItem: UiTreeNode) { this.pinnedItemChange.emit(newPinnedItem); } propagateNewSelectedTree(newTree: UiTreeNode) { this.selectedTreeChange.emit(newTree); } isClickable() { return !this.isLeaf(this.item) || this.itemsClickable; } toggleTree() { this.setCollapseValue(!this.isCollapsed()); } expandTree() { this.setCollapseValue(true); } isCollapsed() { if (this.isAlwaysCollapsed || this.isLeaf(this.item)) { return true; } if (this.useGlobalCollapsedState) { return ( this.store.get(`collapsedState.item.${this.dependencies}.${this.item?.stableId}`) === 'true' ?? this.isCollapsedByDefault ); } return this.localCollapsedState; } children(): UiTreeNode[] { return this.item?.children ?? []; } hasChildren() { if (!this.item) { return false; } const isParentEntryInFlatView = UiTreeUtils.isParentNode(this.item.kind ?? '') && this.isFlattened; return (!this.isFlattened || isParentEntryInFlatView) && !this.isLeaf(this.item); } private setCollapseValue(isCollapsed: boolean) { if (this.useGlobalCollapsedState) { this.store.add( `collapsedState.item.${this.dependencies}.${this.item?.stableId}`, `${isCollapsed}` ); } else { this.localCollapsedState = isCollapsed; } } private nodeMouseDownEventListener = (event: MouseEvent) => { if (event.detail > 1) { event.preventDefault(); return false; } return true; }; private nodeMouseEnterEventListener = () => { this.nodeHover = true; this.hoverStart.emit(); }; private nodeMouseLeaveEventListener = () => { this.nodeHover = false; this.hoverEnd.emit(); }; }