• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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