1<!-- Copyright (C) 2017 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-->
15<template>
16  <div id="app">
17    <md-whiteframe md-tag="md-toolbar">
18      <h1 class="md-title" style="flex: 1">{{title}}</h1>
19      <a class="md-button md-accent md-raised md-theme-default" @click="clear()" v-if="dataLoaded">Clear</a>
20    </md-whiteframe>
21    <div class="main-content">
22      <md-layout v-if="!dataLoaded" class="m-2">
23        <dataadb ref="adb" :store="store" @dataReady="onDataReady" @statusChange="setStatus"/>
24        <datainput ref="input" :store="store" @dataReady="onDataReady" @statusChange="setStatus"/>
25      </md-layout>
26      <md-card v-if="dataLoaded">
27        <md-whiteframe md-tag="md-toolbar" md-elevation="0" class="card-toolbar md-transparent md-dense">
28          <h2 class="md-title">Timeline</h2>
29          <datafilter v-for="file in files" :key="file.filename" :store="store" :file="file" />
30        </md-whiteframe>
31        <md-list>
32          <md-list-item v-for="(file, idx) in files" :key="file.filename">
33            <md-icon>{{file.type.icon}}</md-icon>
34            <timeline :items="file.timeline" :selected-index="file.selectedIndex" :scale="scale" @item-selected="onTimelineItemSelected($event, idx)" class="timeline" />
35          </md-list-item>
36        </md-list>
37      </md-card>
38      <dataview v-for="file in files" :key="file.filename" :ref="file.filename" :store="store" :file="file" @focus="onDataViewFocus(file.filename)" />
39    </div>
40  </div>
41</template>
42<script>
43import TreeView from './TreeView.vue'
44import Timeline from './Timeline.vue'
45import Rects from './Rects.vue'
46import DataView from './DataView.vue'
47import DataInput from './DataInput.vue'
48import LocalStore from './localstore.js'
49import DataAdb from './DataAdb.vue'
50import DataFilter from './DataFilter.vue'
51
52const APP_NAME = "Winscope"
53
54// Find the index of the last element matching the predicate in a sorted array
55function findLastMatchingSorted(array, predicate) {
56  var a = 0;
57  var b = array.length - 1;
58  while (b - a > 1) {
59    var m = Math.floor((a + b) / 2);
60    if (predicate(array, m)) {
61      a = m;
62    } else {
63      b = m - 1;
64    }
65  }
66  return predicate(array, b) ? b : a;
67}
68
69export default {
70  name: 'app',
71  data() {
72    return {
73      files: [],
74      title: APP_NAME,
75      currentTimestamp: 0,
76      activeDataView: null,
77      store: LocalStore('app', {
78        flattened: false,
79        onlyVisible: false,
80        displayDefaults: true,
81      }),
82    }
83  },
84  created() {
85    window.addEventListener('keydown', this.onKeyDown);
86    document.title = this.title;
87  },
88  methods: {
89    clear() {
90      this.files.forEach(function(item) { item.destroy(); })
91      this.files = [];
92      this.activeDataView = null;
93    },
94    onTimelineItemSelected(index, timelineIndex) {
95      this.files[timelineIndex].selectedIndex = index;
96      var t = parseInt(this.files[timelineIndex].timeline[index]);
97      for (var i = 0; i < this.files.length; i++) {
98        if (i != timelineIndex) {
99          this.files[i].selectedIndex = findLastMatchingSorted(this.files[i].timeline, function(array, idx) {
100            return parseInt(array[idx]) <= t;
101          });
102        }
103      }
104      this.currentTimestamp = t;
105    },
106    advanceTimeline(direction) {
107      var closestTimeline = -1;
108      var timeDiff = Infinity;
109      for (var idx = 0; idx < this.files.length; idx++) {
110        var file = this.files[idx];
111        var cur = file.selectedIndex;
112        if (cur + direction < 0 || cur + direction >= this.files[idx].timeline.length) {
113          continue;
114        }
115        var d = Math.abs(parseInt(file.timeline[cur + direction]) - this.currentTimestamp);
116        if (timeDiff > d) {
117          timeDiff = d;
118          closestTimeline = idx;
119        }
120      }
121      if (closestTimeline >= 0) {
122        this.files[closestTimeline].selectedIndex += direction;
123        this.currentTimestamp = parseInt(this.files[closestTimeline].timeline[this.files[closestTimeline].selectedIndex]);
124      }
125    },
126    onDataViewFocus(view) {
127      this.activeDataView = view;
128    },
129    onKeyDown(event) {
130      event = event || window.event;
131      if (event.keyCode == 37 /* left */ ) {
132        this.advanceTimeline(-1);
133      } else if (event.keyCode == 39 /* right */ ) {
134        this.advanceTimeline(1);
135      } else if (event.keyCode == 38 /* up */ ) {
136        this.$refs[this.activeView][0].arrowUp();
137      } else if (event.keyCode == 40 /* down */ ) {
138        this.$refs[this.activeView][0].arrowDown();
139      } else {
140        return false;
141      }
142      event.preventDefault();
143      return true;
144    },
145    onDataReady(files) {
146      this.files = files;
147    },
148    setStatus(status) {
149      if (status) {
150        this.title = status;
151      } else {
152        this.title = APP_NAME;
153      }
154    }
155  },
156  computed: {
157    prettyDump: function() { return JSON.stringify(this.dump, null, 2); },
158    dataLoaded: function() { return this.files.length > 0 },
159    scale() {
160      var mx = Math.max(...(this.files.map(f => Math.max(...f.timeline))));
161      var mi = Math.min(...(this.files.map(f => Math.min(...f.timeline))));
162      return [mi, mx];
163    },
164    activeView: function() {
165      if (!this.activeDataView && this.files.length > 0) {
166        this.activeDataView = this.files[0].filename;
167      }
168      return this.activeDataView;
169    }
170  },
171  watch: {
172    title() {
173      document.title = this.title;
174    }
175  },
176  components: {
177    'timeline': Timeline,
178    'dataview': DataView,
179    'datainput': DataInput,
180    'dataadb': DataAdb,
181    'datafilter': DataFilter,
182  },
183}
184
185</script>
186<style>
187.main-content>* {
188  margin: 1em;
189}
190
191.card-toolbar {
192  border-bottom: 1px solid rgba(0, 0, 0, .12);
193}
194
195.timeline {
196  margin: 16px;
197}
198
199.container {
200  display: flex;
201  flex-wrap: wrap;
202}
203
204.md-layout > .md-card {
205  margin: 0.5em;
206}
207
208.md-button {
209  margin-top: 1em
210}
211
212h1,
213h2 {
214  font-weight: normal;
215}
216
217ul {
218  list-style-type: none;
219  padding: 0;
220}
221
222li {
223  display: inline-block;
224  margin: 0 10px;
225}
226
227a {
228  color: #42b983;
229}
230
231</style>
232