1// Copyright (C) 2019 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import * as m from 'mithril'; 16 17import {LogExists, LogExistsKey} from '../common/logs'; 18 19import {AggregationPanel} from './aggregation_panel'; 20import {ChromeSliceDetailsPanel} from './chrome_slice_panel'; 21import {CounterDetailsPanel} from './counter_panel'; 22import {CpuProfileDetailsPanel} from './cpu_profile_panel'; 23import {DragGestureHandler} from './drag_gesture_handler'; 24import {globals} from './globals'; 25import {HeapProfileDetailsPanel} from './heap_profile_panel'; 26import {LogPanel} from './logs_panel'; 27import {NotesEditorPanel} from './notes_panel'; 28import {AnyAttrsVnode, PanelContainer} from './panel_container'; 29import {SliceDetailsPanel} from './slice_panel'; 30import {ThreadStatePanel} from './thread_state_panel'; 31 32const UP_ICON = 'keyboard_arrow_up'; 33const DOWN_ICON = 'keyboard_arrow_down'; 34const DRAG_HANDLE_HEIGHT_PX = 28; 35const DEFAULT_DETAILS_HEIGHT_PX = 230 + DRAG_HANDLE_HEIGHT_PX; 36 37function getFullScreenHeight() { 38 const panelContainer = 39 document.querySelector('.pan-and-zoom-content') as HTMLElement; 40 if (panelContainer !== null) { 41 return panelContainer.clientHeight; 42 } else { 43 return DEFAULT_DETAILS_HEIGHT_PX; 44 } 45} 46 47function hasLogs(): boolean { 48 const data = globals.trackDataStore.get(LogExistsKey) as LogExists; 49 return data && data.exists; 50} 51 52interface DragHandleAttrs { 53 height: number; 54 resize: (height: number) => void; 55 tabs: string[]; 56} 57 58class DragHandle implements m.ClassComponent<DragHandleAttrs> { 59 private dragStartHeight = 0; 60 private height = 0; 61 private previousHeight = this.height; 62 private resize: (height: number) => void = () => {}; 63 private isClosed = this.height <= DRAG_HANDLE_HEIGHT_PX; 64 private isFullscreen = false; 65 // We can't get real fullscreen height until the pan_and_zoom_handler exists. 66 private fullscreenHeight = DEFAULT_DETAILS_HEIGHT_PX; 67 private tabNames = new Map<string, string>([ 68 ['current_selection', 'Current Selection'], 69 ['android_logs', 'Android Logs'], 70 ]); 71 72 73 oncreate({dom, attrs}: m.CVnodeDOM<DragHandleAttrs>) { 74 this.resize = attrs.resize; 75 this.height = attrs.height; 76 this.isClosed = this.height <= DRAG_HANDLE_HEIGHT_PX; 77 this.fullscreenHeight = getFullScreenHeight(); 78 const elem = dom as HTMLElement; 79 new DragGestureHandler( 80 elem, 81 this.onDrag.bind(this), 82 this.onDragStart.bind(this), 83 this.onDragEnd.bind(this)); 84 } 85 86 onupdate({attrs}: m.CVnodeDOM<DragHandleAttrs>) { 87 this.resize = attrs.resize; 88 this.height = attrs.height; 89 this.isClosed = this.height <= DRAG_HANDLE_HEIGHT_PX; 90 } 91 92 onDrag(_x: number, y: number) { 93 const newHeight = 94 Math.floor(this.dragStartHeight + (DRAG_HANDLE_HEIGHT_PX / 2) - y); 95 this.isClosed = newHeight <= DRAG_HANDLE_HEIGHT_PX; 96 this.isFullscreen = newHeight >= this.fullscreenHeight; 97 this.resize(newHeight); 98 globals.rafScheduler.scheduleFullRedraw(); 99 } 100 101 onDragStart(_x: number, _y: number) { 102 this.dragStartHeight = this.height; 103 } 104 105 onDragEnd() {} 106 107 view({attrs}: m.CVnode<DragHandleAttrs>) { 108 const icon = this.isClosed ? UP_ICON : DOWN_ICON; 109 const title = this.isClosed ? 'Show panel' : 'Hide panel'; 110 const renderTab = (key: string) => { 111 if (globals.frontendLocalState.currentTab === key || 112 globals.frontendLocalState.currentTab === undefined && 113 attrs.tabs[0] === key) { 114 return m( 115 '.tab[active]', 116 this.tabNames.get(key) === undefined ? key : 117 this.tabNames.get(key)); 118 } 119 return m( 120 '.tab', 121 { 122 onclick: () => { 123 globals.frontendLocalState.currentTab = key; 124 globals.rafScheduler.scheduleFullRedraw(); 125 } 126 }, 127 this.tabNames.get(key) === undefined ? key : this.tabNames.get(key)); 128 }; 129 return m( 130 '.handle', 131 m('.tabs', attrs.tabs.map(renderTab)), 132 m('.buttons', 133 m('i.material-icons', 134 { 135 onclick: () => { 136 this.isClosed = false; 137 this.isFullscreen = true; 138 this.resize(this.fullscreenHeight); 139 globals.rafScheduler.scheduleFullRedraw(); 140 }, 141 title: 'Open fullscreen', 142 disabled: this.isFullscreen 143 }, 144 'vertical_align_top'), 145 m('i.material-icons', 146 { 147 onclick: () => { 148 if (this.height === DRAG_HANDLE_HEIGHT_PX) { 149 this.isClosed = false; 150 this.resize(this.previousHeight); 151 } else { 152 this.isFullscreen = false; 153 this.isClosed = true; 154 this.previousHeight = this.height; 155 this.resize(DRAG_HANDLE_HEIGHT_PX); 156 } 157 globals.rafScheduler.scheduleFullRedraw(); 158 }, 159 title 160 }, 161 icon))); 162 } 163} 164 165export class DetailsPanel implements m.ClassComponent { 166 private detailsHeight = DRAG_HANDLE_HEIGHT_PX; 167 // Used to set details panel to default height on selection. 168 private showDetailsPanel = true; 169 170 view() { 171 const detailsPanels: Map<string, AnyAttrsVnode> = new Map(); 172 const curSelection = globals.state.currentSelection; 173 if (curSelection) { 174 switch (curSelection.kind) { 175 case 'NOTE': 176 detailsPanels.set('current_selection', m(NotesEditorPanel, { 177 key: 'notes', 178 id: curSelection.id, 179 })); 180 break; 181 case 'SLICE': 182 detailsPanels.set('current_selection', m(SliceDetailsPanel, { 183 key: 'slice', 184 })); 185 break; 186 case 'COUNTER': 187 detailsPanels.set('current_selection', m(CounterDetailsPanel, { 188 key: 'counter', 189 })); 190 break; 191 case 'HEAP_PROFILE': 192 detailsPanels.set( 193 'current_selection', 194 m(HeapProfileDetailsPanel, {key: 'heap_profile'})); 195 break; 196 case 'CPU_PROFILE_SAMPLE': 197 detailsPanels.set('current_selection', m(CpuProfileDetailsPanel, { 198 key: 'cpu_profile_sample', 199 })); 200 break; 201 case 'CHROME_SLICE': 202 detailsPanels.set('current_selection', m(ChromeSliceDetailsPanel)); 203 break; 204 case 'THREAD_STATE': 205 detailsPanels.set('current_selection', m(ThreadStatePanel, { 206 key: 'thread_state', 207 ts: curSelection.ts, 208 dur: curSelection.dur, 209 utid: curSelection.utid, 210 state: curSelection.state, 211 cpu: curSelection.cpu 212 })); 213 break; 214 default: 215 break; 216 } 217 } 218 if (hasLogs()) { 219 detailsPanels.set('android_logs', m(LogPanel, {})); 220 } 221 222 for (const [key, value] of globals.aggregateDataStore.entries()) { 223 if (value.columns.length > 0 && value.columns[0].data.length > 0) { 224 detailsPanels.set( 225 value.tabName, m(AggregationPanel, {kind: key, data: value})); 226 } 227 } 228 229 const wasShowing = this.showDetailsPanel; 230 this.showDetailsPanel = detailsPanels.size > 0; 231 // The first time the details panel appears, it should be default height. 232 if (!wasShowing && this.showDetailsPanel) { 233 this.detailsHeight = DEFAULT_DETAILS_HEIGHT_PX; 234 } 235 236 const panel = globals.frontendLocalState.currentTab && 237 detailsPanels.has(globals.frontendLocalState.currentTab) ? 238 detailsPanels.get(globals.frontendLocalState.currentTab) : 239 detailsPanels.values().next().value; 240 const panels = panel ? [panel] : []; 241 242 return m( 243 '.details-content', 244 { 245 style: { 246 height: `${this.detailsHeight}px`, 247 display: this.showDetailsPanel ? null : 'none' 248 } 249 }, 250 m(DragHandle, { 251 resize: (height: number) => { 252 this.detailsHeight = Math.max(height, DRAG_HANDLE_HEIGHT_PX); 253 }, 254 height: this.detailsHeight, 255 tabs: [...detailsPanels.keys()], 256 }), 257 m('.details-panel-container', 258 m(PanelContainer, {doesScroll: true, panels, kind: 'DETAILS'}))); 259 } 260} 261