1 /*
2  * Copyright (c) 2011, 2013, 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 
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 formatMap)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 
setDefaultDragImage(Component component)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 
setDefaultDragImage(JTextComponent component)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 
setDefaultDragImage(JTree component)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 
setDefaultDragImage(JTable component)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 
setDefaultDragImage(JList component)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 
setDefaultDragImage()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 
setOutlineDragImage(Rectangle outline)337     private void setOutlineDragImage(Rectangle outline) {
338         setOutlineDragImage(outline, false);
339     }
340 
setOutlineDragImage(Rectangle outline, Boolean shouldScale)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      */
dragMouseMoved(final int targetActions, final int modifiers, final int x, final int y)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
getDropTargetAt(Component root, int x, int y)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      */
resetHovering()475     private void resetHovering() {
476         hoveringComponent = null;
477     }
478 
479     @Override
setNativeCursor(long nativeCtxt, Cursor c, int cType)480     protected void setNativeCursor(long nativeCtxt, Cursor c, int cType) {
481         CCursorManager.getInstance().setCursor(c);
482     }
483 
484     // Native support:
createNativeDragSource(Component component, long nativePeer, Transferable transferable, InputEvent triggerEvent, int dragPosX, int dragPosY, int extModifiers, int clickCount, long timestamp, CImage nsDragImage, int dragImageOffsetX, int dragImageOffsetY, int sourceActions, long[] formats, Map formatMap)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 
doDragging(long nativeDragSource)490     private native void doDragging(long nativeDragSource);
491 
releaseNativeDragSource(long nativeDragSource)492     private native void releaseNativeDragSource(long nativeDragSource);
493 }
494