/*
* Copyright (C) 2009 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 com.android.ide.eclipse.adt.editors.layout.gscripts.DropZone;
import com.android.ide.eclipse.adt.editors.layout.gscripts.Rect;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
import com.android.layoutlib.api.ILayoutResult;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DropTarget;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.PaletteData;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.awt.image.Raster;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
/**
* Displays the image rendered by the {@link GraphicalEditorPart} and handles
* the interaction with the widgets.
* <p/>
*
* @since GLE2
*
* TODO list:
* - gray on error, keep select but disable d'n'd.
* - make sure it is scrollable (Canvas derives from Scrollable, so prolly just setting bounds.)
* - handle drop target (from palette).
* - handle drag'n'drop (internal, for moving/duplicating).
* - handle context menu (depending on selection).
* - selection synchronization with the outline (both ways).
*/
/* package */ class LayoutCanvas extends Canvas {
/**
* Margin around the rendered image. Should be enough space to display the layout
* width and height pseudo widgets.
*/
static final int IMAGE_MARGIN = 5;
/** The Groovy Rules Engine, associated with the current project. */
private RulesEngine mRulesEngine;
/*
* The last valid ILayoutResult passed to {@link #setResult(ILayoutResult)}.
* This can be null.
* When non null, {@link #mLastValidViewInfoRoot} is guaranteed to be non-null too.
*/
private ILayoutResult mLastValidResult;
/**
* The CanvasViewInfo root created for the last update of {@link #mLastValidResult}.
* This is null when {@link #mLastValidResult} is null.
* When non null, {@link #mLastValidResult} is guaranteed to be non-null too.
*/
private CanvasViewInfo mLastValidViewInfoRoot;
/**
* True when the last {@link #setResult(ILayoutResult)} provided a valid {@link ILayoutResult}
* in which case it is also available in {@link #mLastValidResult}.
* When false this means the canvas is displaying an out-dated result image & bounds and some
* features should be disabled accordingly such a drag'n'drop.
* <p/>
* When this is false, {@link #mLastValidResult} can be non-null and points to an older
* layout result.
*/
private boolean mIsResultValid;
/** Current background image. Null when there's no image. */
private Image mImage;
/** The current selection list. The list is never null, however it can be empty. */
private final LinkedList<CanvasSelection> mSelections = new LinkedList<CanvasSelection>();
/** CanvasSelection border color. Do not dispose, it's a system color. */
private Color mSelectionFgColor;
/** CanvasSelection name font. Do not dispose, it's a system font. */
private Font mSelectionFont;
/** Pixel height of the font displaying the selection name. Initially set to 0 and only
* initialized in onPaint() when we have a GC. */
private int mSelectionFontHeight;
/** Current hover view info. Null when no mouse hover. */
private CanvasViewInfo mHoverViewInfo;
/** Current mouse hover border rectangle. Null when there's no mouse hover. */
private Rectangle mHoverRect;
/** Hover border color. Must be disposed, it's NOT a system color. */
private Color mHoverFgColor;
/** Outline color. Do not dispose, it's a system color. */
private Color mOutlineColor;
/**
* The <em>current</em> alternate selection, if any, which changes when the Alt key is
* used during a selection. Can be null.
*/
private CanvasAlternateSelection mAltSelection;
/** When true, always display the outline of all views. */
private boolean mShowOutline;
/** Drop target associated with this composite. */
private DropTarget mDropTarget;
/** Drop listener, with feedback from current drop */
private CanvasDropListener mDropListener;
/** Drop color. Do not dispose, it's a system color. */
private Color mDropFgColor;
public LayoutCanvas(RulesEngine rulesEngine, Composite parent, int style) {
super(parent, style | SWT.DOUBLE_BUFFERED);
mRulesEngine = rulesEngine;
Display d = getDisplay();
mSelectionFgColor = d.getSystemColor(SWT.COLOR_RED);
mHoverFgColor = new Color(d, 0xFF, 0x99, 0x00); // orange
mOutlineColor = d.getSystemColor(SWT.COLOR_GREEN);
mSelectionFont = d.getSystemFont();
mDropFgColor = d.getSystemColor(SWT.COLOR_YELLOW);
addPaintListener(new PaintListener() {
public void paintControl(PaintEvent e) {
onPaint(e);
}
});
addMouseMoveListener(new MouseMoveListener() {
public void mouseMove(MouseEvent e) {
onMouseMove(e);
}
});
addMouseListener(new MouseListener() {
public void mouseUp(MouseEvent e) {
onMouseUp(e);
}
public void mouseDown(MouseEvent e) {
onMouseDown(e);
}
public void mouseDoubleClick(MouseEvent e) {
onDoubleClick(e);
}
});
mDropTarget = new DropTarget(this, DND.DROP_COPY | DND.DROP_MOVE | DND.DROP_DEFAULT);
mDropTarget.setTransfer(new Transfer[] { ElementDescTransfer.getInstance() });
mDropListener = new CanvasDropListener(this);
mDropTarget.addDropListener(mDropListener);
}
@Override
public void dispose() {
super.dispose();
if (mHoverFgColor != null) {
mHoverFgColor.dispose();
mHoverFgColor = null;
}
if (mDropTarget != null) {
mDropTarget.dispose();
mDropTarget = null;
}
if (mRulesEngine != null) {
mRulesEngine.dispose();
mRulesEngine = null;
}
}
/**
* Returns true when the last {@link #setResult(ILayoutResult)} provided a valid
* {@link ILayoutResult} in which case it is also available in {@link #mLastValidResult}.
* When false this means the canvas is displaying an out-dated result image & bounds and some
* features should be disabled accordingly such a drag'n'drop.
* <p/>
* When this is false, {@link #mLastValidResult} can be non-null and points to an older
* layout result.
*/
/* package */ boolean isResultValid() {
return mIsResultValid;
}
/** Returns the Groovy Rules Engine, associated with the current project. */
/* package */ RulesEngine getRulesEngine() {
return mRulesEngine;
}
/** Sets the Groovy Rules Engine, associated with the current project. */
/* package */ void setRulesEngine(RulesEngine rulesEngine) {
mRulesEngine = rulesEngine;
}
/**
* Sets the result of the layout rendering. The result object indicates if the layout
* rendering succeeded. If it did, it contains a bitmap and the objects rectangles.
*
* Implementation detail: the bridge's computeLayout() method already returns a newly
* allocated ILayourResult. That means we can keep this result and hold on to it
* when it is valid.
*
* @param result The new rendering result, either valid or not.
*/
public void setResult(ILayoutResult result) {
// disable any hover
mHoverRect = null;
mIsResultValid = (result != null && result.getSuccess() == ILayoutResult.SUCCESS);
if (mIsResultValid && result != null) {
mLastValidResult = result;
mLastValidViewInfoRoot = new CanvasViewInfo(result.getRootView());
setImage(result.getImage());
// Check if the selection is still the same (based on the object keys)
// and eventually recompute their bounds.
for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) {
CanvasSelection s = it.next();
// Check the if the selected object still exists
Object key = s.getViewInfo().getUiViewKey();
CanvasViewInfo vi = findViewInfoKey(key, mLastValidViewInfoRoot);
// Remove the previous selection -- if the selected object still exists
// we need to recompute its bounds in case it moved so we'll insert a new one
// at the same place.
it.remove();
if (vi != null) {
it.add(new CanvasSelection(vi, mRulesEngine));
}
}
// remove the current alternate selection views
mAltSelection = null;
}
redraw();
}
public void setShowOutline(boolean newState) {
mShowOutline = newState;
redraw();
}
/**
* Called by the {@link GraphicalEditorPart} when the Copy action is requested.
*
* @param clipboard The shared clipboard. Must not be disposed.
*/
public void onCopy(Clipboard clipboard) {
// TODO implement copy to clipbard. Also will need to provide feedback to enable
// copy only when there's a selection.
}
/**
* Called by the {@link GraphicalEditorPart} when the Cut action is requested.
*
* @param clipboard The shared clipboard. Must not be disposed.
*/
public void onCut(Clipboard clipboard) {
// TODO implement copy to clipbard. Also will need to provide feedback to enable
// cut only when there's a selection.
}
/**
* Called by the {@link GraphicalEditorPart} when the Paste action is requested.
*
* @param clipboard The shared clipboard. Must not be disposed.
*/
public void onPaste(Clipboard clipboard) {
}
/**
* Called by the {@link GraphicalEditorPart} when the Select All action is requested.
*/
public void onSelectAll() {
// First clear the current selection, if any.
mSelections.clear();
mAltSelection = null;
// Now select everything if there's a valid layout
if (mIsResultValid && mLastValidResult != null) {
selectAllViewInfos(mLastValidViewInfoRoot);
redraw();
}
}
/**
* Delete action
*/
public void onDelete() {
// TODO not implemented yet, not even hooked in yet!
}
//---
/**
* Sets the image of the last *successful* rendering.
* Converts the AWT image into an SWT image.
*/
private void setImage(BufferedImage awtImage) {
int width = awtImage.getWidth();
int height = awtImage.getHeight();
Raster raster = awtImage.getData(new java.awt.Rectangle(width, height));
int[] imageDataBuffer = ((DataBufferInt)raster.getDataBuffer()).getData();
ImageData imageData = new ImageData(width, height, 32,
new PaletteData(0x00FF0000, 0x0000FF00, 0x000000FF));
imageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0);
mImage = new Image(getDisplay(), imageData);
}
/**
* Sets the alpha for the given GC.
* <p/>
* Alpha may not work on all platforms and may fail with an exception.
*
* @param gc the GC to change
* @param alpha the new alpha, 0 for transparent, 255 for opaque.
* @return True if the operation worked, false if it failed with an exception.
*
* @see GC#setAlpha(int)
*/
private boolean gc_setAlpha(GC gc, int alpha) {
try {
gc.setAlpha(alpha);
return true;
} catch (SWTException e) {
return false;
}
}
/**
* Paints the canvas in response to paint events.
*/
private void onPaint(PaintEvent e) {
GC gc = e.gc;
if (mImage != null) {
if (!mIsResultValid) {
gc_setAlpha(gc, 128); // half-transparent
}
gc.drawImage(mImage, IMAGE_MARGIN, IMAGE_MARGIN);
if (!mIsResultValid) {
gc_setAlpha(gc, 255); // opaque
}
}
if (mShowOutline) {
gc.setForeground(mOutlineColor);
gc.setLineStyle(SWT.LINE_DOT);
drawOutline(gc, mLastValidViewInfoRoot);
}
if (mHoverRect != null) {
gc.setForeground(mHoverFgColor);
gc.setLineStyle(SWT.LINE_DOT);
gc.drawRectangle(mHoverRect);
}
// initialize the selection font height once. We need the GC to do that.
if (mSelectionFontHeight == 0) {
gc.setFont(mSelectionFont);
FontMetrics fm = gc.getFontMetrics();
mSelectionFontHeight = fm.getHeight();
}
for (CanvasSelection s : mSelections) {
drawSelection(gc, s);
}
drawDropZones(gc);
}
private void drawOutline(GC gc, CanvasViewInfo info) {
Rectangle r = info.getAbsRect();
gc.drawRectangle(r.x + IMAGE_MARGIN, r.y + IMAGE_MARGIN, r.width, r.height);
for (CanvasViewInfo vi : info.getChildren()) {
drawOutline(gc, vi);
}
}
private void drawSelection(GC gc, CanvasSelection s) {
Rectangle r = s.getRect();
gc.setForeground(mSelectionFgColor);
gc.setLineStyle(SWT.LINE_SOLID);
gc.drawRectangle(s.getRect());
String name = s.getName();
if (name != null) {
int xs = r.x + 2;
int ys = r.y - mSelectionFontHeight;
if (ys < 0) {
ys = r.y + r.height;
}
gc.drawString(name, xs, ys, true /*transparent*/);
}
}
private void drawDropZones(GC gc) {
if (mDropListener == null) {
return;
}
CanvasViewInfo vi = mDropListener.getTargetView();
if (vi == null) {
return;
}
gc.setForeground(mDropFgColor);
ArrayList<DropZone> zones = mDropListener.getZones();
if (zones != null) {
gc.setLineStyle(SWT.LINE_SOLID);
gc.setLineWidth(1);
DropZone curr = mDropListener.getCurrentZone();
for (DropZone zone : zones) {
Rect r = zone.bounds;
if (r != null && r.w > 0 && r.h > 0) {
int x = r.x + IMAGE_MARGIN;
int y = r.y + IMAGE_MARGIN;
int alpha = 128; // half-transparent
if (zone == curr) {
alpha = 192;
}
if (gc_setAlpha(gc, alpha)) {
gc.fillRectangle(x, y, r.w, r.h);
gc_setAlpha(gc, 255); // opaque
}
gc.drawRectangle(x, y, r.w, r.h);
}
}
}
gc.setLineStyle(SWT.LINE_DOT);
gc.setLineWidth(3);
Rectangle r = vi.getAbsRect();
gc.drawRectangle(r.x + IMAGE_MARGIN, r.y + IMAGE_MARGIN, r.width, r.height);
gc.setLineWidth(1);
}
/**
* Hover on top of a known child.
*/
private void onMouseMove(MouseEvent e) {
if (mLastValidResult != null) {
CanvasViewInfo root = mLastValidViewInfoRoot;
CanvasViewInfo vi = findViewInfoAt(e.x - IMAGE_MARGIN, e.y - IMAGE_MARGIN);
// We don't hover on the root since it's not a widget per see and it is always there.
if (vi == root) {
vi = null;
}
boolean needsUpdate = vi != mHoverViewInfo;
mHoverViewInfo = vi;
if (vi == null) {
mHoverRect = null;
} else {
Rectangle r = vi.getSelectionRect();
mHoverRect = new Rectangle(r.x + IMAGE_MARGIN, r.y + IMAGE_MARGIN,
r.width, r.height);
}
if (needsUpdate) {
redraw();
}
}
}
private void onMouseDown(MouseEvent e) {
// pass, not used yet.
}
/**
* Performs selection on mouse up (not mouse down).
* <p/>
* Shift key is used to toggle in multi-selection.
* Alt key is used to cycle selection through objects at the same level than the one
* pointed at (i.e. click on an object then alt-click to cycle).
*/
private void onMouseUp(MouseEvent e) {
if (mLastValidResult != null) {
boolean isShift = (e.stateMask & SWT.SHIFT) != 0;
boolean isAlt = (e.stateMask & SWT.ALT) != 0;
int x = e.x - IMAGE_MARGIN;
int y = e.y - IMAGE_MARGIN;
CanvasViewInfo vi = findViewInfoAt(x, y);
if (isShift && !isAlt) {
// Case where shift is pressed: pointed object is toggled.
// reset alternate selection if any
mAltSelection = null;
// If nothing has been found at the cursor, assume it might be a user error
// and avoid clearing the existing selection.
if (vi != null) {
// toggle this selection on-off: remove it if already selected
if (deselect(vi)) {
redraw();
return;
}
// otherwise add it.
mSelections.add(new CanvasSelection(vi, mRulesEngine));
redraw();
}
} else if (isAlt) {
// Case where alt is pressed: select or cycle the object pointed at.
// Note: if shift and alt are pressed, shift is ignored. The alternate selection
// mechanism does not reset the current multiple selection unless they intersect.
// We need to remember the "origin" of the alternate selection, to be
// able to continue cycling through it later. If there's no alternate selection,
// create one. If there's one but not for the same origin object, create a new
// one too.
if (mAltSelection == null || mAltSelection.getOriginatingView() != vi) {
mAltSelection = new CanvasAlternateSelection(vi, findAltViewInfoAt(
x, y, mLastValidViewInfoRoot, null));
// deselect them all, in case they were partially selected
deselectAll(mAltSelection.getAltViews());
// select the current one
CanvasViewInfo vi2 = mAltSelection.getCurrent();
if (vi2 != null) {
mSelections.addFirst(new CanvasSelection(vi2, mRulesEngine));
}
} else {
// We're trying to cycle through the current alternate selection.
// First remove the current object.
CanvasViewInfo vi2 = mAltSelection.getCurrent();
deselect(vi2);
// Now select the next one.
vi2 = mAltSelection.getNext();
if (vi2 != null) {
mSelections.addFirst(new CanvasSelection(vi2, mRulesEngine));
}
}
redraw();
} else {
// Case where no modifier is pressed: either select or reset the selection.
// reset alternate selection if any
mAltSelection = null;
// reset (multi)selection if any
if (mSelections.size() > 0) {
if (mSelections.size() == 1 && mSelections.getFirst().getViewInfo() == vi) {
// CanvasSelection remains the same, don't touch it.
return;
}
mSelections.clear();
}
if (vi != null) {
mSelections.add(new CanvasSelection(vi, mRulesEngine));
}
redraw();
}
}
}
/** Deselects a view info. Returns true if the object was actually selected. */
private boolean deselect(CanvasViewInfo canvasViewInfo) {
if (canvasViewInfo == null) {
return false;
}
for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) {
CanvasSelection s = it.next();
if (canvasViewInfo == s.getViewInfo()) {
it.remove();
return true;
}
}
return false;
}
/** Deselects multiple view infos, */
private void deselectAll(List<CanvasViewInfo> canvasViewInfos) {
for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) {
CanvasSelection s = it.next();
if (canvasViewInfos.contains(s.getViewInfo())) {
it.remove();
}
}
}
private void onDoubleClick(MouseEvent e) {
// pass, not used yet.
}
/**
* Tries to find a child with the same view key in the view info sub-tree.
* Returns null if not found.
*/
private CanvasViewInfo findViewInfoKey(Object viewKey, CanvasViewInfo canvasViewInfo) {
if (canvasViewInfo.getUiViewKey() == viewKey) {
return canvasViewInfo;
}
// try to find a matching child
for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
CanvasViewInfo v = findViewInfoKey(viewKey, child);
if (v != null) {
return v;
}
}
return null;
}
/**
* Tries to find the inner most child matching the given x,y coordinates in the view
* info sub-tree, starting at the last know view info root.
* This uses the potentially-expanded selection bounds.
*
* Returns null if not found or if there's view info root.
*/
/* package */ CanvasViewInfo findViewInfoAt(int x, int y) {
if (mLastValidViewInfoRoot == null) {
return null;
} else {
return findViewInfoAt(x, y, mLastValidViewInfoRoot);
}
}
/**
* Tries to find the inner most child matching the given x,y coordinates in the view
* info sub-tree. This uses the potentially-expanded selection bounds.
*
* Returns null if not found.
*/
private CanvasViewInfo findViewInfoAt(int x, int y, CanvasViewInfo canvasViewInfo) {
Rectangle r = canvasViewInfo.getSelectionRect();
if (r.contains(x, y)) {
// try to find a matching child first
for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
CanvasViewInfo v = findViewInfoAt(x, y, child);
if (v != null) {
return v;
}
}
// if no children matched, this is the view that we're looking for
return canvasViewInfo;
}
return null;
}
private ArrayList<CanvasViewInfo> findAltViewInfoAt(
int x, int y, CanvasViewInfo parent, ArrayList<CanvasViewInfo> outList) {
Rectangle r;
if (outList == null) {
outList = new ArrayList<CanvasViewInfo>();
// add the parent root only once
r = parent.getSelectionRect();
if (r.contains(x, y)) {
outList.add(parent);
}
}
if (parent.getChildren().size() > 0) {
// then add all children that match the position
for (CanvasViewInfo child : parent.getChildren()) {
r = child.getSelectionRect();
if (r.contains(x, y)) {
outList.add(child);
}
}
// finally recurse in the children
for (CanvasViewInfo child : parent.getChildren()) {
r = child.getSelectionRect();
if (r.contains(x, y)) {
findAltViewInfoAt(x, y, child, outList);
}
}
}
return outList;
}
/**
* Used by {@link #onSelectAll()} to add all current view infos to the selection list.
*
* @param canvasViewInfo The root to add. This info and all its children will be added to the
* selection list.
*/
private void selectAllViewInfos(CanvasViewInfo canvasViewInfo) {
mSelections.add(new CanvasSelection(canvasViewInfo, mRulesEngine));
for (CanvasViewInfo vi : canvasViewInfo.getChildren()) {
selectAllViewInfos(vi);
}
}
}