1 /* 2 * Copyright (C) 2011 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 static com.android.ide.common.layout.LayoutConstants.ANDROID_URI; 19 import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; 20 21 import com.android.ide.common.api.INode; 22 import com.android.ide.common.api.RuleAction; 23 import com.android.ide.common.api.RuleAction.Choices; 24 import com.android.ide.common.api.RuleAction.Separator; 25 import com.android.ide.common.api.RuleAction.Toggle; 26 import com.android.ide.common.layout.BaseViewRule; 27 import com.android.ide.eclipse.adt.internal.editors.IconFactory; 28 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite; 29 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; 30 import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; 31 import com.android.ide.eclipse.adt.internal.lint.EclipseLintClient; 32 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; 33 import com.android.sdkuilib.internal.widgets.ResolutionChooserDialog; 34 35 import org.eclipse.core.resources.IFile; 36 import org.eclipse.jface.window.Window; 37 import org.eclipse.swt.SWT; 38 import org.eclipse.swt.events.SelectionAdapter; 39 import org.eclipse.swt.events.SelectionEvent; 40 import org.eclipse.swt.graphics.Image; 41 import org.eclipse.swt.graphics.Point; 42 import org.eclipse.swt.layout.GridData; 43 import org.eclipse.swt.layout.GridLayout; 44 import org.eclipse.swt.widgets.Composite; 45 import org.eclipse.swt.widgets.Display; 46 import org.eclipse.swt.widgets.Event; 47 import org.eclipse.swt.widgets.Listener; 48 import org.eclipse.swt.widgets.Menu; 49 import org.eclipse.swt.widgets.MenuItem; 50 import org.eclipse.swt.widgets.ToolBar; 51 import org.eclipse.swt.widgets.ToolItem; 52 import org.eclipse.ui.ISharedImages; 53 import org.eclipse.ui.PlatformUI; 54 55 import java.net.URL; 56 import java.util.ArrayList; 57 import java.util.Collections; 58 import java.util.List; 59 60 /** 61 * Toolbar shown at the top of the layout editor, which adds a number of context-sensitive 62 * layout actions (as well as zooming controls on the right). 63 */ 64 public class LayoutActionBar extends Composite { 65 private GraphicalEditorPart mEditor; 66 private ToolBar mLayoutToolBar; 67 private ToolBar mLintToolBar; 68 private ToolBar mZoomToolBar; 69 private ToolItem mZoomRealSizeButton; 70 private ToolItem mZoomOutButton; 71 private ToolItem mZoomResetButton; 72 private ToolItem mZoomInButton; 73 private ToolItem mZoomFitButton; 74 private ToolItem mLintButton; 75 76 /** 77 * Creates a new {@link LayoutActionBar} and adds it to the given parent. 78 * 79 * @param parent the parent composite to add the actions bar to 80 * @param style the SWT style to apply 81 * @param editor the associated layout editor 82 */ LayoutActionBar(Composite parent, int style, GraphicalEditorPart editor)83 public LayoutActionBar(Composite parent, int style, GraphicalEditorPart editor) { 84 super(parent, style | SWT.NO_FOCUS); 85 mEditor = editor; 86 87 GridLayout layout = new GridLayout(3, false); 88 setLayout(layout); 89 90 mLayoutToolBar = new ToolBar(this, SWT.FLAT | SWT.RIGHT | SWT.HORIZONTAL); 91 mLayoutToolBar.setLayoutData(new GridData(SWT.BEGINNING, SWT.BEGINNING, true, false)); 92 mZoomToolBar = createZoomControls(); 93 mZoomToolBar.setLayoutData(new GridData(SWT.END, SWT.BEGINNING, true, false)); 94 mLintToolBar = createLintControls(); 95 96 GridData lintData = new GridData(SWT.END, SWT.BEGINNING, false, false); 97 lintData.exclude = true; 98 mLintToolBar.setLayoutData(lintData); 99 } 100 101 /** Updates the layout contents based on the current selection */ updateSelection()102 void updateSelection() { 103 // Get rid of any previous children 104 for (ToolItem c : mLayoutToolBar.getItems()) { 105 c.dispose(); 106 } 107 mLayoutToolBar.pack(); 108 109 NodeProxy parent = null; 110 LayoutCanvas canvas = mEditor.getCanvasControl(); 111 SelectionManager selectionManager = canvas.getSelectionManager(); 112 List<SelectionItem> selections = selectionManager.getSelections(); 113 if (selections.size() > 0) { 114 // TODO: better handle multi-selection -- maybe we should disable it or 115 // something. 116 // What if you select children with different parents? Of different types? 117 // etc. 118 NodeProxy node = selections.get(0).getNode(); 119 if (node != null && node.getParent() != null) { 120 parent = (NodeProxy) node.getParent(); 121 } 122 } 123 124 if (parent == null) { 125 // Show the background's properties 126 CanvasViewInfo root = canvas.getViewHierarchy().getRoot(); 127 if (root == null) { 128 return; 129 } 130 parent = canvas.getNodeFactory().create(root); 131 selections = Collections.emptyList(); 132 } 133 134 RulesEngine engine = mEditor.getRulesEngine(); 135 List<NodeProxy> selectedNodes = new ArrayList<NodeProxy>(); 136 for (SelectionItem item : selections) { 137 selectedNodes.add(item.getNode()); 138 } 139 List<RuleAction> actions = new ArrayList<RuleAction>(); 140 engine.callAddLayoutActions(actions, parent, selectedNodes); 141 142 // Place actions in the correct order (the actions may come from different 143 // rules and should be merged properly via sorting keys) 144 Collections.sort(actions); 145 146 // Add in actions for the child as well, if there is exactly one. 147 // These are not merged into the parent list of actions; they are appended 148 // at the end. 149 int index = -1; 150 String label = null; 151 if (selectedNodes.size() == 1) { 152 List<RuleAction> itemActions = new ArrayList<RuleAction>(); 153 NodeProxy selectedNode = selectedNodes.get(0); 154 engine.callAddLayoutActions(itemActions, selectedNode, null); 155 if (itemActions.size() > 0) { 156 Collections.sort(itemActions); 157 158 if (!(itemActions.get(0) instanceof RuleAction.Separator)) { 159 actions.add(RuleAction.createSeparator(0)); 160 } 161 label = selectedNode.getStringAttr(ANDROID_URI, ATTR_ID); 162 if (label != null) { 163 label = BaseViewRule.stripIdPrefix(label); 164 index = actions.size(); 165 } 166 actions.addAll(itemActions); 167 } 168 } 169 170 addActions(actions, index, label); 171 172 mLayoutToolBar.pack(); 173 mLayoutToolBar.layout(); 174 } 175 addActions(List<RuleAction> actions, int labelIndex, String label)176 private void addActions(List<RuleAction> actions, int labelIndex, String label) { 177 if (actions.size() > 0) { 178 // Flag used to indicate that if there are any actions -after- this, it 179 // should be separated from this current action (we don't unconditionally 180 // add a separator at the end of these groups in case there are no more 181 // actions at the end so that we don't have a trailing separator) 182 boolean needSeparator = false; 183 184 int index = 0; 185 for (RuleAction action : actions) { 186 if (index == labelIndex) { 187 final ToolItem button = new ToolItem(mLayoutToolBar, SWT.PUSH); 188 button.setText(label); 189 needSeparator = false; 190 } 191 index++; 192 193 if (action instanceof Separator) { 194 addSeparator(mLayoutToolBar); 195 needSeparator = false; 196 continue; 197 } else if (needSeparator) { 198 addSeparator(mLayoutToolBar); 199 needSeparator = false; 200 } 201 202 if (action instanceof RuleAction.Choices) { 203 RuleAction.Choices choices = (Choices) action; 204 if (!choices.isRadio()) { 205 addDropdown(choices); 206 } else { 207 addSeparator(mLayoutToolBar); 208 addRadio(choices); 209 needSeparator = true; 210 } 211 } else if (action instanceof RuleAction.Toggle) { 212 addToggle((Toggle) action); 213 } else { 214 addPlainAction(action); 215 } 216 } 217 } 218 } 219 220 /** Add a separator to the toolbar, unless there already is one there at the end already */ addSeparator(ToolBar toolBar)221 private static void addSeparator(ToolBar toolBar) { 222 int n = toolBar.getItemCount(); 223 if (n > 0 && (toolBar.getItem(n - 1).getStyle() & SWT.SEPARATOR) == 0) { 224 ToolItem separator = new ToolItem(toolBar, SWT.SEPARATOR); 225 separator.setWidth(15); 226 } 227 } 228 addToggle(final Toggle toggle)229 private void addToggle(final Toggle toggle) { 230 final ToolItem button = new ToolItem(mLayoutToolBar, SWT.CHECK); 231 232 URL iconUrl = toggle.getIconUrl(); 233 String title = toggle.getTitle(); 234 if (iconUrl != null) { 235 button.setImage(IconFactory.getInstance().getIcon(iconUrl)); 236 button.setToolTipText(title); 237 } else { 238 button.setText(title); 239 } 240 241 button.addSelectionListener(new SelectionAdapter() { 242 @Override 243 public void widgetSelected(SelectionEvent e) { 244 toggle.getCallback().action(toggle, getSelectedNodes(), 245 toggle.getId(), button.getSelection()); 246 updateSelection(); 247 } 248 }); 249 if (toggle.isChecked()) { 250 button.setSelection(true); 251 } 252 } 253 getSelectedNodes()254 private List<INode> getSelectedNodes() { 255 List<SelectionItem> selections = 256 mEditor.getCanvasControl().getSelectionManager().getSelections(); 257 List<INode> nodes = new ArrayList<INode>(selections.size()); 258 for (SelectionItem item : selections) { 259 nodes.add(item.getNode()); 260 } 261 262 return nodes; 263 } 264 265 addPlainAction(final RuleAction menuAction)266 private void addPlainAction(final RuleAction menuAction) { 267 final ToolItem button = new ToolItem(mLayoutToolBar, SWT.PUSH); 268 269 URL iconUrl = menuAction.getIconUrl(); 270 String title = menuAction.getTitle(); 271 if (iconUrl != null) { 272 button.setImage(IconFactory.getInstance().getIcon(iconUrl)); 273 button.setToolTipText(title); 274 } else { 275 button.setText(title); 276 } 277 278 button.addSelectionListener(new SelectionAdapter() { 279 @Override 280 public void widgetSelected(SelectionEvent e) { 281 menuAction.getCallback().action(menuAction, getSelectedNodes(), menuAction.getId(), 282 false); 283 updateSelection(); 284 } 285 }); 286 } 287 addRadio(final RuleAction.Choices choices)288 private void addRadio(final RuleAction.Choices choices) { 289 List<URL> icons = choices.getIconUrls(); 290 List<String> titles = choices.getTitles(); 291 List<String> ids = choices.getIds(); 292 String current = choices.getCurrent() != null ? choices.getCurrent() : ""; //$NON-NLS-1$ 293 294 assert icons != null; 295 assert icons.size() == titles.size(); 296 297 for (int i = 0; i < icons.size(); i++) { 298 URL iconUrl = icons.get(i); 299 String title = titles.get(i); 300 final String id = ids.get(i); 301 final ToolItem item = new ToolItem(mLayoutToolBar, SWT.RADIO); 302 item.setToolTipText(title); 303 item.setImage(IconFactory.getInstance().getIcon(iconUrl)); 304 item.addSelectionListener(new SelectionAdapter() { 305 @Override 306 public void widgetSelected(SelectionEvent e) { 307 if (item.getSelection()) { 308 choices.getCallback().action(choices, getSelectedNodes(), id, null); 309 updateSelection(); 310 } 311 } 312 }); 313 boolean selected = current.equals(id); 314 if (selected) { 315 item.setSelection(true); 316 } 317 } 318 } 319 addDropdown(final RuleAction.Choices choices)320 private void addDropdown(final RuleAction.Choices choices) { 321 final ToolItem combo = new ToolItem(mLayoutToolBar, SWT.DROP_DOWN); 322 URL iconUrl = choices.getIconUrl(); 323 if (iconUrl != null) { 324 combo.setImage(IconFactory.getInstance().getIcon(iconUrl)); 325 combo.setToolTipText(choices.getTitle()); 326 } else { 327 combo.setText(choices.getTitle()); 328 } 329 330 Listener menuListener = new Listener() { 331 @Override 332 public void handleEvent(Event event) { 333 // if (event.detail == SWT.ARROW) { 334 Point point = new Point(event.x, event.y); 335 point = combo.getDisplay().map(mLayoutToolBar, null, point); 336 337 Menu menu = new Menu(mLayoutToolBar.getShell(), SWT.POP_UP); 338 339 List<URL> icons = choices.getIconUrls(); 340 List<String> titles = choices.getTitles(); 341 List<String> ids = choices.getIds(); 342 String current = choices.getCurrent() != null ? choices.getCurrent() : ""; //$NON-NLS-1$ 343 344 for (int i = 0; i < titles.size(); i++) { 345 String title = titles.get(i); 346 final String id = ids.get(i); 347 URL itemIconUrl = icons != null && icons.size() > 0 ? icons.get(i) : null; 348 MenuItem item = new MenuItem(menu, SWT.CHECK); 349 item.setText(title); 350 if (itemIconUrl != null) { 351 Image itemIcon = IconFactory.getInstance().getIcon(itemIconUrl); 352 item.setImage(itemIcon); 353 } 354 355 boolean selected = id.equals(current); 356 if (selected) { 357 item.setSelection(true); 358 } 359 360 item.addSelectionListener(new SelectionAdapter() { 361 @Override 362 public void widgetSelected(SelectionEvent e) { 363 choices.getCallback().action(choices, getSelectedNodes(), id, null); 364 updateSelection(); 365 } 366 }); 367 } 368 369 // TODO - how do I dispose of this? 370 371 menu.setLocation(point); 372 menu.setVisible(true); 373 } 374 }; 375 combo.addListener(SWT.Selection, menuListener); 376 } 377 378 // ---- Zoom Controls ---- 379 380 @SuppressWarnings("unused") // SWT constructors have side effects, they are not unused createZoomControls()381 private ToolBar createZoomControls() { 382 ToolBar toolBar = new ToolBar(this, SWT.FLAT | SWT.RIGHT | SWT.HORIZONTAL); 383 384 IconFactory iconFactory = IconFactory.getInstance(); 385 mZoomRealSizeButton = new ToolItem(toolBar, SWT.CHECK); 386 mZoomRealSizeButton.setToolTipText("Emulate Real Size"); 387 mZoomRealSizeButton.setImage(iconFactory.getIcon("zoomreal")); //$NON-NLS-1$); 388 mZoomRealSizeButton.addSelectionListener(new SelectionAdapter() { 389 @Override 390 public void widgetSelected(SelectionEvent e) { 391 boolean newState = mZoomRealSizeButton.getSelection(); 392 if (rescaleToReal(newState)) { 393 mZoomOutButton.setEnabled(!newState); 394 mZoomResetButton.setEnabled(!newState); 395 mZoomInButton.setEnabled(!newState); 396 mZoomFitButton.setEnabled(!newState); 397 } else { 398 mZoomRealSizeButton.setSelection(!newState); 399 } 400 } 401 }); 402 403 mZoomFitButton = new ToolItem(toolBar, SWT.PUSH); 404 mZoomFitButton.setToolTipText("Zoom to Fit (0)"); 405 mZoomFitButton.setImage(iconFactory.getIcon("zoomfit")); //$NON-NLS-1$); 406 mZoomFitButton.addSelectionListener(new SelectionAdapter() { 407 @Override 408 public void widgetSelected(SelectionEvent e) { 409 rescaleToFit(true); 410 } 411 }); 412 413 mZoomResetButton = new ToolItem(toolBar, SWT.PUSH); 414 mZoomResetButton.setToolTipText("Reset Zoom to 100% (1)"); 415 mZoomResetButton.setImage(iconFactory.getIcon("zoom100")); //$NON-NLS-1$); 416 mZoomResetButton.addSelectionListener(new SelectionAdapter() { 417 @Override 418 public void widgetSelected(SelectionEvent e) { 419 resetScale(); 420 } 421 }); 422 423 // Group zoom in/out separately 424 new ToolItem(toolBar, SWT.SEPARATOR); 425 426 mZoomOutButton = new ToolItem(toolBar, SWT.PUSH); 427 mZoomOutButton.setToolTipText("Zoom Out (-)"); 428 mZoomOutButton.setImage(iconFactory.getIcon("zoomminus")); //$NON-NLS-1$); 429 mZoomOutButton.addSelectionListener(new SelectionAdapter() { 430 @Override 431 public void widgetSelected(SelectionEvent e) { 432 rescale(-1); 433 } 434 }); 435 436 mZoomInButton = new ToolItem(toolBar, SWT.PUSH); 437 mZoomInButton.setToolTipText("Zoom In (+)"); 438 mZoomInButton.setImage(iconFactory.getIcon("zoomplus")); //$NON-NLS-1$); 439 mZoomInButton.addSelectionListener(new SelectionAdapter() { 440 @Override 441 public void widgetSelected(SelectionEvent e) { 442 rescale(+1); 443 } 444 }); 445 446 return toolBar; 447 } 448 449 @SuppressWarnings("unused") // SWT constructors have side effects, they are not unused createLintControls()450 private ToolBar createLintControls() { 451 ToolBar toolBar = new ToolBar(this, SWT.FLAT | SWT.RIGHT | SWT.HORIZONTAL); 452 453 // Separate from adjacent toolbar 454 new ToolItem(toolBar, SWT.SEPARATOR); 455 456 ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); 457 mLintButton = new ToolItem(toolBar, SWT.PUSH); 458 mLintButton.setToolTipText("Show Lint Warnings for this Layout"); 459 mLintButton.setImage(sharedImages.getImage(ISharedImages.IMG_OBJS_WARN_TSK)); 460 mLintButton.addSelectionListener(new SelectionAdapter() { 461 @Override 462 public void widgetSelected(SelectionEvent e) { 463 IFile file = mEditor.getEditorDelegate().getEditor().getInputFile(); 464 if (file != null) { 465 EclipseLintClient.showErrors(getShell(), file); 466 } 467 } 468 }); 469 470 return toolBar; 471 } 472 473 /** 474 * Updates the lint indicator state in the given layout editor 475 */ updateErrorIndicator()476 public void updateErrorIndicator() { 477 updateErrorIndicator(EclipseLintClient.hasMarkers(mEditor.getEditedFile())); 478 } 479 480 /** 481 * Sets whether the action bar should show the "lint warnings" button 482 * 483 * @param hasLintWarnings whether there are lint errors to be shown 484 */ updateErrorIndicator(final boolean hasLintWarnings)485 void updateErrorIndicator(final boolean hasLintWarnings) { 486 Display display = getDisplay(); 487 if (display.getThread() != Thread.currentThread()) { 488 display.asyncExec(new Runnable() { 489 @Override 490 public void run() { 491 if (!isDisposed()) { 492 updateErrorIndicator(hasLintWarnings); 493 } 494 } 495 }); 496 return; 497 } 498 499 GridData layoutData = (GridData) mLintToolBar.getLayoutData(); 500 if (layoutData.exclude == hasLintWarnings) { 501 layoutData.exclude = !hasLintWarnings; 502 mLintToolBar.setVisible(hasLintWarnings); 503 layout(); 504 } 505 } 506 507 /** 508 * Returns true if zooming in/out/to-fit/etc is allowed (which is not the case while 509 * emulating real size) 510 * 511 * @return true if zooming is allowed 512 */ isZoomingAllowed()513 boolean isZoomingAllowed() { 514 return mZoomInButton.isEnabled(); 515 } 516 isZoomingRealSize()517 boolean isZoomingRealSize() { 518 return mZoomRealSizeButton.getSelection(); 519 } 520 521 /** 522 * Rescales canvas. 523 * @param direction +1 for zoom in, -1 for zoom out 524 */ rescale(int direction)525 void rescale(int direction) { 526 LayoutCanvas canvas = mEditor.getCanvasControl(); 527 double s = canvas.getScale(); 528 529 if (direction > 0) { 530 s = s * 1.2; 531 } else { 532 s = s / 1.2; 533 } 534 535 // Some operations are faster if the zoom is EXACTLY 1.0 rather than ALMOST 1.0. 536 // (This is because there is a fast-path when image copying and the scale is 1.0; 537 // in that case it does not have to do any scaling). 538 // 539 // If you zoom out 10 times and then back in 10 times, small rounding errors mean 540 // that you end up with a scale=1.0000000000000004. In the cases, when you get close 541 // to 1.0, just make the zoom an exact 1.0. 542 if (Math.abs(s-1.0) < 0.0001) { 543 s = 1.0; 544 } 545 546 canvas.setScale(s, true /*redraw*/); 547 } 548 549 /** 550 * Reset the canvas scale to 100% 551 */ resetScale()552 void resetScale() { 553 mEditor.getCanvasControl().setScale(1, true /*redraw*/); 554 } 555 556 /** 557 * Reset the canvas scale to best fit (so content is as large as possible without scrollbars) 558 */ rescaleToFit(boolean onlyZoomOut)559 void rescaleToFit(boolean onlyZoomOut) { 560 mEditor.getCanvasControl().setFitScale(onlyZoomOut); 561 } 562 rescaleToReal(boolean real)563 boolean rescaleToReal(boolean real) { 564 if (real) { 565 return computeAndSetRealScale(true /*redraw*/); 566 } else { 567 // reset the scale to 100% 568 mEditor.getCanvasControl().setScale(1, true /*redraw*/); 569 return true; 570 } 571 } 572 computeAndSetRealScale(boolean redraw)573 boolean computeAndSetRealScale(boolean redraw) { 574 // compute average dpi of X and Y 575 ConfigurationComposite config = mEditor.getConfigurationComposite(); 576 float dpi = (config.getXDpi() + config.getYDpi()) / 2.f; 577 578 // get the monitor dpi 579 float monitor = AdtPrefs.getPrefs().getMonitorDensity(); 580 if (monitor == 0.f) { 581 ResolutionChooserDialog dialog = new ResolutionChooserDialog( 582 config.getShell()); 583 if (dialog.open() == Window.OK) { 584 monitor = dialog.getDensity(); 585 AdtPrefs.getPrefs().setMonitorDensity(monitor); 586 } else { 587 return false; 588 } 589 } 590 591 mEditor.getCanvasControl().setScale(monitor / dpi, redraw); 592 return true; 593 } 594 } 595