2 * Copyright (C) 2009 The Android Open Source Project
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
8 * http://www.eclipse.org/org/documents/epl-v10.php
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.
17 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
19 import com.android.ide.eclipse.adt.AdtPlugin;
20 import com.android.ide.eclipse.adt.editors.layout.gscripts.INode;
21 import com.android.ide.eclipse.adt.editors.layout.gscripts.Point;
22 import com.android.ide.eclipse.adt.editors.layout.gscripts.Rect;
23 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
24 import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
25 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
26 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
27 import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
28 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
29 import com.android.ide.eclipse.adt.internal.editors.ui.tree.CopyCutAction;
30 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
31 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
32 import com.android.layoutlib.api.ILayoutResult;
34 import org.eclipse.jface.text.BadLocationException;
35 import org.eclipse.swt.SWT;
36 import org.eclipse.swt.SWTException;
37 import org.eclipse.swt.dnd.Clipboard;
38 import org.eclipse.swt.dnd.DND;
39 import org.eclipse.swt.dnd.DragSource;
40 import org.eclipse.swt.dnd.DragSourceEvent;
41 import org.eclipse.swt.dnd.DragSourceListener;
42 import org.eclipse.swt.dnd.DropTarget;
43 import org.eclipse.swt.dnd.TextTransfer;
44 import org.eclipse.swt.dnd.Transfer;
45 import org.eclipse.swt.events.ControlAdapter;
46 import org.eclipse.swt.events.ControlEvent;
47 import org.eclipse.swt.events.MouseEvent;
48 import org.eclipse.swt.events.MouseListener;
49 import org.eclipse.swt.events.MouseMoveListener;
50 import org.eclipse.swt.events.PaintEvent;
51 import org.eclipse.swt.events.PaintListener;
52 import org.eclipse.swt.events.SelectionAdapter;
53 import org.eclipse.swt.events.SelectionEvent;
54 import org.eclipse.swt.graphics.Color;
55 import org.eclipse.swt.graphics.Font;
56 import org.eclipse.swt.graphics.GC;
57 import org.eclipse.swt.graphics.Image;
58 import org.eclipse.swt.graphics.ImageData;
59 import org.eclipse.swt.graphics.PaletteData;
60 import org.eclipse.swt.graphics.Rectangle;
61 import org.eclipse.swt.widgets.Canvas;
62 import org.eclipse.swt.widgets.Composite;
63 import org.eclipse.swt.widgets.Control;
64 import org.eclipse.swt.widgets.Display;
65 import org.eclipse.swt.widgets.Event;
66 import org.eclipse.swt.widgets.ScrollBar;
67 import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
68 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
69 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
70 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
71 import org.eclipse.wst.xml.core.internal.document.NodeContainer;
72 import org.w3c.dom.Node;
74 import java.awt.image.BufferedImage;
75 import java.awt.image.DataBufferInt;
76 import java.awt.image.Raster;
77 import java.util.ArrayList;
78 import java.util.Iterator;
79 import java.util.LinkedList;
80 import java.util.List;
81 import java.util.ListIterator;
84 * Displays the image rendered by the {@link GraphicalEditorPart} and handles
85 * the interaction with the widgets.
91 * - gray on error, keep select but disable d'n'd.
92 * - handle drag'n'drop (internal, for moving/duplicating).
93 * - handle context menu (depending on selection).
94 * - delete, copy/paste linked with menus and in context menu
95 * - context menu handling of layout + local props (via IViewRules)
96 * - selection synchronization with the outline (both ways).
97 * - outline should include same context menu + delete/copy/paste ops.
98 * - outline should include drop support (from canvas or from palette)
100 /* package */ class LayoutCanvas extends Canvas {
102 /** The layout editor that uses this layout canvas. */
103 private final LayoutEditor mLayoutEditor;
105 /** The Groovy Rules Engine, associated with the current project. */
106 private RulesEngine mRulesEngine;
108 /** SWT clipboard instance. */
109 private Clipboard mClipboard;
112 * The last valid ILayoutResult passed to {@link #setResult(ILayoutResult)}.
114 * When non null, {@link #mLastValidViewInfoRoot} is guaranteed to be non-null too.
116 private ILayoutResult mLastValidResult;
119 * The CanvasViewInfo root created for the last update of {@link #mLastValidResult}.
120 * This is null when {@link #mLastValidResult} is null.
121 * When non null, {@link #mLastValidResult} is guaranteed to be non-null too.
123 private CanvasViewInfo mLastValidViewInfoRoot;
126 * True when the last {@link #setResult(ILayoutResult)} provided a valid {@link ILayoutResult}
127 * in which case it is also available in {@link #mLastValidResult}.
128 * When false this means the canvas is displaying an out-dated result image & bounds and some
129 * features should be disabled accordingly such a drag'n'drop.
131 * When this is false, {@link #mLastValidResult} can be non-null and points to an older
134 private boolean mIsResultValid;
136 /** Current background image. Null when there's no image. */
137 private Image mImage;
139 /** The current selection list. The list is never null, however it can be empty. */
140 private final LinkedList<CanvasSelection> mSelections = new LinkedList<CanvasSelection>();
142 /** CanvasSelection border color. Do not dispose, it's a system color. */
143 private Color mSelectionFgColor;
145 /** GC wrapper given to the IViewRule methods. The GC itself is only defined in the
146 * context of {@link #onPaint(PaintEvent)}; otherwise it is null. */
147 private final GCWrapper mGCWrapper;
149 /** Default font used on the canvas. Do not dispose, it's a system font. */
152 /** Current hover view info. Null when no mouse hover. */
153 private CanvasViewInfo mHoverViewInfo;
155 /** Current mouse hover border rectangle. Null when there's no mouse hover.
156 * The rectangle coordinates do not take account of the translation, which must
157 * be applied to the rectangle when drawing.
159 private Rectangle mHoverRect;
161 /** Hover border color. Must be disposed, it's NOT a system color. */
162 private Color mHoverFgColor;
164 /** Outline color. Do not dispose, it's a system color. */
165 private Color mOutlineColor;
168 * The <em>current</em> alternate selection, if any, which changes when the Alt key is
169 * used during a selection. Can be null.
171 private CanvasAlternateSelection mAltSelection;
173 /** When true, always display the outline of all views. */
174 private boolean mShowOutline;
176 /** Drop target associated with this composite. */
177 private DropTarget mDropTarget;
179 /** Drop listener, with feedback from current drop */
180 private CanvasDropListener mDropListener;
182 /** Factory that can create {@link INode} proxies. */
183 private final NodeFactory mNodeFactory = new NodeFactory();
185 /** Vertical scaling & scrollbar information. */
186 private ScaleInfo mVScale;
188 /** Horizontal scaling & scrollbar information. */
189 private ScaleInfo mHScale;
191 private DragSource mSource;
193 /** The current Outline Page, to synchronize the selection both ways. */
194 private OutlinePage2 mOutlinePage;
196 public LayoutCanvas(LayoutEditor layoutEditor, RulesEngine rulesEngine, Composite parent, int style) {
197 super(parent, style | SWT.DOUBLE_BUFFERED | SWT.V_SCROLL | SWT.H_SCROLL);
198 mLayoutEditor = layoutEditor;
199 mRulesEngine = rulesEngine;
201 mClipboard = new Clipboard(parent.getDisplay());
203 mHScale = new ScaleInfo(getHorizontalBar());
204 mVScale = new ScaleInfo(getVerticalBar());
206 mGCWrapper = new GCWrapper(mHScale, mVScale);
208 Display d = getDisplay();
209 mSelectionFgColor = d.getSystemColor(SWT.COLOR_RED);
210 mHoverFgColor = new Color(d, 0xFF, 0x99, 0x00); // orange
211 mOutlineColor = d.getSystemColor(SWT.COLOR_GREEN);
213 mFont = d.getSystemFont();
215 addPaintListener(new PaintListener() {
216 public void paintControl(PaintEvent e) {
221 addControlListener(new ControlAdapter() {
223 public void controlResized(ControlEvent e) {
224 super.controlResized(e);
225 mHScale.setClientSize(getClientArea().width);
226 mVScale.setClientSize(getClientArea().height);
230 addMouseMoveListener(new MouseMoveListener() {
231 public void mouseMove(MouseEvent e) {
236 addMouseListener(new MouseListener() {
237 public void mouseUp(MouseEvent e) {
241 public void mouseDown(MouseEvent e) {
245 public void mouseDoubleClick(MouseEvent e) {
250 // DND Reference: http://www.eclipse.org/articles/Article-SWT-DND/DND-in-SWT.html
252 mDropTarget = new DropTarget(this, DND.DROP_COPY | DND.DROP_MOVE | DND.DROP_DEFAULT);
253 mDropTarget.setTransfer(new Transfer[] { SimpleXmlTransfer.getInstance() } );
254 mDropListener = new CanvasDropListener(this);
255 mDropTarget.addDropListener(mDropListener);
257 mSource = new DragSource(this, DND.DROP_COPY | DND.DROP_MOVE);
258 mSource.setTransfer(new Transfer[] {
259 TextTransfer.getInstance(),
260 SimpleXmlTransfer.getInstance()
262 mSource.addDragListener(new CanvasDragSourceListener());
264 // Get the outline associated with this editor, if any and of the right type.
265 Object outline = layoutEditor.getAdapter(IContentOutlinePage.class);
266 if (outline instanceof OutlinePage2) {
267 mOutlinePage = (OutlinePage2) outline;
272 public void dispose() {
275 if (mHoverFgColor != null) {
276 mHoverFgColor.dispose();
277 mHoverFgColor = null;
280 if (mDropTarget != null) {
281 mDropTarget.dispose();
285 if (mRulesEngine != null) {
286 mRulesEngine.dispose();
290 if (mClipboard != null) {
291 mClipboard.dispose();
297 * Returns true when the last {@link #setResult(ILayoutResult)} provided a valid
298 * {@link ILayoutResult} in which case it is also available in {@link #mLastValidResult}.
299 * When false this means the canvas is displaying an out-dated result image & bounds and some
300 * features should be disabled accordingly such a drag'n'drop.
302 * When this is false, {@link #mLastValidResult} can be non-null and points to an older
305 /* package */ boolean isResultValid() {
306 return mIsResultValid;
309 /** Returns the Groovy Rules Engine, associated with the current project. */
310 /* package */ RulesEngine getRulesEngine() {
314 /** Sets the Groovy Rules Engine, associated with the current project. */
315 /* package */ void setRulesEngine(RulesEngine rulesEngine) {
316 mRulesEngine = rulesEngine;
320 * Returns the factory to use to convert from {@link CanvasViewInfo} or from
321 * {@link UiViewElementNode} to {@link INode} proxies.
323 public NodeFactory getNodeFactory() {
327 /** Returns the shared SWT keyboard. */
328 public Clipboard getClipboard() {
333 * Sets the result of the layout rendering. The result object indicates if the layout
334 * rendering succeeded. If it did, it contains a bitmap and the objects rectangles.
336 * Implementation detail: the bridge's computeLayout() method already returns a newly
337 * allocated ILayourResult. That means we can keep this result and hold on to it
340 * @param result The new rendering result, either valid or not.
342 public void setResult(ILayoutResult result) {
346 mIsResultValid = (result != null && result.getSuccess() == ILayoutResult.SUCCESS);
348 if (mIsResultValid && result != null) {
349 mLastValidResult = result;
350 mLastValidViewInfoRoot = new CanvasViewInfo(result.getRootView());
351 setImage(result.getImage());
353 updateNodeProxies(mLastValidViewInfoRoot);
354 mOutlinePage.setModel(mLastValidViewInfoRoot);
356 // Check if the selection is still the same (based on the object keys)
357 // and eventually recompute their bounds.
358 for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) {
359 CanvasSelection s = it.next();
361 // Check if the selected object still exists
362 Object key = s.getViewInfo().getUiViewKey();
363 CanvasViewInfo vi = findViewInfoKey(key, mLastValidViewInfoRoot);
365 // Remove the previous selection -- if the selected object still exists
366 // we need to recompute its bounds in case it moved so we'll insert a new one
367 // at the same place.
370 it.add(new CanvasSelection(vi, mRulesEngine, mNodeFactory));
373 updateOulineSelection();
375 // remove the current alternate selection views
376 mAltSelection = null;
378 mHScale.setSize(mImage.getImageData().width, getClientArea().width);
379 mVScale.setSize(mImage.getImageData().height, getClientArea().height);
381 // Pre-load the android.view.View rule in the Rules Engine. Doing it here means
382 // it will be done after the first rendering is finished. Successive calls are
383 // superfluous but harmless since the rule will be cached.
384 mRulesEngine.preloadAndroidView();
390 public void setShowOutline(boolean newState) {
391 mShowOutline = newState;
395 public double getScale() {
396 return mHScale.getScale();
399 public void setScale(double scale) {
400 mHScale.setScale(scale);
401 mVScale.setScale(scale);
406 * Called by the {@link GraphicalEditorPart} when the Copy action is requested.
408 * @param clipboard The shared clipboard. Must not be disposed.
410 public void onCopy(Clipboard clipboard) {
411 // TODO implement copy to clipbard. Also will need to provide feedback to enable
412 // copy only when there's a selection.
416 * Called by the {@link GraphicalEditorPart} when the Cut action is requested.
418 * @param clipboard The shared clipboard. Must not be disposed.
420 public void onCut(Clipboard clipboard) {
421 // TODO implement copy to clipbard. Also will need to provide feedback to enable
422 // cut only when there's a selection.
426 * Called by the {@link GraphicalEditorPart} when the Paste action is requested.
428 * @param clipboard The shared clipboard. Must not be disposed.
430 public void onPaste(Clipboard clipboard) {
435 * Called by the {@link GraphicalEditorPart} when the Select All action is requested.
437 public void onSelectAll() {
438 // First clear the current selection, if any.
440 mAltSelection = null;
442 // Now select everything if there's a valid layout
443 if (mIsResultValid && mLastValidResult != null) {
444 selectAllViewInfos(mLastValidViewInfoRoot);
448 updateOulineSelection();
454 public void onDelete() {
455 // TODO not implemented yet, not even hooked in yet!
459 * Transforms a point, expressed in SWT display coordinates
460 * (e.g. from a Drag'n'Drop {@link Event}, not local {@link Control} coordinates)
461 * into the canvas' image coordinates according to the current zoom and scroll.
463 * @param displayX X in SWT display coordinates
464 * @param displayY Y in SWT display coordinates
465 * @return A new {@link Point} in canvas coordinates
467 public Point displayToCanvasPoint(int displayX, int displayY) {
468 // convert screen coordinates to local SWT control coordinates
469 org.eclipse.swt.graphics.Point p = this.toControl(displayX, displayY);
471 int x = mHScale.inverseTranslate(p.x);
472 int y = mVScale.inverseTranslate(p.y);
473 return new Point(x, y);
478 public interface ScaleTransform {
480 * Computes the transformation from a X/Y canvas image coordinate
481 * to client pixel coordinate.
483 * This takes into account the {@link ScaleInfo#IMAGE_MARGIN},
484 * the current scaling and the current translation.
486 * @param canvasX A canvas image coordinate (X or Y).
487 * @return The transformed coordinate in client pixel space.
489 public int translate(int canvasX);
492 * Computes the transformation from a canvas image size (width or height) to
493 * client pixel coordinates.
495 * @param canwasW A canvas image size (W or H).
496 * @return The transformed coordinate in client pixel space.
498 public int scale(int canwasW);
501 * Computes the transformation from a X/Y client pixel coordinate
502 * to canvas image coordinate.
504 * This takes into account the {@link ScaleInfo#IMAGE_MARGIN},
505 * the current scaling and the current translation.
507 * This is the inverse of {@link #translate(int)}.
509 * @param screenX A client pixel space coordinate (X or Y).
510 * @return The transformed coordinate in canvas image space.
512 public int inverseTranslate(int screenX);
515 private class ScaleInfo implements ScaleTransform {
517 * Margin around the rendered image.
518 * Should be enough space to display the layout width and height pseudo widgets.
520 private static final int IMAGE_MARGIN = 25;
522 /** Canvas image size (original, before zoom), in pixels */
523 private int mImgSize;
525 /** Client size, in pixels */
526 private int mClientSize;
528 /** Left-top offset in client pixel coordinates */
529 private int mTranslate;
531 /** Scaling factor, > 0 */
532 private double mScale;
534 /** Scrollbar widget */
535 ScrollBar mScrollbar;
537 public ScaleInfo(ScrollBar scrollbar) {
538 mScrollbar = scrollbar;
542 mScrollbar.addSelectionListener(new SelectionAdapter() {
544 public void widgetSelected(SelectionEvent e) {
545 // User requested scrolling. Changes translation and redraw canvas.
546 mTranslate = mScrollbar.getSelection();
553 * Sets the new scaling factor. Recomputes scrollbars.
554 * @param scale Scaling factor, > 0.
556 public void setScale(double scale) {
557 if (mScale != scale) {
563 /** Returns current scaling factor. */
564 public double getScale() {
568 /** Returns Canvas image size (original, before zoom), in pixels. */
569 public int getImgSize() {
573 /** Returns the scaled image size in pixels. */
574 public int getScalledImgSize() {
575 return (int) (mImgSize * mScale);
578 /** Changes the size of the canvas image and the client size. Recomputes scrollbars. */
579 public void setSize(int imgSize, int clientSize) {
581 setClientSize(clientSize);
584 /** Changes the size of the client size. Recomputes scrollbars. */
585 public void setClientSize(int clientSize) {
586 mClientSize = clientSize;
590 private void resizeScrollbar() {
592 int sx = (int) (mImgSize * mScale);
594 // actual client area is always reduced by the margins
595 int cx = mClientSize - 2 * IMAGE_MARGIN;
598 mScrollbar.setEnabled(false);
600 mScrollbar.setEnabled(true);
602 // max scroll value is the scaled image size
603 // thumb value is the actual viewable area out of the scaled img size
604 mScrollbar.setMaximum(sx);
605 mScrollbar.setThumb(cx);
609 public int translate(int canvasX) {
610 return IMAGE_MARGIN - mTranslate + (int)(mScale * canvasX);
613 public int scale(int canwasW) {
614 return (int)(mScale * canwasW);
617 public int inverseTranslate(int screenX) {
618 return (int) ((screenX - IMAGE_MARGIN + mTranslate) / mScale);
623 * Creates or updates the node proxy for this canvas view info.
625 * Since proxies are reused, this will update the bounds of an existing proxy when the
626 * canvas is refreshed and a view changes position or size.
628 * This is a recursive call that updates the whole hierarchy starting at the given
631 private void updateNodeProxies(CanvasViewInfo vi) {
637 UiViewElementNode key = vi.getUiViewKey();
640 mNodeFactory.create(vi);
643 for (CanvasViewInfo child : vi.getChildren()) {
644 updateNodeProxies(child);
649 * Sets the image of the last *successful* rendering.
650 * Converts the AWT image into an SWT image.
652 private void setImage(BufferedImage awtImage) {
653 int width = awtImage.getWidth();
654 int height = awtImage.getHeight();
656 Raster raster = awtImage.getData(new java.awt.Rectangle(width, height));
657 int[] imageDataBuffer = ((DataBufferInt)raster.getDataBuffer()).getData();
659 ImageData imageData = new ImageData(width, height, 32,
660 new PaletteData(0x00FF0000, 0x0000FF00, 0x000000FF));
662 imageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0);
664 mImage = new Image(getDisplay(), imageData);
668 * Sets the alpha for the given GC.
670 * Alpha may not work on all platforms and may fail with an exception.
672 * @param gc the GC to change
673 * @param alpha the new alpha, 0 for transparent, 255 for opaque.
674 * @return True if the operation worked, false if it failed with an exception.
676 * @see GC#setAlpha(int)
678 private boolean gc_setAlpha(GC gc, int alpha) {
682 } catch (SWTException e) {
688 * Sets the non-text antialias flag for the given GC.
690 * Antialias may not work on all platforms and may fail with an exception.
692 * @param gc the GC to change
693 * @param alias One of {@link SWT#DEFAULT}, {@link SWT#ON}, {@link SWT#OFF}.
694 * @return The previous aliasing mode if the operation worked,
695 * or -2 if it failed with an exception.
697 * @see GC#setAntialias(int)
699 private int gc_setAntialias(GC gc, int alias) {
701 int old = gc.getAntialias();
702 gc.setAntialias(alias);
704 } catch (SWTException e) {
710 * Paints the canvas in response to paint events.
712 private void onPaint(PaintEvent e) {
715 mGCWrapper.setGC(gc);
718 if (mImage != null) {
719 if (!mIsResultValid) {
720 gc_setAlpha(gc, 128); // half-transparent
723 ScaleInfo hi = mHScale;
724 ScaleInfo vi = mVScale;
726 // we only anti-alias when reducing the image size.
728 if (hi.getScale() < 1.0) {
729 oldAlias = gc_setAntialias(gc, SWT.ON);
735 hi.getImgSize(), // srcWidth
736 vi.getImgSize(), // srcHeight
737 hi.translate(0), // destX
738 vi.translate(0), // destY
739 hi.getScalledImgSize(), // destWidth
740 vi.getScalledImgSize() // destHeight
743 if (oldAlias != -2) {
744 gc_setAntialias(gc, oldAlias);
747 if (!mIsResultValid) {
748 gc_setAlpha(gc, 255); // opaque
752 if (mShowOutline && mLastValidViewInfoRoot != null) {
753 gc.setForeground(mOutlineColor);
754 gc.setLineStyle(SWT.LINE_DOT);
755 drawOutline(gc, mLastValidViewInfoRoot);
758 if (mHoverRect != null) {
759 gc.setForeground(mHoverFgColor);
760 gc.setLineStyle(SWT.LINE_DOT);
762 int x = mHScale.translate(mHoverRect.x);
763 int y = mVScale.translate(mHoverRect.y);
764 int w = mHScale.scale(mHoverRect.width);
765 int h = mVScale.scale(mHoverRect.height);
767 gc.drawRectangle(x, y, w, h);
770 int n = mSelections.size();
772 boolean isMultipleSelection = n > 1;
775 gc.setForeground(mSelectionFgColor);
776 mSelections.get(0).paintParentSelection(mRulesEngine, mGCWrapper);
779 for (CanvasSelection s : mSelections) {
780 gc.setForeground(mSelectionFgColor);
781 s.paintSelection(mRulesEngine, mGCWrapper, isMultipleSelection);
785 if (mDropListener != null) {
786 mDropListener.paintFeedback(mGCWrapper);
790 mGCWrapper.setGC(null);
794 private void drawOutline(GC gc, CanvasViewInfo info) {
796 Rectangle r = info.getAbsRect();
798 int x = mHScale.translate(r.x);
799 int y = mVScale.translate(r.y);
800 int w = mHScale.scale(r.width);
801 int h = mVScale.scale(r.height);
803 gc.drawRectangle(x, y, w, h);
805 for (CanvasViewInfo vi : info.getChildren()) {
811 * Hover on top of a known child.
813 private void onMouseMove(MouseEvent e) {
814 if (mLastValidResult != null) {
815 CanvasViewInfo root = mLastValidViewInfoRoot;
817 int x = mHScale.inverseTranslate(e.x);
818 int y = mVScale.inverseTranslate(e.y);
820 CanvasViewInfo vi = findViewInfoAt(x, y);
822 // We don't hover on the root since it's not a widget per see and it is always there.
827 boolean needsUpdate = vi != mHoverViewInfo;
833 Rectangle r = vi.getSelectionRect();
834 mHoverRect = new Rectangle(r.x, r.y, r.width, r.height);
843 private void onMouseDown(MouseEvent e) {
844 // pass, not used yet.
848 * Performs selection on mouse up (not mouse down).
850 * Shift key is used to toggle in multi-selection.
851 * Alt key is used to cycle selection through objects at the same level than the one
852 * pointed at (i.e. click on an object then alt-click to cycle).
854 private void onMouseUp(MouseEvent e) {
855 if (mLastValidResult != null) {
857 boolean isShift = (e.stateMask & SWT.SHIFT) != 0;
858 boolean isAlt = (e.stateMask & SWT.ALT) != 0;
860 int x = mHScale.inverseTranslate(e.x);
861 int y = mVScale.inverseTranslate(e.y);
863 CanvasViewInfo vi = findViewInfoAt(x, y);
865 if (isShift && !isAlt) {
866 // Case where shift is pressed: pointed object is toggled.
868 // reset alternate selection if any
869 mAltSelection = null;
871 // If nothing has been found at the cursor, assume it might be a user error
872 // and avoid clearing the existing selection.
875 // toggle this selection on-off: remove it if already selected
882 mSelections.add(new CanvasSelection(vi, mRulesEngine, mNodeFactory));
883 updateOulineSelection();
888 // Case where alt is pressed: select or cycle the object pointed at.
890 // Note: if shift and alt are pressed, shift is ignored. The alternate selection
891 // mechanism does not reset the current multiple selection unless they intersect.
893 // We need to remember the "origin" of the alternate selection, to be
894 // able to continue cycling through it later. If there's no alternate selection,
895 // create one. If there's one but not for the same origin object, create a new
897 if (mAltSelection == null || mAltSelection.getOriginatingView() != vi) {
898 mAltSelection = new CanvasAlternateSelection(vi, findAltViewInfoAt(
899 x, y, mLastValidViewInfoRoot, null));
901 // deselect them all, in case they were partially selected
902 deselectAll(mAltSelection.getAltViews());
904 // select the current one
905 CanvasViewInfo vi2 = mAltSelection.getCurrent();
907 mSelections.addFirst(new CanvasSelection(vi2, mRulesEngine, mNodeFactory));
908 updateOulineSelection();
911 // We're trying to cycle through the current alternate selection.
912 // First remove the current object.
913 CanvasViewInfo vi2 = mAltSelection.getCurrent();
916 // Now select the next one.
917 vi2 = mAltSelection.getNext();
919 mSelections.addFirst(new CanvasSelection(vi2, mRulesEngine, mNodeFactory));
920 updateOulineSelection();
926 // Case where no modifier is pressed: either select or reset the selection.
934 * Removes all the currently selected item and only select the given item.
935 * Issues a {@link #redraw()} if the selection changes.
937 * @param vi The new selected item if non-null. Selection becomes empty if null.
939 private void selectSingle(CanvasViewInfo vi) {
940 // reset alternate selection if any
941 mAltSelection = null;
943 // reset (multi)selection if any
944 if (mSelections.size() > 0) {
945 if (mSelections.size() == 1 && mSelections.getFirst().getViewInfo() == vi) {
946 // CanvasSelection remains the same, don't touch it.
953 mSelections.add(new CanvasSelection(vi, mRulesEngine, mNodeFactory));
955 updateOulineSelection();
960 * Deselects a view info.
961 * Returns true if the object was actually selected.
962 * Callers are responsible for calling redraw() and updateOulineSelection() after.
964 private boolean deselect(CanvasViewInfo canvasViewInfo) {
965 if (canvasViewInfo == null) {
969 for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) {
970 CanvasSelection s = it.next();
971 if (canvasViewInfo == s.getViewInfo()) {
981 * Deselects multiple view infos.
982 * Callers are responsible for calling redraw() and updateOulineSelection() after.
984 private void deselectAll(List<CanvasViewInfo> canvasViewInfos) {
985 for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) {
986 CanvasSelection s = it.next();
987 if (canvasViewInfos.contains(s.getViewInfo())) {
993 private void onDoubleClick(MouseEvent e) {
994 // pass, not used yet.
998 * Tries to find a child with the same view key in the view info sub-tree.
999 * Returns null if not found.
1001 private CanvasViewInfo findViewInfoKey(Object viewKey, CanvasViewInfo canvasViewInfo) {
1002 if (canvasViewInfo.getUiViewKey() == viewKey) {
1003 return canvasViewInfo;
1006 // try to find a matching child
1007 for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
1008 CanvasViewInfo v = findViewInfoKey(viewKey, child);
1019 * Tries to find the inner most child matching the given x,y coordinates in the view
1020 * info sub-tree, starting at the last know view info root.
1021 * This uses the potentially-expanded selection bounds.
1023 * Returns null if not found or if there's view info root.
1025 /* package */ CanvasViewInfo findViewInfoAt(int x, int y) {
1026 if (mLastValidViewInfoRoot == null) {
1029 return findViewInfoAt(x, y, mLastValidViewInfoRoot);
1034 * Tries to find the inner most child matching the given x,y coordinates in the view
1035 * info sub-tree. This uses the potentially-expanded selection bounds.
1037 * Returns null if not found.
1039 private CanvasViewInfo findViewInfoAt(int x, int y, CanvasViewInfo canvasViewInfo) {
1040 Rectangle r = canvasViewInfo.getSelectionRect();
1041 if (r.contains(x, y)) {
1043 // try to find a matching child first
1044 for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
1045 CanvasViewInfo v = findViewInfoAt(x, y, child);
1051 // if no children matched, this is the view that we're looking for
1052 return canvasViewInfo;
1058 private ArrayList<CanvasViewInfo> findAltViewInfoAt(
1059 int x, int y, CanvasViewInfo parent, ArrayList<CanvasViewInfo> outList) {
1062 if (outList == null) {
1063 outList = new ArrayList<CanvasViewInfo>();
1065 // add the parent root only once
1066 r = parent.getSelectionRect();
1067 if (r.contains(x, y)) {
1068 outList.add(parent);
1072 if (parent.getChildren().size() > 0) {
1073 // then add all children that match the position
1074 for (CanvasViewInfo child : parent.getChildren()) {
1075 r = child.getSelectionRect();
1076 if (r.contains(x, y)) {
1081 // finally recurse in the children
1082 for (CanvasViewInfo child : parent.getChildren()) {
1083 r = child.getSelectionRect();
1084 if (r.contains(x, y)) {
1085 findAltViewInfoAt(x, y, child, outList);
1094 * Used by {@link #onSelectAll()} to add all current view infos to the selection list.
1096 * @param canvasViewInfo The root to add. This info and all its children will be added to the
1099 private void selectAllViewInfos(CanvasViewInfo canvasViewInfo) {
1100 mSelections.add(new CanvasSelection(canvasViewInfo, mRulesEngine, mNodeFactory));
1101 for (CanvasViewInfo vi : canvasViewInfo.getChildren()) {
1102 selectAllViewInfos(vi);
1107 * Update the selection in the outline page to match the current one from {@link #mSelections}
1109 private void updateOulineSelection() {
1110 if (mOutlinePage == null) {
1114 if (mSelections.size() == 0) {
1115 mOutlinePage.selectAndReveal(null);
1119 ArrayList<CanvasViewInfo> selectedVis = new ArrayList<CanvasViewInfo>();
1120 for (CanvasSelection cs : mSelections) {
1121 CanvasViewInfo vi = cs.getViewInfo();
1123 selectedVis.add(vi);
1127 mOutlinePage.selectAndReveal(selectedVis.toArray(new CanvasViewInfo[selectedVis.size()]));
1133 private class CanvasDragSourceListener implements DragSourceListener {
1136 * The current selection being dragged.
1137 * This may be a subset of the canvas selection.
1138 * Can be empty but never null.
1140 final ArrayList<CanvasSelection> mDragSelection = new ArrayList<CanvasSelection>();
1141 private SimpleElement[] mDragElements;
1144 * The user has begun the actions required to drag the widget.
1146 * Initiate a drag only if there is one or more item selected.
1147 * If there's none, try to auto-select the one under the cursor.
1151 public void dragStart(DragSourceEvent e) {
1152 // We need a selection (simple or multiple) to do any transfer.
1153 // If there's a selection *and* the cursor is over this selection, use all the
1154 // currently selected elements.
1155 // If there is no selection or the cursor is not over a selected element, drag
1156 // the element under the cursor.
1157 // If nothing can be selected, abort the drag operation.
1159 mDragSelection.clear();
1161 if (mSelections.size() > 0) {
1162 // Is the cursor on top of a selected element?
1163 int x = mHScale.inverseTranslate(e.x);
1164 int y = mVScale.inverseTranslate(e.y);
1166 for (CanvasSelection cs : mSelections) {
1167 if (cs.getRect().contains(x, y)) {
1168 mDragSelection.addAll(mSelections);
1173 if (mDragSelection.isEmpty()) {
1174 // There is no selected element under the cursor.
1175 // We'll now try to find another element.
1177 CanvasViewInfo vi = findViewInfoAt(x, y);
1179 mDragSelection.add(new CanvasSelection(vi, mRulesEngine, mNodeFactory));
1184 if (mDragSelection.size() > 0) {
1185 // Sanitize the list to make sure all elements have a valid XML attached to it.
1186 // This avoids us from making repeated checks in dragSetData.
1188 // In case of multiple selection, we also need to remove all children when their
1189 // parent is already selected since parents will always be added with all their
1192 for (Iterator<CanvasSelection> it = mDragSelection.iterator(); it.hasNext(); ) {
1193 CanvasSelection cs = it.next();
1194 CanvasViewInfo vi = cs.getViewInfo();
1195 UiViewElementNode key = vi == null ? null : vi.getUiViewKey();
1196 Node node = key == null ? null : key.getXmlNode();
1198 // Missing ViewInfo or view key or XML, discard this.
1204 for (Iterator<CanvasSelection> it2 = mDragSelection.iterator();
1206 CanvasSelection cs2 = it2.next();
1208 CanvasViewInfo vi2 = cs2.getViewInfo();
1209 if (vi.isParent(vi2)) {
1210 // vi2 is a parent for vi. Remove vi.
1220 e.doit = mDragSelection.size() > 0;
1222 mDragElements = getSelectionAsElements();
1223 GlobalCanvasDragInfo.getInstance().startDrag(
1225 mDragSelection.toArray(new CanvasSelection[mDragSelection.size()]),
1231 * Callback invoked when data is needed for the event, typically right before drop.
1232 * The drop side decides what type of transfer to use and this side must now provide
1233 * the adequate data.
1237 public void dragSetData(DragSourceEvent e) {
1238 if (TextTransfer.getInstance().isSupportedType(e.dataType)) {
1239 e.data = getSelectionAsText();
1243 if (SimpleXmlTransfer.getInstance().isSupportedType(e.dataType)) {
1244 e.data = mDragElements;
1248 // otherwise we failed
1249 e.detail = DND.DROP_NONE;
1253 private SimpleElement[] getSelectionAsElements() {
1254 ArrayList<SimpleElement> elements = new ArrayList<SimpleElement>();
1256 for (CanvasSelection cs : mDragSelection) {
1257 CanvasViewInfo vi = cs.getViewInfo();
1259 SimpleElement e = transformToSimpleElement(vi);
1263 return elements.toArray(new SimpleElement[elements.size()]);
1266 private SimpleElement transformToSimpleElement(CanvasViewInfo vi) {
1268 UiViewElementNode uiNode = vi.getUiViewKey();
1270 String fqcn = SimpleXmlTransfer.getFqcn(uiNode.getDescriptor());
1271 String parentFqcn = null;
1272 Rect bounds = new Rect(vi.getAbsRect());
1273 Rect parentBounds = null;
1275 UiElementNode uiParent = uiNode.getUiParent();
1276 if (uiParent != null) {
1277 parentFqcn = SimpleXmlTransfer.getFqcn(uiParent.getDescriptor());
1279 if (vi.getParent() != null) {
1280 parentBounds = new Rect(vi.getParent().getAbsRect());
1283 SimpleElement e = new SimpleElement(fqcn, parentFqcn, bounds, parentBounds);
1285 for (UiAttributeNode attr : uiNode.getUiAttributes()) {
1286 String value = attr.getCurrentValue();
1287 if (value != null && value.length() > 0) {
1288 AttributeDescriptor attrDesc = attr.getDescriptor();
1289 SimpleAttribute a = new SimpleAttribute(
1290 attrDesc.getNamespaceUri(),
1291 attrDesc.getXmlLocalName(),
1297 for (CanvasViewInfo childVi : vi.getChildren()) {
1298 SimpleElement e2 = transformToSimpleElement(childVi);
1300 e.addInnerElement(e2);
1307 /** Get the XML text from the current drag selection for a text transfer. */
1308 private String getSelectionAsText() {
1309 StringBuilder sb = new StringBuilder();
1311 for (CanvasSelection cs : mDragSelection) {
1312 CanvasViewInfo vi = cs.getViewInfo();
1313 UiViewElementNode key = vi.getUiViewKey();
1314 Node node = key.getXmlNode();
1315 String t = getXmlTextFromEditor(mLayoutEditor, node);
1317 if (sb.length() > 0) {
1324 return sb.toString();
1327 /** Get the XML text directly from the editor. */
1328 private String getXmlTextFromEditor(AndroidXmlEditor editor, Node xml_node) {
1330 IStructuredModel model = editor.getModelForRead();
1332 IStructuredDocument sse_doc = editor.getStructuredDocument();
1333 if (xml_node instanceof NodeContainer) {
1334 // The easy way to get the source of an SSE XML node.
1335 data = ((NodeContainer) xml_node).getSource();
1336 } else if (xml_node instanceof IndexedRegion && sse_doc != null) {
1338 IndexedRegion region = (IndexedRegion) xml_node;
1339 int start = region.getStartOffset();
1340 int end = region.getEndOffset();
1343 data = sse_doc.get(start, end - start);
1346 } catch (BadLocationException e) {
1347 // the region offset was invalid. ignore.
1349 model.releaseFromRead();
1355 * Callback invoked when the drop has been finished either way.
1356 * On a successful move, remove the originating elements.
1358 public void dragFinished(DragSourceEvent e) {
1359 if (e.detail == DND.DROP_MOVE) {
1360 // Remove from source. Since we know the selection, we'll simply
1361 // create a cut operation on the existing drag selection.
1362 AdtPlugin.printToConsole("CanvasDND", "dragFinished => MOVE");
1364 // Create an undo wrapper, which takes a runnable
1365 mLayoutEditor.wrapUndoRecording(
1366 "Remove drag'n'drop source elements",
1369 // Create an edit-XML wrapper, which takes a runnable
1370 mLayoutEditor.editXmlModel(new Runnable() {
1379 // Clear the selection
1380 mDragSelection.clear();
1381 mDragElements = null;
1382 GlobalCanvasDragInfo.getInstance().stopDrag();
1385 private void cutDragSelection() {
1386 List<UiElementNode> selected = new ArrayList<UiElementNode>();
1388 for (CanvasSelection cs : mDragSelection) {
1389 selected.add(cs.getViewInfo().getUiViewKey());
1392 CopyCutAction action = new CopyCutAction(
1395 null, /* xml commit callback */