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.ide.eclipse.gltrace.GlTracePlugin;
20import com.android.ide.eclipse.gltrace.editors.GLCallGroups.GLCallNode;
21import com.android.ide.eclipse.gltrace.model.GLCall;
22import com.android.ide.eclipse.gltrace.model.GLTrace;
23import com.android.ide.eclipse.gltrace.state.GLState;
24import com.android.ide.eclipse.gltrace.state.IGLProperty;
25import com.android.ide.eclipse.gltrace.state.StatePrettyPrinter;
26import com.android.ide.eclipse.gltrace.state.transforms.IStateTransform;
27import com.google.common.base.Charsets;
28import com.google.common.io.Files;
29
30import org.eclipse.core.runtime.IProgressMonitor;
31import org.eclipse.core.runtime.IStatus;
32import org.eclipse.core.runtime.Status;
33import org.eclipse.core.runtime.jobs.ILock;
34import org.eclipse.core.runtime.jobs.Job;
35import org.eclipse.jface.action.Action;
36import org.eclipse.jface.action.IToolBarManager;
37import org.eclipse.jface.dialogs.ErrorDialog;
38import org.eclipse.jface.layout.GridDataFactory;
39import org.eclipse.jface.viewers.ISelection;
40import org.eclipse.jface.viewers.ISelectionChangedListener;
41import org.eclipse.jface.viewers.ISelectionProvider;
42import org.eclipse.jface.viewers.TreeSelection;
43import org.eclipse.jface.viewers.TreeViewer;
44import org.eclipse.swt.SWT;
45import org.eclipse.swt.layout.GridData;
46import org.eclipse.swt.widgets.Composite;
47import org.eclipse.swt.widgets.Control;
48import org.eclipse.swt.widgets.Display;
49import org.eclipse.swt.widgets.FileDialog;
50import org.eclipse.swt.widgets.Shell;
51import org.eclipse.swt.widgets.Tree;
52import org.eclipse.swt.widgets.TreeColumn;
53import org.eclipse.ui.ISelectionListener;
54import org.eclipse.ui.ISharedImages;
55import org.eclipse.ui.IWorkbenchPart;
56import org.eclipse.ui.PlatformUI;
57import org.eclipse.ui.part.IPageSite;
58import org.eclipse.ui.part.Page;
59
60import java.io.File;
61import java.io.IOException;
62import java.util.ArrayList;
63import java.util.Collections;
64import java.util.HashSet;
65import java.util.List;
66import java.util.Set;
67
68/**
69 * A tree view of the OpenGL state. It listens to the current GLCall that is selected
70 * in the Function Trace view, and updates its view to reflect the state as of the selected call.
71 */
72public class StateViewPage extends Page implements ISelectionListener, ISelectionProvider {
73    public static final String ID = "com.android.ide.eclipse.gltrace.views.GLState"; //$NON-NLS-1$
74    private static String sLastUsedPath;
75    private static final ILock sGlStateLock = Job.getJobManager().newLock();
76
77    private GLTrace mTrace;
78    private List<GLCall> mGLCalls;
79
80    /** OpenGL State as of call {@link #mCurrentStateIndex}. */
81    private IGLProperty mState;
82    private int mCurrentStateIndex;
83
84    private String[] TREE_PROPERTIES = { "Name", "Value" };
85    private TreeViewer mTreeViewer;
86    private StateLabelProvider mLabelProvider;
87
88    public StateViewPage(GLTrace trace) {
89        setInput(trace);
90    }
91
92    public void setInput(GLTrace trace) {
93        mTrace = trace;
94        if (trace != null) {
95            mGLCalls = trace.getGLCalls();
96        } else {
97            mGLCalls = null;
98        }
99
100        mState = GLState.createDefaultState();
101        mCurrentStateIndex = -1;
102
103        if (mTreeViewer != null) {
104            mTreeViewer.setInput(mState);
105            mTreeViewer.refresh();
106        }
107    }
108
109    @Override
110    public void createControl(Composite parent) {
111        final Tree tree = new Tree(parent, SWT.VIRTUAL | SWT.H_SCROLL | SWT.V_SCROLL);
112        GridDataFactory.fillDefaults().grab(true, true).applyTo(tree);
113
114        tree.setHeaderVisible(true);
115        tree.setLinesVisible(true);
116        tree.setLayoutData(new GridData(GridData.FILL_BOTH));
117
118        TreeColumn col1 = new TreeColumn(tree, SWT.LEFT);
119        col1.setText(TREE_PROPERTIES[0]);
120        col1.setWidth(200);
121
122        TreeColumn col2 = new TreeColumn(tree, SWT.LEFT);
123        col2.setText(TREE_PROPERTIES[1]);
124        col2.setWidth(200);
125
126        mTreeViewer = new TreeViewer(tree);
127        mTreeViewer.setContentProvider(new StateContentProvider());
128        mLabelProvider = new StateLabelProvider();
129        mTreeViewer.setLabelProvider(mLabelProvider);
130        mTreeViewer.setInput(mState);
131        mTreeViewer.refresh();
132
133        final IToolBarManager manager = getSite().getActionBars().getToolBarManager();
134        manager.add(new Action("Save to File",
135                PlatformUI.getWorkbench().getSharedImages().getImageDescriptor(
136                        ISharedImages.IMG_ETOOL_SAVEAS_EDIT)) {
137            @Override
138            public void run() {
139                saveCurrentState();
140            }
141        });
142    }
143
144    private void saveCurrentState() {
145        final Shell shell = mTreeViewer.getTree().getShell();
146        FileDialog fd = new FileDialog(shell, SWT.SAVE);
147        fd.setFilterExtensions(new String[] { "*.txt" });
148        if (sLastUsedPath != null) {
149            fd.setFilterPath(sLastUsedPath);
150        }
151
152        String path = fd.open();
153        if (path == null) {
154            return;
155        }
156
157        File f = new File(path);
158        sLastUsedPath = f.getParent();
159
160        // export state to f
161        StatePrettyPrinter pp = new StatePrettyPrinter();
162        synchronized (sGlStateLock) {
163            mState.prettyPrint(pp);
164        }
165
166        try {
167            Files.write(pp.toString(), f, Charsets.UTF_8);
168        } catch (IOException e) {
169            ErrorDialog.openError(shell,
170                    "Export GL State",
171                    "Unexpected error while writing GL state to file.",
172                    new Status(Status.ERROR, GlTracePlugin.PLUGIN_ID, e.toString()));
173        }
174    }
175
176    @Override
177    public void init(IPageSite pageSite) {
178        super.init(pageSite);
179        pageSite.getPage().addSelectionListener(this);
180    }
181
182    @Override
183    public void dispose() {
184        getSite().getPage().removeSelectionListener(this);
185        super.dispose();
186    }
187
188    @Override
189    public void selectionChanged(IWorkbenchPart part, ISelection selection) {
190        if (!(part instanceof GLFunctionTraceViewer)) {
191            return;
192        }
193
194        if (((GLFunctionTraceViewer) part).getTrace() != mTrace) {
195            return;
196        }
197
198        if (!(selection instanceof TreeSelection)) {
199            return;
200        }
201
202        GLCall selectedCall = null;
203
204        Object data = ((TreeSelection) selection).getFirstElement();
205        if (data instanceof GLCallNode) {
206            selectedCall = ((GLCallNode) data).getCall();
207        }
208
209        if (selectedCall == null) {
210            return;
211        }
212
213        final int selectedCallIndex = selectedCall.getIndex();
214
215        // Creation of texture images takes a few seconds on the first run. So run
216        // the update task as an Eclipse job.
217        Job job = new Job("Updating GL State") {
218            @Override
219            protected IStatus run(IProgressMonitor monitor) {
220                Set<IGLProperty> changedProperties = null;
221
222                try {
223                    sGlStateLock.acquire();
224                    changedProperties = updateState(mCurrentStateIndex,
225                            selectedCallIndex);
226                    mCurrentStateIndex = selectedCallIndex;
227                } catch (Exception e) {
228                    GlTracePlugin.getDefault().logMessage(
229                            "Unexpected error while updating GL State.");
230                    GlTracePlugin.getDefault().logMessage(e.getMessage());
231                    return new Status(Status.ERROR,
232                            GlTracePlugin.PLUGIN_ID,
233                            "Unexpected error while updating GL State.",
234                            e);
235                } finally {
236                    sGlStateLock.release();
237                }
238
239                mLabelProvider.setChangedProperties(changedProperties);
240                Display.getDefault().syncExec(new Runnable() {
241                    @Override
242                    public void run() {
243                        if (!mTreeViewer.getTree().isDisposed()) {
244                            mTreeViewer.refresh();
245                        }
246                    }
247                });
248
249                return Status.OK_STATUS;
250            }
251        };
252        job.setPriority(Job.SHORT);
253        job.schedule();
254    }
255
256    @Override
257    public Control getControl() {
258        if (mTreeViewer == null) {
259            return null;
260        }
261
262        return mTreeViewer.getControl();
263    }
264
265    @Override
266    public void setFocus() {
267    }
268
269    /**
270     * Update GL state from GL call at fromIndex to the call at toIndex.
271     * If fromIndex < toIndex, the GL state will be updated by applying all the transformations
272     * corresponding to calls from (fromIndex + 1) to toIndex (inclusive).
273     * If fromIndex > toIndex, the GL state will be updated by reverting all the calls from
274     * fromIndex (inclusive) to (toIndex + 1).
275     * @return GL state properties that changed as a result of this update.
276     */
277    private Set<IGLProperty> updateState(int fromIndex, int toIndex) {
278        assert fromIndex >= -1 && fromIndex < mGLCalls.size();
279        assert toIndex >= 0 && toIndex < mGLCalls.size();
280
281        if (fromIndex < toIndex) {
282            return applyTransformations(fromIndex, toIndex);
283        } else if (fromIndex > toIndex) {
284            return revertTransformations(fromIndex, toIndex);
285        } else {
286            return Collections.emptySet();
287        }
288    }
289
290    private Set<IGLProperty> applyTransformations(int fromIndex, int toIndex) {
291        int setSizeHint = 3 * (toIndex - fromIndex) + 10;
292        Set<IGLProperty> changedProperties = new HashSet<IGLProperty>(setSizeHint);
293
294        for (int i = fromIndex + 1; i <= toIndex; i++) {
295            GLCall call = mGLCalls.get(i);
296            for (IStateTransform f : call.getStateTransformations()) {
297                try {
298                    f.apply(mState);
299                    IGLProperty changedProperty = f.getChangedProperty(mState);
300                    if (changedProperty != null) {
301                        changedProperties.addAll(getHierarchy(changedProperty));
302                    }
303                } catch (Exception e) {
304                    GlTracePlugin.getDefault().logMessage("Error applying transformations for "
305                            + call);
306                    GlTracePlugin.getDefault().logMessage(e.toString());
307                }
308            }
309        }
310
311        return changedProperties;
312    }
313
314    private Set<IGLProperty> revertTransformations(int fromIndex, int toIndex) {
315        int setSizeHint = 3 * (fromIndex - toIndex) + 10;
316        Set<IGLProperty> changedProperties = new HashSet<IGLProperty>(setSizeHint);
317
318        for (int i = fromIndex; i > toIndex; i--) {
319            List<IStateTransform> transforms = mGLCalls.get(i).getStateTransformations();
320            // When reverting transformations, iterate from the last to first so that the reversals
321            // are performed in the correct sequence.
322            for (int j = transforms.size() - 1; j >= 0; j--) {
323                IStateTransform f = transforms.get(j);
324                f.revert(mState);
325
326                IGLProperty changedProperty = f.getChangedProperty(mState);
327                if (changedProperty != null) {
328                    changedProperties.addAll(getHierarchy(changedProperty));
329                }
330            }
331        }
332
333        return changedProperties;
334    }
335
336    /**
337     * Obtain the list of properties starting from the provided property up to
338     * the root of GL state.
339     */
340    private List<IGLProperty> getHierarchy(IGLProperty changedProperty) {
341        List<IGLProperty> changedProperties = new ArrayList<IGLProperty>(5);
342        changedProperties.add(changedProperty);
343
344        // add the entire parent chain until we reach the root
345        IGLProperty prop = changedProperty;
346        while ((prop = prop.getParent()) != null) {
347            changedProperties.add(prop);
348        }
349
350        return changedProperties;
351    }
352
353    @Override
354    public void addSelectionChangedListener(ISelectionChangedListener listener) {
355        mTreeViewer.addSelectionChangedListener(listener);
356    }
357
358    @Override
359    public ISelection getSelection() {
360        return mTreeViewer.getSelection();
361    }
362
363    @Override
364    public void removeSelectionChangedListener(ISelectionChangedListener listener) {
365        mTreeViewer.removeSelectionChangedListener(listener);
366    }
367
368    @Override
369    public void setSelection(ISelection selection) {
370        mTreeViewer.setSelection(selection);
371    }
372}
373