1 /*
   2  * Copyright (c) 2011, 2015, 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 
  35 import javax.swing.*;
  36 import javax.swing.text.*;
  37 import javax.accessibility.*;
  38 
  39 import java.util.Map;
  40 import java.util.concurrent.Callable;
  41 
  42 import sun.awt.AWTAccessor;
  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<Long, DataFlavor> 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         // If there isn't any drag image make one of default appearance:
 111         if (fDragImage == null)
 112             this.setDefaultDragImage(component);
 113 
 114         // Get drag image (if any) as BufferedImage and convert that to CImage:
 115         Point dragImageOffset;
 116 
 117         if (fDragImage != null) {
 118             try {
 119                 fDragCImage = CImage.getCreator().createFromImageImmediately(fDragImage);
 120             } catch(Exception e) {
 121                 // image creation may fail for any reason
 122                 throw new InvalidDnDOperationException("Drag image can not be created.");
 123             }
 124             if (fDragCImage == null) {
 125                 throw new InvalidDnDOperationException("Drag image is not ready.");
 126             }
 127 
 128             dragImageOffset = fDragImageOffset;
 129         } else {
 130 
 131             fDragCImage = null;
 132             dragImageOffset = new Point(0, 0);
 133         }
 134 
 135         try {
 136             //It sure will be LWComponentPeer instance as rootComponent is a Window
 137             LWComponentPeer<?, ?> peer = AWTAccessor.getComponentAccessor()
 138                                                     .getPeer(rootComponent);
 139             PlatformWindow platformWindow = peer.getPlatformWindow();
 140             long nativeViewPtr = CPlatformWindow.getNativeViewPtr(platformWindow);
 141             if (nativeViewPtr == 0L) throw new InvalidDnDOperationException("Unsupported platform window implementation");
 142 
 143             // Create native dragging source:
 144             final long nativeDragSource = createNativeDragSource(component, nativeViewPtr, transferable, triggerEvent,
 145                 (int) (dragOrigin.getX()), (int) (dragOrigin.getY()), extModifiers,
 146                 clickCount, timestamp, fDragCImage != null ? fDragCImage.ptr : 0L, dragImageOffset.x, dragImageOffset.y,
 147                 getDragSourceContext().getSourceActions(), formats, formatMap);
 148 
 149             if (nativeDragSource == 0)
 150                 throw new InvalidDnDOperationException("");
 151 
 152             setNativeContext(nativeDragSource);
 153         }
 154 
 155         catch (Exception e) {
 156             throw new InvalidDnDOperationException("failed to create native peer: " + e);
 157         }
 158 
 159         SunDropTargetContextPeer.setCurrentJVMLocalSourceTransferable(transferable);
 160 
 161         CCursorManager.getInstance().setCursor(getCursor());
 162 
 163         // Create a new thread to run the dragging operation since it's synchronous, only coming back
 164         // after dragging is finished. This leaves the AWT event thread free to handle AWT events which
 165         // are posted during dragging by native event handlers.
 166 
 167         try {
 168             Runnable dragRunnable = () -> {
 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             new Thread(null, dragRunnable, "Drag", 0, false).start();
 184         } catch (Exception e) {
 185             final long nativeDragSource = getNativeContext();
 186             setNativeContext(0);
 187             releaseNativeDragSource(nativeDragSource);
 188             SunDropTargetContextPeer.setCurrentJVMLocalSourceTransferable(null);
 189             throw new InvalidDnDOperationException("failed to start dragging thread: " + e);
 190         }
 191     }
 192 
 193     private void setDefaultDragImage(Component component) {
 194         boolean handled = false;
 195 
 196         // Special-case default drag image, depending on the drag source type:
 197         if (component.isLightweight()) {
 198             if (component instanceof JTextComponent) {
 199                 this.setDefaultDragImage((JTextComponent) component);
 200                 handled = true;
 201             } else if (component instanceof JTree) {
 202                             this.setDefaultDragImage((JTree) component);
 203                             handled = true;
 204                         } else if (component instanceof JTable) {
 205                             this.setDefaultDragImage((JTable) component);
 206                             handled = true;
 207                         } else if (component instanceof JList) {
 208                             this.setDefaultDragImage((JList) component);
 209                             handled = true;
 210                         }
 211         }
 212 
 213         if (handled == false)
 214             this.setDefaultDragImage();
 215     }
 216 
 217     private void setDefaultDragImage(JTextComponent component) {
 218         DragGestureEvent trigger = getTrigger();
 219         int selectionStart = component.getSelectionStart();
 220         int selectionEnd = component.getSelectionEnd();
 221         boolean handled = false;
 222 
 223         // Make sure we're dragging current selection:
 224         int index = component.viewToModel(trigger.getDragOrigin());
 225         if ((selectionStart < selectionEnd) && (index >= selectionStart) && (index <= selectionEnd)) {
 226             try {
 227                 Rectangle selectionStartBounds = component.modelToView(selectionStart);
 228                 Rectangle selectionEndBounds = component.modelToView(selectionEnd);
 229 
 230                 Rectangle selectionBounds = null;
 231 
 232                 // Single-line selection:
 233                 if (selectionStartBounds.y == selectionEndBounds.y) {
 234                     selectionBounds = new Rectangle(selectionStartBounds.x, selectionStartBounds.y,
 235                         selectionEndBounds.x - selectionStartBounds.x + selectionEndBounds.width,
 236                         selectionEndBounds.y - selectionStartBounds.y + selectionEndBounds.height);
 237                 }
 238 
 239                 // Multi-line selection:
 240                 else {
 241                     AccessibleContext ctx = component.getAccessibleContext();
 242                     AccessibleText at = (AccessibleText) ctx;
 243 
 244                     selectionBounds = component.modelToView(selectionStart);
 245                     for (int i = selectionStart + 1; i <= selectionEnd; i++) {
 246                                             Rectangle charBounds = at.getCharacterBounds(i);
 247                                             // Invalid index returns null Rectangle
 248                                             // Note that this goes against jdk doc - should be empty, but is null instead
 249                                             if (charBounds != null) {
 250                                                 selectionBounds.add(charBounds);
 251                                             }
 252                     }
 253                 }
 254 
 255                 this.setOutlineDragImage(selectionBounds);
 256                 handled = true;
 257             }
 258 
 259             catch (BadLocationException exc) {
 260                 // Default the drag image to component bounds.
 261             }
 262         }
 263 
 264         if (handled == false)
 265             this.setDefaultDragImage();
 266     }
 267 
 268 
 269     private void setDefaultDragImage(JTree component) {
 270         Rectangle selectedOutline = null;
 271 
 272         int[] selectedRows = component.getSelectionRows();
 273         for (int i=0; i<selectedRows.length; i++) {
 274             Rectangle r = component.getRowBounds(selectedRows[i]);
 275             if (selectedOutline == null)
 276                 selectedOutline = r;
 277             else
 278                 selectedOutline.add(r);
 279         }
 280 
 281         if (selectedOutline != null) {
 282             this.setOutlineDragImage(selectedOutline);
 283         } else {
 284             this.setDefaultDragImage();
 285         }
 286     }
 287 
 288     private void setDefaultDragImage(JTable component) {
 289         Rectangle selectedOutline = null;
 290 
 291         // This code will likely break once multiple selections works (3645873)
 292         int[] selectedRows = component.getSelectedRows();
 293         int[] selectedColumns = component.getSelectedColumns();
 294         for (int row=0; row<selectedRows.length; row++) {
 295             for (int col=0; col<selectedColumns.length; col++) {
 296                 Rectangle r = component.getCellRect(selectedRows[row], selectedColumns[col], true);
 297                 if (selectedOutline == null)
 298                     selectedOutline = r;
 299                 else
 300                     selectedOutline.add(r);
 301             }
 302         }
 303 
 304         if (selectedOutline != null) {
 305             this.setOutlineDragImage(selectedOutline);
 306         } else {
 307             this.setDefaultDragImage();
 308         }
 309     }
 310 
 311     private void setDefaultDragImage(JList<?> component) {
 312         Rectangle selectedOutline = null;
 313 
 314         // This code actually works, even under the (non-existant) multiple-selections, because we only draw a union outline
 315         int[] selectedIndices = component.getSelectedIndices();
 316         if (selectedIndices.length > 0)
 317             selectedOutline = component.getCellBounds(selectedIndices[0], selectedIndices[selectedIndices.length-1]);
 318 
 319         if (selectedOutline != null) {
 320             this.setOutlineDragImage(selectedOutline);
 321         } else {
 322             this.setDefaultDragImage();
 323         }
 324     }
 325 
 326 
 327     private void setDefaultDragImage() {
 328         DragGestureEvent trigger = this.getTrigger();
 329         Component comp = trigger.getComponent();
 330 
 331         setOutlineDragImage(new Rectangle(0, 0, comp.getWidth(), comp.getHeight()), true);
 332     }
 333 
 334     private void setOutlineDragImage(Rectangle outline) {
 335         setOutlineDragImage(outline, false);
 336     }
 337 
 338     private void setOutlineDragImage(Rectangle outline, Boolean shouldScale) {
 339         int width = (int)outline.getWidth();
 340         int height = (int)outline.getHeight();
 341 
 342         double scale = 1.0;
 343         if (shouldScale) {
 344             final int area = width * height;
 345             final int maxArea = (int)(fMaxImageSize * fMaxImageSize);
 346 
 347             if (area > maxArea) {
 348                 scale = (double)area / (double)maxArea;
 349                 width /= scale;
 350                 height /= scale;
 351             }
 352         }
 353 
 354         if (width <=0) width = 1;
 355         if (height <=0) height = 1;
 356 
 357         DragGestureEvent trigger = this.getTrigger();
 358         Component comp = trigger.getComponent();
 359         Point compOffset = comp.getLocation();
 360 
 361         // For lightweight components add some special treatment:
 362         if (comp instanceof JComponent) {
 363             // Intersect requested bounds with visible bounds:
 364             Rectangle visibleBounds = ((JComponent) comp).getVisibleRect();
 365             Rectangle clipedOutline = outline.intersection(visibleBounds);
 366             if (clipedOutline.isEmpty() == false)
 367                 outline = clipedOutline;
 368 
 369             // Compensate for the component offset (e.g. when contained in a JScrollPane):
 370             outline.translate(compOffset.x, compOffset.y);
 371         }
 372 
 373         GraphicsConfiguration config = comp.getGraphicsConfiguration();
 374         BufferedImage dragImage = config.createCompatibleImage(width, height, Transparency.TRANSLUCENT);
 375 
 376         Color paint = Color.gray;
 377         BasicStroke stroke = new BasicStroke(2.0f);
 378         int halfLineWidth = (int) (stroke.getLineWidth() + 1) / 2; // Rounded up.
 379 
 380         Graphics2D g2 = (Graphics2D) dragImage.getGraphics();
 381         g2.setPaint(paint);
 382         g2.setStroke(stroke);
 383         g2.drawRect(halfLineWidth, halfLineWidth, width - 2 * halfLineWidth - 1, height - 2 * halfLineWidth - 1);
 384         g2.dispose();
 385 
 386         fDragImage = dragImage;
 387 
 388 
 389         Point dragOrigin = trigger.getDragOrigin();
 390         Point dragImageOffset = new Point(outline.x - dragOrigin.x, outline.y - dragOrigin.y);
 391         if (comp instanceof JComponent) {
 392             dragImageOffset.translate(-compOffset.x, -compOffset.y);
 393         }
 394 
 395         if (shouldScale) {
 396             dragImageOffset.x /= scale;
 397             dragImageOffset.y /= scale;
 398         }
 399 
 400         fDragImageOffset = dragImageOffset;
 401     }
 402 
 403     /**
 404      * upcall from native code
 405      */
 406     private void dragMouseMoved(final int targetActions,
 407                                 final int modifiers,
 408                                 final int x, final int y) {
 409 
 410         try {
 411             Component componentAt = LWCToolkit.invokeAndWait(
 412                     new Callable<Component>() {
 413                         @Override
 414                         public Component call() {
 415                             LWWindowPeer mouseEventComponent = LWWindowPeer.getWindowUnderCursor();
 416                             if (mouseEventComponent == null) {
 417                                 return null;
 418                             }
 419                             Component root = SwingUtilities.getRoot(mouseEventComponent.getTarget());
 420                             if (root == null) {
 421                                 return null;
 422                             }
 423                             Point rootLocation = root.getLocationOnScreen();
 424                             return getDropTargetAt(root, x - rootLocation.x, y - rootLocation.y);
 425                         }
 426                     }, getComponent());
 427 
 428             if(componentAt != hoveringComponent) {
 429                 if(hoveringComponent != null) {
 430                     dragExit(x, y);
 431                 }
 432                 if(componentAt != null) {
 433                     dragEnter(targetActions, modifiers, x, y);
 434                 }
 435                 hoveringComponent = componentAt;
 436             }
 437 
 438             postDragSourceDragEvent(targetActions, modifiers, x, y,
 439                     DISPATCH_MOUSE_MOVED);
 440         } catch (Exception e) {
 441             throw new InvalidDnDOperationException("Failed to handle DragMouseMoved event");
 442         }
 443     }
 444 
 445     //Returns the first lightweight or heavyweight Component which has a dropTarget ready to accept the drag
 446     //Should be called from the EventDispatchThread
 447     private static Component getDropTargetAt(Component root, int x, int y) {
 448         if (!root.contains(x, y) || !root.isEnabled() || !root.isVisible()) {
 449             return null;
 450         }
 451 
 452         if (root.getDropTarget() != null && root.getDropTarget().isActive()) {
 453             return root;
 454         }
 455 
 456         if (root instanceof Container) {
 457             for (Component comp : ((Container) root).getComponents()) {
 458                 Point loc = comp.getLocation();
 459                 Component dropTarget = getDropTargetAt(comp, x - loc.x, y - loc.y);
 460                 if (dropTarget != null) {
 461                     return dropTarget;
 462                 }
 463             }
 464         }
 465 
 466         return null;
 467     }
 468 
 469     /**
 470      * upcall from native code - reset hovering component
 471      */
 472     private void resetHovering() {
 473         hoveringComponent = null;
 474     }
 475 
 476     @Override
 477     protected void setNativeCursor(long nativeCtxt, Cursor c, int cType) {
 478         CCursorManager.getInstance().setCursor(c);
 479     }
 480 
 481     // Native support:
 482     private native long createNativeDragSource(Component component, long nativePeer, Transferable transferable,
 483         InputEvent triggerEvent, int dragPosX, int dragPosY, int extModifiers, int clickCount, long timestamp,
 484         long nsDragImagePtr, int dragImageOffsetX, int dragImageOffsetY,
 485         int sourceActions, long[] formats, Map<Long, DataFlavor> formatMap);
 486 
 487     private native void doDragging(long nativeDragSource);
 488 
 489     private native void releaseNativeDragSource(long nativeDragSource);
 490 }