aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutWindowCoordinator.java
blob: 56b86aa853e1d2cd46ad5ad89e37cfdadcc1a134 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
/*
 * Copyright (C) 2012 The Android Open Source Project
 *
 * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
 *
 * 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.
 */
package com.android.ide.eclipse.adt.internal.editors.layout.gle2;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
import com.google.common.collect.Maps;

import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.IPartListener2;
import org.eclipse.ui.IPartService;
import org.eclipse.ui.IViewReference;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchPartReference;
import org.eclipse.ui.IWorkbenchWindow;

import java.util.Map;

/**
 * The {@link LayoutWindowCoordinator} keeps track of Eclipse window events (opening, closing,
 * fronting, etc) and uses this information to manage the propertysheet and outline
 * views such that they are always(*) showing:
 * <ul>
 *  <li> If the Property Sheet and Outline Eclipse views are showing, it does nothing.
 *       "Showing" means "is open", not necessary "is visible", e.g. in a tabbed view
 *       there could be a different view on top.
 *  <li> If just the outline is showing, then the property sheet is shown in a sashed
 *       pane below or to the right of the outline (depending on the dominant dimension
 *       of the window).
 *  <li> TBD: If just the property sheet is showing, should the outline be showed
 *       inside that window? Not yet done.
 *  <li> If the outline is *not* showing, then the outline is instead shown
 *       <b>inside</b> the editor area, in a right-docked view! This right docked view
 *       also includes the property sheet!
 *  <li> If the property sheet is not showing (which includes not showing in the outline
 *       view as well), then it will be shown inside the editor area, along with the outline
 *       which should also be there (since if the outline was showing outside the editor
 *       area, the property sheet would have docked there).
 *  <li> When the editor is maximized, then all views are temporarily hidden. In this
 *       case, the property sheet and outline will show up inside the editor.
 *       When the editor view is un-maximized, the view state will return to what it
 *       was before.
 * </ul>
 * </p>
 * There is one coordinator per workbench window, shared between all editors in that window.
 * <p>
 * TODO: Rename this class to AdtWindowCoordinator. It is used for more than just layout
 * window coordination now. For example, it's also used to dispatch {@code activated()} and
 * {@code deactivated()} events to all the XML editors, to ensure that key bindings are
 * properly dispatched to the right editors in Eclipse 4.x.
 */
public class LayoutWindowCoordinator implements IPartListener2 {
    static final String PROPERTY_SHEET_PART_ID = "org.eclipse.ui.views.PropertySheet"; //$NON-NLS-1$
    static final String OUTLINE_PART_ID = "org.eclipse.ui.views.ContentOutline"; //$NON-NLS-1$
    /** The workbench window */
    private final IWorkbenchWindow mWindow;
    /** Is the Eclipse property sheet ViewPart open? */
    private boolean mPropertiesOpen;
    /** Is the Eclipse outline ViewPart open? */
    private boolean mOutlineOpen;
    /** Is the editor maximized? */
    private boolean mEditorMaximized;
    /**
     * Has the coordinator been initialized? We may have to delay initialization
     * and perform it lazily if the workbench window does not have an active
     * page when the coordinator is first started
     */
    private boolean mInitialized;

    /** Map from workbench windows to each layout window coordinator instance for that window */
    private static Map<IWorkbenchWindow, LayoutWindowCoordinator> sCoordinators =
            Maps.newHashMapWithExpectedSize(2);

    /**
     * Returns the coordinator for the given window.
     *
     * @param window the associated window
     * @param create whether to create the window if it does not already exist
     * @return the new coordinator, never null if {@code create} is true
     */
    @Nullable
    public static LayoutWindowCoordinator get(@NonNull IWorkbenchWindow window, boolean create) {
        synchronized (LayoutWindowCoordinator.class){
            LayoutWindowCoordinator coordinator = sCoordinators.get(window);
            if (coordinator == null && create) {
                coordinator = new LayoutWindowCoordinator(window);

                IPartService service = window.getPartService();
                if (service != null) {
                    // What if the editor part is *already* open? How do I deal with that?
                    service.addPartListener(coordinator);
                }

                sCoordinators.put(window, coordinator);
            }

            return coordinator;
        }
    }


