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