1/*
2 * Copyright (C) 2011 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 */
16
17package com.android.ide.eclipse.gltrace.editors;
18
19import com.android.ddmuilib.AbstractBufferFindTarget;
20import com.android.ddmuilib.FindDialog;
21import com.android.ide.eclipse.gltrace.GLProtoBuf.GLMessage.Function;
22import com.android.ide.eclipse.gltrace.GlTracePlugin;
23import com.android.ide.eclipse.gltrace.SwtUtils;
24import com.android.ide.eclipse.gltrace.TraceFileParserTask;
25import com.android.ide.eclipse.gltrace.editors.DurationMinimap.ICallSelectionListener;
26import com.android.ide.eclipse.gltrace.editors.GLCallGroups.GLCallNode;
27import com.android.ide.eclipse.gltrace.model.GLCall;
28import com.android.ide.eclipse.gltrace.model.GLFrame;
29import com.android.ide.eclipse.gltrace.model.GLTrace;
30import com.android.ide.eclipse.gltrace.views.FrameSummaryViewPage;
31import com.android.ide.eclipse.gltrace.views.detail.DetailsPage;
32import com.google.common.base.Charsets;
33import com.google.common.io.Files;
34
35import org.eclipse.core.runtime.IProgressMonitor;
36import org.eclipse.core.runtime.IStatus;
37import org.eclipse.core.runtime.Status;
38import org.eclipse.core.runtime.jobs.Job;
39import org.eclipse.jface.action.Action;
40import org.eclipse.jface.dialogs.ErrorDialog;
41import org.eclipse.jface.dialogs.MessageDialog;
42import org.eclipse.jface.dialogs.ProgressMonitorDialog;
43import org.eclipse.jface.resource.ImageDescriptor;
44import org.eclipse.jface.viewers.CellLabelProvider;
45import org.eclipse.jface.viewers.ColumnLabelProvider;
46import org.eclipse.jface.viewers.ISelection;
47import org.eclipse.jface.viewers.ISelectionChangedListener;
48import org.eclipse.jface.viewers.ISelectionProvider;
49import org.eclipse.jface.viewers.ITreeContentProvider;
50import org.eclipse.jface.viewers.TreeViewer;
51import org.eclipse.jface.viewers.TreeViewerColumn;
52import org.eclipse.jface.viewers.Viewer;
53import org.eclipse.jface.viewers.ViewerCell;
54import org.eclipse.jface.viewers.ViewerFilter;
55import org.eclipse.swt.SWT;
56import org.eclipse.swt.dnd.Clipboard;
57import org.eclipse.swt.dnd.TextTransfer;
58import org.eclipse.swt.dnd.Transfer;
59import org.eclipse.swt.events.ControlAdapter;
60import org.eclipse.swt.events.ControlEvent;
61import org.eclipse.swt.events.ModifyEvent;
62import org.eclipse.swt.events.ModifyListener;
63import org.eclipse.swt.events.SelectionAdapter;
64import org.eclipse.swt.events.SelectionEvent;
65import org.eclipse.swt.events.SelectionListener;
66import org.eclipse.swt.graphics.Color;
67import org.eclipse.swt.graphics.Image;
68import org.eclipse.swt.layout.GridData;
69import org.eclipse.swt.layout.GridLayout;
70import org.eclipse.swt.widgets.Combo;
71import org.eclipse.swt.widgets.Composite;
72import org.eclipse.swt.widgets.Display;
73import org.eclipse.swt.widgets.FileDialog;
74import org.eclipse.swt.widgets.Label;
75import org.eclipse.swt.widgets.Scale;
76import org.eclipse.swt.widgets.ScrollBar;
77import org.eclipse.swt.widgets.Shell;
78import org.eclipse.swt.widgets.Spinner;
79import org.eclipse.swt.widgets.Text;
80import org.eclipse.swt.widgets.ToolBar;
81import org.eclipse.swt.widgets.ToolItem;
82import org.eclipse.swt.widgets.Tree;
83import org.eclipse.swt.widgets.TreeColumn;
84import org.eclipse.swt.widgets.TreeItem;
85import org.eclipse.ui.IActionBars;
86import org.eclipse.ui.IEditorInput;
87import org.eclipse.ui.IEditorSite;
88import org.eclipse.ui.ISharedImages;
89import org.eclipse.ui.IURIEditorInput;
90import org.eclipse.ui.PartInitException;
91import org.eclipse.ui.PlatformUI;
92import org.eclipse.ui.actions.ActionFactory;
93import org.eclipse.ui.part.EditorPart;
94
95import java.io.File;
96import java.io.IOException;
97import java.lang.reflect.InvocationTargetException;
98import java.util.ArrayList;
99import java.util.List;
100import java.util.regex.Matcher;
101import java.util.regex.Pattern;
102
103/** Display OpenGL function trace in a tabular view. */
104public class GLFunctionTraceViewer extends EditorPart implements ISelectionProvider {
105    public static final String ID = "com.android.ide.eclipse.gltrace.GLFunctionTrace"; //$NON-NLS-1$
106
107    private static final String DEFAULT_FILTER_MESSAGE = "Filter list of OpenGL calls. Accepts Java regexes.";
108    private static final String NEWLINE = System.getProperty("line.separator"); //$NON-NLS-1$
109
110    private static Image sExpandAllIcon;
111
112    private static String sLastExportedToFolder;
113
114    private String mFilePath;
115    private Scale mFrameSelectionScale;
116    private Spinner mFrameSelectionSpinner;
117
118    private GLTrace mTrace;
119
120    private TreeViewer mFrameTreeViewer;
121    private List<GLCallNode> mTreeViewerNodes;
122
123    private Text mFilterText;
124    private GLCallFilter mGLCallFilter;
125
126    private Color mGldrawTextColor;
127    private Color mGlCallErrorColor;
128
129    /**
130     * Job to refresh the tree view & frame summary view.
131     *
132     * When the currently displayed frame is changed, either via the {@link #mFrameSelectionScale}
133     * or via {@link #mFrameSelectionSpinner}, we need to update the displayed tree of calls for
134     * that frame, and the frame summary view. Both these operations need to happen on the UI
135     * thread, but are time consuming. This works out ok if the frame selection is not changing
136     * rapidly (i.e., when the spinner or scale is moved to the target frame in a single action).
137     * However, if the spinner is constantly pressed, then the user is scrolling through a sequence
138     * of frames, and rather than refreshing the details for each of the intermediate frames,
139     * we create a job to refresh the details and schedule the job after a short interval
140     * {@link #TREE_REFRESH_INTERVAL}. This allows us to stay responsive to the spinner/scale,
141     * and not do the costly refresh for each of the intermediate frames.
142     */
143    private Job mTreeRefresherJob;
144    private final Object mTreeRefresherLock = new Object();
145    private static final int TREE_REFRESH_INTERVAL_MS = 250;
146
147    private int mCurrentFrame;
148
149    // Currently displayed frame's start and end call indices.
150    private int mCallStartIndex;
151    private int mCallEndIndex;
152
153    private DurationMinimap mDurationMinimap;
154    private ScrollBar mVerticalScrollBar;
155
156    private Combo mContextSwitchCombo;
157    private boolean mShowContextSwitcher;
158    private int mCurrentlyDisplayedContext = -1;
159
160    private StateViewPage mStateViewPage;
161    private FrameSummaryViewPage mFrameSummaryViewPage;
162    private DetailsPage mDetailsPage;
163
164    private ToolItem mExpandAllToolItem;
165    private ToolItem mCollapseAllToolItem;
166    private ToolItem mSaveAsToolItem;
167
168    public GLFunctionTraceViewer() {
169        mGldrawTextColor = Display.getDefault().getSystemColor(SWT.COLOR_BLUE);
170        mGlCallErrorColor = Display.getDefault().getSystemColor(SWT.COLOR_RED);
171    }
172
173    @Override
174    public void doSave(IProgressMonitor monitor) {
175    }
176
177    @Override
178    public void doSaveAs() {
179    }
180
181    @Override
182    public void init(IEditorSite site, IEditorInput input) throws PartInitException {
183        // we use a IURIEditorInput to allow opening files not within the workspace
184        if (!(input instanceof IURIEditorInput)) {
185            throw new PartInitException("GL Function Trace View: unsupported input type.");
186        }
187
188        setSite(site);
189        setInput(input);
190        mFilePath = ((IURIEditorInput) input).getURI().getPath();
191
192        // set the editor part name to be the name of the file.
193        File f = new File(mFilePath);
194        setPartName(f.getName());
195    }
196
197    @Override
198    public boolean isDirty() {
199        return false;
200    }
201
202    @Override
203    public boolean isSaveAsAllowed() {
204        return false;
205    }
206
207    @Override
208    public void createPartControl(Composite parent) {
209        Composite c = new Composite(parent, SWT.NONE);
210        c.setLayout(new GridLayout(1, false));
211        GridData gd = new GridData(GridData.FILL_BOTH);
212        c.setLayoutData(gd);
213
214        setInput(parent.getShell(), mFilePath);
215
216        createFrameSelectionControls(c);
217        createOptionsBar(c);
218        createFrameTraceView(c);
219
220        getSite().setSelectionProvider(mFrameTreeViewer);
221
222        IActionBars actionBars = getEditorSite().getActionBars();
223        actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(),
224                new Action("Copy") {
225            @Override
226            public void run() {
227                copySelectionToClipboard();
228            }
229        });
230
231        actionBars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(),
232                new Action("Select All") {
233            @Override
234            public void run() {
235                selectAll();
236            }
237        });
238
239        actionBars.setGlobalActionHandler(ActionFactory.FIND.getId(),
240                new Action("Find") {
241            @Override
242            public void run() {
243                showFindDialog();
244            }
245        });
246    }
247
248    public void setInput(Shell shell, String tracePath) {
249        ProgressMonitorDialog dlg = new ProgressMonitorDialog(shell);
250        TraceFileParserTask parser = new TraceFileParserTask(mFilePath);
251        try {
252            dlg.run(true, true, parser);
253        } catch (InvocationTargetException e) {
254            // exception while parsing, display error to user
255            MessageDialog.openError(shell,
256                    "Error parsing OpenGL Trace File",
257                    e.getCause().getMessage());
258            return;
259        } catch (InterruptedException e) {
260            // operation canceled by user, just return
261            return;
262        }
263
264        mTrace = parser.getTrace();
265        mShowContextSwitcher = (mTrace == null) ? false : mTrace.getContexts().size() > 1;
266        if (mStateViewPage != null) {
267            mStateViewPage.setInput(mTrace);
268        }
269        if (mFrameSummaryViewPage != null) {
270            mFrameSummaryViewPage.setInput(mTrace);
271        }
272        if (mDetailsPage != null) {
273            mDetailsPage.setInput(mTrace);
274        }
275        if (mDurationMinimap != null) {
276            mDurationMinimap.setInput(mTrace);
277        }
278
279        Display.getDefault().asyncExec(new Runnable() {
280            @Override
281            public void run() {
282                refreshUI();
283            }
284        });
285    }
286
287    private void refreshUI() {
288        if (mTrace == null || mTrace.getGLCalls().size() == 0) {
289            setFrameCount(0);
290            return;
291        }
292
293        setFrameCount(mTrace.getFrames().size());
294        selectFrame(1);
295    }
296
297    private void createFrameSelectionControls(Composite parent) {
298        Composite c = new Composite(parent, SWT.NONE);
299        c.setLayout(new GridLayout(3, false));
300        GridData gd = new GridData(GridData.FILL_HORIZONTAL);
301        c.setLayoutData(gd);
302
303        Label l = new Label(c, SWT.NONE);
304        l.setText("Select Frame:");
305
306        mFrameSelectionScale = new Scale(c, SWT.HORIZONTAL);
307        mFrameSelectionScale.setMinimum(1);
308        mFrameSelectionScale.setMaximum(1);
309        mFrameSelectionScale.setSelection(0);
310        gd = new GridData(GridData.FILL_HORIZONTAL);
311        mFrameSelectionScale.setLayoutData(gd);
312
313        mFrameSelectionScale.addSelectionListener(new SelectionAdapter() {
314            @Override
315            public void widgetSelected(SelectionEvent e) {
316                int selectedFrame = mFrameSelectionScale.getSelection();
317                mFrameSelectionSpinner.setSelection(selectedFrame);
318                selectFrame(selectedFrame);
319            }
320        });
321
322        mFrameSelectionSpinner = new Spinner(c, SWT.BORDER);
323        gd = new GridData();
324        // width to hold atleast 6 digits
325        gd.widthHint = SwtUtils.getApproximateFontWidth(mFrameSelectionSpinner) * 6;
326        mFrameSelectionSpinner.setLayoutData(gd);
327
328        mFrameSelectionSpinner.setMinimum(1);
329        mFrameSelectionSpinner.setMaximum(1);
330        mFrameSelectionSpinner.setSelection(0);
331        mFrameSelectionSpinner.addSelectionListener(new SelectionAdapter() {
332            @Override
333            public void widgetSelected(SelectionEvent e) {
334                int selectedFrame = mFrameSelectionSpinner.getSelection();
335                mFrameSelectionScale.setSelection(selectedFrame);
336                selectFrame(selectedFrame);
337            }
338        });
339    }
340
341    private void setFrameCount(int nFrames) {
342        boolean en = nFrames > 0;
343        mFrameSelectionScale.setEnabled(en);
344        mFrameSelectionSpinner.setEnabled(en);
345
346        mFrameSelectionScale.setMaximum(nFrames);
347        mFrameSelectionSpinner.setMaximum(nFrames);
348    }
349
350    private void selectFrame(int selectedFrame) {
351        mFrameSelectionScale.setSelection(selectedFrame);
352        mFrameSelectionSpinner.setSelection(selectedFrame);
353
354        synchronized (mTreeRefresherLock) {
355            if (mTrace != null) {
356                GLFrame f = mTrace.getFrame(selectedFrame - 1);
357                mCallStartIndex = f.getStartIndex();
358                mCallEndIndex = f.getEndIndex();
359            } else {
360                mCallStartIndex = mCallEndIndex = 0;
361            }
362
363            mCurrentFrame = selectedFrame - 1;
364
365            scheduleNewRefreshJob();
366        }
367
368        // update minimap view
369        mDurationMinimap.setCallRangeForCurrentFrame(mCallStartIndex, mCallEndIndex);
370    }
371
372    /**
373     * Show only calls from the given context
374     * @param context context id whose calls should be displayed. Illegal values will result in
375     *                calls from all contexts being displayed.
376     */
377    private void selectContext(int context) {
378        if (mCurrentlyDisplayedContext == context) {
379            return;
380        }
381
382        synchronized (mTreeRefresherLock) {
383            mCurrentlyDisplayedContext = context;
384            scheduleNewRefreshJob();
385        }
386    }
387
388    private void scheduleNewRefreshJob() {
389        if (mTreeRefresherJob != null) {
390            return;
391        }
392
393        mTreeRefresherJob = new Job("Refresh GL Trace View Tree") {
394            @Override
395            protected IStatus run(IProgressMonitor monitor) {
396                final int start, end, context;
397
398                synchronized (mTreeRefresherLock) {
399                    start = mCallStartIndex;
400                    end = mCallEndIndex;
401                    context = mCurrentlyDisplayedContext;
402
403                    mTreeRefresherJob = null;
404                }
405
406                // update tree view in the editor
407                Display.getDefault().syncExec(new Runnable() {
408                    @Override
409                    public void run() {
410                        refreshTree(start, end, context);
411
412                        // update the frame summary view
413                        if (mFrameSummaryViewPage != null) {
414                            mFrameSummaryViewPage.setSelectedFrame(mCurrentFrame);
415                        }
416                    }
417                });
418                return Status.OK_STATUS;
419            }
420        };
421        mTreeRefresherJob.setPriority(Job.SHORT);
422        mTreeRefresherJob.schedule(TREE_REFRESH_INTERVAL_MS);
423    }
424
425    private void refreshTree(int startCallIndex, int endCallIndex, int contextToDisplay) {
426        mTreeViewerNodes = GLCallGroups.constructCallHierarchy(mTrace,
427                startCallIndex, endCallIndex,
428                contextToDisplay);
429        mFrameTreeViewer.setInput(mTreeViewerNodes);
430        mFrameTreeViewer.refresh();
431        mFrameTreeViewer.expandAll();
432    }
433
434    private void createOptionsBar(Composite parent) {
435        int numColumns = mShowContextSwitcher ? 4 : 3;
436
437        Composite c = new Composite(parent, SWT.NONE);
438        c.setLayout(new GridLayout(numColumns, false));
439        GridData gd = new GridData(GridData.FILL_HORIZONTAL);
440        c.setLayoutData(gd);
441
442        Label l = new Label(c, SWT.NONE);
443        l.setText("Filter:");
444
445        mFilterText = new Text(c, SWT.BORDER | SWT.ICON_SEARCH | SWT.SEARCH | SWT.ICON_CANCEL);
446        mFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
447        mFilterText.setMessage(DEFAULT_FILTER_MESSAGE);
448        mFilterText.addModifyListener(new ModifyListener() {
449            @Override
450            public void modifyText(ModifyEvent e) {
451                updateAppliedFilters();
452            }
453        });
454
455        if (mShowContextSwitcher) {
456            mContextSwitchCombo = new Combo(c, SWT.BORDER | SWT.READ_ONLY);
457
458            // Setup the combo such that "All Contexts" is the first item,
459            // and then we have an item for each context.
460            mContextSwitchCombo.add("All Contexts");
461            mContextSwitchCombo.select(0);
462            mCurrentlyDisplayedContext = -1; // showing all contexts
463            for (int i = 0; i < mTrace.getContexts().size(); i++) {
464                mContextSwitchCombo.add("Context " + i);
465            }
466
467            mContextSwitchCombo.addSelectionListener(new SelectionAdapter() {
468                @Override
469                public void widgetSelected(SelectionEvent e) {
470                    selectContext(mContextSwitchCombo.getSelectionIndex() - 1);
471                }
472            });
473        } else {
474            mCurrentlyDisplayedContext = 0;
475        }
476
477        ToolBar toolBar = new ToolBar(c, SWT.FLAT | SWT.BORDER);
478
479        mExpandAllToolItem = new ToolItem(toolBar, SWT.PUSH);
480        mExpandAllToolItem.setToolTipText("Expand All");
481        if (sExpandAllIcon == null) {
482            ImageDescriptor id = GlTracePlugin.getImageDescriptor("/icons/expandall.png");
483            sExpandAllIcon = id.createImage();
484        }
485        if (sExpandAllIcon != null) {
486            mExpandAllToolItem.setImage(sExpandAllIcon);
487        }
488
489        mCollapseAllToolItem = new ToolItem(toolBar, SWT.PUSH);
490        mCollapseAllToolItem.setToolTipText("Collapse All");
491        mCollapseAllToolItem.setImage(
492                PlatformUI.getWorkbench().getSharedImages().getImage(
493                        ISharedImages.IMG_ELCL_COLLAPSEALL));
494
495        mSaveAsToolItem = new ToolItem(toolBar, SWT.PUSH);
496        mSaveAsToolItem.setToolTipText("Export Trace");
497        mSaveAsToolItem.setImage(
498                PlatformUI.getWorkbench().getSharedImages().getImage(
499                        ISharedImages.IMG_ETOOL_SAVEAS_EDIT));
500
501        SelectionListener toolbarSelectionListener = new SelectionAdapter() {
502            @Override
503            public void widgetSelected(SelectionEvent e) {
504                if (e.getSource() == mCollapseAllToolItem) {
505                    setTreeItemsExpanded(false);
506                } else if (e.getSource() == mExpandAllToolItem) {
507                    setTreeItemsExpanded(true);
508                } else if (e.getSource() == mSaveAsToolItem) {
509                    exportTrace();
510                }
511            }
512        };
513        mExpandAllToolItem.addSelectionListener(toolbarSelectionListener);
514        mCollapseAllToolItem.addSelectionListener(toolbarSelectionListener);
515        mSaveAsToolItem.addSelectionListener(toolbarSelectionListener);
516    }
517
518    private void updateAppliedFilters() {
519        mGLCallFilter.setFilters(mFilterText.getText().trim());
520        mFrameTreeViewer.refresh();
521    }
522
523    private void createFrameTraceView(Composite parent) {
524        Composite c = new Composite(parent, SWT.NONE);
525        c.setLayout(new GridLayout(2, false));
526        GridData gd = new GridData(GridData.FILL_BOTH);
527        c.setLayoutData(gd);
528
529        final Tree tree = new Tree(c, SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI);
530        gd = new GridData(GridData.FILL_BOTH);
531        tree.setLayoutData(gd);
532        tree.setLinesVisible(true);
533        tree.setHeaderVisible(true);
534
535        mFrameTreeViewer = new TreeViewer(tree);
536        CellLabelProvider labelProvider = new GLFrameLabelProvider();
537
538        // column showing the GL context id
539        TreeViewerColumn tvc = new TreeViewerColumn(mFrameTreeViewer, SWT.NONE);
540        tvc.setLabelProvider(labelProvider);
541        TreeColumn column = tvc.getColumn();
542        column.setText("Function");
543        column.setWidth(500);
544
545        // column showing the GL function duration (wall clock time)
546        tvc = new TreeViewerColumn(mFrameTreeViewer, SWT.NONE);
547        tvc.setLabelProvider(labelProvider);
548        column = tvc.getColumn();
549        column.setText("Wall Time (ns)");
550        column.setWidth(150);
551        column.setAlignment(SWT.RIGHT);
552
553        // column showing the GL function duration (thread time)
554        tvc = new TreeViewerColumn(mFrameTreeViewer, SWT.NONE);
555        tvc.setLabelProvider(labelProvider);
556        column = tvc.getColumn();
557        column.setText("Thread Time (ns)");
558        column.setWidth(150);
559        column.setAlignment(SWT.RIGHT);
560
561        mFrameTreeViewer.setContentProvider(new GLFrameContentProvider());
562
563        mGLCallFilter = new GLCallFilter();
564        mFrameTreeViewer.addFilter(mGLCallFilter);
565
566        // when the control is resized, give all the additional space
567        // to the function name column.
568        tree.addControlListener(new ControlAdapter() {
569            @Override
570            public void controlResized(ControlEvent e) {
571                int w = mFrameTreeViewer.getTree().getClientArea().width;
572                if (w > 200) {
573                    mFrameTreeViewer.getTree().getColumn(2).setWidth(100);
574                    mFrameTreeViewer.getTree().getColumn(1).setWidth(100);
575                    mFrameTreeViewer.getTree().getColumn(0).setWidth(w - 200);
576                }
577            }
578        });
579
580        mDurationMinimap = new DurationMinimap(c, mTrace);
581        gd = new GridData(GridData.FILL_VERTICAL);
582        gd.widthHint = gd.minimumWidth = mDurationMinimap.getMinimumWidth();
583        mDurationMinimap.setLayoutData(gd);
584        mDurationMinimap.addCallSelectionListener(new ICallSelectionListener() {
585            @Override
586            public void callSelected(int selectedCallIndex) {
587                if (selectedCallIndex > 0 && selectedCallIndex < mTreeViewerNodes.size()) {
588                    TreeItem item = tree.getItem(selectedCallIndex);
589                    tree.select(item);
590                    tree.setTopItem(item);
591                }
592            }
593        });
594
595        mVerticalScrollBar = tree.getVerticalBar();
596        mVerticalScrollBar.addSelectionListener(new SelectionAdapter() {
597            @Override
598            public void widgetSelected(SelectionEvent e) {
599                updateVisibleRange();
600            }
601        });
602    }
603
604    private void updateVisibleRange() {
605        int visibleCallTopIndex = mCallStartIndex;
606        int visibleCallBottomIndex = mCallEndIndex;
607
608        if (mVerticalScrollBar.isEnabled()) {
609            int selection = mVerticalScrollBar.getSelection();
610            int thumb = mVerticalScrollBar.getThumb();
611            int max = mVerticalScrollBar.getMaximum();
612
613            // from the scrollbar values, compute the visible fraction
614            double top = (double) selection / max;
615            double bottom = (double) (selection + thumb) / max;
616
617            // map the fraction to the call indices
618            int range = mCallEndIndex - mCallStartIndex;
619            visibleCallTopIndex = mCallStartIndex + (int) Math.floor(range * top);
620            visibleCallBottomIndex = mCallStartIndex + (int) Math.ceil(range * bottom);
621        }
622
623        mDurationMinimap.setVisibleCallRange(visibleCallTopIndex, visibleCallBottomIndex);
624    }
625
626    @Override
627    public void setFocus() {
628        mFrameTreeViewer.getTree().setFocus();
629    }
630
631    private static class GLFrameContentProvider implements ITreeContentProvider {
632        @Override
633        public void dispose() {
634        }
635
636        @Override
637        public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
638        }
639
640        @Override
641        public Object[] getElements(Object inputElement) {
642            return getChildren(inputElement);
643        }
644
645        @Override
646        public Object[] getChildren(Object parentElement) {
647            if (parentElement instanceof List<?>) {
648                return ((List<?>) parentElement).toArray();
649            }
650
651            if (!(parentElement instanceof GLCallNode)) {
652                return null;
653            }
654
655            GLCallNode parent = (GLCallNode) parentElement;
656            if (parent.hasChildren()) {
657                return parent.getChildren().toArray();
658            } else {
659                return new Object[0];
660            }
661        }
662
663        @Override
664        public Object getParent(Object element) {
665            if (!(element instanceof GLCallNode)) {
666                return null;
667            }
668
669            return ((GLCallNode) element).getParent();
670        }
671
672        @Override
673        public boolean hasChildren(Object element) {
674            if (!(element instanceof GLCallNode)) {
675                return false;
676            }
677
678            return ((GLCallNode) element).hasChildren();
679        }
680    }
681
682    private class GLFrameLabelProvider extends ColumnLabelProvider {
683        @Override
684        public void update(ViewerCell cell) {
685            Object element = cell.getElement();
686            if (!(element instanceof GLCallNode)) {
687                return;
688            }
689
690            GLCall c = ((GLCallNode) element).getCall();
691
692            if (c.getFunction() == Function.glDrawArrays
693                    || c.getFunction() == Function.glDrawElements) {
694                cell.setForeground(mGldrawTextColor);
695            }
696
697            if (c.hasErrors()) {
698                cell.setForeground(mGlCallErrorColor);
699            }
700
701            cell.setText(getColumnText(c, cell.getColumnIndex()));
702        }
703
704        private String getColumnText(GLCall c, int columnIndex) {
705            switch (columnIndex) {
706            case 0:
707                if (c.getFunction() == Function.glPushGroupMarkerEXT) {
708                    Object marker = c.getProperty(GLCall.PROPERTY_MARKERNAME);
709                    if (marker instanceof String) {
710                        return ((String) marker);
711                    }
712                }
713                return c.toString();
714            case 1:
715                return formatDuration(c.getWallDuration());
716            case 2:
717                return formatDuration(c.getThreadDuration());
718            default:
719                return Integer.toString(c.getContextId());
720            }
721        }
722
723        private String formatDuration(int time) {
724            // Max duration is in the 10s of milliseconds, so xx,xxx,xxx ns
725            // So we require a format specifier that is 10 characters wide
726            return String.format("%,10d", time);            //$NON-NLS-1$
727        }
728    }
729
730    private static class GLCallFilter extends ViewerFilter {
731        private final List<Pattern> mPatterns = new ArrayList<Pattern>();
732
733        public void setFilters(String filter) {
734            mPatterns.clear();
735
736            // split the user input into multiple regexes
737            // we assume that the regexes are OR'ed together i.e., all text that matches
738            // any one of the regexes will be displayed
739            for (String regex : filter.split(" ")) {
740                mPatterns.add(Pattern.compile(regex, Pattern.CASE_INSENSITIVE));
741            }
742        }
743
744        @Override
745        public boolean select(Viewer viewer, Object parentElement, Object element) {
746            if (!(element instanceof GLCallNode)) {
747                return true;
748            }
749
750            String text = getTextUnderNode((GLCallNode) element);
751
752            if (mPatterns.size() == 0) {
753                // match if there are no regex filters
754                return true;
755            }
756
757            for (Pattern p : mPatterns) {
758                Matcher matcher = p.matcher(text);
759                if (matcher.find()) {
760                    // match if atleast one of the regexes matches this text
761                    return true;
762                }
763            }
764
765            return false;
766        }
767
768        /** Obtain a string representation of all functions under a given tree node. */
769        private String getTextUnderNode(GLCallNode element) {
770            String func = element.getCall().getFunction().toString();
771            if (!element.hasChildren()) {
772                return func;
773            }
774
775            StringBuilder sb = new StringBuilder(100);
776            sb.append(func);
777
778            for (GLCallNode child : element.getChildren()) {
779                sb.append(getTextUnderNode(child));
780            }
781
782            return sb.toString();
783        }
784    }
785
786    @Override
787    public void addSelectionChangedListener(ISelectionChangedListener listener) {
788        if (mFrameTreeViewer != null) {
789            mFrameTreeViewer.addSelectionChangedListener(listener);
790        }
791    }
792
793    @Override
794    public ISelection getSelection() {
795        if (mFrameTreeViewer != null) {
796            return mFrameTreeViewer.getSelection();
797        } else {
798            return null;
799        }
800    }
801
802    @Override
803    public void removeSelectionChangedListener(ISelectionChangedListener listener) {
804        if (mFrameTreeViewer != null) {
805            mFrameTreeViewer.removeSelectionChangedListener(listener);
806        }
807    }
808
809    @Override
810    public void setSelection(ISelection selection) {
811        if (mFrameTreeViewer != null) {
812            mFrameTreeViewer.setSelection(selection);
813        }
814    }
815
816    public GLTrace getTrace() {
817        return mTrace;
818    }
819
820    public StateViewPage getStateViewPage() {
821        if (mStateViewPage == null) {
822            mStateViewPage = new StateViewPage(mTrace);
823        }
824
825        return mStateViewPage;
826    }
827
828    public FrameSummaryViewPage getFrameSummaryViewPage() {
829        if (mFrameSummaryViewPage == null) {
830            mFrameSummaryViewPage = new FrameSummaryViewPage(mTrace);
831        }
832
833        return mFrameSummaryViewPage;
834    }
835
836    public DetailsPage getDetailsPage() {
837        if (mDetailsPage == null) {
838            mDetailsPage = new DetailsPage(mTrace);
839        }
840
841        return mDetailsPage;
842    }
843
844    private void copySelectionToClipboard() {
845        if (mFrameTreeViewer == null || mFrameTreeViewer.getTree().isDisposed()) {
846            return;
847        }
848
849        StringBuilder sb = new StringBuilder();
850
851        for (TreeItem it: mFrameTreeViewer.getTree().getSelection()) {
852            Object data = it.getData();
853            if (data instanceof GLCallNode) {
854                sb.append(((GLCallNode) data).getCall());
855                sb.append(NEWLINE);
856            }
857        }
858
859        if (sb.length() > 0) {
860            Clipboard cb = new Clipboard(Display.getDefault());
861            cb.setContents(
862                    new Object[] { sb.toString() },
863                    new Transfer[] { TextTransfer.getInstance() });
864            cb.dispose();
865        }
866    }
867
868    private void selectAll() {
869        if (mFrameTreeViewer == null || mFrameTreeViewer.getTree().isDisposed()) {
870            return;
871        }
872
873        mFrameTreeViewer.getTree().selectAll();
874    }
875
876    private void exportTrace() {
877        if (mFrameTreeViewer == null || mFrameTreeViewer.getTree().isDisposed()) {
878            return;
879        }
880
881        if (mCallEndIndex == 0) {
882            return;
883        }
884
885        FileDialog fd = new FileDialog(mFrameTreeViewer.getTree().getShell(), SWT.SAVE);
886        fd.setFilterExtensions(new String[] { "*.txt" });
887        if (sLastExportedToFolder != null) {
888            fd.setFilterPath(sLastExportedToFolder);
889        }
890
891        String path = fd.open();
892        if (path == null) {
893            return;
894        }
895
896        File f = new File(path);
897        sLastExportedToFolder = f.getParent();
898        try {
899            exportFrameTo(f);
900        } catch (IOException e) {
901            ErrorDialog.openError(mFrameTreeViewer.getTree().getShell(),
902                    "Export trace file.",
903                    "Unexpected error exporting trace file.",
904                    new Status(Status.ERROR, GlTracePlugin.PLUGIN_ID, e.toString()));
905        }
906    }
907
908    private void exportFrameTo(File f) throws IOException {
909        String glCalls = serializeGlCalls(mTrace.getGLCalls(), mCallStartIndex, mCallEndIndex);
910        Files.write(glCalls, f, Charsets.UTF_8);
911    }
912
913    private String serializeGlCalls(List<GLCall> glCalls, int start, int end) {
914        StringBuilder sb = new StringBuilder();
915        while (start < end) {
916            sb.append(glCalls.get(start).toString());
917            sb.append("\n"); //$NON-NLS-1$
918            start++;
919        }
920
921        return sb.toString();
922    }
923
924    private void setTreeItemsExpanded(boolean expand) {
925        if (mFrameTreeViewer == null || mFrameTreeViewer.getTree().isDisposed()) {
926            return;
927        }
928
929        if (expand) {
930            mFrameTreeViewer.expandAll();
931        } else {
932            mFrameTreeViewer.collapseAll();
933        }
934    }
935
936    private class TraceViewerFindTarget extends AbstractBufferFindTarget {
937        @Override
938        public int getItemCount() {
939            return mFrameTreeViewer.getTree().getItemCount();
940        }
941
942        @Override
943        public String getItem(int index) {
944            Object data = mFrameTreeViewer.getTree().getItem(index).getData();
945            if (data instanceof GLCallNode) {
946                return ((GLCallNode) data).getCall().toString();
947            }
948            return null;
949        }
950
951        @Override
952        public void selectAndReveal(int index) {
953            Tree t = mFrameTreeViewer.getTree();
954            t.deselectAll();
955            t.select(t.getItem(index));
956            t.showSelection();
957        }
958
959        @Override
960        public int getStartingIndex() {
961            return 0;
962        }
963    };
964
965    private FindDialog mFindDialog;
966    private TraceViewerFindTarget mFindTarget = new TraceViewerFindTarget();
967
968    private void showFindDialog() {
969        if (mFindDialog != null) {
970            // the dialog is already displayed
971            return;
972        }
973
974        mFindDialog = new FindDialog(Display.getDefault().getActiveShell(),
975                mFindTarget,
976                FindDialog.FIND_NEXT_ID);
977        mFindDialog.open(); // blocks until find dialog is closed
978        mFindDialog = null;
979    }
980
981    public String getInputPath() {
982        return mFilePath;
983    }
984}
985