    /** Disposes this coordinator (when a window is closed) */
    public void dispose() {
        IPartService service = mWindow.getPartService();
        if (service != null) {
            service.removePartListener(this);
        }

        synchronized (LayoutWindowCoordinator.class){
            sCoordinators.remove(mWindow);
        }
    }

    /**
     * Returns true if the main editor window is maximized
     *
     * @return true if the main editor window is maximized
     */
    public boolean isEditorMaximized() {
        return mEditorMaximized;
    }

    private LayoutWindowCoordinator(@NonNull IWorkbenchWindow window) {
        mWindow = window;

        initialize();
    }

    private void initialize() {
        if (mInitialized) {
            return;
        }

        IWorkbenchPage activePage = mWindow.getActivePage();
        if (activePage == null) {
            return;
        }

        mInitialized = true;

        // Look up current state of the properties and outline windows (in case
        // they have already been opened before we added our part listener)
        IViewReference ref = findPropertySheetView(activePage);
        if (ref != null) {
            IWorkbenchPart part = ref.getPart(false /*restore*/);
            if (activePage.isPartVisible(part)) {
                mPropertiesOpen = true;
            }
        }
        ref = findOutlineView(activePage);
        if (ref != null) {
            IWorkbenchPart part = ref.getPart(false /*restore*/);
            if (activePage.isPartVisible(part)) {
                mOutlineOpen = true;
            }
        }
        if (!syncMaximizedState(activePage)) {
            syncActive();
        }
    }

    static IViewReference findPropertySheetView(IWorkbenchPage activePage) {
        return activePage.findViewReference(PROPERTY_SHEET_PART_ID);
    }

    static IViewReference findOutlineView(IWorkbenchPage activePage) {
        return activePage.findViewReference(OUTLINE_PART_ID);
    }

    /**
     * Checks the maximized state of the page and updates internal state if
     * necessary.
     * <p>
     * This is used in Eclipse 4.x, where the {@link IPartListener2} does not
     * fire {@link IPartListener2#partHidden(IWorkbenchPartReference)} when the
     * editor is maximized anymore (see issue
     * https://bugs.eclipse.org/bugs/show_bug.cgi?id=382120 for details).
     * Instead, the layout editor listens for resize events, and upon resize it
     * looks up the part state and calls this method to ensure that the right
     * maximized state is known to the layout coordinator.
     *
     * @param page the active workbench page
     * @return true if the state changed, false otherwise
     */
    public boolean syncMaximizedState(IWorkbenchPage page) {
        boolean maximized = isPageZoomed(page);
        if (mEditorMaximized != maximized) {
            mEditorMaximized = maximized;
            syncActive();
            return true;
        }
        return false;
    }

    private boolean isPageZoomed(IWorkbenchPage page) {
        IWorkbenchPartReference reference = page.getActivePartReference();
        if (reference != null && reference instanceof IEditorReference) {
            int state = page.getPartState(reference);
            boolean maximized = (state & IWorkbenchPage.STATE_MAXIMIZED) != 0;
            return maximized;
        }

        // If the active reference isn't the editor, then the editor can't be maximized
        return false;
    }

    /**
     * Syncs the given editor's view state such that the property sheet and or
     * outline are shown or hidden according to the visibility of the global
     * outline and property sheet views.
     * <p>
     * This is typically done when a layout editor is fronted. For view updates
     * when the view is already showing, the {@link LayoutWindowCoordinator}
     * will automatically handle the current fronted window.
     *
     * @param editor the editor to sync
     */
    private void sync(@Nullable GraphicalEditorPart editor) {
        if (editor == null) {
            return;
        }
        if (mEditorMaximized) {
            editor.showStructureViews(true /*outline*/, true /*properties*/, true /*layout*/);
        } else if (mOutlineOpen) {
            editor.showStructureViews(false /*outline*/, false /*properties*/, true /*layout*/);
            editor.getCanvasControl().getOutlinePage().setShowPropertySheet(!mPropertiesOpen);
        } else {
            editor.showStructureViews(true /*outline*/, !mPropertiesOpen /*properties*/,
                    true /*layout*/);
        }
    }

