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