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