1 /*
   2  * Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 
  27 package sun.lwawt.macosx;
  28 
  29 import java.awt.*;
  30 import java.awt.datatransfer.*;
  31 import java.awt.dnd.*;
  32 import java.awt.event.*;
  33 import java.awt.image.*;
  34 import java.awt.peer.*;
  35 
  36 import javax.swing.*;
  37 import javax.swing.text.*;
  38 import javax.accessibility.*;
  39 
  40 import java.util.Map;
  41 import java.util.concurrent.Callable;
  42 
  43 import sun.awt.dnd.*;
  44 import sun.lwawt.LWComponentPeer;
  45 import sun.lwawt.LWWindowPeer;
  46 import sun.lwawt.PlatformWindow;
  47 
  48 
  49 public final class CDragSourceContextPeer extends SunDragSourceContextPeer {
  50 
  51     private static final CDragSourceContextPeer fInstance = new CDragSourceContextPeer(null);
  52 
  53     private Image  fDragImage;
  54     private CImage fDragCImage;
  55     private Point  fDragImageOffset;
  56 
  57     private static Component hoveringComponent = null;
  58 
  59     private static double fMaxImageSize = 128.0;
  60 
  61     static {
  62         String propValue = java.security.AccessController.doPrivileged(new sun.security.action.GetPropertyAction("apple.awt.dnd.defaultDragImageSize"));
  63         if (propValue != null) {
  64             try {
  65                 double value = Double.parseDouble(propValue);
  66                 if (value > 0) {
  67                     fMaxImageSize = value;
  68                 }
  69             } catch(NumberFormatException e) {}
  70         }
  71     }
  72 
  73     private CDragSourceContextPeer(DragGestureEvent dge) {
  74         super(dge);
  75     }
  76 
  77     public static CDragSourceContextPeer createDragSourceContextPeer(DragGestureEvent dge) throws InvalidDnDOperationException {
  78         fInstance.setTrigger(dge);
  79 
  80         return fInstance;
  81     }
  82 
  83     // We have to overload this method just to be able to grab the drag image and its offset as shared code doesn't store it:
  84     public void startDrag(DragSourceContext dsc, Cursor cursor, Image dragImage, Point dragImageOffset) throws InvalidDnDOperationException {
  85         fDragImage = dragImage;
  86         fDragImageOffset = dragImageOffset;
  87 
  88         super.startDrag(dsc, cursor, dragImage, dragImageOffset);
  89     }
  90 
  91     protected void startDrag(Transferable transferable, long[] formats, Map formatMap) {
  92         DragGestureEvent trigger = getTrigger();
  93         InputEvent         triggerEvent = trigger.getTriggerEvent();
  94 
  95         Point dragOrigin = new Point(trigger.getDragOrigin());
  96         int extModifiers = (triggerEvent.getModifiers() | triggerEvent.getModifiersEx());
  97         long timestamp   = triggerEvent.getWhen();
  98         int clickCount   = ((triggerEvent instanceof MouseEvent) ? (((MouseEvent) triggerEvent).getClickCount()) : 1);
  99 
 100         Component component = trigger.getComponent();
 101         // For a lightweight component traverse up the hierarchy to the root
 102         Point loc = component.getLocation();
 103         Component rootComponent = component;
 104         while (!(rootComponent instanceof Window)) {
 105             dragOrigin.translate(loc.x, loc.y);
 106             rootComponent = rootComponent.getParent();
 107             loc = rootComponent.getLocation();
 108         }
 109 
 110         //It sure will be LWComponentPeer instance as rootComponent is a Window
 111         long nativeViewPtr = ((LWComponentPeer)rootComponent.getPeer()).getPlatformWindow().getViewPtr();
 112         if (nativeViewPtr == 0) {
 113             throw new InvalidDnDOperationException("Unsupported PlatformWindow implementation");
 114         }
 115 
 116         // If there isn't any drag image make one of default appearance:
 117         if (fDragImage == null)
 118             this.setDefaultDragImage(component);
 119 
 120         // Get drag image (if any) as BufferedImage and convert that to CImage:
 121         Point dragImageOffset;
 122 
 123         if (fDragImage != null) {
 124             try {
 125                 fDragCImage = CImage.getCreator().createFromImageImmediately(fDragImage);
 126             } catch(Exception e) {
 127                 // image creation may fail for any reason
 128                 throw new InvalidDnDOperationException("Drag image can not be created.");
 129             }
 130             if (fDragCImage == null) {
 131                 throw new InvalidDnDOperationException("Drag image is not ready.");
 132             }
 133 
 134             dragImageOffset = fDragImageOffset;
 135         } else {
 136 
 137             fDragCImage = null;
 138             dragImageOffset = new Point(0, 0);
 139         }
 140 
 141         try {
 142             // Create native dragging source:
 143             final long nativeDragSource = createNativeDragSource(component, nativeViewPtr, transferable, triggerEvent,
 144                 (int) (dragOrigin.getX()), (int) (dragOrigin.getY()), extModifiers,
 145                 clickCount, timestamp, fDragCImage, dragImageOffset.x, dragImageOffset.y,
 146                 getDragSourceContext().getSourceActions(), formats, formatMap);
 147 
 148             if (nativeDragSource == 0)
 149                 throw new InvalidDnDOperationException("");
 150 
 151             setNativeContext(nativeDragSource);
 152         }
 153 
 154         catch (Exception e) {
 155             throw new InvalidDnDOperationException("failed to create native peer: " + e);
 156         }
 157 
 158         SunDropTargetContextPeer.setCurrentJVMLocalSourceTransferable(transferable);
 159 
 160         CCursorManager.getInstance().setCursor(getCursor());
 161 
 162         // Create a new thread to run the dragging operation since it's synchronous, only coming back
 163         // after dragging is finished. This leaves the AWT event thread free to handle AWT events which
 164         // are posted during dragging by native event handlers.
 165 
 166         try {
 167             Thread dragThread = new Thread() {
 168                 public void run() {
 169                     final long nativeDragSource = getNativeContext();
 170                     try {
 171                         doDragging(nativeDragSource);
 172                     } catch (Exception e) {
 173                         e.printStackTrace();
 174                     } finally {
 175                         releaseNativeDragSource(nativeDragSource);
 176                         fDragImage = null;
 177                         if (fDragCImage != null) {
 178                             fDragCImage.dispose();
 179                             fDragCImage = null;
 180                         }
 181                     }
 182                 }
 183             };
 184 
 185             dragThread.start();
 186         }
 187 
 188         catch (Exception e) {
 189             final long nativeDragSource = getNativeContext();
 190             setNativeContext(0);
 191             releaseNativeDragSource(nativeDragSource);
 192             SunDropTargetContextPeer.setCurrentJVMLocalSourceTransferable(null);
 193             throw new InvalidDnDOperationException("failed to start dragging thread: " + e);
 194         }
 195     }
 196 
 197     private void setDefaultDragImage(Component component) {
 198         boolean handled = false;
 199 
 200         // Special-case default drag image, depending on the drag source type:
 201         if (component.isLightweight()) {
 202             if (component instanceof JTextComponent) {
 203                 this.setDefaultDragImage((JTextComponent) component);
 204                 handled = true;
 205             } else if (component instanceof JTree) {
 206                             this.setDefaultDragImage((JTree) component);
 207                             handled = true;
 208                         } else if (component instanceof JTable) {
 209                             this.setDefaultDragImage((JTable) component);
 210                             handled = true;
 211                         } else if (component instanceof JList) {
 212                             this.setDefaultDragImage((JList) component);
 213                             handled = true;
 214                         }
 215         }
 216 
 217         if (handled == false)
 218             this.setDefaultDragImage();
 219     }
 220 
 221     private void setDefaultDragImage(JTextComponent component) {
 222         DragGestureEvent trigger = getTrigger();
 223         int selectionStart = component.getSelectionStart();
 224         int selectionEnd = component.getSelectionEnd();
 225         boolean handled = false;
 226 
 227         // Make sure we're dragging current selection:
 228         int index = component.viewToModel(trigger.getDragOrigin());
 229         if ((selectionStart < selectionEnd) && (index >= selectionStart) && (index <= selectionEnd)) {
 230             try {
 231                 Rectangle selectionStartBounds = component.modelToView(selectionStart);
 232                 Rectangle selectionEndBounds = component.modelToView(selectionEnd);
 233 
 234                 Rectangle selectionBounds = null;
 235 
 236                 // Single-line selection:
 237                 if (selectionStartBounds.y == selectionEndBounds.y) {
 238                     selectionBounds = new Rectangle(selectionStartBounds.x, selectionStartBounds.y,
 239                         selectionEndBounds.x - selectionStartBounds.x + selectionEndBounds.width,
 240                         selectionEndBounds.y - selectionStartBounds.y + selectionEndBounds.height);
 241                 }
 242 
 243                 // Multi-line selection:
 244                 else {
 245                     AccessibleContext ctx = component.getAccessibleContext();
 246                     AccessibleText at = (AccessibleText) ctx;
 247 
 248                     selectionBounds = component.modelToView(selectionStart);
 249                     for (int i = selectionStart + 1; i <= selectionEnd; i++) {
 250                                             Rectangle charBounds = at.getCharacterBounds(i);
 251                                             // Invalid index returns null Rectangle
 252                                             // Note that this goes against jdk doc - should be empty, but is null instead
 253                                             if (charBounds != null) {
 254                                                 selectionBounds.add(charBounds);
 255                                             }
 256                     }
 257                 }
 258 
 259                 this.setOutlineDragImage(selectionBounds);
 260                 handled = true;
 261             }
 262 
 263             catch (BadLocationException exc) {
 264                 // Default the drag image to component bounds.
 265             }
 266         }
 267 
 268         if (handled == false)
 269             this.setDefaultDragImage();
 270     }
 271 
 272 
 273     private void setDefaultDragImage(JTree component) {
 274         Rectangle selectedOutline = null;
 275 
 276         int[] selectedRows = component.getSelectionRows();
 277         for (int i=0; i<selectedRows.length; i++) {
 278             Rectangle r = component.getRowBounds(selectedRows[i]);
 279             if (selectedOutline == null)
 280                 selectedOutline = r;
 281             else
 282                 selectedOutline.add(r);
 283         }
 284 
 285         if (selectedOutline != null) {
 286             this.setOutlineDragImage(selectedOutline);
 287         } else {
 288             this.setDefaultDragImage();
 289         }
 290     }
 291 
 292     private void setDefaultDragImage(JTable component) {
 293         Rectangle selectedOutline = null;
 294 
 295         // This code will likely break once multiple selections works (3645873)
 296         int[] selectedRows = component.getSelectedRows();
 297         int[] selectedColumns = component.getSelectedColumns();
 298         for (int row=0; row<selectedRows.length; row++) {
 299             for (int col=0; col<selectedColumns.length; col++) {
 300                 Rectangle r = component.getCellRect(selectedRows[row], selectedColumns[col], true);
 301                 if (selectedOutline == null)
 302                     selectedOutline = r;
 303                 else
 304                     selectedOutline.add(r);
 305             }
 306         }
 307 
 308         if (selectedOutline != null) {
 309             this.setOutlineDragImage(selectedOutline);
 310         } else {
 311             this.setDefaultDragImage();
 312         }
 313     }
 314 
 315     private void setDefaultDragImage(JList component) {
 316         Rectangle selectedOutline = null;
 317 
 318         // This code actually works, even under the (non-existant) multiple-selections, because we only draw a union outline
 319         int[] selectedIndices = component.getSelectedIndices();
 320         if (selectedIndices.length > 0)
 321             selectedOutline = component.getCellBounds(selectedIndices[0], selectedIndices[selectedIndices.length-1]);
 322 
 323         if (selectedOutline != null) {
 324             this.setOutlineDragImage(selectedOutline);
 325         } else {
 326             this.setDefaultDragImage();
 327         }
 328     }
 329 
 330 
 331     private void setDefaultDragImage() {
 332         DragGestureEvent trigger = this.getTrigger();
 333         Component comp = trigger.getComponent();
 334 
 335         setOutlineDragImage(new Rectangle(0, 0, comp.getWidth(), comp.getHeight()), true);
 336     }
 337 
 338     private void setOutlineDragImage(Rectangle outline) {
 339         setOutlineDragImage(outline, false);
 340     }
 341 
 342     private void setOutlineDragImage(Rectangle outline, Boolean shouldScale) {
 343         int width = (int)outline.getWidth();
 344         int height = (int)outline.getHeight();
 345 
 346         double scale = 1.0;
 347         if (shouldScale) {
 348             final int area = width * height;
 349             final int maxArea = (int)(fMaxImageSize * fMaxImageSize);
 350 
 351             if (area > maxArea) {
 352                 scale = (double)area / (double)maxArea;
 353                 width /= scale;
 354                 height /= scale;
 355             }
 356         }
 357 
 358         if (width <=0) width = 1;
 359         if (height <=0) height = 1;
 360 
 361         DragGestureEvent trigger = this.getTrigger();
 362         Component comp = trigger.getComponent();
 363         Point compOffset = comp.getLocation();
 364 
 365         // For lightweight components add some special treatment:
 366         if (comp instanceof JComponent) {
 367             // Intersect requested bounds with visible bounds:
 368             Rectangle visibleBounds = ((JComponent) comp).getVisibleRect();
 369             Rectangle clipedOutline = outline.intersection(visibleBounds);
 370             if (clipedOutline.isEmpty() == false)
 371                 outline = clipedOutline;
 372 
 373             // Compensate for the component offset (e.g. when contained in a JScrollPane):
 374             outline.translate(compOffset.x, compOffset.y);
 375         }
 376 
 377         GraphicsConfiguration config = comp.getGraphicsConfiguration();
 378         BufferedImage dragImage = config.createCompatibleImage(width, height, Transparency.TRANSLUCENT);
 379 
 380         Color paint = Color.gray;
 381         BasicStroke stroke = new BasicStroke(2.0f);
 382         int halfLineWidth = (int) (stroke.getLineWidth() + 1) / 2; // Rounded up.
 383 
 384         Graphics2D g2 = (Graphics2D) dragImage.getGraphics();
 385         g2.setPaint(paint);
 386         g2.setStroke(stroke);
 387         g2.drawRect(halfLineWidth, halfLineWidth, width - 2 * halfLineWidth - 1, height - 2 * halfLineWidth - 1);
 388         g2.dispose();
 389 
 390         fDragImage = dragImage;
 391 
 392 
 393         Point dragOrigin = trigger.getDragOrigin();
 394         Point dragImageOffset = new Point(outline.x - dragOrigin.x, outline.y - dragOrigin.y);
 395         if (comp instanceof JComponent) {
 396             dragImageOffset.translate(-compOffset.x, -compOffset.y);
 397         }
 398 
 399         if (shouldScale) {
 400             dragImageOffset.x /= scale;
 401             dragImageOffset.y /= scale;
 402         }
 403 
 404         fDragImageOffset = dragImageOffset;
 405     }
 406 
 407     /**
 408      * upcall from native code
 409      */
 410     private void dragMouseMoved(final int targetActions,
 411                                 final int modifiers,
 412                                 final int x, final int y) {
 413 
 414         try {
 415             Component componentAt = LWCToolkit.invokeAndWait(
 416                     new Callable<Component>() {
 417                         @Override
 418                         public Component call() {
 419                             LWWindowPeer mouseEventComponent = LWWindowPeer.getWindowUnderCursor();
 420                             if (mouseEventComponent == null) {
 421                                 return null;
 422                             }
 423                             Component root = SwingUtilities.getRoot(mouseEventComponent.getTarget());
 424                             Point rootLocation = root.getLocationOnScreen();
 425                             return getDropTargetAt(root, x - rootLocation.x, y - rootLocation.y);
 426                         }
 427                     }, getComponent());
 428 
 429             if(componentAt != hoveringComponent) {
 430                 if(hoveringComponent != null) {
 431                     dragExit(x, y);
 432                 }
 433                 if(componentAt != null) {
 434                     dragEnter(targetActions, modifiers, x, y);
 435                 }
 436                 hoveringComponent = componentAt;
 437             }
 438 
 439             postDragSourceDragEvent(targetActions, modifiers, x, y,
 440                     DISPATCH_MOUSE_MOVED);
 441         } catch (Exception e) {
 442             throw new InvalidDnDOperationException("Failed to handle DragMouseMoved event");
 443         }
 444     }
 445 
 446     //Returns the first Component which has a dropTarget associated with it
 447     //Should be called from the EventDispatchThread
 448     private static Component getDropTargetAt(Component root, int x, int y) {
 449         if (!(root.contains(x, y) && root.isVisible() && root.isEnabled())) {
 450             return null;
 451         }
 452 
 453         if (root.getDropTarget() != null) {
 454             return root;
 455         }
 456 
 457         if (root instanceof Container) {
 458             for (Component comp : ((Container) root).getComponents()) {
 459                 Point loc = comp.getLocation();
 460                 Component dropTarget = getDropTargetAt(comp, x - loc.x, y - loc.y);
 461                 if (dropTarget != null) {
 462                     return dropTarget;
 463                 }
 464             }
 465         }
 466 
 467         return null;
 468     }
 469 
 470     /**
 471      * upcall from native code - reset hovering component
 472      */
 473     private void resetHovering() {
 474         hoveringComponent = null;
 475     }
 476 
 477     @Override
 478     protected void setNativeCursor(long nativeCtxt, Cursor c, int cType) {
 479         CCursorManager.getInstance().setCursor(c);
 480     }
 481 
 482     // Native support:
 483     private native long createNativeDragSource(Component component, long nativePeer, Transferable transferable,
 484         InputEvent triggerEvent, int dragPosX, int dragPosY, int extModifiers, int clickCount, long timestamp,
 485         CImage nsDragImage, int dragImageOffsetX, int dragImageOffsetY,
 486         int sourceActions, long[] formats, Map formatMap);
 487 
 488     private native void doDragging(long nativeDragSource);
 489 
 490     private native void releaseNativeDragSource(long nativeDragSource);
 491 }