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
27package sun.lwawt.macosx;
28
29import java.awt.*;
30import java.awt.datatransfer.*;
31import java.awt.dnd.*;
32import java.awt.event.*;
33import java.awt.image.*;
34
35import javax.swing.*;
36import javax.swing.text.*;
37import javax.accessibility.*;
38
39import java.util.Map;
40import java.util.concurrent.Callable;
41
42import sun.awt.AWTAccessor;
43import sun.awt.dnd.*;
44import sun.lwawt.LWComponentPeer;
45import sun.lwawt.LWWindowPeer;
46import sun.lwawt.PlatformWindow;
47
48
49public 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<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
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")
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
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
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
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
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
336    private void setOutlineDragImage(Rectangle outline) {
337        setOutlineDragImage(outline, false);
338    }
339
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     */
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
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     */
474    private void resetHovering() {
475        hoveringComponent = null;
476    }
477
478    @Override
479    protected void setNativeCursor(long nativeCtxt, Cursor c, int cType) {
480        CCursorManager.getInstance().setCursor(c);
481    }
482
483    // Native support:
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
489    private native void doDragging(long nativeDragSource);
490
491    private native void releaseNativeDragSource(long nativeDragSource);
492}
493