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 
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 
CDragSourceContextPeer(DragGestureEvent dge)73     private CDragSourceContextPeer(DragGestureEvent dge) {
74         super(dge);
75     }
76 
createDragSourceContextPeer(DragGestureEvent dge)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:
startDrag(DragSourceContext dsc, Cursor cursor, Image dragImage, Point dragImageOffset)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 
startDrag(Transferable transferable, long[] formats, Map<Long, DataFlavor> formatMap)91     protected void startDrag(Transferable transferable, long[] formats, Map<Long, DataFlavor> formatMap) {
92         DragGestureEvent trigger = getTrigger();
93         InputEvent         triggerEvent = trigger.getTriggerEvent();
94 
95         Point dragOrigin = new Point(trigger.getDragOrigin());
96         @SuppressWarnings("deprecation")
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 Thread(null, dragRunnable, "Drag", 0, false).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 
setDefaultDragImage(Component component)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     @SuppressWarnings("deprecation")
setDefaultDragImage(JTextComponent component)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 
setDefaultDragImage(JTree component)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 
setDefaultDragImage(JTable component)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 
setDefaultDragImage(JList<?> component)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 
setDefaultDragImage()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 
setOutlineDragImage(Rectangle outline)336     private void setOutlineDragImage(Rectangle outline) {
337         setOutlineDragImage(outline, false);
338     }
339 
setOutlineDragImage(Rectangle outline, Boolean shouldScale)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      */
dragMouseMoved(final int targetActions, final int modifiers, final int x, final int y)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
getDropTargetAt(Component root, int x, int y)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      */
resetHovering()474     private void resetHovering() {
475         hoveringComponent = null;
476     }
477 
478     @Override
setNativeCursor(long nativeCtxt, Cursor c, int cType)479     protected void setNativeCursor(long nativeCtxt, Cursor c, int cType) {
480         CCursorManager.getInstance().setCursor(c);
481     }
482 
483     // Native support:
createNativeDragSource(Component component, long nativePeer, Transferable transferable, InputEvent triggerEvent, int dragPosX, int dragPosY, int extModifiers, int clickCount, long timestamp, long nsDragImagePtr, int dragImageOffsetX, int dragImageOffsetY, int sourceActions, long[] formats, Map<Long, DataFlavor> formatMap)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         long nsDragImagePtr, int dragImageOffsetX, int dragImageOffsetY,
487         int sourceActions, long[] formats, Map<Long, DataFlavor> formatMap);
488 
doDragging(long nativeDragSource)489     private native void doDragging(long nativeDragSource);
490 
releaseNativeDragSource(long nativeDragSource)491     private native void releaseNativeDragSource(long nativeDragSource);
492 }
493