/* * 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(); } } }