    private void sync(IWorkbenchPart part) {
        if (part instanceof AndroidXmlEditor) {
            LayoutEditorDelegate editor = LayoutEditorDelegate.fromEditor((IEditorPart) part);
            if (editor != null) {
                sync(editor.getGraphicalEditor());
            }
        }
    }

    private void syncActive() {
        IWorkbenchPage activePage = mWindow.getActivePage();
        if (activePage != null) {
            IEditorPart editor = activePage.getActiveEditor();
            sync(editor);
        }
    }

    private void propertySheetClosed() {
        mPropertiesOpen = false;
        syncActive();
    }

    private void propertySheetOpened() {
        mPropertiesOpen = true;
        syncActive();
    }

    private void outlineClosed() {
        mOutlineOpen = false;
        syncActive();
    }

    private void outlineOpened() {
        mOutlineOpen = true;
        syncActive();
    }

    // ---- Implements IPartListener2 ----

    @Override
    public void partOpened(IWorkbenchPartReference partRef) {
        // We ignore partOpened() and partClosed() because these methods are only
        // called when a view is opened in the first perspective, and closed in the
        // last perspective. The outline is typically used in multiple perspectives,
        // so closing it in the Java perspective does *not* fire a partClosed event.
        // There is no notification for "part closed in perspective" (see issue
        // https://bugs.eclipse.org/bugs/show_bug.cgi?id=54559 for details).
        // However, the workaround we can use is to listen to partVisible() and
        // partHidden(). These will be called more often than we'd like (e.g.
        // when the tab order causes a view to be obscured), however, we can use
        // the workaround of looking up IWorkbenchPage.findViewReference(id) after
        // partHidden(), which will return null if the view is closed in the current
        // perspective. For partOpened, we simply look in partVisible() for whether
        // our flags tracking the view state have been initialized already.
    }

    @Override
    public void partClosed(IWorkbenchPartReference partRef) {
        // partClosed() doesn't get called when a window is closed unless it has
        // been closed in *all* perspectives. See partOpened() for more.
    }

    @Override
    public void partHidden(IWorkbenchPartReference partRef) {
        IWorkbenchPage activePage = mWindow.getActivePage();
        if (activePage == null) {
            return;
        }
        initialize();

        // See if this looks like the window was closed in this workspace
        // See partOpened() for an explanation.
        String id = partRef.getId();
        if (PROPERTY_SHEET_PART_ID.equals(id)) {
            if (activePage.findViewReference(id) == null) {
                propertySheetClosed();
                return;
            }
        } else if (OUTLINE_PART_ID.equals(id)) {
            if (activePage.findViewReference(id) == null) {
                outlineClosed();
                return;
            }
        }

        // Does this look like a window getting maximized?
        syncMaximizedState(activePage);
    }

    @Override
    public void partVisible(IWorkbenchPartReference partRef) {
        IWorkbenchPage activePage = mWindow.getActivePage();
        if (activePage == null) {
            return;
        }
        initialize();

        String id = partRef.getId();
        if (mEditorMaximized) {
            // Return to their non-maximized state
            mEditorMaximized = false;
            syncActive();
        }

        IWorkbenchPart part = partRef.getPart(false /*restore*/);
        sync(part);

        // See partOpened() for an explanation
        if (PROPERTY_SHEET_PART_ID.equals(id)) {
            if (!mPropertiesOpen) {
                propertySheetOpened();
                assert mPropertiesOpen;
            }
        } else if (OUTLINE_PART_ID.equals(id)) {
            if (!mOutlineOpen) {
                outlineOpened();
                assert mOutlineOpen;
            }
        }
    }

    @Override
    public void partInputChanged(IWorkbenchPartReference partRef) {
    }

    @Override
    public void partActivated(IWorkbenchPartReference partRef) {
        IWorkbenchPart part = partRef.getPart(false);
        if (part instanceof AndroidXmlEditor) {
            ((AndroidXmlEditor)part).activated();
        }
    }

    @Override
    public void partBroughtToTop(IWorkbenchPartReference partRef) {
    }

    @Override
    public void partDeactivated(IWorkbenchPartReference partRef) {
        IWorkbenchPart part = partRef.getPart(false);
        if (part instanceof AndroidXmlEditor) {
            ((AndroidXmlEditor)part).deactivated();
        }
    }
}