/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 7 End-User License Agreement and JUCE Privacy Policy. End User License Agreement: www.juce.com/juce-7-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { static Cursor createDraggingHandCursor(); ComponentPeer* getPeerFor (::Window); //============================================================================== class X11DragState { public: X11DragState() = default; //============================================================================== bool isDragging() const noexcept { return dragging; } //============================================================================== void handleExternalSelectionClear() { if (dragging) externalResetDragAndDrop(); } void handleExternalSelectionRequest (const XEvent& evt) { auto targetType = evt.xselectionrequest.target; XEvent s; s.xselection.type = SelectionNotify; s.xselection.requestor = evt.xselectionrequest.requestor; s.xselection.selection = evt.xselectionrequest.selection; s.xselection.target = targetType; s.xselection.property = None; s.xselection.time = evt.xselectionrequest.time; auto* display = getDisplay(); if (allowedTypes.contains (targetType)) { s.xselection.property = evt.xselectionrequest.property; X11Symbols::getInstance()->xChangeProperty (display, evt.xselectionrequest.requestor, evt.xselectionrequest.property, targetType, 8, PropModeReplace, reinterpret_cast (textOrFiles.toRawUTF8()), (int) textOrFiles.getNumBytesAsUTF8()); } X11Symbols::getInstance()->xSendEvent (display, evt.xselectionrequest.requestor, True, 0, &s); } void handleExternalDragAndDropStatus (const XClientMessageEvent& clientMsg) { if (expectingStatus) { expectingStatus = false; canDrop = false; silentRect = {}; const auto& atoms = getAtoms(); if ((clientMsg.data.l[1] & 1) != 0 && ((Atom) clientMsg.data.l[4] == atoms.XdndActionCopy || (Atom) clientMsg.data.l[4] == atoms.XdndActionPrivate)) { if ((clientMsg.data.l[1] & 2) == 0) // target requests silent rectangle silentRect.setBounds ((int) clientMsg.data.l[2] >> 16, (int) clientMsg.data.l[2] & 0xffff, (int) clientMsg.data.l[3] >> 16, (int) clientMsg.data.l[3] & 0xffff); canDrop = true; } } } void handleExternalDragButtonReleaseEvent() { if (dragging) X11Symbols::getInstance()->xUngrabPointer (getDisplay(), CurrentTime); if (canDrop) { sendExternalDragAndDropDrop(); } else { sendExternalDragAndDropLeave(); externalResetDragAndDrop(); } } void handleExternalDragMotionNotify() { auto* display = getDisplay(); auto newTargetWindow = externalFindDragTargetWindow (X11Symbols::getInstance() ->xRootWindow (display, X11Symbols::getInstance()->xDefaultScreen (display))); if (targetWindow != newTargetWindow) { if (targetWindow != None) sendExternalDragAndDropLeave(); canDrop = false; silentRect = {}; if (newTargetWindow == None) return; xdndVersion = getDnDVersionForWindow (newTargetWindow); if (xdndVersion == -1) return; targetWindow = newTargetWindow; sendExternalDragAndDropEnter(); } if (! expectingStatus) sendExternalDragAndDropPosition(); } void handleDragAndDropPosition (const XClientMessageEvent& clientMsg, ComponentPeer* peer) { if (dragAndDropSourceWindow == 0) return; dragAndDropSourceWindow = (::Window) clientMsg.data.l[0]; if (windowH == 0) windowH = (::Window) peer->getNativeHandle(); const auto displays = Desktop::getInstance().getDisplays(); const auto logicalPos = displays.physicalToLogical (Point ((int) clientMsg.data.l[2] >> 16, (int) clientMsg.data.l[2] & 0xffff)); const auto dropPos = detail::ScalingHelpers::screenPosToLocalPos (peer->getComponent(), logicalPos.toFloat()).roundToInt(); const auto& atoms = getAtoms(); auto targetAction = atoms.XdndActionCopy; for (int i = numElementsInArray (atoms.allowedActions); --i >= 0;) { if ((Atom) clientMsg.data.l[4] == atoms.allowedActions[i]) { targetAction = atoms.allowedActions[i]; break; } } sendDragAndDropStatus (true, targetAction); if (dragInfo.position != dropPos) { dragInfo.position = dropPos; if (dragInfo.isEmpty()) updateDraggedFileList (clientMsg, (::Window) peer->getNativeHandle()); if (! dragInfo.isEmpty()) peer->handleDragMove (dragInfo); } } void handleDragAndDropDrop (const XClientMessageEvent& clientMsg, ComponentPeer* peer) { if (dragInfo.isEmpty()) { // no data, transaction finished in handleDragAndDropSelection() finishAfterDropDataReceived = true; updateDraggedFileList (clientMsg, (::Window) peer->getNativeHandle()); } else { handleDragAndDropDataReceived(); // data was already received } } void handleDragAndDropEnter (const XClientMessageEvent& clientMsg, ComponentPeer* peer) { dragInfo.clear(); srcMimeTypeAtomList.clear(); dragAndDropCurrentMimeType = 0; auto dndCurrentVersion = (static_cast (clientMsg.data.l[1]) & 0xff000000) >> 24; if (dndCurrentVersion < 3 || dndCurrentVersion > XWindowSystemUtilities::Atoms::DndVersion) { dragAndDropSourceWindow = 0; return; } const auto& atoms = getAtoms(); dragAndDropSourceWindow = (::Window) clientMsg.data.l[0]; if ((clientMsg.data.l[1] & 1) != 0) { XWindowSystemUtilities::ScopedXLock xLock; XWindowSystemUtilities::GetXProperty prop (getDisplay(), dragAndDropSourceWindow, atoms.XdndTypeList, 0, 0x8000000L, false, XA_ATOM); if (prop.success && prop.actualType == XA_ATOM && prop.actualFormat == 32 && prop.numItems != 0) { auto* types = prop.data; for (unsigned long i = 0; i < prop.numItems; ++i) { unsigned long type; memcpy (&type, types, sizeof (unsigned long)); if (type != None) srcMimeTypeAtomList.add (type); types += sizeof (unsigned long); } } } if (srcMimeTypeAtomList.isEmpty()) { for (int i = 2; i < 5; ++i) if (clientMsg.data.l[i] != None) srcMimeTypeAtomList.add ((unsigned long) clientMsg.data.l[i]); if (srcMimeTypeAtomList.isEmpty()) { dragAndDropSourceWindow = 0; return; } } for (int i = 0; i < srcMimeTypeAtomList.size() && dragAndDropCurrentMimeType == 0; ++i) for (int j = 0; j < numElementsInArray (atoms.allowedMimeTypes); ++j) if (srcMimeTypeAtomList[i] == atoms.allowedMimeTypes[j]) dragAndDropCurrentMimeType = atoms.allowedMimeTypes[j]; handleDragAndDropPosition (clientMsg, peer); } void handleDragAndDropExit() { if (auto* peer = getPeerFor (windowH)) peer->handleDragExit (dragInfo); resetDragAndDrop(); } void handleDragAndDropSelection (const XEvent& evt) { dragInfo.clear(); if (evt.xselection.property != None) { StringArray lines; { MemoryBlock dropData; for (;;) { XWindowSystemUtilities::GetXProperty prop (getDisplay(), evt.xany.window, evt.xselection.property, (long) (dropData.getSize() / 4), 65536, false, AnyPropertyType); if (! prop.success) break; dropData.append (prop.data, (size_t) (prop.actualFormat / 8) * prop.numItems); if (prop.bytesLeft <= 0) break; } lines.addLines (dropData.toString()); } if (XWindowSystemUtilities::Atoms::isMimeTypeFile (getDisplay(), dragAndDropCurrentMimeType)) { for (const auto& line : lines) { const auto escaped = line.replace ("+", "%2B").replace ("file://", String(), true); dragInfo.files.add (URL::removeEscapeChars (escaped)); } dragInfo.files.trim(); dragInfo.files.removeEmptyStrings(); } else { dragInfo.text = lines.joinIntoString ("\n"); } if (finishAfterDropDataReceived) handleDragAndDropDataReceived(); } } void externalResetDragAndDrop() { if (dragging) { XWindowSystemUtilities::ScopedXLock xLock; X11Symbols::getInstance()->xUngrabPointer (getDisplay(), CurrentTime); } NullCheckedInvocation::invoke (completionCallback); dragging = false; } bool externalDragInit (::Window window, bool text, const String& str, std::function&& cb) { windowH = window; isText = text; textOrFiles = str; targetWindow = windowH; completionCallback = std::move (cb); auto* display = getDisplay(); allowedTypes.add (XWindowSystemUtilities::Atoms::getCreating (display, isText ? "text/plain" : "text/uri-list")); auto pointerGrabMask = (unsigned int) (Button1MotionMask | ButtonReleaseMask); XWindowSystemUtilities::ScopedXLock xLock; if (X11Symbols::getInstance()->xGrabPointer (display, windowH, True, pointerGrabMask, GrabModeAsync, GrabModeAsync, None, None, CurrentTime) == GrabSuccess) { const auto& atoms = getAtoms(); // No other method of changing the pointer seems to work, this call is needed from this very context X11Symbols::getInstance()->xChangeActivePointerGrab (display, pointerGrabMask, (Cursor) createDraggingHandCursor(), CurrentTime); X11Symbols::getInstance()->xSetSelectionOwner (display, atoms.XdndSelection, windowH, CurrentTime); // save the available types to XdndTypeList X11Symbols::getInstance()->xChangeProperty (display, windowH, atoms.XdndTypeList, XA_ATOM, 32, PropModeReplace, reinterpret_cast (allowedTypes.getRawDataPointer()), allowedTypes.size()); dragging = true; xdndVersion = getDnDVersionForWindow (targetWindow); sendExternalDragAndDropEnter(); handleExternalDragMotionNotify(); return true; } return false; } private: //============================================================================== const XWindowSystemUtilities::Atoms& getAtoms() const noexcept { return XWindowSystem::getInstance()->getAtoms(); } ::Display* getDisplay() const noexcept { return XWindowSystem::getInstance()->getDisplay(); } //============================================================================== void sendDragAndDropMessage (XClientMessageEvent& msg) { auto* display = getDisplay(); msg.type = ClientMessage; msg.display = display; msg.window = dragAndDropSourceWindow; msg.format = 32; msg.data.l[0] = (long) windowH; XWindowSystemUtilities::ScopedXLock xLock; X11Symbols::getInstance()->xSendEvent (display, dragAndDropSourceWindow, False, 0, (XEvent*) &msg); } bool sendExternalDragAndDropMessage (XClientMessageEvent& msg) { auto* display = getDisplay(); msg.type = ClientMessage; msg.display = display; msg.window = targetWindow; msg.format = 32; msg.data.l[0] = (long) windowH; XWindowSystemUtilities::ScopedXLock xLock; return X11Symbols::getInstance()->xSendEvent (display, targetWindow, False, 0, (XEvent*) &msg) != 0; } void sendExternalDragAndDropDrop() { XClientMessageEvent msg; zerostruct (msg); msg.message_type = getAtoms().XdndDrop; msg.data.l[2] = CurrentTime; sendExternalDragAndDropMessage (msg); } void sendExternalDragAndDropEnter() { XClientMessageEvent msg; zerostruct (msg); msg.message_type = getAtoms().XdndEnter; msg.data.l[1] = (xdndVersion << 24); for (int i = 0; i < 3; ++i) msg.data.l[i + 2] = (long) allowedTypes[i]; sendExternalDragAndDropMessage (msg); } void sendExternalDragAndDropPosition() { XClientMessageEvent msg; zerostruct (msg); const auto& atoms = getAtoms(); msg.message_type = atoms.XdndPosition; auto mousePos = Desktop::getInstance().getMousePosition(); if (silentRect.contains (mousePos)) // we've been asked to keep silent return; mousePos = Desktop::getInstance().getDisplays().logicalToPhysical (mousePos); msg.data.l[1] = 0; msg.data.l[2] = (mousePos.x << 16) | mousePos.y; msg.data.l[3] = CurrentTime; msg.data.l[4] = (long) atoms.XdndActionCopy; // this is all JUCE currently supports expectingStatus = sendExternalDragAndDropMessage (msg); } void sendDragAndDropStatus (bool acceptDrop, Atom dropAction) { XClientMessageEvent msg; zerostruct (msg); msg.message_type = getAtoms().XdndStatus; msg.data.l[1] = (acceptDrop ? 1 : 0) | 2; // 2 indicates that we want to receive position messages msg.data.l[4] = (long) dropAction; sendDragAndDropMessage (msg); } void sendExternalDragAndDropLeave() { XClientMessageEvent msg; zerostruct (msg); msg.message_type = getAtoms().XdndLeave; sendExternalDragAndDropMessage (msg); } void sendDragAndDropFinish() { XClientMessageEvent msg; zerostruct (msg); msg.message_type = getAtoms().XdndFinished; sendDragAndDropMessage (msg); } void updateDraggedFileList (const XClientMessageEvent& clientMsg, ::Window requestor) { jassert (dragInfo.isEmpty()); if (dragAndDropSourceWindow != None && dragAndDropCurrentMimeType != None) { auto* display = getDisplay(); XWindowSystemUtilities::ScopedXLock xLock; X11Symbols::getInstance()->xConvertSelection (display, getAtoms().XdndSelection, dragAndDropCurrentMimeType, XWindowSystemUtilities::Atoms::getCreating (display, "JXSelectionWindowProperty"), requestor, (::Time) clientMsg.data.l[2]); } } bool isWindowDnDAware (::Window w) const { int numProperties = 0; auto* properties = X11Symbols::getInstance()->xListProperties (getDisplay(), w, &numProperties); bool dndAwarePropFound = false; for (int i = 0; i < numProperties; ++i) if (properties[i] == getAtoms().XdndAware) dndAwarePropFound = true; if (properties != nullptr) X11Symbols::getInstance()->xFree (properties); return dndAwarePropFound; } int getDnDVersionForWindow (::Window target) { XWindowSystemUtilities::GetXProperty prop (getDisplay(), target, getAtoms().XdndAware, 0, 2, false, AnyPropertyType); if (prop.success && prop.data != nullptr && prop.actualFormat == 32 && prop.numItems == 1) return jmin ((int) prop.data[0], (int) XWindowSystemUtilities::Atoms::DndVersion); return -1; } ::Window externalFindDragTargetWindow (::Window target) { if (target == None) return None; if (isWindowDnDAware (target)) return target; ::Window child, phonyWin; int phony; unsigned int uphony; X11Symbols::getInstance()->xQueryPointer (getDisplay(), target, &phonyWin, &child, &phony, &phony, &phony, &phony, &uphony); return externalFindDragTargetWindow (child); } void handleDragAndDropDataReceived() { ComponentPeer::DragInfo dragInfoCopy (dragInfo); sendDragAndDropFinish(); resetDragAndDrop(); if (! dragInfoCopy.isEmpty()) if (auto* peer = getPeerFor (windowH)) peer->handleDragDrop (dragInfoCopy); } void resetDragAndDrop() { dragInfo.clear(); dragInfo.position = Point (-1, -1); dragAndDropCurrentMimeType = 0; dragAndDropSourceWindow = 0; srcMimeTypeAtomList.clear(); finishAfterDropDataReceived = false; } //============================================================================== ::Window windowH = 0, targetWindow = 0, dragAndDropSourceWindow = 0; int xdndVersion = -1; bool isText = false, dragging = false, expectingStatus = false, canDrop = false, finishAfterDropDataReceived = false; Atom dragAndDropCurrentMimeType; Array allowedTypes, srcMimeTypeAtomList; ComponentPeer::DragInfo dragInfo; Rectangle silentRect; String textOrFiles; std::function completionCallback = nullptr; //============================================================================== JUCE_LEAK_DETECTOR (X11DragState) }; } // namespace juce