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.core.runtime.ListenerList; 35 import org.eclipse.jface.text.BadLocationException; 36 import org.eclipse.jface.util.SafeRunnable; 37 import org.eclipse.jface.viewers.ISelection; 38 import org.eclipse.jface.viewers.ISelectionChangedListener; 39 import org.eclipse.jface.viewers.ISelectionProvider; 40 import org.eclipse.jface.viewers.ITreeSelection; 41 import org.eclipse.jface.viewers.SelectionChangedEvent; 42 import org.eclipse.jface.viewers.TreePath; 43 import org.eclipse.jface.viewers.TreeSelection; 44 import org.eclipse.swt.SWT; 45 import org.eclipse.swt.SWTException; 46 import org.eclipse.swt.dnd.Clipboard; 47 import org.eclipse.swt.dnd.DND; 48 import org.eclipse.swt.dnd.DragSource; 49 import org.eclipse.swt.dnd.DragSourceEvent; 50 import org.eclipse.swt.dnd.DragSourceListener; 51 import org.eclipse.swt.dnd.DropTarget; 52 import org.eclipse.swt.dnd.TextTransfer; 53 import org.eclipse.swt.dnd.Transfer; 54 import org.eclipse.swt.events.ControlAdapter; 55 import org.eclipse.swt.events.ControlEvent; 56 import org.eclipse.swt.events.MouseEvent; 57 import org.eclipse.swt.events.MouseListener; 58 import org.eclipse.swt.events.MouseMoveListener; 59 import org.eclipse.swt.events.PaintEvent; 60 import org.eclipse.swt.events.PaintListener; 61 import org.eclipse.swt.events.SelectionAdapter; 62 import org.eclipse.swt.events.SelectionEvent; 63 import org.eclipse.swt.graphics.Color; 64 import org.eclipse.swt.graphics.Font; 65 import org.eclipse.swt.graphics.GC; 66 import org.eclipse.swt.graphics.Image; 67 import org.eclipse.swt.graphics.ImageData; 68 import org.eclipse.swt.graphics.PaletteData; 69 import org.eclipse.swt.graphics.Rectangle; 70 import org.eclipse.swt.widgets.Canvas; 71 import org.eclipse.swt.widgets.Composite; 72 import org.eclipse.swt.widgets.Control; 73 import org.eclipse.swt.widgets.Display; 74 import org.eclipse.swt.widgets.Event; 75 import org.eclipse.swt.widgets.ScrollBar; 76 import org.eclipse.ui.views.contentoutline.IContentOutlinePage; 77 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; 78 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; 79 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; 80 import org.eclipse.wst.xml.core.internal.document.NodeContainer; 81 import org.w3c.dom.Node; 82 83 import java.awt.image.BufferedImage; 84 import java.awt.image.DataBufferInt; 85 import java.awt.image.Raster; 86 import java.util.ArrayList; 87 import java.util.HashSet; 88 import java.util.Iterator; 89 import java.util.LinkedList; 90 import java.util.List; 91 import java.util.ListIterator; 92 import java.util.Set; 93 94 /** 95 * Displays the image rendered by the {@link GraphicalEditorPart} and handles 96 * the interaction with the widgets. 97 * <p/> 98 * {@link LayoutCanvas} implements the "Canvas" control. The editor part 99 * actually uses the {@link LayoutCanvasViewer}, which is a JFace viewer wrapper 100 * around this control. 101 * <p/> 102 * This class implements {@link ISelectionProvider} so that it can delegate 103 * the selection provider from the {@link LayoutCanvasViewer}. 104 * <p/> 105 * Note that {@link LayoutCanvasViewer} sets a selection change listener on this 106 * control so that it can invoke its own fireSelectionChanged when the control's 107 * selection changes. 108 * 109 * @since GLE2 110 * 111 * TODO list: 112 * - gray on error, keep select but disable d'n'd. 113 * - handle context menu (depending on selection). 114 * - delete, copy/paste linked with menus and in context menu 115 * - context menu handling of layout + local props (via IViewRules) 116 * - outline should include same context menu + delete/copy/paste ops. 117 * - outline should include drop support (from canvas or from palette) 118 */ 119 class LayoutCanvas extends Canvas implements ISelectionProvider { 120 121 /** The layout editor that uses this layout canvas. */ 122 private final LayoutEditor mLayoutEditor; 123 124 /** The Groovy Rules Engine, associated with the current project. */ 125 private RulesEngine mRulesEngine; 126 127 /** SWT clipboard instance. */ 128 private Clipboard mClipboard; 129 130 /* 131 * The last valid ILayoutResult passed to {@link #setResult(ILayoutResult)}. 132 * This can be null. 133 * When non null, {@link #mLastValidViewInfoRoot} is guaranteed to be non-null too. 134 */ 135 private ILayoutResult mLastValidResult; 136 137 /** 138 * The CanvasViewInfo root created for the last update of {@link #mLastValidResult}. 139 * This is null when {@link #mLastValidResult} is null. 140 * When non null, {@link #mLastValidResult} is guaranteed to be non-null too. 141 */ 142 private CanvasViewInfo mLastValidViewInfoRoot; 143 144 /** 145 * True when the last {@link #setResult(ILayoutResult)} provided a valid {@link ILayoutResult} 146 * in which case it is also available in {@link #mLastValidResult}. 147 * When false this means the canvas is displaying an out-dated result image & bounds and some 148 * features should be disabled accordingly such a drag'n'drop. 149 * <p/> 150 * When this is false, {@link #mLastValidResult} can be non-null and points to an older 151 * layout result. 152 */ 153 private boolean mIsResultValid; 154 155 /** Current background image. Null when there's no image. */ 156 private Image mImage; 157 158 /** The current selection list. The list is never null, however it can be empty. */ 159 private final LinkedList<CanvasSelection> mSelections = new LinkedList<CanvasSelection>(); 160 161 /** CanvasSelection border color. Do not dispose, it's a system color. */ 162 private Color mSelectionFgColor; 163 164 /** GC wrapper given to the IViewRule methods. The GC itself is only defined in the 165 * context of {@link #onPaint(PaintEvent)}; otherwise it is null. */ 166 private final GCWrapper mGCWrapper; 167 168 /** Default font used on the canvas. Do not dispose, it's a system font. */ 169 private Font mFont; 170 171 /** Current hover view info. Null when no mouse hover. */ 172 private CanvasViewInfo mHoverViewInfo; 173 174 /** Current mouse hover border rectangle. Null when there's no mouse hover. 175 * The rectangle coordinates do not take account of the translation, which must 176 * be applied to the rectangle when drawing. 177 */ 178 private Rectangle mHoverRect; 179 180 /** Hover border color. Must be disposed, it's NOT a system color. */ 181 private Color mHoverFgColor; 182 183 /** Outline color. Do not dispose, it's a system color. */ 184 private Color mOutlineColor; 185 186 /** 187 * The <em>current</em> alternate selection, if any, which changes when the Alt key is 188 * used during a selection. Can be null. 189 */ 190 private CanvasAlternateSelection mAltSelection; 191 192 /** When true, always display the outline of all views. */ 193 private boolean mShowOutline; 194 195 /** Drop target associated with this composite. */ 196 private DropTarget mDropTarget; 197 198 /** Drop listener, with feedback from current drop */ 199 private CanvasDropListener mDropListener; 200 201 /** Factory that can create {@link INode} proxies. */ 202 private final NodeFactory mNodeFactory = new NodeFactory(); 203 204 /** Vertical scaling & scrollbar information. */ 205 private ScaleInfo mVScale; 206 207 /** Horizontal scaling & scrollbar information. */ 208 private ScaleInfo mHScale; 209 210 /** Drag source associated with this canvas. */ 211 private DragSource mSource; 212 213 /** List of clients listening to selection changes. */ 214 private final ListenerList mSelectionListeners = new ListenerList(); 215 216 /** The current Outline Page, to set its model. */ 217 private OutlinePage2 mOutlinePage; 218 219 /** Barrier set when updating the selection to prevent from recursively 220 * invoking ourselves. */ 221 private boolean mInsideUpdateSelection; 222 223 LayoutCanvas(LayoutEditor layoutEditor, RulesEngine rulesEngine, Composite parent, int style)224 public LayoutCanvas(LayoutEditor layoutEditor, 225 RulesEngine rulesEngine, 226 Composite parent, 227 int style) { 228 super(parent, style | SWT.DOUBLE_BUFFERED | SWT.V_SCROLL | SWT.H_SCROLL); 229 mLayoutEditor = layoutEditor; 230 mRulesEngine = rulesEngine; 231 232 mClipboard = new Clipboard(parent.getDisplay()); 233 234 mHScale = new ScaleInfo(getHorizontalBar()); 235 mVScale = new ScaleInfo(getVerticalBar()); 236 237 mGCWrapper = new GCWrapper(mHScale, mVScale); 238 239 Display d = getDisplay(); 240 mSelectionFgColor = d.getSystemColor(SWT.COLOR_RED); 241 mHoverFgColor = new Color(d, 0xFF, 0x99, 0x00); // orange 242 mOutlineColor = d.getSystemColor(SWT.COLOR_GREEN); 243 244 mFont = d.getSystemFont(); 245 246 addPaintListener(new PaintListener() { 247 public void paintControl(PaintEvent e) { 248 onPaint(e); 249 } 250 }); 251 252 addControlListener(new ControlAdapter() { 253 @Override 254 public void controlResized(ControlEvent e) { 255 super.controlResized(e); 256 mHScale.setClientSize(getClientArea().width); 257 mVScale.setClientSize(getClientArea().height); 258 } 259 }); 260 261 addMouseMoveListener(new MouseMoveListener() { 262 public void mouseMove(MouseEvent e) { 263 onMouseMove(e); 264 } 265 }); 266 267 addMouseListener(new MouseListener() { 268 public void mouseUp(MouseEvent e) { 269 onMouseUp(e); 270 } 271 272 public void mouseDown(MouseEvent e) { 273 onMouseDown(e); 274 } 275 276 public void mouseDoubleClick(MouseEvent e) { 277 onDoubleClick(e); 278 } 279 }); 280 281 // DND Reference: http://www.eclipse.org/articles/Article-SWT-DND/DND-in-SWT.html 282 283 mDropTarget = new DropTarget(this, DND.DROP_COPY | DND.DROP_MOVE | DND.DROP_DEFAULT); 284 mDropTarget.setTransfer(new Transfer[] { SimpleXmlTransfer.getInstance() } ); 285 mDropListener = new CanvasDropListener(this); 286 mDropTarget.addDropListener(mDropListener); 287 288 mSource = new DragSource(this, DND.DROP_COPY | DND.DROP_MOVE); 289 mSource.setTransfer(new Transfer[] { 290 TextTransfer.getInstance(), 291 SimpleXmlTransfer.getInstance() 292 } ); 293 mSource.addDragListener(new CanvasDragSourceListener()); 294 295 // Get the outline associated with this editor, if any and of the right type. 296 Object outline = layoutEditor.getAdapter(IContentOutlinePage.class); 297 if (outline instanceof OutlinePage2) { 298 mOutlinePage = (OutlinePage2) outline; 299 } 300 } 301 302 303 304 @Override dispose()305 public void dispose() { 306 super.dispose(); 307 308 if (mOutlinePage != null) { 309 mOutlinePage.setModel(null); 310 mOutlinePage = null; 311 } 312 313 if (mHoverFgColor != null) { 314 mHoverFgColor.dispose(); 315 mHoverFgColor = null; 316 } 317 318 if (mDropTarget != null) { 319 mDropTarget.dispose(); 320 mDropTarget = null; 321 } 322 323 if (mRulesEngine != null) { 324 mRulesEngine.dispose(); 325 mRulesEngine = null; 326 } 327 328 if (mClipboard != null) { 329 mClipboard.dispose(); 330 mClipboard = null; 331 } 332 } 333 334 /** 335 * Returns true when the last {@link #setResult(ILayoutResult)} provided a valid 336 * {@link ILayoutResult} in which case it is also available in {@link #mLastValidResult}. 337 * When false this means the canvas is displaying an out-dated result image & bounds and some 338 * features should be disabled accordingly such a drag'n'drop. 339 * <p/> 340 * When this is false, {@link #mLastValidResult} can be non-null and points to an older 341 * layout result. 342 */ isResultValid()343 /* package */ boolean isResultValid() { 344 return mIsResultValid; 345 } 346 347 /** Returns the Groovy Rules Engine, associated with the current project. */ getRulesEngine()348 /* package */ RulesEngine getRulesEngine() { 349 return mRulesEngine; 350 } 351 352 /** Sets the Groovy Rules Engine, associated with the current project. */ setRulesEngine(RulesEngine rulesEngine)353 /* package */ void setRulesEngine(RulesEngine rulesEngine) { 354 mRulesEngine = rulesEngine; 355 } 356 357 /** 358 * Returns the factory to use to convert from {@link CanvasViewInfo} or from 359 * {@link UiViewElementNode} to {@link INode} proxies. 360 */ getNodeFactory()361 public NodeFactory getNodeFactory() { 362 return mNodeFactory; 363 } 364 365 /** Returns the shared SWT keyboard. */ getClipboard()366 public Clipboard getClipboard() { 367 return mClipboard; 368 } 369 370 /** 371 * Sets the result of the layout rendering. The result object indicates if the layout 372 * rendering succeeded. If it did, it contains a bitmap and the objects rectangles. 373 * 374 * Implementation detail: the bridge's computeLayout() method already returns a newly 375 * allocated ILayourResult. That means we can keep this result and hold on to it 376 * when it is valid. 377 * 378 * @param result The new rendering result, either valid or not. 379 */ setResult(ILayoutResult result)380 public void setResult(ILayoutResult result) { 381 // disable any hover 382 mHoverRect = null; 383 384 mIsResultValid = (result != null && result.getSuccess() == ILayoutResult.SUCCESS); 385 386 if (mIsResultValid && result != null) { 387 mLastValidResult = result; 388 mLastValidViewInfoRoot = new CanvasViewInfo(result.getRootView()); 389 setImage(result.getImage()); 390 391 updateNodeProxies(mLastValidViewInfoRoot); 392 mOutlinePage.setModel(mLastValidViewInfoRoot); 393 394 // Check if the selection is still the same (based on the object keys) 395 // and eventually recompute their bounds. 396 for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) { 397 CanvasSelection s = it.next(); 398 399 // Check if the selected object still exists 400 Object key = s.getViewInfo().getUiViewKey(); 401 CanvasViewInfo vi = findViewInfoKey(key, mLastValidViewInfoRoot); 402 403 // Remove the previous selection -- if the selected object still exists 404 // we need to recompute its bounds in case it moved so we'll insert a new one 405 // at the same place. 406 it.remove(); 407 if (vi != null) { 408 it.add(new CanvasSelection(vi, mRulesEngine, mNodeFactory)); 409 } 410 } 411 fireSelectionChanged(); 412 413 // remove the current alternate selection views 414 mAltSelection = null; 415 416 mHScale.setSize(mImage.getImageData().width, getClientArea().width); 417 mVScale.setSize(mImage.getImageData().height, getClientArea().height); 418 419 // Pre-load the android.view.View rule in the Rules Engine. Doing it here means 420 // it will be done after the first rendering is finished. Successive calls are 421 // superfluous but harmless since the rule will be cached. 422 mRulesEngine.preloadAndroidView(); 423 } 424 425 redraw(); 426 } 427 setShowOutline(boolean newState)428 public void setShowOutline(boolean newState) { 429 mShowOutline = newState; 430 redraw(); 431 } 432 getScale()433 public double getScale() { 434 return mHScale.getScale(); 435 } 436 setScale(double scale)437 public void setScale(double scale) { 438 mHScale.setScale(scale); 439 mVScale.setScale(scale); 440 redraw(); 441 } 442 443 /** 444 * Called by the {@link GraphicalEditorPart} when the Copy action is requested. 445 * 446 * @param clipboard The shared clipboard. Must not be disposed. 447 */ onCopy(Clipboard clipboard)448 public void onCopy(Clipboard clipboard) { 449 // TODO implement copy to clipbard. Also will need to provide feedback to enable 450 // copy only when there's a selection. 451 } 452 453 /** 454 * Called by the {@link GraphicalEditorPart} when the Cut action is requested. 455 * 456 * @param clipboard The shared clipboard. Must not be disposed. 457 */ onCut(Clipboard clipboard)458 public void onCut(Clipboard clipboard) { 459 // TODO implement copy to clipbard. Also will need to provide feedback to enable 460 // cut only when there's a selection. 461 } 462 463 /** 464 * Called by the {@link GraphicalEditorPart} when the Paste action is requested. 465 * 466 * @param clipboard The shared clipboard. Must not be disposed. 467 */ onPaste(Clipboard clipboard)468 public void onPaste(Clipboard clipboard) { 469 470 } 471 472 /** 473 * Called by the {@link GraphicalEditorPart} when the Select All action is requested. 474 */ onSelectAll()475 public void onSelectAll() { 476 // First clear the current selection, if any. 477 mSelections.clear(); 478 mAltSelection = null; 479 480 // Now select everything if there's a valid layout 481 if (mIsResultValid && mLastValidResult != null) { 482 selectAllViewInfos(mLastValidViewInfoRoot); 483 redraw(); 484 } 485 486 fireSelectionChanged(); 487 } 488 489 /** 490 * Delete action 491 */ onDelete()492 public void onDelete() { 493 // TODO not implemented yet, not even hooked in yet! 494 } 495 496 /** 497 * Transforms a point, expressed in SWT display coordinates 498 * (e.g. from a Drag'n'Drop {@link Event}, not local {@link Control} coordinates) 499 * into the canvas' image coordinates according to the current zoom and scroll. 500 * 501 * @param displayX X in SWT display coordinates 502 * @param displayY Y in SWT display coordinates 503 * @return A new {@link Point} in canvas coordinates 504 */ displayToCanvasPoint(int displayX, int displayY)505 public Point displayToCanvasPoint(int displayX, int displayY) { 506 // convert screen coordinates to local SWT control coordinates 507 org.eclipse.swt.graphics.Point p = this.toControl(displayX, displayY); 508 509 int x = mHScale.inverseTranslate(p.x); 510 int y = mVScale.inverseTranslate(p.y); 511 return new Point(x, y); 512 } 513 514 515 //---- 516 // Implementation of ISelectionProvider 517 518 /** 519 * Returns a {@link TreeSelection} compatible with a TreeViewer 520 * where each {@link TreePath} item is actually a {@link CanvasViewInfo}. 521 */ getSelection()522 public ISelection getSelection() { 523 if (mSelections.size() == 0) { 524 return TreeSelection.EMPTY; 525 } 526 527 ArrayList<TreePath> paths = new ArrayList<TreePath>(); 528 529 for (CanvasSelection cs : mSelections) { 530 CanvasViewInfo vi = cs.getViewInfo(); 531 if (vi != null) { 532 ArrayList<Object> segments = new ArrayList<Object>(); 533 while (vi != null) { 534 segments.add(0, vi); 535 vi = vi.getParent(); 536 } 537 paths.add(new TreePath(segments.toArray())); 538 } 539 } 540 541 return new TreeSelection(paths.toArray(new TreePath[paths.size()])); 542 } 543 544 /** 545 * Sets the selection. It must be an {@link ITreeSelection} where each segment 546 * of the tree path is a {@link CanvasViewInfo}. A null selection is considered 547 * as an empty selection. 548 */ setSelection(ISelection selection)549 public void setSelection(ISelection selection) { 550 if (mInsideUpdateSelection) { 551 return; 552 } 553 554 try { 555 mInsideUpdateSelection = true; 556 557 if (selection == null) { 558 selection = TreeSelection.EMPTY; 559 } 560 561 if (selection instanceof ITreeSelection) { 562 ITreeSelection treeSel = (ITreeSelection) selection; 563 564 if (treeSel.isEmpty()) { 565 // Clear existing selection, if any 566 if (mSelections.size() > 0) { 567 mSelections.clear(); 568 mAltSelection = null; 569 redraw(); 570 } 571 return; 572 } 573 574 boolean changed = false; 575 576 // Create a list of all currently selected view infos 577 Set<CanvasViewInfo> oldSelected = new HashSet<CanvasViewInfo>(); 578 for (CanvasSelection cs : mSelections) { 579 oldSelected.add(cs.getViewInfo()); 580 } 581 582 // Go thru new selection and take care of selecting new items 583 // or marking those which are the same as in the current selection 584 for (TreePath path : treeSel.getPaths()) { 585 Object seg = path.getLastSegment(); 586 if (seg instanceof CanvasViewInfo) { 587 CanvasViewInfo newVi = (CanvasViewInfo) seg; 588 if (oldSelected.contains(newVi)) { 589 // This view info is already selected. Remove it from the 590 // oldSelected list so that we don't de-select it later. 591 oldSelected.remove(newVi); 592 } else { 593 // This view info is not already selected. Select it now. 594 595 // reset alternate selection if any 596 mAltSelection = null; 597 // otherwise add it. 598 mSelections.add( 599 new CanvasSelection(newVi, mRulesEngine, mNodeFactory)); 600 changed = true; 601 } 602 } 603 } 604 605 // De-select old selected items that are not in the new one 606 for (CanvasViewInfo vi : oldSelected) { 607 deselect(vi); 608 changed = true; 609 } 610 611 if (changed) { 612 redraw(); 613 } 614 } 615 } finally { 616 mInsideUpdateSelection = false; 617 } 618 } 619 addSelectionChangedListener(ISelectionChangedListener listener)620 public void addSelectionChangedListener(ISelectionChangedListener listener) { 621 mSelectionListeners.add(listener); 622 } 623 removeSelectionChangedListener(ISelectionChangedListener listener)624 public void removeSelectionChangedListener(ISelectionChangedListener listener) { 625 mSelectionListeners.remove(listener); 626 } 627 628 //--- 629 630 private class ScaleInfo implements ICanvasTransform { 631 /** Canvas image size (original, before zoom), in pixels */ 632 private int mImgSize; 633 634 /** Client size, in pixels */ 635 private int mClientSize; 636 637 /** Left-top offset in client pixel coordinates */ 638 private int mTranslate; 639 640 /** Scaling factor, > 0 */ 641 private double mScale; 642 643 /** Scrollbar widget */ 644 ScrollBar mScrollbar; 645 ScaleInfo(ScrollBar scrollbar)646 public ScaleInfo(ScrollBar scrollbar) { 647 mScrollbar = scrollbar; 648 mScale = 1.0; 649 mTranslate = 0; 650 651 mScrollbar.addSelectionListener(new SelectionAdapter() { 652 @Override 653 public void widgetSelected(SelectionEvent e) { 654 // User requested scrolling. Changes translation and redraw canvas. 655 mTranslate = mScrollbar.getSelection(); 656 redraw(); 657 } 658 }); 659 } 660 661 /** 662 * Sets the new scaling factor. Recomputes scrollbars. 663 * @param scale Scaling factor, > 0. 664 */ setScale(double scale)665 public void setScale(double scale) { 666 if (mScale != scale) { 667 mScale = scale; 668 resizeScrollbar(); 669 } 670 } 671 672 /** Returns current scaling factor. */ getScale()673 public double getScale() { 674 return mScale; 675 } 676 677 /** Returns Canvas image size (original, before zoom), in pixels. */ getImgSize()678 public int getImgSize() { 679 return mImgSize; 680 } 681 682 /** Returns the scaled image size in pixels. */ getScalledImgSize()683 public int getScalledImgSize() { 684 return (int) (mImgSize * mScale); 685 } 686 687 /** Changes the size of the canvas image and the client size. Recomputes scrollbars. */ setSize(int imgSize, int clientSize)688 public void setSize(int imgSize, int clientSize) { 689 mImgSize = imgSize; 690 setClientSize(clientSize); 691 } 692 693 /** Changes the size of the client size. Recomputes scrollbars. */ setClientSize(int clientSize)694 public void setClientSize(int clientSize) { 695 mClientSize = clientSize; 696 resizeScrollbar(); 697 } 698 resizeScrollbar()699 private void resizeScrollbar() { 700 // scaled image size 701 int sx = (int) (mImgSize * mScale); 702 703 // actual client area is always reduced by the margins 704 int cx = mClientSize - 2 * IMAGE_MARGIN; 705 706 if (sx < cx) { 707 mScrollbar.setEnabled(false); 708 } else { 709 mScrollbar.setEnabled(true); 710 711 // max scroll value is the scaled image size 712 // thumb value is the actual viewable area out of the scaled img size 713 mScrollbar.setMaximum(sx); 714 mScrollbar.setThumb(cx); 715 } 716 } 717 translate(int canvasX)718 public int translate(int canvasX) { 719 return IMAGE_MARGIN - mTranslate + (int)(mScale * canvasX); 720 } 721 scale(int canwasW)722 public int scale(int canwasW) { 723 return (int)(mScale * canwasW); 724 } 725 inverseTranslate(int screenX)726 public int inverseTranslate(int screenX) { 727 return (int) ((screenX - IMAGE_MARGIN + mTranslate) / mScale); 728 } 729 } 730 731 /** 732 * Creates or updates the node proxy for this canvas view info. 733 * <p/> 734 * Since proxies are reused, this will update the bounds of an existing proxy when the 735 * canvas is refreshed and a view changes position or size. 736 * <p/> 737 * This is a recursive call that updates the whole hierarchy starting at the given 738 * view info. 739 */ updateNodeProxies(CanvasViewInfo vi)740 private void updateNodeProxies(CanvasViewInfo vi) { 741 742 if (vi == null) { 743 return; 744 } 745 746 UiViewElementNode key = vi.getUiViewKey(); 747 748 if (key != null) { 749 mNodeFactory.create(vi); 750 } 751 752 for (CanvasViewInfo child : vi.getChildren()) { 753 updateNodeProxies(child); 754 } 755 } 756 757 /** 758 * Sets the image of the last *successful* rendering. 759 * Converts the AWT image into an SWT image. 760 */ setImage(BufferedImage awtImage)761 private void setImage(BufferedImage awtImage) { 762 int width = awtImage.getWidth(); 763 int height = awtImage.getHeight(); 764 765 Raster raster = awtImage.getData(new java.awt.Rectangle(width, height)); 766 int[] imageDataBuffer = ((DataBufferInt)raster.getDataBuffer()).getData(); 767 768 ImageData imageData = new ImageData(width, height, 32, 769 new PaletteData(0x00FF0000, 0x0000FF00, 0x000000FF)); 770 771 imageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0); 772 773 mImage = new Image(getDisplay(), imageData); 774 } 775 776 /** 777 * Sets the alpha for the given GC. 778 * <p/> 779 * Alpha may not work on all platforms and may fail with an exception. 780 * 781 * @param gc the GC to change 782 * @param alpha the new alpha, 0 for transparent, 255 for opaque. 783 * @return True if the operation worked, false if it failed with an exception. 784 * 785 * @see GC#setAlpha(int) 786 */ gc_setAlpha(GC gc, int alpha)787 private boolean gc_setAlpha(GC gc, int alpha) { 788 try { 789 gc.setAlpha(alpha); 790 return true; 791 } catch (SWTException e) { 792 return false; 793 } 794 } 795 796 /** 797 * Sets the non-text antialias flag for the given GC. 798 * <p/> 799 * Antialias may not work on all platforms and may fail with an exception. 800 * 801 * @param gc the GC to change 802 * @param alias One of {@link SWT#DEFAULT}, {@link SWT#ON}, {@link SWT#OFF}. 803 * @return The previous aliasing mode if the operation worked, 804 * or -2 if it failed with an exception. 805 * 806 * @see GC#setAntialias(int) 807 */ gc_setAntialias(GC gc, int alias)808 private int gc_setAntialias(GC gc, int alias) { 809 try { 810 int old = gc.getAntialias(); 811 gc.setAntialias(alias); 812 return old; 813 } catch (SWTException e) { 814 return -2; 815 } 816 } 817 818 /** 819 * Paints the canvas in response to paint events. 820 */ onPaint(PaintEvent e)821 private void onPaint(PaintEvent e) { 822 GC gc = e.gc; 823 gc.setFont(mFont); 824 mGCWrapper.setGC(gc); 825 try { 826 827 if (mImage != null) { 828 if (!mIsResultValid) { 829 gc_setAlpha(gc, 128); // half-transparent 830 } 831 832 ScaleInfo hi = mHScale; 833 ScaleInfo vi = mVScale; 834 835 // we only anti-alias when reducing the image size. 836 int oldAlias = -2; 837 if (hi.getScale() < 1.0) { 838 oldAlias = gc_setAntialias(gc, SWT.ON); 839 } 840 841 gc.drawImage(mImage, 842 0, // srcX 843 0, // srcY 844 hi.getImgSize(), // srcWidth 845 vi.getImgSize(), // srcHeight 846 hi.translate(0), // destX 847 vi.translate(0), // destY 848 hi.getScalledImgSize(), // destWidth 849 vi.getScalledImgSize() // destHeight 850 ); 851 852 if (oldAlias != -2) { 853 gc_setAntialias(gc, oldAlias); 854 } 855 856 if (!mIsResultValid) { 857 gc_setAlpha(gc, 255); // opaque 858 } 859 } 860 861 if (mShowOutline && mLastValidViewInfoRoot != null) { 862 gc.setForeground(mOutlineColor); 863 gc.setLineStyle(SWT.LINE_DOT); 864 drawOutline(gc, mLastValidViewInfoRoot); 865 } 866 867 if (mHoverRect != null) { 868 gc.setForeground(mHoverFgColor); 869 gc.setLineStyle(SWT.LINE_DOT); 870 871 int x = mHScale.translate(mHoverRect.x); 872 int y = mVScale.translate(mHoverRect.y); 873 int w = mHScale.scale(mHoverRect.width); 874 int h = mVScale.scale(mHoverRect.height); 875 876 gc.drawRectangle(x, y, w, h); 877 } 878 879 int n = mSelections.size(); 880 if (n > 0) { 881 boolean isMultipleSelection = n > 1; 882 883 if (n == 1) { 884 gc.setForeground(mSelectionFgColor); 885 mSelections.get(0).paintParentSelection(mRulesEngine, mGCWrapper); 886 } 887 888 for (CanvasSelection s : mSelections) { 889 gc.setForeground(mSelectionFgColor); 890 s.paintSelection(mRulesEngine, mGCWrapper, isMultipleSelection); 891 } 892 } 893 894 if (mDropListener != null) { 895 mDropListener.paintFeedback(mGCWrapper); 896 } 897 898 } finally { 899 mGCWrapper.setGC(null); 900 } 901 } 902 drawOutline(GC gc, CanvasViewInfo info)903 private void drawOutline(GC gc, CanvasViewInfo info) { 904 905 Rectangle r = info.getAbsRect(); 906 907 int x = mHScale.translate(r.x); 908 int y = mVScale.translate(r.y); 909 int w = mHScale.scale(r.width); 910 int h = mVScale.scale(r.height); 911 912 gc.drawRectangle(x, y, w, h); 913 914 for (CanvasViewInfo vi : info.getChildren()) { 915 drawOutline(gc, vi); 916 } 917 } 918 919 /** 920 * Hover on top of a known child. 921 */ onMouseMove(MouseEvent e)922 private void onMouseMove(MouseEvent e) { 923 if (mLastValidResult != null) { 924 CanvasViewInfo root = mLastValidViewInfoRoot; 925 926 int x = mHScale.inverseTranslate(e.x); 927 int y = mVScale.inverseTranslate(e.y); 928 929 CanvasViewInfo vi = findViewInfoAt(x, y); 930 931 // We don't hover on the root since it's not a widget per see and it is always there. 932 if (vi == root) { 933 vi = null; 934 } 935 936 boolean needsUpdate = vi != mHoverViewInfo; 937 mHoverViewInfo = vi; 938 939 if (vi == null) { 940 mHoverRect = null; 941 } else { 942 Rectangle r = vi.getSelectionRect(); 943 mHoverRect = new Rectangle(r.x, r.y, r.width, r.height); 944 } 945 946 if (needsUpdate) { 947 redraw(); 948 } 949 } 950 } 951 onMouseDown(MouseEvent e)952 private void onMouseDown(MouseEvent e) { 953 // pass, not used yet. 954 } 955 956 /** 957 * Performs selection on mouse up (not mouse down). 958 * <p/> 959 * Shift key is used to toggle in multi-selection. 960 * Alt key is used to cycle selection through objects at the same level than the one 961 * pointed at (i.e. click on an object then alt-click to cycle). 962 */ onMouseUp(MouseEvent e)963 private void onMouseUp(MouseEvent e) { 964 if (mLastValidResult != null) { 965 966 boolean isShift = (e.stateMask & SWT.SHIFT) != 0; 967 boolean isAlt = (e.stateMask & SWT.ALT) != 0; 968 969 int x = mHScale.inverseTranslate(e.x); 970 int y = mVScale.inverseTranslate(e.y); 971 972 CanvasViewInfo vi = findViewInfoAt(x, y); 973 974 if (isShift && !isAlt) { 975 // Case where shift is pressed: pointed object is toggled. 976 977 // reset alternate selection if any 978 mAltSelection = null; 979 980 // If nothing has been found at the cursor, assume it might be a user error 981 // and avoid clearing the existing selection. 982 983 if (vi != null) { 984 // toggle this selection on-off: remove it if already selected 985 if (deselect(vi)) { 986 redraw(); 987 return; 988 } 989 990 // otherwise add it. 991 mSelections.add(new CanvasSelection(vi, mRulesEngine, mNodeFactory)); 992 fireSelectionChanged(); 993 redraw(); 994 } 995 996 } else if (isAlt) { 997 // Case where alt is pressed: select or cycle the object pointed at. 998 999 // Note: if shift and alt are pressed, shift is ignored. The alternate selection 1000 // mechanism does not reset the current multiple selection unless they intersect. 1001 1002 // We need to remember the "origin" of the alternate selection, to be 1003 // able to continue cycling through it later. If there's no alternate selection, 1004 // create one. If there's one but not for the same origin object, create a new 1005 // one too. 1006 if (mAltSelection == null || mAltSelection.getOriginatingView() != vi) { 1007 mAltSelection = new CanvasAlternateSelection(vi, findAltViewInfoAt( 1008 x, y, mLastValidViewInfoRoot, null)); 1009 1010 // deselect them all, in case they were partially selected 1011 deselectAll(mAltSelection.getAltViews()); 1012 1013 // select the current one 1014 CanvasViewInfo vi2 = mAltSelection.getCurrent(); 1015 if (vi2 != null) { 1016 mSelections.addFirst(new CanvasSelection(vi2, mRulesEngine, mNodeFactory)); 1017 fireSelectionChanged(); 1018 } 1019 } else { 1020 // We're trying to cycle through the current alternate selection. 1021 // First remove the current object. 1022 CanvasViewInfo vi2 = mAltSelection.getCurrent(); 1023 deselect(vi2); 1024 1025 // Now select the next one. 1026 vi2 = mAltSelection.getNext(); 1027 if (vi2 != null) { 1028 mSelections.addFirst(new CanvasSelection(vi2, mRulesEngine, mNodeFactory)); 1029 fireSelectionChanged(); 1030 } 1031 } 1032 redraw(); 1033 1034 } else { 1035 // Case where no modifier is pressed: either select or reset the selection. 1036 1037 selectSingle(vi); 1038 } 1039 } 1040 } 1041 1042 /** 1043 * Removes all the currently selected item and only select the given item. 1044 * Issues a {@link #redraw()} if the selection changes. 1045 * 1046 * @param vi The new selected item if non-null. Selection becomes empty if null. 1047 */ selectSingle(CanvasViewInfo vi)1048 private void selectSingle(CanvasViewInfo vi) { 1049 // reset alternate selection if any 1050 mAltSelection = null; 1051 1052 // reset (multi)selection if any 1053 if (mSelections.size() > 0) { 1054 if (mSelections.size() == 1 && mSelections.getFirst().getViewInfo() == vi) { 1055 // CanvasSelection remains the same, don't touch it. 1056 return; 1057 } 1058 mSelections.clear(); 1059 } 1060 1061 if (vi != null) { 1062 mSelections.add(new CanvasSelection(vi, mRulesEngine, mNodeFactory)); 1063 } 1064 fireSelectionChanged(); 1065 redraw(); 1066 } 1067 1068 /** 1069 * Deselects a view info. 1070 * Returns true if the object was actually selected. 1071 * Callers are responsible for calling redraw() and updateOulineSelection() after. 1072 */ deselect(CanvasViewInfo canvasViewInfo)1073 private boolean deselect(CanvasViewInfo canvasViewInfo) { 1074 if (canvasViewInfo == null) { 1075 return false; 1076 } 1077 1078 for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) { 1079 CanvasSelection s = it.next(); 1080 if (canvasViewInfo == s.getViewInfo()) { 1081 it.remove(); 1082 return true; 1083 } 1084 } 1085 1086 return false; 1087 } 1088 1089 /** 1090 * Deselects multiple view infos. 1091 * Callers are responsible for calling redraw() and updateOulineSelection() after. 1092 */ deselectAll(List<CanvasViewInfo> canvasViewInfos)1093 private void deselectAll(List<CanvasViewInfo> canvasViewInfos) { 1094 for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) { 1095 CanvasSelection s = it.next(); 1096 if (canvasViewInfos.contains(s.getViewInfo())) { 1097 it.remove(); 1098 } 1099 } 1100 } 1101 onDoubleClick(MouseEvent e)1102 private void onDoubleClick(MouseEvent e) { 1103 // pass, not used yet. 1104 } 1105 1106 /** 1107 * Tries to find a child with the same view key in the view info sub-tree. 1108 * Returns null if not found. 1109 */ findViewInfoKey(Object viewKey, CanvasViewInfo canvasViewInfo)1110 private CanvasViewInfo findViewInfoKey(Object viewKey, CanvasViewInfo canvasViewInfo) { 1111 if (canvasViewInfo.getUiViewKey() == viewKey) { 1112 return canvasViewInfo; 1113 } 1114 1115 // try to find a matching child 1116 for (CanvasViewInfo child : canvasViewInfo.getChildren()) { 1117 CanvasViewInfo v = findViewInfoKey(viewKey, child); 1118 if (v != null) { 1119 return v; 1120 } 1121 } 1122 1123 return null; 1124 } 1125 1126 1127 /** 1128 * Tries to find the inner most child matching the given x,y coordinates in the view 1129 * info sub-tree, starting at the last know view info root. 1130 * This uses the potentially-expanded selection bounds. 1131 * 1132 * Returns null if not found or if there's view info root. 1133 */ findViewInfoAt(int x, int y)1134 /* package */ CanvasViewInfo findViewInfoAt(int x, int y) { 1135 if (mLastValidViewInfoRoot == null) { 1136 return null; 1137 } else { 1138 return findViewInfoAt(x, y, mLastValidViewInfoRoot); 1139 } 1140 } 1141 1142 /** 1143 * Tries to find the inner most child matching the given x,y coordinates in the view 1144 * info sub-tree. This uses the potentially-expanded selection bounds. 1145 * 1146 * Returns null if not found. 1147 */ findViewInfoAt(int x, int y, CanvasViewInfo canvasViewInfo)1148 private CanvasViewInfo findViewInfoAt(int x, int y, CanvasViewInfo canvasViewInfo) { 1149 Rectangle r = canvasViewInfo.getSelectionRect(); 1150 if (r.contains(x, y)) { 1151 1152 // try to find a matching child first 1153 for (CanvasViewInfo child : canvasViewInfo.getChildren()) { 1154 CanvasViewInfo v = findViewInfoAt(x, y, child); 1155 if (v != null) { 1156 return v; 1157 } 1158 } 1159 1160 // if no children matched, this is the view that we're looking for 1161 return canvasViewInfo; 1162 } 1163 1164 return null; 1165 } 1166 findAltViewInfoAt( int x, int y, CanvasViewInfo parent, ArrayList<CanvasViewInfo> outList)1167 private ArrayList<CanvasViewInfo> findAltViewInfoAt( 1168 int x, int y, CanvasViewInfo parent, ArrayList<CanvasViewInfo> outList) { 1169 Rectangle r; 1170 1171 if (outList == null) { 1172 outList = new ArrayList<CanvasViewInfo>(); 1173 1174 // add the parent root only once 1175 r = parent.getSelectionRect(); 1176 if (r.contains(x, y)) { 1177 outList.add(parent); 1178 } 1179 } 1180 1181 if (parent.getChildren().size() > 0) { 1182 // then add all children that match the position 1183 for (CanvasViewInfo child : parent.getChildren()) { 1184 r = child.getSelectionRect(); 1185 if (r.contains(x, y)) { 1186 outList.add(child); 1187 } 1188 } 1189 1190 // finally recurse in the children 1191 for (CanvasViewInfo child : parent.getChildren()) { 1192 r = child.getSelectionRect(); 1193 if (r.contains(x, y)) { 1194 findAltViewInfoAt(x, y, child, outList); 1195 } 1196 } 1197 } 1198 1199 return outList; 1200 } 1201 1202 /** 1203 * Used by {@link #onSelectAll()} to add all current view infos to the selection list. 1204 * 1205 * @param canvasViewInfo The root to add. This info and all its children will be added to the 1206 * selection list. 1207 */ selectAllViewInfos(CanvasViewInfo canvasViewInfo)1208 private void selectAllViewInfos(CanvasViewInfo canvasViewInfo) { 1209 mSelections.add(new CanvasSelection(canvasViewInfo, mRulesEngine, mNodeFactory)); 1210 for (CanvasViewInfo vi : canvasViewInfo.getChildren()) { 1211 selectAllViewInfos(vi); 1212 } 1213 } 1214 1215 /** 1216 * Notifies listeners that the selection has changed. 1217 */ fireSelectionChanged()1218 private void fireSelectionChanged() { 1219 if (mInsideUpdateSelection) { 1220 return; 1221 } 1222 try { 1223 mInsideUpdateSelection = true; 1224 1225 final SelectionChangedEvent event = new SelectionChangedEvent(this, getSelection()); 1226 1227 SafeRunnable.run(new SafeRunnable() { 1228 public void run() { 1229 for (Object listener : mSelectionListeners.getListeners()) { 1230 ((ISelectionChangedListener)listener).selectionChanged(event); 1231 } 1232 } 1233 }); 1234 } finally { 1235 mInsideUpdateSelection = false; 1236 } 1237 } 1238 1239 1240 //--------------- 1241 1242 private class CanvasDragSourceListener implements DragSourceListener { 1243 1244 /** 1245 * The current selection being dragged. 1246 * This may be a subset of the canvas selection. 1247 * Can be empty but never null. 1248 */ 1249 final ArrayList<CanvasSelection> mDragSelection = new ArrayList<CanvasSelection>(); 1250 private SimpleElement[] mDragElements; 1251 1252 /** 1253 * The user has begun the actions required to drag the widget. 1254 * <p/> 1255 * Initiate a drag only if there is one or more item selected. 1256 * If there's none, try to auto-select the one under the cursor. 1257 * 1258 * {@inheritDoc} 1259 */ dragStart(DragSourceEvent e)1260 public void dragStart(DragSourceEvent e) { 1261 // We need a selection (simple or multiple) to do any transfer. 1262 // If there's a selection *and* the cursor is over this selection, use all the 1263 // currently selected elements. 1264 // If there is no selection or the cursor is not over a selected element, drag 1265 // the element under the cursor. 1266 // If nothing can be selected, abort the drag operation. 1267 1268 mDragSelection.clear(); 1269 1270 if (mSelections.size() > 0) { 1271 // Is the cursor on top of a selected element? 1272 int x = mHScale.inverseTranslate(e.x); 1273 int y = mVScale.inverseTranslate(e.y); 1274 1275 for (CanvasSelection cs : mSelections) { 1276 if (cs.getRect().contains(x, y)) { 1277 mDragSelection.addAll(mSelections); 1278 break; 1279 } 1280 } 1281 1282 if (mDragSelection.isEmpty()) { 1283 // There is no selected element under the cursor. 1284 // We'll now try to find another element. 1285 1286 CanvasViewInfo vi = findViewInfoAt(x, y); 1287 if (vi != null) { 1288 mDragSelection.add(new CanvasSelection(vi, mRulesEngine, mNodeFactory)); 1289 } 1290 } 1291 } 1292 1293 if (mDragSelection.size() > 0) { 1294 // Sanitize the list to make sure all elements have a valid XML attached to it. 1295 // This avoids us from making repeated checks in dragSetData. 1296 1297 // In case of multiple selection, we also need to remove all children when their 1298 // parent is already selected since parents will always be added with all their 1299 // children. 1300 1301 for (Iterator<CanvasSelection> it = mDragSelection.iterator(); it.hasNext(); ) { 1302 CanvasSelection cs = it.next(); 1303 CanvasViewInfo vi = cs.getViewInfo(); 1304 UiViewElementNode key = vi == null ? null : vi.getUiViewKey(); 1305 Node node = key == null ? null : key.getXmlNode(); 1306 if (node == null) { 1307 // Missing ViewInfo or view key or XML, discard this. 1308 it.remove(); 1309 continue; 1310 } 1311 1312 if (vi != null) { 1313 for (Iterator<CanvasSelection> it2 = mDragSelection.iterator(); 1314 it2.hasNext(); ) { 1315 CanvasSelection cs2 = it2.next(); 1316 if (cs != cs2) { 1317 CanvasViewInfo vi2 = cs2.getViewInfo(); 1318 if (vi.isParent(vi2)) { 1319 // vi2 is a parent for vi. Remove vi. 1320 it.remove(); 1321 break; 1322 } 1323 } 1324 } 1325 } 1326 } 1327 } 1328 1329 e.doit = mDragSelection.size() > 0; 1330 if (e.doit) { 1331 mDragElements = getSelectionAsElements(); 1332 GlobalCanvasDragInfo.getInstance().startDrag( 1333 mDragElements, 1334 mDragSelection.toArray(new CanvasSelection[mDragSelection.size()]), 1335 LayoutCanvas.this); 1336 } 1337 } 1338 1339 /** 1340 * Callback invoked when data is needed for the event, typically right before drop. 1341 * The drop side decides what type of transfer to use and this side must now provide 1342 * the adequate data. 1343 * 1344 * {@inheritDoc} 1345 */ dragSetData(DragSourceEvent e)1346 public void dragSetData(DragSourceEvent e) { 1347 if (TextTransfer.getInstance().isSupportedType(e.dataType)) { 1348 e.data = getSelectionAsText(); 1349 return; 1350 } 1351 1352 if (SimpleXmlTransfer.getInstance().isSupportedType(e.dataType)) { 1353 e.data = mDragElements; 1354 return; 1355 } 1356 1357 // otherwise we failed 1358 e.detail = DND.DROP_NONE; 1359 e.doit = false; 1360 } 1361 getSelectionAsElements()1362 private SimpleElement[] getSelectionAsElements() { 1363 ArrayList<SimpleElement> elements = new ArrayList<SimpleElement>(); 1364 1365 for (CanvasSelection cs : mDragSelection) { 1366 CanvasViewInfo vi = cs.getViewInfo(); 1367 1368 SimpleElement e = transformToSimpleElement(vi); 1369 elements.add(e); 1370 } 1371 1372 return elements.toArray(new SimpleElement[elements.size()]); 1373 } 1374 transformToSimpleElement(CanvasViewInfo vi)1375 private SimpleElement transformToSimpleElement(CanvasViewInfo vi) { 1376 1377 UiViewElementNode uiNode = vi.getUiViewKey(); 1378 1379 String fqcn = SimpleXmlTransfer.getFqcn(uiNode.getDescriptor()); 1380 String parentFqcn = null; 1381 Rect bounds = new Rect(vi.getAbsRect()); 1382 Rect parentBounds = null; 1383 1384 UiElementNode uiParent = uiNode.getUiParent(); 1385 if (uiParent != null) { 1386 parentFqcn = SimpleXmlTransfer.getFqcn(uiParent.getDescriptor()); 1387 } 1388 if (vi.getParent() != null) { 1389 parentBounds = new Rect(vi.getParent().getAbsRect()); 1390 } 1391 1392 SimpleElement e = new SimpleElement(fqcn, parentFqcn, bounds, parentBounds); 1393 1394 for (UiAttributeNode attr : uiNode.getUiAttributes()) { 1395 String value = attr.getCurrentValue(); 1396 if (value != null && value.length() > 0) { 1397 AttributeDescriptor attrDesc = attr.getDescriptor(); 1398 SimpleAttribute a = new SimpleAttribute( 1399 attrDesc.getNamespaceUri(), 1400 attrDesc.getXmlLocalName(), 1401 value); 1402 e.addAttribute(a); 1403 } 1404 } 1405 1406 for (CanvasViewInfo childVi : vi.getChildren()) { 1407 SimpleElement e2 = transformToSimpleElement(childVi); 1408 if (e2 != null) { 1409 e.addInnerElement(e2); 1410 } 1411 } 1412 1413 return e; 1414 } 1415 1416 /** Get the XML text from the current drag selection for a text transfer. */ getSelectionAsText()1417 private String getSelectionAsText() { 1418 StringBuilder sb = new StringBuilder(); 1419 1420 for (CanvasSelection cs : mDragSelection) { 1421 CanvasViewInfo vi = cs.getViewInfo(); 1422 UiViewElementNode key = vi.getUiViewKey(); 1423 Node node = key.getXmlNode(); 1424 String t = getXmlTextFromEditor(mLayoutEditor, node); 1425 if (t != null) { 1426 if (sb.length() > 0) { 1427 sb.append('\n'); 1428 } 1429 sb.append(t); 1430 } 1431 } 1432 1433 return sb.toString(); 1434 } 1435 1436 /** Get the XML text directly from the editor. */ getXmlTextFromEditor(AndroidXmlEditor editor, Node xml_node)1437 private String getXmlTextFromEditor(AndroidXmlEditor editor, Node xml_node) { 1438 String data = null; 1439 IStructuredModel model = editor.getModelForRead(); 1440 try { 1441 IStructuredDocument sse_doc = editor.getStructuredDocument(); 1442 if (xml_node instanceof NodeContainer) { 1443 // The easy way to get the source of an SSE XML node. 1444 data = ((NodeContainer) xml_node).getSource(); 1445 } else if (xml_node instanceof IndexedRegion && sse_doc != null) { 1446 // Try harder. 1447 IndexedRegion region = (IndexedRegion) xml_node; 1448 int start = region.getStartOffset(); 1449 int end = region.getEndOffset(); 1450 1451 if (end > start) { 1452 data = sse_doc.get(start, end - start); 1453 } 1454 } 1455 } catch (BadLocationException e) { 1456 // the region offset was invalid. ignore. 1457 } finally { 1458 model.releaseFromRead(); 1459 } 1460 return data; 1461 } 1462 1463 /** 1464 * Callback invoked when the drop has been finished either way. 1465 * On a successful move, remove the originating elements. 1466 */ dragFinished(DragSourceEvent e)1467 public void dragFinished(DragSourceEvent e) { 1468 if (e.detail == DND.DROP_MOVE) { 1469 // Remove from source. Since we know the selection, we'll simply 1470 // create a cut operation on the existing drag selection. 1471 AdtPlugin.printToConsole("CanvasDND", "dragFinished => MOVE"); 1472 1473 // Create an undo wrapper, which takes a runnable 1474 mLayoutEditor.wrapUndoRecording( 1475 "Remove drag'n'drop source elements", 1476 new Runnable() { 1477 public void run() { 1478 // Create an edit-XML wrapper, which takes a runnable 1479 mLayoutEditor.editXmlModel(new Runnable() { 1480 public void run() { 1481 cutDragSelection(); 1482 } 1483 }); 1484 } 1485 }); 1486 } 1487 1488 // Clear the selection 1489 mDragSelection.clear(); 1490 mDragElements = null; 1491 GlobalCanvasDragInfo.getInstance().stopDrag(); 1492 } 1493 cutDragSelection()1494 private void cutDragSelection() { 1495 List<UiElementNode> selected = new ArrayList<UiElementNode>(); 1496 1497 for (CanvasSelection cs : mDragSelection) { 1498 selected.add(cs.getViewInfo().getUiViewKey()); 1499 } 1500 1501 CopyCutAction action = new CopyCutAction( 1502 mLayoutEditor, 1503 getClipboard(), 1504 null, /* xml commit callback */ 1505 selected, 1506 true /* cut */); 1507 1508 action.run(); 1509 } 1510 1511 } 1512 } 1513