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 import sun.misc.ManagedLocalsThread;
  48 
  49 
  50 public final class CDragSourceContextPeer extends SunDragSourceContextPeer {
  51 
  52     private static final CDragSourceContextPeer fInstance = new CDragSourceContextPeer(null);
  53 
  54     private Image  fDragImage;
  55     private CImage fDragCImage;
  56     private Point  fDragImageOffset;
  57 
  58     private static Component hoveringComponent = null;
  59 
  60     private static double fMaxImageSize = 128.0;
  61 
  62     static {
  63         String propValue = java.security.AccessController.doPrivileged(new sun.security.action.GetPropertyAction("apple.awt.dnd.defaultDragImageSize"));
  64         if (propValue != null) {
  65             try {
  66                 double value = Double.parseDouble(propValue);
  67                 if (value > 0) {
  68                     fMaxImageSize = value;
  69                 }
  70             } catch(NumberFormatException e) {}
  71         }
  72     }
  73 
  74     private CDragSourceContextPeer(DragGestureEvent dge) {
  75         super(dge);
  76     }
  77 
  78     public static CDragSourceContextPeer createDragSourceContextPeer(DragGestureEvent dge) throws InvalidDnDOperationException {
  79         fInstance.setTrigger(dge);
  80 
  81         return fInstance;
  82     }
  83 
  84     // 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:
  85     public void startDrag(DragSourceContext dsc, Cursor cursor, Image dragImage, Point dragImageOffset) throws InvalidDnDOperationException {
  86         fDragImage = dragImage;
  87         fDragImageOffset = dragImageOffset;
  88 
  89         super.startDrag(dsc, cursor, dragImage, dragImageOffset);
  90     }
  91 
  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             LWComponentPeer<?, ?> peer = AWTAccessor.getComponentAccessor()
 139                                                     .getPeer(rootComponent);
 140             PlatformWindow platformWindow = peer.getPlatformWindow();
 141             long nativeViewPtr = CPlatformWindow.getNativeViewPtr(platformWindow);
 142             if (nativeViewPtr == 0L) throw new InvalidDnDOperationException("Unsupported platform window implementation");
 143 
 144             // Create native dragging source:
 145             final long nativeDragSource = createNativeDragSource(component, nativeViewPtr, transferable, triggerEvent,
 146                 (int) (dragOrigin.getX()), (int) (dragOrigin.getY()), extModifiers,
 147                 clickCount, timestamp, fDragCImage != null ? fDragCImage.ptr : 0L, dragImageOffset.x, dragImageOffset.y,
 148                 getDragSourceContext().getSourceActions(), formats, formatMap);
 149 
 150             if (nativeDragSource == 0)
 151                 throw new InvalidDnDOperationException("");
 152 
 153             setNativeContext(nativeDragSource);
 154         }
 155 
 156         catch (Exception e) {
 157             throw new InvalidDnDOperationException("failed to create native peer: " + e);
 158         }
 159 
 160         SunDropTargetContextPeer.setCurrentJVMLocalSourceTransferable(transferable);
 161 
 162         CCursorManager.getInstance().setCursor(getCursor());
 163 
 164         // Create a new thread to run the dragging operation since it's synchronous, only coming back
 165         // after dragging is finished. This leaves the AWT event thread free to handle AWT events which
 166         // are posted during dragging by native event handlers.
 167 
 168         try {
 169             Runnable dragRunnable = () -> {
 170                 final long nativeDragSource = getNativeContext();
 171                 try {
 172                     doDragging(nativeDragSource);
 173                 } catch (Exception e) {
 174                     e.printStackTrace();
 175                 } finally {
 176                     releaseNativeDragSource(nativeDragSource);
 177                     fDragImage = null;
 178                     if (fDragCImage != null) {
 179                         fDragCImage.dispose();
 180                         fDragCImage = null;
 181                     }
 182                 }
 183             };
 184             new ManagedLocalsThread(dragRunnable).start();
 185         } catch (Exception e) {
 186             final long nativeDragSource = getNativeContext();
 187             setNativeContext(0);
 188             releaseNativeDragSource(nativeDragSource);
 189             SunDropTargetContextPeer.setCurrentJVMLocalSourceTransferable(null);
 190             throw new InvalidDnDOperationException("failed to start dragging thread: " + e);
 191         }
 192     }
 193 
 194     private void setDefaultDragImage(Component component) {
 195         boolean handled = false;
 196 
 197         // Special-case default drag image, depending on the drag source type:
 198         if (component.isLightweight()) {
 199             if (component instanceof JTextComponent) {
 200                 this.setDefaultDragImage((JTextComponent) component);
 201                 handled = true;
 202             } else if (component instanceof JTree) {
 203                             this.setDefaultDragImage((JTree) component);
 204                             handled = true;
 205                         } else if (component instanceof JTable) {
 206                             this.setDefaultDragImage((JTable) component);
 207                             handled = true;
 208                         } else if (component instanceof JList) {
 209                             this.setDefaultDragImage((JList) component);
 210                             handled = true;
 211                         }
 212         }
 213 
 214         if (handled == false)
 215             this.setDefaultDragImage();
 216     }
 217 
 218     private void setDefaultDragImage(JTextComponent component) {
 219         DragGestureEvent trigger = getTrigger();
 220         int selectionStart = component.getSelectionStart();
 221         int selectionEnd = component.getSelectionEnd();
 222         boolean handled = false;
 223 
 224         // Make sure we're dragging current selection:
 225         int index = component.viewToModel(trigger.getDragOrigin());
 226         if ((selectionStart < selectionEnd) && (index >= selectionStart) && (index <= selectionEnd)) {
 227             try {
 228                 Rectangle selectionStartBounds = component.modelToView(selectionStart);
 229                 Rectangle selectionEndBounds = component.modelToView(selectionEnd);
 230 
 231                 Rectangle selectionBounds = null;
 232 
 233                 // Single-line selection:
 234                 if (selectionStartBounds.y == selectionEndBounds.y) {
 235                     selectionBounds = new Rectangle(selectionStartBounds.x, selectionStartBounds.y,
 236                         selectionEndBounds.x - selectionStartBounds.x + selectionEndBounds.width,
 237                         selectionEndBounds.y - selectionStartBounds.y + selectionEndBounds.height);
 238                 }
 239 
 240                 // Multi-line selection:
 241                 else {
 242                     AccessibleContext ctx = component.getAccessibleContext();
 243                     AccessibleText at = (AccessibleText) ctx;
 244 
 245                     selectionBounds = component.modelToView(selectionStart);
 246                     for (int i = selectionStart + 1; i <= selectionEnd; i++) {
 247                                             Rectangle charBounds = at.getCharacterBounds(i);
 248                                             // Invalid index returns null Rectangle
 249                                             // Note that this goes against jdk doc - should be empty, but is null instead
 250                                             if (charBounds != null) {
 251                                                 selectionBounds.add(charBounds);
 252                                             }
 253                     }
 254                 }
 255 
 256                 this.setOutlineDragImage(selectionBounds);
 257                 handled = true;
 258             }
 259 
 260             catch (BadLocationException exc) {
 261                 // Default the drag image to component bounds.
 262             }
 263         }
 264 
 265         if (handled == false)
 266             this.setDefaultDragImage();
 267     }
 268 
 269 
 270     private void setDefaultDragImage(JTree component) {
 271         Rectangle selectedOutline = null;
 272 
 273         int[] selectedRows = component.getSelectionRows();
 274         for (int i=0; i<selectedRows.length; i++) {
 275             Rectangle r = component.getRowBounds(selectedRows[i]);
 276             if (selectedOutline == null)
 277                 selectedOutline = r;
 278             else
 279                 selectedOutline.add(r);
 280         }
 281 
 282         if (selectedOutline != null) {
 283             this.setOutlineDragImage(selectedOutline);
 284         } else {
 285             this.setDefaultDragImage();
 286         }
 287     }
 288 
 289     private void setDefaultDragImage(JTable component) {
 290         Rectangle selectedOutline = null;
 291 
 292         // This code will likely break once multiple selections works (3645873)
 293         int[] selectedRows = component.getSelectedRows();
 294         int[] selectedColumns = component.getSelectedColumns();
 295         for (int row=0; row<selectedRows.length; row++) {
 296             for (int col=0; col<selectedColumns.length; col++) {
 297                 Rectangle r = component.getCellRect(selectedRows[row], selectedColumns[col], true);
 298                 if (selectedOutline == null)
 299                     selectedOutline = r;
 300                 else
 301                     selectedOutline.add(r);
 302             }
 303         }
 304 
 305         if (selectedOutline != null) {
 306             this.setOutlineDragImage(selectedOutline);
 307         } else {
 308             this.setDefaultDragImage();
 309         }
 310     }
 311 
 312     private void setDefaultDragImage(JList<?> component) {
 313         Rectangle selectedOutline = null;
 314 
 315         // This code actually works, even under the (non-existant) multiple-selections, because we only draw a union outline
 316         int[] selectedIndices = component.getSelectedIndices();
 317         if (selectedIndices.length > 0)
 318             selectedOutline = component.getCellBounds(selectedIndices[0], selectedIndices[selectedIndices.length-1]);
 319 
 320         if (selectedOutline != null) {
 321             this.setOutlineDragImage(selectedOutline);
 322         } else {
 323             this.setDefaultDragImage();
 324         }
 325     }
 326 
 327 
 328     private void setDefaultDragImage() {
 329         DragGestureEvent trigger = this.getTrigger();
 330         Component comp = trigger.getComponent();
 331 
 332         setOutlineDragImage(new Rectangle(0, 0, comp.getWidth(), comp.getHeight()), true);
 333     }
 334 
 335     private void setOutlineDragImage(Rectangle outline) {
 336         setOutlineDragImage(outline, false);
 337     }
 338 
 339     private void setOutlineDragImage(Rectangle outline, Boolean shouldScale) {
 340         int width = (int)outline.getWidth();
 341         int height = (int)outline.getHeight();
 342 
 343         double scale = 1.0;
 344         if (shouldScale) {
 345             final int area = width * height;
 346             final int maxArea = (int)(fMaxImageSize * fMaxImageSize);
 347 
 348             if (area > maxArea) {
 349                 scale = (double)area / (double)maxArea;
 350                 width /= scale;
 351                 height /= scale;
 352             }
 353         }
 354 
 355         if (width <=0) width = 1;
 356         if (height <=0) height = 1;
 357 
 358         DragGestureEvent trigger = this.getTrigger();
 359         Component comp = trigger.getComponent();
 360         Point compOffset = comp.getLocation();
 361 
 362         // For lightweight components add some special treatment:
 363         if (comp instanceof JComponent) {
 364             // Intersect requested bounds with visible bounds:
 365             Rectangle visibleBounds = ((JComponent) comp).getVisibleRect();
 366             Rectangle clipedOutline = outline.intersection(visibleBounds);
 367             if (clipedOutline.isEmpty() == false)
 368                 outline = clipedOutline;
 369 
 370             // Compensate for the component offset (e.g. when contained in a JScrollPane):
 371             outline.translate(compOffset.x, compOffset.y);
 372         }
 373 
 374         GraphicsConfiguration config = comp.getGraphicsConfiguration();
 375         BufferedImage dragImage = config.createCompatibleImage(width, height, Transparency.TRANSLUCENT);
 376 
 377         Color paint = Color.gray;
 378         BasicStroke stroke = new BasicStroke(2.0f);
 379         int halfLineWidth = (int) (stroke.getLineWidth() + 1) / 2; // Rounded up.
 380 
 381         Graphics2D g2 = (Graphics2D) dragImage.getGraphics();
 382         g2.setPaint(paint);
 383         g2.setStroke(stroke);
 384         g2.drawRect(halfLineWidth, halfLineWidth, width - 2 * halfLineWidth - 1, height - 2 * halfLineWidth - 1);
 385         g2.dispose();
 386 
 387         fDragImage = dragImage;
 388 
 389 
 390         Point dragOrigin = trigger.getDragOrigin();
 391         Point dragImageOffset = new Point(outline.x - dragOrigin.x, outline.y - dragOrigin.y);
 392         if (comp instanceof JComponent) {
 393             dragImageOffset.translate(-compOffset.x, -compOffset.y);
 394         }
 395 
 396         if (shouldScale) {
 397             dragImageOffset.x /= scale;
 398             dragImageOffset.y /= scale;
 399         }
 400 
 401         fDragImageOffset = dragImageOffset;
 402     }
 403 
 404     /**
 405      * upcall from native code
 406      */
 407     private void dragMouseMoved(final int targetActions,
 408                                 final int modifiers,
 409                                 final int x, final int y) {
 410 
 411         try {
 412             Component componentAt = LWCToolkit.invokeAndWait(
 413                     new Callable<Component>() {
 414                         @Override
 415                         public Component call() {
 416                             LWWindowPeer mouseEventComponent = LWWindowPeer.getWindowUnderCursor();
 417                             if (mouseEventComponent == null) {
 418                                 return null;
 419                             }
 420                             Component root = SwingUtilities.getRoot(mouseEventComponent.getTarget());
 421                             if (root == null) {
 422                                 return null;
 423                             }
 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 lightweight or heavyweight Component which has a dropTarget ready to accept the drag
 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.isEnabled() || !root.isVisible()) {
 450             return null;
 451         }
 452 
 453         if (root.getDropTarget() != null && root.getDropTarget().isActive()) {
 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         long nsDragImagePtr, int dragImageOffsetX, int dragImageOffsetY,
 486         int sourceActions, long[] formats, Map<Long, DataFlavor> formatMap);
 487 
 488     private native void doDragging(long nativeDragSource);
 489 
 490     private native void releaseNativeDragSource(long nativeDragSource);
 491 }