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 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) */ 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 */ 136 public boolean isEditorMaximized() { 137 return mEditorMaximized; 138 } 139 140 private LayoutWindowCoordinator(@NonNull IWorkbenchWindow window) { 141 mWindow = window; 142 143 initialize(); 144 } 145 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 179 static IViewReference findPropertySheetView(IWorkbenchPage activePage) { 180 return activePage.findViewReference(PROPERTY_SHEET_PART_ID); 181 } 182 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 */ 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 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 */ 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 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 259 private void syncActive() { 260 IWorkbenchPage activePage = mWindow.getActivePage(); 261 if (activePage != null) { 262 IEditorPart editor = activePage.getActiveEditor(); 263 sync(editor); 264 } 265 } 266 267 private void propertySheetClosed() { 268 mPropertiesOpen = false; 269 syncActive(); 270 } 271 272 private void propertySheetOpened() { 273 mPropertiesOpen = true; 274 syncActive(); 275 } 276 277 private void outlineClosed() { 278 mOutlineOpen = false; 279 syncActive(); 280 } 281 282 private void outlineOpened() { 283 mOutlineOpen = true; 284 syncActive(); 285 } 286 287 // ---- Implements IPartListener2 ---- 288 289 @Override 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 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 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 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 372 public void partInputChanged(IWorkbenchPartReference partRef) { 373 } 374 375 @Override 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 384 public void partBroughtToTop(IWorkbenchPartReference partRef) { 385 } 386 387 @Override 388 public void partDeactivated(IWorkbenchPartReference partRef) { 389 IWorkbenchPart part = partRef.getPart(false); 390 if (part instanceof AndroidXmlEditor) { 391 ((AndroidXmlEditor)part).deactivated(); 392 } 393 } 394 }