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