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