blob: 1b1bd23c46a1e68cf0eeb45f4383fe176e36b40a [file] [log] [blame]
/*
* Copyright (C) 2011 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 static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_ID;
import com.android.annotations.NonNull;
import com.android.ide.common.api.INode;
import com.android.ide.common.api.RuleAction;
import com.android.ide.common.api.RuleAction.Choices;
import com.android.ide.common.api.RuleAction.Separator;
import com.android.ide.common.api.RuleAction.Toggle;
import com.android.ide.common.layout.BaseViewRule;
import com.android.ide.eclipse.adt.internal.editors.IconFactory;
import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration;
import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
import com.android.ide.eclipse.adt.internal.lint.EclipseLintClient;
import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
import com.android.sdklib.devices.Device;
import com.android.sdklib.devices.Screen;
import com.android.sdkuilib.internal.widgets.ResolutionChooserDialog;
import com.google.common.base.Strings;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.PlatformUI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Toolbar shown at the top of the layout editor, which adds a number of context-sensitive
* layout actions (as well as zooming controls on the right).
*/
public class LayoutActionBar extends Composite {
private GraphicalEditorPart mEditor;
private ToolBar mLayoutToolBar;
private ToolBar mLintToolBar;
private ToolBar mZoomToolBar;
private ToolItem mZoomRealSizeButton;
private ToolItem mZoomOutButton;
private ToolItem mZoomResetButton;
private ToolItem mZoomInButton;
private ToolItem mZoomFitButton;
private ToolItem mLintButton;
private List<RuleAction> mPrevActions;
/**
* Creates a new {@link LayoutActionBar} and adds it to the given parent.
*
* @param parent the parent composite to add the actions bar to
* @param style the SWT style to apply
* @param editor the associated layout editor
*/
public LayoutActionBar(Composite parent, int style, GraphicalEditorPart editor) {
super(parent, style | SWT.NO_FOCUS);
mEditor = editor;
GridLayout layout = new GridLayout(3, false);
setLayout(layout);
mLayoutToolBar = new ToolBar(this, /*SWT.WRAP |*/ SWT.FLAT | SWT.RIGHT | SWT.HORIZONTAL);
mLayoutToolBar.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false));
mZoomToolBar = createZoomControls();
mZoomToolBar.setLayoutData(new GridData(SWT.END, SWT.BEGINNING, false, false));
mLintToolBar = createLintControls();
GridData lintData = new GridData(SWT.END, SWT.BEGINNING, false, false);
lintData.exclude = true;
mLintToolBar.setLayoutData(lintData);
}
@Override
public void dispose() {
super.dispose();
mPrevActions = null;
}
/** Updates the layout contents based on the current selection */
void updateSelection() {
NodeProxy parent = null;
LayoutCanvas canvas = mEditor.getCanvasControl();
SelectionManager selectionManager = canvas.getSelectionManager();
List<SelectionItem> selections = selectionManager.getSelections();
if (selections.size() > 0) {
// TODO: better handle multi-selection -- maybe we should disable it or
// something.
// What if you select children with different parents? Of different types?
// etc.
NodeProxy node = selections.get(0).getNode();
if (node != null && node.getParent() != null) {
parent = (NodeProxy) node.getParent();
}
}
if (parent == null) {
// Show the background's properties
CanvasViewInfo root = canvas.getViewHierarchy().getRoot();
if (root == null) {
return;
}
parent = canvas.getNodeFactory().create(root);
selections = Collections.emptyList();
}
RulesEngine engine = mEditor.getRulesEngine();
List<NodeProxy> selectedNodes = new ArrayList<NodeProxy>();
for (SelectionItem item : selections) {
selectedNodes.add(item.getNode());
}
List<RuleAction> actions = new ArrayList<RuleAction>();
engine.callAddLayoutActions(actions, parent, selectedNodes);
// Place actions in the correct order (the actions may come from different
// rules and should be merged properly via sorting keys)
Collections.sort(actions);
// Add in actions for the child as well, if there is exactly one.
// These are not merged into the parent list of actions; they are appended
// at the end.
int index = -1;
String label = null;
if (selectedNodes.size() == 1) {
List<RuleAction> itemActions = new ArrayList<RuleAction>();
NodeProxy selectedNode = selectedNodes.get(0);
engine.callAddLayoutActions(itemActions, selectedNode, null);
if (itemActions.size() > 0) {
Collections.sort(itemActions);
if (!(itemActions.get(0) instanceof RuleAction.Separator)) {
actions.add(RuleAction.createSeparator(0));
}
label = selectedNode.getStringAttr(ANDROID_URI, ATTR_ID);
if (label != null) {
label = BaseViewRule.stripIdPrefix(label);
index = actions.size();
}
actions.addAll(itemActions);
}
}
if (!updateActions(actions)) {
updateToolbar(actions, index, label);
}
mPrevActions = actions;
}
/** Update the toolbar widgets */
private void updateToolbar(final List<RuleAction> actions, final int labelIndex,
final String label) {
if (mLayoutToolBar == null || mLayoutToolBar.isDisposed()) {
return;
}
for (ToolItem c : mLayoutToolBar.getItems()) {
c.dispose();
}
mLayoutToolBar.pack();
addActions(actions, labelIndex, label);
mLayoutToolBar.pack();
mLayoutToolBar.layout();
}
/**
* Attempts to update the existing toolbar actions, if the action list is
* similar to the current list. Returns false if this cannot be done and the
* contents must be replaced.
*/
private boolean updateActions(@NonNull List<RuleAction> actions) {
List<RuleAction> before = mPrevActions;
List<RuleAction> after = actions;
if (before == null) {
return false;
}
if (!before.equals(after) || after.size() > mLayoutToolBar.getItemCount()) {
return false;
}
int actionIndex = 0;
for (int i = 0, max = mLayoutToolBar.getItemCount(); i < max; i++) {
ToolItem item = mLayoutToolBar.getItem(i);
int style = item.getStyle();
Object data = item.getData();
if (data != null) {
// One action can result in multiple toolbar items (e.g. a choice action
// can result in multiple radio buttons), so we've have to replace all of
// them with the corresponding new action
RuleAction prevAction = before.get(actionIndex);
while (prevAction != data) {
actionIndex++;
if (actionIndex == before.size()) {
return false;
}
prevAction = before.get(actionIndex);
if (prevAction == data) {
break;
} else if (!(prevAction instanceof RuleAction.Separator)) {
return false;
}
}
RuleAction newAction = after.get(actionIndex);
assert newAction.equals(prevAction); // Maybe I can do this lazily instead?
// Update action binding to the new action
item.setData(newAction);
// Sync button states: the checked state is not considered part of
// RuleAction equality
if ((style & SWT.CHECK) != 0) {
assert newAction instanceof Toggle;
Toggle toggle = (Toggle) newAction;
item.setSelection(toggle.isChecked());
} else if ((style & SWT.RADIO) != 0) {
assert newAction instanceof Choices;
Choices choices = (Choices) newAction;
String current = choices.getCurrent();
String id = (String) item.getData(ATTR_ID);
boolean selected = Strings.nullToEmpty(current).equals(id);
item.setSelection(selected);
}
} else {
// Must be a separator, or a label (which we insert for nested widgets)
assert (style & SWT.SEPARATOR) != 0 || !item.getText().isEmpty() : item;
}
}
return true;
}
private void addActions(List<RuleAction> actions, int labelIndex, String label) {
if (actions.size() > 0) {
// Flag used to indicate that if there are any actions -after- this, it
// should be separated from this current action (we don't unconditionally
// add a separator at the end of these groups in case there are no more
// actions at the end so that we don't have a trailing separator)
boolean needSeparator = false;
int index = 0;
for (RuleAction action : actions) {
if (index == labelIndex) {
final ToolItem button = new ToolItem(mLayoutToolBar, SWT.PUSH);
button.setText(label);
needSeparator = false;
}
index++;
if (action instanceof Separator) {
addSeparator(mLayoutToolBar);
needSeparator = false;
continue;
} else if (needSeparator) {
addSeparator(mLayoutToolBar);
needSeparator = false;
}
if (action instanceof RuleAction.Choices) {
RuleAction.Choices choices = (Choices) action;
if (!choices.isRadio()) {
addDropdown(choices);
} else {
addSeparator(mLayoutToolBar);
addRadio(choices);
needSeparator = true;
}
} else if (action instanceof RuleAction.Toggle) {
addToggle((Toggle) action);
} else {
addPlainAction(action);
}
}
}
}
/** Add a separator to the toolbar, unless there already is one there at the end already */
private static void addSeparator(ToolBar toolBar) {
int n = toolBar.getItemCount();
if (n > 0 && (toolBar.getItem(n - 1).getStyle() & SWT.SEPARATOR) == 0) {
ToolItem separator = new ToolItem(toolBar, SWT.SEPARATOR);
separator.setWidth(15);
}
}
private void addToggle(Toggle toggle) {
final ToolItem button = new ToolItem(mLayoutToolBar, SWT.CHECK);
URL iconUrl = toggle.getIconUrl();
String title = toggle.getTitle();
if (iconUrl != null) {
button.setImage(IconFactory.getInstance().getIcon(iconUrl));
button.setToolTipText(title);
} else {
button.setText(title);
}
button.setData(toggle);
button.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
Toggle toggle = (Toggle) button.getData();
toggle.getCallback().action(toggle, getSelectedNodes(),
toggle.getId(), button.getSelection());
updateSelection();
}
});
if (toggle.isChecked()) {
button.setSelection(true);
}
}
private List<INode> getSelectedNodes() {
List<SelectionItem> selections =
mEditor.getCanvasControl().getSelectionManager().getSelections();
List<INode> nodes = new ArrayList<INode>(selections.size());
for (SelectionItem item : selections) {
nodes.add(item.getNode());
}
return nodes;
}
private void addPlainAction(RuleAction menuAction) {
final ToolItem button = new ToolItem(mLayoutToolBar, SWT.PUSH);
URL iconUrl = menuAction.getIconUrl();
String title = menuAction.getTitle();
if (iconUrl != null) {
button.setImage(IconFactory.getInstance().getIcon(iconUrl));
button.setToolTipText(title);
} else {
button.setText(title);
}
button.setData(menuAction);
button.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
RuleAction menuAction = (RuleAction) button.getData();
menuAction.getCallback().action(menuAction, getSelectedNodes(), menuAction.getId(),
false);
updateSelection();
}
});
}
private void addRadio(RuleAction.Choices choices) {
List<URL> icons = choices.getIconUrls();
List<String> titles = choices.getTitles();
List<String> ids = choices.getIds();
String current = choices.getCurrent() != null ? choices.getCurrent() : ""; //$NON-NLS-1$
assert icons != null;
assert icons.size() == titles.size();
for (int i = 0; i < icons.size(); i++) {
URL iconUrl = icons.get(i);
String title = titles.get(i);
final String id = ids.get(i);
final ToolItem item = new ToolItem(mLayoutToolBar, SWT.RADIO);
item.setToolTipText(title);
item.setImage(IconFactory.getInstance().getIcon(iconUrl));
item.setData(choices);
item.setData(ATTR_ID, id);
item.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
if (item.getSelection()) {
RuleAction.Choices choices = (Choices) item.getData();
choices.getCallback().action(choices, getSelectedNodes(), id, null);
updateSelection();
}
}
});
boolean selected = current.equals(id);
if (selected) {
item.setSelection(true);
}
}
}
private void addDropdown(RuleAction.Choices choices) {
final ToolItem combo = new ToolItem(mLayoutToolBar, SWT.DROP_DOWN);
URL iconUrl = choices.getIconUrl();
if (iconUrl != null) {
combo.setImage(IconFactory.getInstance().getIcon(iconUrl));
combo.setToolTipText(choices.getTitle());
} else {
combo.setText(choices.getTitle());
}
combo.setData(choices);
Listener menuListener = new Listener() {
@Override
public void handleEvent(Event event) {
Menu menu = new Menu(mLayoutToolBar.getShell(), SWT.POP_UP);
RuleAction.Choices choices = (Choices) combo.getData();
List<URL> icons = choices.getIconUrls();
List<String> titles = choices.getTitles();
List<String> ids = choices.getIds();
String current = choices.getCurrent() != null ? choices.getCurrent() : ""; //$NON-NLS-1$
for (int i = 0; i < titles.size(); i++) {
String title = titles.get(i);
final String id = ids.get(i);
URL itemIconUrl = icons != null && icons.size() > 0 ? icons.get(i) : null;
MenuItem item = new MenuItem(menu, SWT.CHECK);
item.setText(title);
if (itemIconUrl != null) {
Image itemIcon = IconFactory.getInstance().getIcon(itemIconUrl);
item.setImage(itemIcon);
}
boolean selected = id.equals(current);
if (selected) {
item.setSelection(true);
}
item.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
RuleAction.Choices choices = (Choices) combo.getData();
choices.getCallback().action(choices, getSelectedNodes(), id, null);
updateSelection();
}
});
}
Rectangle bounds = combo.getBounds();
Point location = new Point(bounds.x, bounds.y + bounds.height);
location = combo.getParent().toDisplay(location);
menu.setLocation(location.x, location.y);
menu.setVisible(true);
}
};
combo.addListener(SWT.Selection, menuListener);
}
// ---- Zoom Controls ----
@SuppressWarnings("unused") // SWT constructors have side effects, they are not unused
private ToolBar createZoomControls() {
ToolBar toolBar = new ToolBar(this, SWT.FLAT | SWT.RIGHT | SWT.HORIZONTAL);
IconFactory iconFactory = IconFactory.getInstance();
mZoomRealSizeButton = new ToolItem(toolBar, SWT.CHECK);
mZoomRealSizeButton.setToolTipText("Emulate Real Size");
mZoomRealSizeButton.setImage(iconFactory.getIcon("zoomreal")); //$NON-NLS-1$);
mZoomRealSizeButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
boolean newState = mZoomRealSizeButton.getSelection();
if (rescaleToReal(newState)) {
mZoomOutButton.setEnabled(!newState);
mZoomResetButton.setEnabled(!newState);
mZoomInButton.setEnabled(!newState);
mZoomFitButton.setEnabled(!newState);
} else {
mZoomRealSizeButton.setSelection(!newState);
}
}
});
mZoomFitButton = new ToolItem(toolBar, SWT.PUSH);
mZoomFitButton.setToolTipText("Zoom to Fit (0)");
mZoomFitButton.setImage(iconFactory.getIcon("zoomfit")); //$NON-NLS-1$);
mZoomFitButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
rescaleToFit(true);
}
});
mZoomResetButton = new ToolItem(toolBar, SWT.PUSH);
mZoomResetButton.setToolTipText("Reset Zoom to 100% (1)");
mZoomResetButton.setImage(iconFactory.getIcon("zoom100")); //$NON-NLS-1$);
mZoomResetButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
resetScale();
}
});
// Group zoom in/out separately
new ToolItem(toolBar, SWT.SEPARATOR);
mZoomOutButton = new ToolItem(toolBar, SWT.PUSH);
mZoomOutButton.setToolTipText("Zoom Out (-)");
mZoomOutButton.setImage(iconFactory.getIcon("zoomminus")); //$NON-NLS-1$);
mZoomOutButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
rescale(-1);
}
});
mZoomInButton = new ToolItem(toolBar, SWT.PUSH);
mZoomInButton.setToolTipText("Zoom In (+)");
mZoomInButton.setImage(iconFactory.getIcon("zoomplus")); //$NON-NLS-1$);
mZoomInButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
rescale(+1);
}
});
return toolBar;
}
@SuppressWarnings("unused") // SWT constructors have side effects, they are not unused
private ToolBar createLintControls() {
ToolBar toolBar = new ToolBar(this, SWT.FLAT | SWT.RIGHT | SWT.HORIZONTAL);
// Separate from adjacent toolbar
new ToolItem(toolBar, SWT.SEPARATOR);
ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages();
mLintButton = new ToolItem(toolBar, SWT.PUSH);
mLintButton.setToolTipText("Show Lint Warnings for this Layout");
mLintButton.setImage(sharedImages.getImage(ISharedImages.IMG_OBJS_WARN_TSK));
mLintButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
CommonXmlEditor editor = mEditor.getEditorDelegate().getEditor();
IFile file = editor.getInputFile();
if (file != null) {
EclipseLintClient.showErrors(getShell(), file, editor);
}
}
});
return toolBar;
}
/**
* Updates the lint indicator state in the given layout editor
*/
public void updateErrorIndicator() {
updateErrorIndicator(mEditor.getEditedFile());
}
/**
* Updates the lint indicator state for the given file
*
* @param file the file to show the indicator status for
*/
public void updateErrorIndicator(IFile file) {
IMarker[] markers = EclipseLintClient.getMarkers(file);
updateErrorIndicator(markers.length);
}
/**
* Sets whether the action bar should show the "lint warnings" button
*
* @param hasLintWarnings whether there are lint errors to be shown
*/
private void updateErrorIndicator(final int markerCount) {
Display display = getDisplay();
if (display.getThread() != Thread.currentThread()) {
display.asyncExec(new Runnable() {
@Override
public void run() {
if (!isDisposed()) {
updateErrorIndicator(markerCount);
}
}
});
return;
}
GridData layoutData = (GridData) mLintToolBar.getLayoutData();
Integer existing = (Integer) mLintToolBar.getData();
Integer current = Integer.valueOf(markerCount);
if (!current.equals(existing)) {
mLintToolBar.setData(current);
boolean layout = false;
boolean hasLintWarnings = markerCount > 0 && AdtPrefs.getPrefs().isLintOnSave();
if (layoutData.exclude == hasLintWarnings) {
layoutData.exclude = !hasLintWarnings;
mLintToolBar.setVisible(hasLintWarnings);
layout = true;
}
if (markerCount > 0) {
String iconName = "";
switch (markerCount) {
case 1: iconName = "lint1"; break; //$NON-NLS-1$
case 2: iconName = "lint2"; break; //$NON-NLS-1$
case 3: iconName = "lint3"; break; //$NON-NLS-1$
case 4: iconName = "lint4"; break; //$NON-NLS-1$
case 5: iconName = "lint5"; break; //$NON-NLS-1$
case 6: iconName = "lint6"; break; //$NON-NLS-1$
case 7: iconName = "lint7"; break; //$NON-NLS-1$
case 8: iconName = "lint8"; break; //$NON-NLS-1$
case 9: iconName = "lint9"; break; //$NON-NLS-1$
default: iconName = "lint9p"; break;//$NON-NLS-1$
}
mLintButton.setImage(IconFactory.getInstance().getIcon(iconName));
}
if (layout) {
layout();
}
redraw();
}
}
/**
* Returns true if zooming in/out/to-fit/etc is allowed (which is not the case while
* emulating real size)
*
* @return true if zooming is allowed
*/
boolean isZoomingAllowed() {
return mZoomInButton.isEnabled();
}
boolean isZoomingRealSize() {
return mZoomRealSizeButton.getSelection();
}
/**
* Rescales canvas.
* @param direction +1 for zoom in, -1 for zoom out
*/
void rescale(int direction) {
LayoutCanvas canvas = mEditor.getCanvasControl();
double s = canvas.getScale();
if (direction > 0) {
s = s * 1.2;
} else {
s = s / 1.2;
}
// Some operations are faster if the zoom is EXACTLY 1.0 rather than ALMOST 1.0.
// (This is because there is a fast-path when image copying and the scale is 1.0;
// in that case it does not have to do any scaling).
//
// If you zoom out 10 times and then back in 10 times, small rounding errors mean
// that you end up with a scale=1.0000000000000004. In the cases, when you get close
// to 1.0, just make the zoom an exact 1.0.
if (Math.abs(s-1.0) < 0.0001) {
s = 1.0;
}
canvas.setScale(s, true /*redraw*/);
}
/**
* Reset the canvas scale to 100%
*/
void resetScale() {
mEditor.getCanvasControl().setScale(1, true /*redraw*/);
}
/**
* Reset the canvas scale to best fit (so content is as large as possible without scrollbars)
*/
void rescaleToFit(boolean onlyZoomOut) {
mEditor.getCanvasControl().setFitScale(onlyZoomOut, true /*allowZoomIn*/);
}
boolean rescaleToReal(boolean real) {
if (real) {
return computeAndSetRealScale(true /*redraw*/);
} else {
// reset the scale to 100%
mEditor.getCanvasControl().setScale(1, true /*redraw*/);
return true;
}
}
boolean computeAndSetRealScale(boolean redraw) {
// compute average dpi of X and Y
ConfigurationChooser chooser = mEditor.getConfigurationChooser();
Configuration config = chooser.getConfiguration();
Device device = config.getDevice();
Screen screen = device.getDefaultHardware().getScreen();
double dpi = (screen.getXdpi() + screen.getYdpi()) / 2.;
// get the monitor dpi
float monitor = AdtPrefs.getPrefs().getMonitorDensity();
if (monitor == 0.f) {
ResolutionChooserDialog dialog = new ResolutionChooserDialog(chooser.getShell());
if (dialog.open() == Window.OK) {
monitor = dialog.getDensity();
AdtPrefs.getPrefs().setMonitorDensity(monitor);
} else {
return false;
}
}
mEditor.getCanvasControl().setScale(monitor / dpi, redraw);
return true;
}
}