OSDN Git Service

40c3ec6996dd78112df783b431d1f477b0a6d5e3
[android-x86/sdk.git] / eclipse / plugins / com.android.ide.eclipse.adt / src / com / android / ide / eclipse / adt / internal / editors / layout / gle2 / LayoutCanvas.java
1 /*
2  * Copyright (C) 2009 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
17 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
18
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;
33
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;
73
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;
82
83 /**
84  * Displays the image rendered by the {@link GraphicalEditorPart} and handles
85  * the interaction with the widgets.
86  * <p/>
87  *
88  * @since GLE2
89  *
90  * TODO list:
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)
99  */
100 /* package */  class LayoutCanvas extends Canvas {
101
102     /** The layout editor that uses this layout canvas. */
103     private final LayoutEditor mLayoutEditor;
104
105     /** The Groovy Rules Engine, associated with the current project. */
106     private RulesEngine mRulesEngine;
107
108     /** SWT clipboard instance. */
109     private Clipboard mClipboard;
110
111     /*
112      * The last valid ILayoutResult passed to {@link #setResult(ILayoutResult)}.
113      * This can be null.
114      * When non null, {@link #mLastValidViewInfoRoot} is guaranteed to be non-null too.
115     */
116     private ILayoutResult mLastValidResult;
117
118     /**
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.
122      */
123     private CanvasViewInfo mLastValidViewInfoRoot;
124
125     /**
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.
130      * <p/>
131      * When this is false, {@link #mLastValidResult} can be non-null and points to an older
132      * layout result.
133      */
134     private boolean mIsResultValid;
135
136     /** Current background image. Null when there's no image. */
137     private Image mImage;
138
139     /** The current selection list. The list is never null, however it can be empty. */
140     private final LinkedList<CanvasSelection> mSelections = new LinkedList<CanvasSelection>();
141
142     /** CanvasSelection border color. Do not dispose, it's a system color. */
143     private Color mSelectionFgColor;
144
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;
148
149     /** Default font used on the canvas. Do not dispose, it's a system font. */
150     private Font mFont;
151
152     /** Current hover view info. Null when no mouse hover. */
153     private CanvasViewInfo mHoverViewInfo;
154
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.
158      */
159     private Rectangle mHoverRect;
160
161     /** Hover border color. Must be disposed, it's NOT a system color. */
162     private Color mHoverFgColor;
163
164     /** Outline color. Do not dispose, it's a system color. */
165     private Color mOutlineColor;
166
167     /**
168      * The <em>current</em> alternate selection, if any, which changes when the Alt key is
169      * used during a selection. Can be null.
170      */
171     private CanvasAlternateSelection mAltSelection;
172
173     /** When true, always display the outline of all views. */
174     private boolean mShowOutline;
175
176     /** Drop target associated with this composite. */
177     private DropTarget mDropTarget;
178
179     /** Drop listener, with feedback from current drop */
180     private CanvasDropListener mDropListener;
181
182     /** Factory that can create {@link INode} proxies. */
183     private final NodeFactory mNodeFactory = new NodeFactory();
184
185     /** Vertical scaling & scrollbar information. */
186     private ScaleInfo mVScale;
187
188     /** Horizontal scaling & scrollbar information. */
189     private ScaleInfo mHScale;
190
191     private DragSource mSource;
192
193     /** The current Outline Page, to synchronize the selection both ways. */
194     private OutlinePage2 mOutlinePage;
195
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;
200
201         mClipboard = new Clipboard(parent.getDisplay());
202
203         mHScale = new ScaleInfo(getHorizontalBar());
204         mVScale = new ScaleInfo(getVerticalBar());
205
206         mGCWrapper = new GCWrapper(mHScale, mVScale);
207
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);
212
213         mFont = d.getSystemFont();
214
215         addPaintListener(new PaintListener() {
216             public void paintControl(PaintEvent e) {
217                 onPaint(e);
218             }
219         });
220
221         addControlListener(new ControlAdapter() {
222             @Override
223             public void controlResized(ControlEvent e) {
224                 super.controlResized(e);
225                 mHScale.setClientSize(getClientArea().width);
226                 mVScale.setClientSize(getClientArea().height);
227             }
228         });
229
230         addMouseMoveListener(new MouseMoveListener() {
231             public void mouseMove(MouseEvent e) {
232                 onMouseMove(e);
233             }
234         });
235
236         addMouseListener(new MouseListener() {
237             public void mouseUp(MouseEvent e) {
238                 onMouseUp(e);
239             }
240
241             public void mouseDown(MouseEvent e) {
242                 onMouseDown(e);
243             }
244
245             public void mouseDoubleClick(MouseEvent e) {
246                 onDoubleClick(e);
247             }
248         });
249
250         // DND Reference: http://www.eclipse.org/articles/Article-SWT-DND/DND-in-SWT.html
251
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);
256
257         mSource = new DragSource(this, DND.DROP_COPY | DND.DROP_MOVE);
258         mSource.setTransfer(new Transfer[] {
259                 TextTransfer.getInstance(),
260                 SimpleXmlTransfer.getInstance()
261             } );
262         mSource.addDragListener(new CanvasDragSourceListener());
263
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;
268         }
269     }
270
271     @Override
272     public void dispose() {
273         super.dispose();
274
275         if (mHoverFgColor != null) {
276             mHoverFgColor.dispose();
277             mHoverFgColor = null;
278         }
279
280         if (mDropTarget != null) {
281             mDropTarget.dispose();
282             mDropTarget = null;
283         }
284
285         if (mRulesEngine != null) {
286             mRulesEngine.dispose();
287             mRulesEngine = null;
288         }
289
290         if (mClipboard != null) {
291             mClipboard.dispose();
292             mClipboard = null;
293         }
294     }
295
296     /**
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.
301      * <p/>
302      * When this is false, {@link #mLastValidResult} can be non-null and points to an older
303      * layout result.
304      */
305     /* package */ boolean isResultValid() {
306         return mIsResultValid;
307     }
308
309     /** Returns the Groovy Rules Engine, associated with the current project. */
310     /* package */ RulesEngine getRulesEngine() {
311         return mRulesEngine;
312     }
313
314     /** Sets the Groovy Rules Engine, associated with the current project. */
315     /* package */ void setRulesEngine(RulesEngine rulesEngine) {
316         mRulesEngine = rulesEngine;
317     }
318
319     /**
320      * Returns the factory to use to convert from {@link CanvasViewInfo} or from
321      * {@link UiViewElementNode} to {@link INode} proxies.
322      */
323     public NodeFactory getNodeFactory() {
324         return mNodeFactory;
325     }
326
327     /** Returns the shared SWT keyboard. */
328     public Clipboard getClipboard() {
329         return mClipboard;
330     }
331
332     /**
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.
335      *
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
338      * when it is valid.
339      *
340      * @param result The new rendering result, either valid or not.
341      */
342     public void setResult(ILayoutResult result) {
343         // disable any hover
344         mHoverRect = null;
345
346         mIsResultValid = (result != null && result.getSuccess() == ILayoutResult.SUCCESS);
347
348         if (mIsResultValid && result != null) {
349             mLastValidResult = result;
350             mLastValidViewInfoRoot = new CanvasViewInfo(result.getRootView());
351             setImage(result.getImage());
352
353             updateNodeProxies(mLastValidViewInfoRoot);
354             mOutlinePage.setModel(mLastValidViewInfoRoot);
355
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();
360
361                 // Check if the selected object still exists
362                 Object key = s.getViewInfo().getUiViewKey();
363                 CanvasViewInfo vi = findViewInfoKey(key, mLastValidViewInfoRoot);
364
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.
368                 it.remove();
369                 if (vi != null) {
370                     it.add(new CanvasSelection(vi, mRulesEngine, mNodeFactory));
371                 }
372             }
373             updateOulineSelection();
374
375             // remove the current alternate selection views
376             mAltSelection = null;
377
378             mHScale.setSize(mImage.getImageData().width, getClientArea().width);
379             mVScale.setSize(mImage.getImageData().height, getClientArea().height);
380
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();
385         }
386
387         redraw();
388     }
389
390     public void setShowOutline(boolean newState) {
391         mShowOutline = newState;
392         redraw();
393     }
394
395     public double getScale() {
396         return mHScale.getScale();
397     }
398
399     public void setScale(double scale) {
400         mHScale.setScale(scale);
401         mVScale.setScale(scale);
402         redraw();
403     }
404
405     /**
406      * Called by the {@link GraphicalEditorPart} when the Copy action is requested.
407      *
408      * @param clipboard The shared clipboard. Must not be disposed.
409      */
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.
413     }
414
415     /**
416      * Called by the {@link GraphicalEditorPart} when the Cut action is requested.
417      *
418      * @param clipboard The shared clipboard. Must not be disposed.
419      */
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.
423     }
424
425     /**
426      * Called by the {@link GraphicalEditorPart} when the Paste action is requested.
427      *
428      * @param clipboard The shared clipboard. Must not be disposed.
429      */
430     public void onPaste(Clipboard clipboard) {
431
432     }
433
434     /**
435      * Called by the {@link GraphicalEditorPart} when the Select All action is requested.
436      */
437     public void onSelectAll() {
438         // First clear the current selection, if any.
439         mSelections.clear();
440         mAltSelection = null;
441
442         // Now select everything if there's a valid layout
443         if (mIsResultValid && mLastValidResult != null) {
444             selectAllViewInfos(mLastValidViewInfoRoot);
445             redraw();
446         }
447
448         updateOulineSelection();
449     }
450
451     /**
452      * Delete action
453      */
454     public void onDelete() {
455         // TODO not implemented yet, not even hooked in yet!
456     }
457
458     /**
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.
462      *
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
466      */
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);
470
471         int x = mHScale.inverseTranslate(p.x);
472         int y = mVScale.inverseTranslate(p.y);
473         return new Point(x, y);
474     }
475
476     //---
477
478     public interface ScaleTransform {
479         /**
480          * Computes the transformation from a X/Y canvas image coordinate
481          * to client pixel coordinate.
482          * <p/>
483          * This takes into account the {@link ScaleInfo#IMAGE_MARGIN},
484          * the current scaling and the current translation.
485          *
486          * @param canvasX A canvas image coordinate (X or Y).
487          * @return The transformed coordinate in client pixel space.
488          */
489         public int translate(int canvasX);
490
491         /**
492          * Computes the transformation from a canvas image size (width or height) to
493          * client pixel coordinates.
494          *
495          * @param canwasW A canvas image size (W or H).
496          * @return The transformed coordinate in client pixel space.
497          */
498         public int scale(int canwasW);
499
500         /**
501          * Computes the transformation from a X/Y client pixel coordinate
502          * to canvas image coordinate.
503          * <p/>
504          * This takes into account the {@link ScaleInfo#IMAGE_MARGIN},
505          * the current scaling and the current translation.
506          * <p/>
507          * This is the inverse of {@link #translate(int)}.
508          *
509          * @param screenX A client pixel space coordinate (X or Y).
510          * @return The transformed coordinate in canvas image space.
511          */
512         public int inverseTranslate(int screenX);
513     }
514
515     private class ScaleInfo implements ScaleTransform {
516         /**
517          * Margin around the rendered image.
518          * Should be enough space to display the layout width and height pseudo widgets.
519          */
520         private static final int IMAGE_MARGIN = 25;
521
522         /** Canvas image size (original, before zoom), in pixels */
523         private int mImgSize;
524
525         /** Client size, in pixels */
526         private int mClientSize;
527
528         /** Left-top offset in client pixel coordinates */
529         private int mTranslate;
530
531         /** Scaling factor, > 0 */
532         private double mScale;
533
534         /** Scrollbar widget */
535         ScrollBar mScrollbar;
536
537         public ScaleInfo(ScrollBar scrollbar) {
538             mScrollbar = scrollbar;
539             mScale = 1.0;
540             mTranslate = 0;
541
542             mScrollbar.addSelectionListener(new SelectionAdapter() {
543                 @Override
544                 public void widgetSelected(SelectionEvent e) {
545                     // User requested scrolling. Changes translation and redraw canvas.
546                     mTranslate = mScrollbar.getSelection();
547                     redraw();
548                 }
549             });
550         }
551
552         /**
553          * Sets the new scaling factor. Recomputes scrollbars.
554          * @param scale Scaling factor, > 0.
555          */
556         public void setScale(double scale) {
557             if (mScale != scale) {
558                 mScale = scale;
559                 resizeScrollbar();
560             }
561         }
562
563         /** Returns current scaling factor. */
564         public double getScale() {
565             return mScale;
566         }
567
568         /** Returns Canvas image size (original, before zoom), in pixels. */
569         public int getImgSize() {
570             return mImgSize;
571         }
572
573         /** Returns the scaled image size in pixels. */
574         public int getScalledImgSize() {
575             return (int) (mImgSize * mScale);
576         }
577
578         /** Changes the size of the canvas image and the client size. Recomputes scrollbars. */
579         public void setSize(int imgSize, int clientSize) {
580             mImgSize = imgSize;
581             setClientSize(clientSize);
582         }
583
584         /** Changes the size of the client size. Recomputes scrollbars. */
585         public void setClientSize(int clientSize) {
586             mClientSize = clientSize;
587             resizeScrollbar();
588         }
589
590         private void resizeScrollbar() {
591             // scaled image size
592             int sx = (int) (mImgSize * mScale);
593
594             // actual client area is always reduced by the margins
595             int cx = mClientSize - 2 * IMAGE_MARGIN;
596
597             if (sx < cx) {
598                 mScrollbar.setEnabled(false);
599             } else {
600                 mScrollbar.setEnabled(true);
601
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);
606             }
607         }
608
609         public int translate(int canvasX) {
610             return IMAGE_MARGIN - mTranslate + (int)(mScale * canvasX);
611         }
612
613         public int scale(int canwasW) {
614             return (int)(mScale * canwasW);
615         }
616
617         public int inverseTranslate(int screenX) {
618             return (int) ((screenX - IMAGE_MARGIN + mTranslate) / mScale);
619         }
620     }
621
622     /**
623      * Creates or updates the node proxy for this canvas view info.
624      * <p/>
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.
627      * <p/>
628      * This is a recursive call that updates the whole hierarchy starting at the given
629      * view info.
630      */
631     private void updateNodeProxies(CanvasViewInfo vi) {
632
633         if (vi == null) {
634             return;
635         }
636
637         UiViewElementNode key = vi.getUiViewKey();
638
639         if (key != null) {
640             mNodeFactory.create(vi);
641         }
642
643         for (CanvasViewInfo child : vi.getChildren()) {
644             updateNodeProxies(child);
645         }
646     }
647
648     /**
649      * Sets the image of the last *successful* rendering.
650      * Converts the AWT image into an SWT image.
651      */
652     private void setImage(BufferedImage awtImage) {
653         int width = awtImage.getWidth();
654         int height = awtImage.getHeight();
655
656         Raster raster = awtImage.getData(new java.awt.Rectangle(width, height));
657         int[] imageDataBuffer = ((DataBufferInt)raster.getDataBuffer()).getData();
658
659         ImageData imageData = new ImageData(width, height, 32,
660                 new PaletteData(0x00FF0000, 0x0000FF00, 0x000000FF));
661
662         imageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0);
663
664         mImage = new Image(getDisplay(), imageData);
665     }
666
667     /**
668      * Sets the alpha for the given GC.
669      * <p/>
670      * Alpha may not work on all platforms and may fail with an exception.
671      *
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.
675      *
676      * @see GC#setAlpha(int)
677      */
678     private boolean gc_setAlpha(GC gc, int alpha) {
679         try {
680             gc.setAlpha(alpha);
681             return true;
682         } catch (SWTException e) {
683             return false;
684         }
685     }
686
687     /**
688      * Sets the non-text antialias flag for the given GC.
689      * <p/>
690      * Antialias may not work on all platforms and may fail with an exception.
691      *
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.
696      *
697      * @see GC#setAntialias(int)
698      */
699     private int gc_setAntialias(GC gc, int alias) {
700         try {
701             int old = gc.getAntialias();
702             gc.setAntialias(alias);
703             return old;
704         } catch (SWTException e) {
705             return -2;
706         }
707     }
708
709     /**
710      * Paints the canvas in response to paint events.
711      */
712     private void onPaint(PaintEvent e) {
713         GC gc = e.gc;
714         gc.setFont(mFont);
715         mGCWrapper.setGC(gc);
716         try {
717
718             if (mImage != null) {
719                 if (!mIsResultValid) {
720                     gc_setAlpha(gc, 128);  // half-transparent
721                 }
722
723                 ScaleInfo hi = mHScale;
724                 ScaleInfo vi = mVScale;
725
726                 // we only anti-alias when reducing the image size.
727                 int oldAlias = -2;
728                 if (hi.getScale() < 1.0) {
729                     oldAlias = gc_setAntialias(gc, SWT.ON);
730                 }
731
732                 gc.drawImage(mImage,
733                         0,                          // srcX
734                         0,                          // srcY
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
741                         );
742
743                 if (oldAlias != -2) {
744                     gc_setAntialias(gc, oldAlias);
745                 }
746
747                 if (!mIsResultValid) {
748                     gc_setAlpha(gc, 255);  // opaque
749                 }
750             }
751
752             if (mShowOutline && mLastValidViewInfoRoot != null) {
753                 gc.setForeground(mOutlineColor);
754                 gc.setLineStyle(SWT.LINE_DOT);
755                 drawOutline(gc, mLastValidViewInfoRoot);
756             }
757
758             if (mHoverRect != null) {
759                 gc.setForeground(mHoverFgColor);
760                 gc.setLineStyle(SWT.LINE_DOT);
761
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);
766
767                 gc.drawRectangle(x, y, w, h);
768             }
769
770             int n = mSelections.size();
771             if (n > 0) {
772                 boolean isMultipleSelection = n > 1;
773
774                 if (n == 1) {
775                     gc.setForeground(mSelectionFgColor);
776                     mSelections.get(0).paintParentSelection(mRulesEngine, mGCWrapper);
777                 }
778
779                 for (CanvasSelection s : mSelections) {
780                     gc.setForeground(mSelectionFgColor);
781                     s.paintSelection(mRulesEngine, mGCWrapper, isMultipleSelection);
782                 }
783             }
784
785             if (mDropListener != null) {
786                 mDropListener.paintFeedback(mGCWrapper);
787             }
788
789         } finally {
790             mGCWrapper.setGC(null);
791         }
792     }
793
794     private void drawOutline(GC gc, CanvasViewInfo info) {
795
796         Rectangle r = info.getAbsRect();
797
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);
802
803         gc.drawRectangle(x, y, w, h);
804
805         for (CanvasViewInfo vi : info.getChildren()) {
806             drawOutline(gc, vi);
807         }
808     }
809
810     /**
811      * Hover on top of a known child.
812      */
813     private void onMouseMove(MouseEvent e) {
814         if (mLastValidResult != null) {
815             CanvasViewInfo root = mLastValidViewInfoRoot;
816
817             int x = mHScale.inverseTranslate(e.x);
818             int y = mVScale.inverseTranslate(e.y);
819
820             CanvasViewInfo vi = findViewInfoAt(x, y);
821
822             // We don't hover on the root since it's not a widget per see and it is always there.
823             if (vi == root) {
824                 vi = null;
825             }
826
827             boolean needsUpdate = vi != mHoverViewInfo;
828             mHoverViewInfo = vi;
829
830             if (vi == null) {
831                 mHoverRect = null;
832             } else {
833                 Rectangle r = vi.getSelectionRect();
834                 mHoverRect = new Rectangle(r.x, r.y, r.width, r.height);
835             }
836
837             if (needsUpdate) {
838                 redraw();
839             }
840         }
841     }
842
843     private void onMouseDown(MouseEvent e) {
844         // pass, not used yet.
845     }
846
847     /**
848      * Performs selection on mouse up (not mouse down).
849      * <p/>
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).
853      */
854     private void onMouseUp(MouseEvent e) {
855         if (mLastValidResult != null) {
856
857             boolean isShift = (e.stateMask & SWT.SHIFT) != 0;
858             boolean isAlt   = (e.stateMask & SWT.ALT)   != 0;
859
860             int x = mHScale.inverseTranslate(e.x);
861             int y = mVScale.inverseTranslate(e.y);
862
863             CanvasViewInfo vi = findViewInfoAt(x, y);
864
865             if (isShift && !isAlt) {
866                 // Case where shift is pressed: pointed object is toggled.
867
868                 // reset alternate selection if any
869                 mAltSelection = null;
870
871                 // If nothing has been found at the cursor, assume it might be a user error
872                 // and avoid clearing the existing selection.
873
874                 if (vi != null) {
875                     // toggle this selection on-off: remove it if already selected
876                     if (deselect(vi)) {
877                         redraw();
878                         return;
879                     }
880
881                     // otherwise add it.
882                     mSelections.add(new CanvasSelection(vi, mRulesEngine, mNodeFactory));
883                     updateOulineSelection();
884                     redraw();
885                 }
886
887             } else if (isAlt) {
888                 // Case where alt is pressed: select or cycle the object pointed at.
889
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.
892
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
896                 // one too.
897                 if (mAltSelection == null || mAltSelection.getOriginatingView() != vi) {
898                     mAltSelection = new CanvasAlternateSelection(vi, findAltViewInfoAt(
899                                                     x, y, mLastValidViewInfoRoot, null));
900
901                     // deselect them all, in case they were partially selected
902                     deselectAll(mAltSelection.getAltViews());
903
904                     // select the current one
905                     CanvasViewInfo vi2 = mAltSelection.getCurrent();
906                     if (vi2 != null) {
907                         mSelections.addFirst(new CanvasSelection(vi2, mRulesEngine, mNodeFactory));
908                         updateOulineSelection();
909                     }
910                 } else {
911                     // We're trying to cycle through the current alternate selection.
912                     // First remove the current object.
913                     CanvasViewInfo vi2 = mAltSelection.getCurrent();
914                     deselect(vi2);
915
916                     // Now select the next one.
917                     vi2 = mAltSelection.getNext();
918                     if (vi2 != null) {
919                         mSelections.addFirst(new CanvasSelection(vi2, mRulesEngine, mNodeFactory));
920                         updateOulineSelection();
921                     }
922                 }
923                 redraw();
924
925             } else {
926                 // Case where no modifier is pressed: either select or reset the selection.
927
928                 selectSingle(vi);
929             }
930         }
931     }
932
933     /**
934      * Removes all the currently selected item and only select the given item.
935      * Issues a {@link #redraw()} if the selection changes.
936      *
937      * @param vi The new selected item if non-null. Selection becomes empty if null.
938      */
939     private void selectSingle(CanvasViewInfo vi) {
940         // reset alternate selection if any
941         mAltSelection = null;
942
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.
947                 return;
948             }
949             mSelections.clear();
950         }
951
952         if (vi != null) {
953             mSelections.add(new CanvasSelection(vi, mRulesEngine, mNodeFactory));
954         }
955         updateOulineSelection();
956         redraw();
957     }
958
959     /**
960      * Deselects a view info.
961      * Returns true if the object was actually selected.
962      * Callers are responsible for calling redraw() and updateOulineSelection() after.
963      */
964     private boolean deselect(CanvasViewInfo canvasViewInfo) {
965         if (canvasViewInfo == null) {
966             return false;
967         }
968
969         for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) {
970             CanvasSelection s = it.next();
971             if (canvasViewInfo == s.getViewInfo()) {
972                 it.remove();
973                 return true;
974             }
975         }
976
977         return false;
978     }
979
980     /**
981      * Deselects multiple view infos.
982      * Callers are responsible for calling redraw() and updateOulineSelection() after.
983      */
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())) {
988                 it.remove();
989             }
990         }
991     }
992
993     private void onDoubleClick(MouseEvent e) {
994         // pass, not used yet.
995     }
996
997     /**
998      * Tries to find a child with the same view key in the view info sub-tree.
999      * Returns null if not found.
1000      */
1001     private CanvasViewInfo findViewInfoKey(Object viewKey, CanvasViewInfo canvasViewInfo) {
1002         if (canvasViewInfo.getUiViewKey() == viewKey) {
1003             return canvasViewInfo;
1004         }
1005
1006         // try to find a matching child
1007         for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
1008             CanvasViewInfo v = findViewInfoKey(viewKey, child);
1009             if (v != null) {
1010                 return v;
1011             }
1012         }
1013
1014         return null;
1015     }
1016
1017
1018     /**
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.
1022      *
1023      * Returns null if not found or if there's view info root.
1024      */
1025     /* package */ CanvasViewInfo findViewInfoAt(int x, int y) {
1026         if (mLastValidViewInfoRoot == null) {
1027             return null;
1028         } else {
1029             return findViewInfoAt(x, y, mLastValidViewInfoRoot);
1030         }
1031     }
1032
1033     /**
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.
1036      *
1037      * Returns null if not found.
1038      */
1039     private CanvasViewInfo findViewInfoAt(int x, int y, CanvasViewInfo canvasViewInfo) {
1040         Rectangle r = canvasViewInfo.getSelectionRect();
1041         if (r.contains(x, y)) {
1042
1043             // try to find a matching child first
1044             for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
1045                 CanvasViewInfo v = findViewInfoAt(x, y, child);
1046                 if (v != null) {
1047                     return v;
1048                 }
1049             }
1050
1051             // if no children matched, this is the view that we're looking for
1052             return canvasViewInfo;
1053         }
1054
1055         return null;
1056     }
1057
1058     private ArrayList<CanvasViewInfo> findAltViewInfoAt(
1059             int x, int y, CanvasViewInfo parent, ArrayList<CanvasViewInfo> outList) {
1060         Rectangle r;
1061
1062         if (outList == null) {
1063             outList = new ArrayList<CanvasViewInfo>();
1064
1065             // add the parent root only once
1066             r = parent.getSelectionRect();
1067             if (r.contains(x, y)) {
1068                 outList.add(parent);
1069             }
1070         }
1071
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)) {
1077                     outList.add(child);
1078                 }
1079             }
1080
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);
1086                 }
1087             }
1088         }
1089
1090         return outList;
1091     }
1092
1093     /**
1094      * Used by {@link #onSelectAll()} to add all current view infos to the selection list.
1095      *
1096      * @param canvasViewInfo The root to add. This info and all its children will be added to the
1097      *                 selection list.
1098      */
1099     private void selectAllViewInfos(CanvasViewInfo canvasViewInfo) {
1100         mSelections.add(new CanvasSelection(canvasViewInfo, mRulesEngine, mNodeFactory));
1101         for (CanvasViewInfo vi : canvasViewInfo.getChildren()) {
1102             selectAllViewInfos(vi);
1103         }
1104     }
1105
1106     /**
1107      * Update the selection in the outline page to match the current one from {@link #mSelections}
1108      */
1109     private void updateOulineSelection() {
1110         if (mOutlinePage == null) {
1111             return;
1112         }
1113
1114         if (mSelections.size() == 0) {
1115             mOutlinePage.selectAndReveal(null);
1116             return;
1117         }
1118
1119         ArrayList<CanvasViewInfo> selectedVis = new ArrayList<CanvasViewInfo>();
1120         for (CanvasSelection cs : mSelections) {
1121             CanvasViewInfo vi = cs.getViewInfo();
1122             if (vi != null) {
1123                 selectedVis.add(vi);
1124             }
1125         }
1126
1127         mOutlinePage.selectAndReveal(selectedVis.toArray(new CanvasViewInfo[selectedVis.size()]));
1128     }
1129
1130
1131     //---------------
1132
1133     private class CanvasDragSourceListener implements DragSourceListener {
1134
1135         /**
1136          * The current selection being dragged.
1137          * This may be a subset of the canvas selection.
1138          * Can be empty but never null.
1139          */
1140         final ArrayList<CanvasSelection> mDragSelection = new ArrayList<CanvasSelection>();
1141         private SimpleElement[] mDragElements;
1142
1143         /**
1144          * The user has begun the actions required to drag the widget.
1145          * <p/>
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.
1148          *
1149          * {@inheritDoc}
1150          */
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.
1158
1159             mDragSelection.clear();
1160
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);
1165
1166                 for (CanvasSelection cs : mSelections) {
1167                     if (cs.getRect().contains(x, y)) {
1168                         mDragSelection.addAll(mSelections);
1169                         break;
1170                     }
1171                 }
1172
1173                 if (mDragSelection.isEmpty()) {
1174                     // There is no selected element under the cursor.
1175                     // We'll now try to find another element.
1176
1177                     CanvasViewInfo vi = findViewInfoAt(x, y);
1178                     if (vi != null) {
1179                         mDragSelection.add(new CanvasSelection(vi, mRulesEngine, mNodeFactory));
1180                     }
1181                 }
1182             }
1183
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.
1187
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
1190                 // children.
1191
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();
1197                     if (node == null) {
1198                         // Missing ViewInfo or view key or XML, discard this.
1199                         it.remove();
1200                         continue;
1201                     }
1202
1203                     if (vi != null) {
1204                         for (Iterator<CanvasSelection> it2 = mDragSelection.iterator();
1205                              it2.hasNext(); ) {
1206                             CanvasSelection cs2 = it2.next();
1207                             if (cs != cs2) {
1208                                 CanvasViewInfo vi2 = cs2.getViewInfo();
1209                                 if (vi.isParent(vi2)) {
1210                                     // vi2 is a parent for vi. Remove vi.
1211                                     it.remove();
1212                                     break;
1213                                 }
1214                             }
1215                         }
1216                     }
1217                 }
1218             }
1219
1220             e.doit = mDragSelection.size() > 0;
1221             if (e.doit) {
1222                 mDragElements = getSelectionAsElements();
1223                 GlobalCanvasDragInfo.getInstance().startDrag(
1224                         mDragElements,
1225                         mDragSelection.toArray(new CanvasSelection[mDragSelection.size()]),
1226                         LayoutCanvas.this);
1227             }
1228         }
1229
1230         /**
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.
1234          *
1235          * {@inheritDoc}
1236          */
1237         public void dragSetData(DragSourceEvent e) {
1238             if (TextTransfer.getInstance().isSupportedType(e.dataType)) {
1239                 e.data = getSelectionAsText();
1240                 return;
1241             }
1242
1243             if (SimpleXmlTransfer.getInstance().isSupportedType(e.dataType)) {
1244                 e.data = mDragElements;
1245                 return;
1246             }
1247
1248             // otherwise we failed
1249             e.detail = DND.DROP_NONE;
1250             e.doit = false;
1251         }
1252
1253         private SimpleElement[] getSelectionAsElements() {
1254             ArrayList<SimpleElement> elements = new ArrayList<SimpleElement>();
1255
1256             for (CanvasSelection cs : mDragSelection) {
1257                 CanvasViewInfo vi = cs.getViewInfo();
1258
1259                 SimpleElement e = transformToSimpleElement(vi);
1260                 elements.add(e);
1261             }
1262
1263             return elements.toArray(new SimpleElement[elements.size()]);
1264         }
1265
1266         private SimpleElement transformToSimpleElement(CanvasViewInfo vi) {
1267
1268             UiViewElementNode uiNode = vi.getUiViewKey();
1269
1270             String fqcn = SimpleXmlTransfer.getFqcn(uiNode.getDescriptor());
1271             String parentFqcn = null;
1272             Rect bounds = new Rect(vi.getAbsRect());
1273             Rect parentBounds = null;
1274
1275             UiElementNode uiParent = uiNode.getUiParent();
1276             if (uiParent != null) {
1277                 parentFqcn = SimpleXmlTransfer.getFqcn(uiParent.getDescriptor());
1278             }
1279             if (vi.getParent() != null) {
1280                 parentBounds = new Rect(vi.getParent().getAbsRect());
1281             }
1282
1283             SimpleElement e = new SimpleElement(fqcn, parentFqcn, bounds, parentBounds);
1284
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(),
1292                             value);
1293                     e.addAttribute(a);
1294                 }
1295             }
1296
1297             for (CanvasViewInfo childVi : vi.getChildren()) {
1298                 SimpleElement e2 = transformToSimpleElement(childVi);
1299                 if (e2 != null) {
1300                     e.addInnerElement(e2);
1301                 }
1302             }
1303
1304             return e;
1305         }
1306
1307         /** Get the XML text from the current drag selection for a text transfer. */
1308         private String getSelectionAsText() {
1309             StringBuilder sb = new StringBuilder();
1310
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);
1316                 if (t != null) {
1317                     if (sb.length() > 0) {
1318                         sb.append('\n');
1319                     }
1320                     sb.append(t);
1321                 }
1322             }
1323
1324             return sb.toString();
1325         }
1326
1327         /** Get the XML text directly from the editor. */
1328         private String getXmlTextFromEditor(AndroidXmlEditor editor, Node xml_node) {
1329             String data = null;
1330             IStructuredModel model = editor.getModelForRead();
1331             try {
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) {
1337                     // Try harder.
1338                     IndexedRegion region = (IndexedRegion) xml_node;
1339                     int start = region.getStartOffset();
1340                     int end = region.getEndOffset();
1341
1342                     if (end > start) {
1343                         data = sse_doc.get(start, end - start);
1344                     }
1345                 }
1346             } catch (BadLocationException e) {
1347                 // the region offset was invalid. ignore.
1348             } finally {
1349                 model.releaseFromRead();
1350             }
1351             return data;
1352         }
1353
1354         /**
1355          * Callback invoked when the drop has been finished either way.
1356          * On a successful move, remove the originating elements.
1357          */
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");
1363
1364                 // Create an undo wrapper, which takes a runnable
1365                 mLayoutEditor.wrapUndoRecording(
1366                         "Remove drag'n'drop source elements",
1367                         new Runnable() {
1368                             public void run() {
1369                                 // Create an edit-XML wrapper, which takes a runnable
1370                                 mLayoutEditor.editXmlModel(new Runnable() {
1371                                     public void run() {
1372                                         cutDragSelection();
1373                                     }
1374                                 });
1375                             }
1376                         });
1377             }
1378
1379             // Clear the selection
1380             mDragSelection.clear();
1381             mDragElements = null;
1382             GlobalCanvasDragInfo.getInstance().stopDrag();
1383         }
1384
1385         private void cutDragSelection() {
1386             List<UiElementNode> selected = new ArrayList<UiElementNode>();
1387
1388             for (CanvasSelection cs : mDragSelection) {
1389                 selected.add(cs.getViewInfo().getUiViewKey());
1390             }
1391
1392             CopyCutAction action = new CopyCutAction(
1393                     mLayoutEditor,
1394                     getClipboard(),
1395                     null, /* xml commit callback */
1396                     selected,
1397                     true /* cut */);
1398
1399             action.run();
1400         }
1401
1402     }
1403 }