1 /*
   2  * Copyright (c) 2011, 2014, 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     @SuppressWarnings("deprecation")
  92     protected void startDrag(Transferable transferable, long[] formats, Map<Long, DataFlavor> formatMap) {
  93         DragGestureEvent trigger = getTrigger();
  94         InputEvent         triggerEvent = trigger.getTriggerEvent();
  95 
  96         Point dragOrigin = new Point(trigger.getDragOrigin());
  97         int extModifiers = (triggerEvent.getModifiers() | triggerEvent.getModifiersEx());
  98         long timestamp   = triggerEvent.getWhen();
  99         int clickCount   = ((triggerEvent instanceof MouseEvent) ? (((MouseEvent) triggerEvent).getClickCount()) : 1);
 100 
 101         Component component = trigger.getComponent();
 102         // For a lightweight component traverse up the hierarchy to the root
 103         Point loc = component.getLocation();
 104         Component rootComponent = component;
 105         while (!(rootComponent instanceof Window)) {
 106             dragOrigin.translate(loc.x, loc.y);
 107             rootComponent = rootComponent.getParent();
 108             loc = rootComponent.getLocation();
 109         }
 110 
 111         // If there isn't any drag image make one of default appearance:
 112         if (fDragImage == null)
 113             this.setDefaultDragImage(component);
 114 
 115         // Get drag image (if any) as BufferedImage and convert that to CImage:
 116         Point dragImageOffset;
 117 
 118         if (fDragImage != null) {
 119             try {
 120                 fDragCImage = CImage.getCreator().createFromImageImmediately(fDragImage);
 121             } catch(Exception e) {
 122                 // image creation may fail for any reason
 123                 throw new InvalidDnDOperationException("Drag image can not be created.");
 124             }
 125             if (fDragCImage == null) {
 126                 throw new InvalidDnDOperationException("Drag image is not ready.");
 127             }
 128 
 129             dragImageOffset = fDragImageOffset;
 130         } else {
 131 
 132             fDragCImage = null;
 133             dragImageOffset = new Point(0, 0);
 134         }
 135 
 136         try {
 137             //It sure will be LWComponentPeer instance as rootComponent is a Window
 138             PlatformWindow platformWindow = ((LWComponentPeer)rootComponent.getPeer()).getPlatformWindow();
 139             long nativeViewPtr = CPlatformWindow.getNativeViewPtr(platformWindow);
 140             if (nativeViewPtr == 0L) throw new InvalidDnDOperationException("Unsupported platform window implementation");
 141 
 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 != null ? fDragCImage.ptr : 0L, 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                             if (root == null) {
 425                                 return null;
 426                             }
 427                             Point rootLocation = root.getLocationOnScreen();
 428                             return getDropTargetAt(root, x - rootLocation.x, y - rootLocation.y);
 429                         }
 430                     }, getComponent());
 431 
 432             if(componentAt != hoveringComponent) {
 433                 if(hoveringComponent != null) {
 434                     dragExit(x, y);
 435                 }
 436                 if(componentAt != null) {
 437                     dragEnter(targetActions, modifiers, x, y);
 438                 }
 439                 hoveringComponent = componentAt;
 440             }
 441 
 442             postDragSourceDragEvent(targetActions, modifiers, x, y,
 443                     DISPATCH_MOUSE_MOVED);
 444         } catch (Exception e) {
 445             throw new InvalidDnDOperationException("Failed to handle DragMouseMoved event");
 446         }
 447     }
 448 
 449     //Returns the first lightweight or heavyweight Component which has a dropTarget ready to accept the drag
 450     //Should be called from the EventDispatchThread
 451     private static Component getDropTargetAt(Component root, int x, int y) {
 452         if (!root.contains(x, y) || !root.isEnabled() || !root.isVisible()) {
 453             return null;
 454         }
 455 
 456         if (root.getDropTarget() != null && root.getDropTarget().isActive()) {
 457             return root;
 458         }
 459 
 460         if (root instanceof Container) {
 461             for (Component comp : ((Container) root).getComponents()) {
 462                 Point loc = comp.getLocation();
 463                 Component dropTarget = getDropTargetAt(comp, x - loc.x, y - loc.y);
 464                 if (dropTarget != null) {
 465                     return dropTarget;
 466                 }
 467             }
 468         }
 469 
 470         return null;
 471     }
 472 
 473     /**
 474      * upcall from native code - reset hovering component
 475      */
 476     private void resetHovering() {
 477         hoveringComponent = null;
 478     }
 479 
 480     @Override
 481     protected void setNativeCursor(long nativeCtxt, Cursor c, int cType) {
 482         CCursorManager.getInstance().setCursor(c);
 483     }
 484 
 485     // Native support:
 486     private native long createNativeDragSource(Component component, long nativePeer, Transferable transferable,
 487         InputEvent triggerEvent, int dragPosX, int dragPosY, int extModifiers, int clickCount, long timestamp,
 488         long nsDragImagePtr, int dragImageOffsetX, int dragImageOffsetY,
 489         int sourceActions, long[] formats, Map<Long, DataFlavor> formatMap);
 490 
 491     private native void doDragging(long nativeDragSource);
 492 
 493     private native void releaseNativeDragSource(long nativeDragSource);
 494 }