1 /*
2  * Copyright (C) 2012 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
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 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
17 
18 import com.android.annotations.NonNull;
19 import com.android.annotations.Nullable;
20 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
21 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
22 import com.google.common.collect.Maps;
23 
24 import org.eclipse.ui.IEditorPart;
25 import org.eclipse.ui.IEditorReference;
26 import org.eclipse.ui.IPartListener2;
27 import org.eclipse.ui.IPartService;
28 import org.eclipse.ui.IViewReference;
29 import org.eclipse.ui.IWorkbenchPage;
30 import org.eclipse.ui.IWorkbenchPart;
31 import org.eclipse.ui.IWorkbenchPartReference;
32 import org.eclipse.ui.IWorkbenchWindow;
33 
34 import java.util.Map;
35 
36 /**
37  * The {@link LayoutWindowCoordinator} keeps track of Eclipse window events (opening, closing,
38  * fronting, etc) and uses this information to manage the propertysheet and outline
39  * views such that they are always(*) showing:
40  * <ul>
41  *  <li> If the Property Sheet and Outline Eclipse views are showing, it does nothing.
42  *       "Showing" means "is open", not necessary "is visible", e.g. in a tabbed view
43  *       there could be a different view on top.
44  *  <li> If just the outline is showing, then the property sheet is shown in a sashed
45  *       pane below or to the right of the outline (depending on the dominant dimension
46  *       of the window).
47  *  <li> TBD: If just the property sheet is showing, should the outline be showed
48  *       inside that window? Not yet done.
49  *  <li> If the outline is *not* showing, then the outline is instead shown
50  *       <b>inside</b> the editor area, in a right-docked view! This right docked view
51  *       also includes the property sheet!
52  *  <li> If the property sheet is not showing (which includes not showing in the outline
53  *       view as well), then it will be shown inside the editor area, along with the outline
54  *       which should also be there (since if the outline was showing outside the editor
55  *       area, the property sheet would have docked there).
56  *  <li> When the editor is maximized, then all views are temporarily hidden. In this
57  *       case, the property sheet and outline will show up inside the editor.
58  *       When the editor view is un-maximized, the view state will return to what it
59  *       was before.
60  * </ul>
61  * </p>
62  * There is one coordinator per workbench window, shared between all editors in that window.
63  * <p>
64  * TODO: Rename this class to AdtWindowCoordinator. It is used for more than just layout
65  * window coordination now. For example, it's also used to dispatch {@code activated()} and
66  * {@code deactivated()} events to all the XML editors, to ensure that key bindings are
67  * properly dispatched to the right editors in Eclipse 4.x.
68  */
69 public class LayoutWindowCoordinator implements IPartListener2 {
70     static final String PROPERTY_SHEET_PART_ID = "org.eclipse.ui.views.PropertySheet"; //$NON-NLS-1$
71     static final String OUTLINE_PART_ID = "org.eclipse.ui.views.ContentOutline"; //$NON-NLS-1$
72     /** The workbench window */
73     private final IWorkbenchWindow mWindow;
74     /** Is the Eclipse property sheet ViewPart open? */
75     private boolean mPropertiesOpen;
76     /** Is the Eclipse outline ViewPart open? */
77     private boolean mOutlineOpen;
78     /** Is the editor maximized? */
79     private boolean mEditorMaximized;
80     /**
81      * Has the coordinator been initialized? We may have to delay initialization
82      * and perform it lazily if the workbench window does not have an active
83      * page when the coordinator is first started
84      */
85     private boolean mInitialized;
86 
87     /** Map from workbench windows to each layout window coordinator instance for that window */
88     private static Map<IWorkbenchWindow, LayoutWindowCoordinator> sCoordinators =
89             Maps.newHashMapWithExpectedSize(2);
90 
91     /**
92      * Returns the coordinator for the given window.
93      *
94      * @param window the associated window
95      * @param create whether to create the window if it does not already exist
96      * @return the new coordinator, never null if {@code create} is true
97      */
98     @Nullable
get(@onNull IWorkbenchWindow window, boolean create)99     public static LayoutWindowCoordinator get(@NonNull IWorkbenchWindow window, boolean create) {
100         synchronized (LayoutWindowCoordinator.class){
101             LayoutWindowCoordinator coordinator = sCoordinators.get(window);
102             if (coordinator == null && create) {
103                 coordinator = new LayoutWindowCoordinator(window);
104 
105                 IPartService service = window.getPartService();
106                 if (service != null) {
107                     // What if the editor part is *already* open? How do I deal with that?
108                     service.addPartListener(coordinator);
109                 }
110 
111                 sCoordinators.put(window, coordinator);
112             }
113 
114             return coordinator;
115         }
116     }
117 
118 
119     /** Disposes this coordinator (when a window is closed) */
dispose()120     public void dispose() {
121         IPartService service = mWindow.getPartService();
122         if (service != null) {
123             service.removePartListener(this);
124         }
125 
126         synchronized (LayoutWindowCoordinator.class){
127             sCoordinators.remove(mWindow);
128         }
129     }
130 
131     /**
132      * Returns true if the main editor window is maximized
133      *
134      * @return true if the main editor window is maximized
135      */
isEditorMaximized()136     public boolean isEditorMaximized() {
137         return mEditorMaximized;
138     }
139 
LayoutWindowCoordinator(@onNull IWorkbenchWindow window)140     private LayoutWindowCoordinator(@NonNull IWorkbenchWindow window) {
141         mWindow = window;
142 
143         initialize();
144     }
145 
initialize()146     private void initialize() {
147         if (mInitialized) {
148             return;
149         }
150 
151         IWorkbenchPage activePage = mWindow.getActivePage();
152         if (activePage == null) {
153             return;
154         }
155 
156         mInitialized = true;
157 
158         // Look up current state of the properties and outline windows (in case
159         // they have already been opened before we added our part listener)
160         IViewReference ref = findPropertySheetView(activePage);
161         if (ref != null) {
162             IWorkbenchPart part = ref.getPart(false /*restore*/);
163             if (activePage.isPartVisible(part)) {
164                 mPropertiesOpen = true;
165             }
166         }
167         ref = findOutlineView(activePage);
168         if (ref != null) {
169             IWorkbenchPart part = ref.getPart(false /*restore*/);
170             if (activePage.isPartVisible(part)) {
171                 mOutlineOpen = true;
172             }
173         }
174         if (!syncMaximizedState(activePage)) {
175             syncActive();
176         }
177     }
178 
findPropertySheetView(IWorkbenchPage activePage)179     static IViewReference findPropertySheetView(IWorkbenchPage activePage) {
180         return activePage.findViewReference(PROPERTY_SHEET_PART_ID);
181     }
182 
findOutlineView(IWorkbenchPage activePage)183     static IViewReference findOutlineView(IWorkbenchPage activePage) {
184         return activePage.findViewReference(OUTLINE_PART_ID);
185     }
186 
187     /**
188      * Checks the maximized state of the page and updates internal state if
189      * necessary.
190      * <p>
191      * This is used in Eclipse 4.x, where the {@link IPartListener2} does not
192      * fire {@link IPartListener2#partHidden(IWorkbenchPartReference)} when the
193      * editor is maximized anymore (see issue
194      * https://bugs.eclipse.org/bugs/show_bug.cgi?id=382120 for details).
195      * Instead, the layout editor listens for resize events, and upon resize it
196      * looks up the part state and calls this method to ensure that the right
197      * maximized state is known to the layout coordinator.
198      *
199      * @param page the active workbench page
200      * @return true if the state changed, false otherwise
201      */
syncMaximizedState(IWorkbenchPage page)202     public boolean syncMaximizedState(IWorkbenchPage page) {
203         boolean maximized = isPageZoomed(page);
204         if (mEditorMaximized != maximized) {
205             mEditorMaximized = maximized;
206             syncActive();
207             return true;
208         }
209         return false;
210     }
211 
isPageZoomed(IWorkbenchPage page)212     private boolean isPageZoomed(IWorkbenchPage page) {
213         IWorkbenchPartReference reference = page.getActivePartReference();
214         if (reference != null && reference instanceof IEditorReference) {
215             int state = page.getPartState(reference);
216             boolean maximized = (state & IWorkbenchPage.STATE_MAXIMIZED) != 0;
217             return maximized;
218         }
219 
220         // If the active reference isn't the editor, then the editor can't be maximized
221         return false;
222     }
223 
224     /**
225      * Syncs the given editor's view state such that the property sheet and or
226      * outline are shown or hidden according to the visibility of the global
227      * outline and property sheet views.
228      * <p>
229      * This is typically done when a layout editor is fronted. For view updates
230      * when the view is already showing, the {@link LayoutWindowCoordinator}
231      * will automatically handle the current fronted window.
232      *
233      * @param editor the editor to sync
234      */
sync(@ullable GraphicalEditorPart editor)235     private void sync(@Nullable GraphicalEditorPart editor) {
236         if (editor == null) {
237             return;
238         }
239         if (mEditorMaximized) {
240             editor.showStructureViews(true /*outline*/, true /*properties*/, true /*layout*/);
241         } else if (mOutlineOpen) {
242             editor.showStructureViews(false /*outline*/, false /*properties*/, true /*layout*/);
243             editor.getCanvasControl().getOutlinePage().setShowPropertySheet(!mPropertiesOpen);
244         } else {
245             editor.showStructureViews(true /*outline*/, !mPropertiesOpen /*properties*/,
246                     true /*layout*/);
247         }
248     }
249 
sync(IWorkbenchPart part)250     private void sync(IWorkbenchPart part) {
251         if (part instanceof AndroidXmlEditor) {
252             LayoutEditorDelegate editor = LayoutEditorDelegate.fromEditor((IEditorPart) part);
253             if (editor != null) {
254                 sync(editor.getGraphicalEditor());
255             }
256         }
257     }
258 
syncActive()259     private void syncActive() {
260         IWorkbenchPage activePage = mWindow.getActivePage();
261         if (activePage != null) {
262             IEditorPart editor = activePage.getActiveEditor();
263             sync(editor);
264         }
265     }
266 
propertySheetClosed()267     private void propertySheetClosed() {
268         mPropertiesOpen = false;
269         syncActive();
270     }
271 
propertySheetOpened()272     private void propertySheetOpened() {
273         mPropertiesOpen = true;
274         syncActive();
275     }
276 
outlineClosed()277     private void outlineClosed() {
278         mOutlineOpen = false;
279         syncActive();
280     }
281 
outlineOpened()282     private void outlineOpened() {
283         mOutlineOpen = true;
284         syncActive();
285     }
286 
287     // ---- Implements IPartListener2 ----
288 
289     @Override
partOpened(IWorkbenchPartReference partRef)290     public void partOpened(IWorkbenchPartReference partRef) {
291         // We ignore partOpened() and partClosed() because these methods are only
292         // called when a view is opened in the first perspective, and closed in the
293         // last perspective. The outline is typically used in multiple perspectives,
294         // so closing it in the Java perspective does *not* fire a partClosed event.
295         // There is no notification for "part closed in perspective" (see issue
296         // https://bugs.eclipse.org/bugs/show_bug.cgi?id=54559 for details).
297         // However, the workaround we can use is to listen to partVisible() and
298         // partHidden(). These will be called more often than we'd like (e.g.
299         // when the tab order causes a view to be obscured), however, we can use
300         // the workaround of looking up IWorkbenchPage.findViewReference(id) after
301         // partHidden(), which will return null if the view is closed in the current
302         // perspective. For partOpened, we simply look in partVisible() for whether
303         // our flags tracking the view state have been initialized already.
304     }
305 
306     @Override
partClosed(IWorkbenchPartReference partRef)307     public void partClosed(IWorkbenchPartReference partRef) {
308         // partClosed() doesn't get called when a window is closed unless it has
309         // been closed in *all* perspectives. See partOpened() for more.
310     }
311 
312     @Override
partHidden(IWorkbenchPartReference partRef)313     public void partHidden(IWorkbenchPartReference partRef) {
314         IWorkbenchPage activePage = mWindow.getActivePage();
315         if (activePage == null) {
316             return;
317         }
318         initialize();
319 
320         // See if this looks like the window was closed in this workspace
321         // See partOpened() for an explanation.
322         String id = partRef.getId();
323         if (PROPERTY_SHEET_PART_ID.equals(id)) {
324             if (activePage.findViewReference(id) == null) {
325                 propertySheetClosed();
326                 return;
327             }
328         } else if (OUTLINE_PART_ID.equals(id)) {
329             if (activePage.findViewReference(id) == null) {
330                 outlineClosed();
331                 return;
332             }
333         }
334 
335         // Does this look like a window getting maximized?
336         syncMaximizedState(activePage);
337     }
338 
339     @Override
partVisible(IWorkbenchPartReference partRef)340     public void partVisible(IWorkbenchPartReference partRef) {
341         IWorkbenchPage activePage = mWindow.getActivePage();
342         if (activePage == null) {
343             return;
344         }
345         initialize();
346 
347         String id = partRef.getId();
348         if (mEditorMaximized) {
349             // Return to their non-maximized state
350             mEditorMaximized = false;
351             syncActive();
352         }
353 
354         IWorkbenchPart part = partRef.getPart(false /*restore*/);
355         sync(part);
356 
357         // See partOpened() for an explanation
358         if (PROPERTY_SHEET_PART_ID.equals(id)) {
359             if (!mPropertiesOpen) {
360                 propertySheetOpened();
361                 assert mPropertiesOpen;
362             }
363         } else if (OUTLINE_PART_ID.equals(id)) {
364             if (!mOutlineOpen) {
365                 outlineOpened();
366                 assert mOutlineOpen;
367             }
368         }
369     }
370 
371     @Override
partInputChanged(IWorkbenchPartReference partRef)372     public void partInputChanged(IWorkbenchPartReference partRef) {
373     }
374 
375     @Override
partActivated(IWorkbenchPartReference partRef)376     public void partActivated(IWorkbenchPartReference partRef) {
377         IWorkbenchPart part = partRef.getPart(false);
378         if (part instanceof AndroidXmlEditor) {
379             ((AndroidXmlEditor)part).activated();
380         }
381     }
382 
383     @Override
partBroughtToTop(IWorkbenchPartReference partRef)384     public void partBroughtToTop(IWorkbenchPartReference partRef) {
385     }
386 
387     @Override
partDeactivated(IWorkbenchPartReference partRef)388     public void partDeactivated(IWorkbenchPartReference partRef) {
389         IWorkbenchPart part = partRef.getPart(false);
390         if (part instanceof AndroidXmlEditor) {
391             ((AndroidXmlEditor)part).deactivated();
392         }
393     }
394 }