diff --git a/include/SFML/Window/Event.hpp b/include/SFML/Window/Event.hpp index a36733636..db2e717eb 100644 --- a/include/SFML/Window/Event.hpp +++ b/include/SFML/Window/Event.hpp @@ -35,6 +35,7 @@ #include #include +#include namespace sf @@ -294,6 +295,16 @@ public: Vector3f value; //!< Current value of the sensor on the X, Y, and Z axes }; + //////////////////////////////////////////////////////////// + /// \brief Files dropped event subtype + /// + //////////////////////////////////////////////////////////// + struct FilesDropped + { + std::vector filenames; //!< The files which were dropped + Vector2i position; //!< The position of the cursor at the time the files were dropped + }; + //////////////////////////////////////////////////////////// /// \brief Construct from a given `sf::Event` subtype /// @@ -364,7 +375,8 @@ private: TouchBegan, TouchMoved, TouchEnded, - SensorChanged> + SensorChanged, + FilesDropped> m_data; //!< Event data //////////////////////////////////////////////////////////// diff --git a/include/SFML/Window/WindowBase.hpp b/include/SFML/Window/WindowBase.hpp index 9c6b5521c..6b5eef4c3 100644 --- a/include/SFML/Window/WindowBase.hpp +++ b/include/SFML/Window/WindowBase.hpp @@ -546,6 +546,18 @@ public: VkSurfaceKHR& surface, const VkAllocationCallbacks* allocator = nullptr); + //////////////////////////////////////////////////////////// + /// \brief Enable or disable file dropping. + /// + /// If this is disabled, then when a user drags a file on to the window + /// the file will be automatically denied. When this is enabled, the file + /// will be accepted, no matter the type + /// + /// \param enabled True to enable, false to disable + /// + //////////////////////////////////////////////////////////// + void setFileDroppingEnabled(bool enabled = true); + protected: //////////////////////////////////////////////////////////// /// \brief Function called after the window has been created diff --git a/src/SFML/Window/Unix/WindowImplX11.cpp b/src/SFML/Window/Unix/WindowImplX11.cpp index 05c587c21..16dc28496 100644 --- a/src/SFML/Window/Unix/WindowImplX11.cpp +++ b/src/SFML/Window/Unix/WindowImplX11.cpp @@ -1608,6 +1608,9 @@ void WindowImplX11::initialize() // Flush the commands queue XFlush(m_display.get()); + // Make sure that file dropping is disabled + setFileDroppingEnabled(false); + // Add this window to the global list of windows (required for focus request) const std::lock_guard lock(allWindowsMutex); allWindows.push_back(this); @@ -1799,6 +1802,146 @@ bool WindowImplX11::processEvent(XEvent& windowEvent) } } } + + // Specifications for Xdnd: https://wiki.freedesktop.org/www/Specifications/XDND/ + + // Drag and drop position update + if (windowEvent.xclient.message_type == getAtom("XdndPosition")) + { + const Atom xdndStatus = XInternAtom(m_display.get(), "XdndStatus", false); + + XEvent message; + message.xclient.type = ClientMessage; + message.xclient.display = windowEvent.xclient.display; + message.xclient.window = m_dropSource; + message.xclient.message_type = xdndStatus; + message.xclient.format = 32; + + message.xclient.data.l[0] = static_cast(m_window); // The current window + + // Specify if we want the drop or not, and if we want XdndPosition events whenever the mouse moves out of the rectangle + message.xclient.data.l[1] = (m_acceptedFileType != None); + + // Send back window rectangle coordinates and width + message.xclient.data.l[2] = 0; + message.xclient.data.l[3] = 0; + + // Specify action we accept + message.xclient.data.l[4] = static_cast(getAtom("XdndActionCopy")); + + XSendEvent(m_display.get(), m_dropSource, false, 0, &message); + } + + if (windowEvent.xclient.message_type == getAtom("XdndEnter")) + { + // Store the source window + m_dropSource = static_cast<::Window>(windowEvent.xclient.data.l[0]); + + m_acceptedFileType = None; + + if (windowEvent.xclient.data.l[1] & 0x1) + { + // There are more than 3 types supported by the source, so we must get the XdndTypeList + Atom actualType = None; + int actualFormat = 0; + unsigned long numOfItems = 0; + unsigned long bytesAfterReturn = 0; + unsigned char* data = nullptr; + // Get the list of types that the source supports + if (XGetWindowProperty(m_display.get(), + m_dropSource, + getAtom("XdndTypeList"), + 0, + 1024, + false, + AnyPropertyType, + &actualType, + &actualFormat, + &numOfItems, + &bytesAfterReturn, + &data) == Success) + { + if (actualType != None) + { + Atom* supportedAtoms = reinterpret_cast(data); + + // Go through all of them and check if we support any of them + for (int i = 0; i < static_cast(numOfItems); i++) + { + if (canAcceptFileType(supportedAtoms[i])) + { + m_acceptedFileType = supportedAtoms[i]; + break; + } + } + } + } + } + else + { + // Go through the 3 types that the source supports and check if we support any of them + for (int i = 2; i < 5; i++) + { + if (canAcceptFileType(static_cast(windowEvent.xclient.data.l[i]))) + { + m_acceptedFileType = static_cast(windowEvent.xclient.data.l[i]); + break; + } + } + } + } + + // An item has been dropped + if (windowEvent.xclient.message_type == getAtom("XdndDrop")) + { + // Make sure that an acceptable file type was found + if (m_acceptedFileType != None) + { + // Get the timestamp + const auto dropTimestamp = static_cast<::Time>(windowEvent.xclient.data.l[2]); + + // Get the selection using the given timestamp + XConvertSelection(m_display.get(), + getAtom("XdndSelection"), + m_acceptedFileType, + getAtom("XDND_DATA"), + m_window, + dropTimestamp); + } + + XEvent message; + + message.xclient.type = ClientMessage; + message.xclient.display = m_display.get(); + message.xclient.window = m_dropSource; + message.xclient.message_type = getAtom("XdndFinished"); + message.xclient.format = 32; + message.xclient.data.l[0] = static_cast(m_window); + if (m_acceptedFileType != None) + { + // Tell the application we copied the data + message.xclient.data.l[1] = 1; + message.xclient.data.l[2] = static_cast(getAtom("XdndActionCopy")); + } + else + { + // Tell the application we did nothing + message.xclient.data.l[1] = 0; + message.xclient.data.l[2] = None; + } + + XSendEvent(m_display.get(), m_dropSource, false, NoEventMask, &message); + + m_acceptedFileType = None; + m_dropSource = 0; + } + + // The cursor left the window, so make sure we clean up + if (windowEvent.xclient.message_type == getAtom("XdndLeave")) + { + m_acceptedFileType = None; + m_dropSource = 0; + } break; } @@ -2073,6 +2216,92 @@ bool WindowImplX11::processEvent(XEvent& windowEvent) break; } + + // XConvertSelection response + case SelectionNotify: + { + if (windowEvent.xclient.message_type == getAtom("XdndSelection")) + { + // Notification that the current selection owner + // has responded to our request + + Atom type = 0; + int format = 0; + unsigned long items = 0; + unsigned long remainingBytes = 0; + unsigned char* data = nullptr; + + // The selection owner should have written the selection + // data to the specified window property + const int result = XGetWindowProperty(m_display.get(), + m_window, + windowEvent.xselection.property, + 0, + 0x7fffffff, + False, + AnyPropertyType, + &type, + &format, + &items, + &remainingBytes, + &data); + + String filenames; + + if (result == Success) + { + // We don't support INCR for now + // It is very unlikely that this will be returned + // for purely text data transfer anyway + if (type != getAtom("INCR", false)) + { + filenames = reinterpret_cast(data); + } + + XFree(data); + + // The selection requestor must always delete the property themselves + XDeleteProperty(m_display.get(), m_window, windowEvent.xselection.property); + } + + // Split sf::String into std::vector by the new lines + + std::vector filenamesVector; + size_t lastPosition = 0; + + while (filenames.find("\n", lastPosition) != std::string::npos) + { + filenamesVector.push_back( + filenames.substring(lastPosition, filenames.find("\n", lastPosition) - lastPosition + 1)); + + lastPosition = filenames.find("\n", lastPosition) + 1; + } + + if (lastPosition < filenames.getSize()) + { + filenamesVector.push_back(filenames.substring(lastPosition, filenames.getSize() - lastPosition)); + } + + for (String& filename : filenamesVector) + { + // To signify that it is giving a file, a program may put file:// at the start, so remove it + if (filename.find("file://") == 0) + { + filename = filename.substring(7, filename.getSize() - 7); + } + + // The last character can be a newline for file lists, so remove it if it is there + while (filename[filename.getSize() - 1] == '\n' || filename[filename.getSize() - 1] == '\r') + { + filename = filename.substring(0, filename.getSize() - 1); + } + } + + pushEvent(Event::FilesDropped{filenamesVector, Mouse::getPosition()}); + } + + break; + } } return true; @@ -2171,4 +2400,58 @@ void WindowImplX11::setWindowSizeConstraints() const XSetWMNormalHints(m_display.get(), m_window, &sizeHints); } +//////////////////////////////////////////////////////////// +void WindowImplX11::setFileDroppingEnabled(bool enabled) +{ + // Xdnd does not work on Wayland, so we check if Wayland is currently active before we enable Xdnd + // Checking if this exists isn't a perfect solution, as a user could set this + // in their environment variables, but it's better than crashing + + const char* value = getenv("WAYLAND_DISPLAY"); + + // If this variable exists, then that (usually) means that wayland is being used instead of X11, so don't turn on file dropping + if (value != nullptr) + { + // If we are enabling it give it an error, but don't give an error if we are disabling it + if (enabled) + { + sf::err() << "Drag and drop is not supported on Xwayland!" << std::endl; + } + + return; + } + + // In order for item dropping to be enabled, the XdndAware property must be set. + if (enabled) + { + Atom xdndVersion = 5; + XChangeProperty(m_display.get(), + m_window, + getAtom("XdndAware"), + XA_ATOM, + 32, + PropModeReplace, + reinterpret_cast(&xdndVersion), + true); + } + else + { + XDeleteProperty(m_display.get(), m_window, getAtom("XdndAware")); + } +} + +bool sf::priv::WindowImplX11::canAcceptFileType(const Atom& fileType) +{ + // We currently only accept uri-lists, but this can be changed if you want to add more types to be supported + + // Array of acceptable file types, this is static so we don't get the Atoms every time + static const std::array acceptableFileTypes({ + getAtom("text/uri-list"), + }); + + return std::any_of(acceptableFileTypes.begin(), + acceptableFileTypes.end(), + [fileType](const Atom& atom) { return atom == fileType; }); +} + } // namespace sf::priv diff --git a/src/SFML/Window/Unix/WindowImplX11.hpp b/src/SFML/Window/Unix/WindowImplX11.hpp index 0b79f292d..e14dc9f4c 100644 --- a/src/SFML/Window/Unix/WindowImplX11.hpp +++ b/src/SFML/Window/Unix/WindowImplX11.hpp @@ -197,6 +197,14 @@ public: //////////////////////////////////////////////////////////// void requestFocus() override; + //////////////////////////////////////////////////////////// + /// \brief Enable or disable file dropping. + /// + /// \param enabled True to enable, false to disable + /// + //////////////////////////////////////////////////////////// + void setFileDroppingEnabled(bool enabled = true) override; + //////////////////////////////////////////////////////////// /// \brief Check whether the window has the input focus /// @@ -212,6 +220,16 @@ protected: //////////////////////////////////////////////////////////// void processEvents() override; + //////////////////////////////////////////////////////////// + /// \brief Check if the given file type can be accepted + /// + /// \param fileType The file type to check + /// + /// \return If the file type is acceptable + /// + //////////////////////////////////////////////////////////// + bool canAcceptFileType(const Atom& fileType); + private: //////////////////////////////////////////////////////////// /// \brief Request the WM to make the current window active @@ -336,6 +354,8 @@ private: Pixmap m_iconPixmap{}; ///< The current icon pixmap if in use Pixmap m_iconMaskPixmap{}; ///< The current icon mask pixmap if in use ::Time m_lastInputTime{}; ///< Last time we received user input + ::Window m_dropSource{0}; ///< The window which is giving the dropped item + Atom m_acceptedFileType{0}; ///< The MIME type that the other window supports that we also support for file dropping }; } // namespace sf::priv diff --git a/src/SFML/Window/Win32/WindowImplWin32.cpp b/src/SFML/Window/Win32/WindowImplWin32.cpp index f89945949..4b67e6570 100644 --- a/src/SFML/Window/Win32/WindowImplWin32.cpp +++ b/src/SFML/Window/Win32/WindowImplWin32.cpp @@ -38,6 +38,7 @@ // or mingw-w64 addresses files in a case insensitive manner. #include #include +#include #include #include @@ -304,6 +305,11 @@ WindowHandle WindowImplWin32::getNativeHandle() const } +void WindowImplWin32::setFileDroppingEnabled(bool enabled) +{ + DragAcceptFiles(m_handle, enabled); +} + //////////////////////////////////////////////////////////// void WindowImplWin32::processEvents() { @@ -1165,6 +1171,38 @@ void WindowImplWin32::processEvent(UINT message, WPARAM wParam, LPARAM lParam) break; } + + // Files dropped event + case WM_DROPFILES: + { + auto* hDrop = reinterpret_cast(wParam); + + const unsigned int count = DragQueryFileW(hDrop, 0xFFFFFFFF, nullptr, 0); + + if (count == 0) + break; + + std::vector files; + + // Get the filenames as wchar_t then add it to the files vector + for (unsigned int i = 0; i < count; i++) + { + std::vector buffer(DragQueryFileW(hDrop, i, nullptr, 0) + 1); + + DragQueryFileW(hDrop, i, buffer.data(), static_cast(buffer.size())); + + files.emplace_back(buffer.data()); + } + + // Let the Windows API know we are done + DragFinish(hDrop); + + const Vector2i mousePosition = Mouse::getPosition(); + + pushEvent(Event::FilesDropped{files, mousePosition}); + + break; + } } } diff --git a/src/SFML/Window/Win32/WindowImplWin32.hpp b/src/SFML/Window/Win32/WindowImplWin32.hpp index d03896ac6..d6531abd1 100644 --- a/src/SFML/Window/Win32/WindowImplWin32.hpp +++ b/src/SFML/Window/Win32/WindowImplWin32.hpp @@ -189,6 +189,14 @@ public: //////////////////////////////////////////////////////////// [[nodiscard]] bool hasFocus() const override; + //////////////////////////////////////////////////////////// + /// \brief Enable or disable file dropping. + /// + /// \param enabled True to enable, false to disable + /// + //////////////////////////////////////////////////////////// + void setFileDroppingEnabled(bool enabled = true) override; + protected: //////////////////////////////////////////////////////////// /// \brief Process incoming events from the operating system diff --git a/src/SFML/Window/WindowBase.cpp b/src/SFML/Window/WindowBase.cpp index 1c47dbc30..21f41d61a 100644 --- a/src/SFML/Window/WindowBase.cpp +++ b/src/SFML/Window/WindowBase.cpp @@ -343,6 +343,12 @@ bool WindowBase::createVulkanSurface(const VkInstance& instance, VkSurfaceKHR& s } +void WindowBase::setFileDroppingEnabled(bool enabled) +{ + if (m_impl) + m_impl->setFileDroppingEnabled(enabled); +} + //////////////////////////////////////////////////////////// void WindowBase::onCreate() { diff --git a/src/SFML/Window/WindowImpl.cpp b/src/SFML/Window/WindowImpl.cpp index 67317a4c6..ebc8e2dc6 100644 --- a/src/SFML/Window/WindowImpl.cpp +++ b/src/SFML/Window/WindowImpl.cpp @@ -398,4 +398,10 @@ bool WindowImpl::createVulkanSurface([[maybe_unused]] const VkInstance& #endif } +//////////////////////////////////////////////////////////// +void WindowImpl::setFileDroppingEnabled(bool /* enabled */) +{ + // Backup for platforms that don't support drag & drop +} + } // namespace sf::priv diff --git a/src/SFML/Window/WindowImpl.hpp b/src/SFML/Window/WindowImpl.hpp index a1f5b01ef..5d462d891 100644 --- a/src/SFML/Window/WindowImpl.hpp +++ b/src/SFML/Window/WindowImpl.hpp @@ -305,6 +305,18 @@ public: //////////////////////////////////////////////////////////// bool createVulkanSurface(const VkInstance& instance, VkSurfaceKHR& surface, const VkAllocationCallbacks* allocator) const; + //////////////////////////////////////////////////////////// + /// \brief Enable or disable file dropping. + /// + /// If this is disabled, then when a user drags a file on to the window + /// the file will be automatically denied. When this is enabled, the file + /// will be accepted, no matter the type + /// + /// \param enabled True to enable, false to disable + /// + //////////////////////////////////////////////////////////// + virtual void setFileDroppingEnabled(bool enabled = true); + protected: //////////////////////////////////////////////////////////// /// \brief Default constructor diff --git a/src/SFML/Window/macOS/SFOpenGLView.h b/src/SFML/Window/macOS/SFOpenGLView.h index 1aed42b73..e17d7dbc5 100644 --- a/src/SFML/Window/macOS/SFOpenGLView.h +++ b/src/SFML/Window/macOS/SFOpenGLView.h @@ -140,6 +140,12 @@ class WindowImplCocoa; //////////////////////////////////////////////////////////// - (CGFloat)displayScaleFactor; +// Event called by MacOS when a file is dragged on top of of the window +- (NSDragOperation)draggingEntered:(id)sender; + +// Function called by MacOS when a file is dropped on top of the window +- (BOOL)performDragOperation:(id)sender; + @end @interface SFOpenGLView (keyboard) diff --git a/src/SFML/Window/macOS/SFOpenGLView.mm b/src/SFML/Window/macOS/SFOpenGLView.mm index deb2aaa2c..effc905ca 100644 --- a/src/SFML/Window/macOS/SFOpenGLView.mm +++ b/src/SFML/Window/macOS/SFOpenGLView.mm @@ -186,6 +186,9 @@ // Now that we have a window, set up correctly the scale factor and cursor grabbing [self updateScaleFactor]; [self updateCursorGrabbed]; // update for fullscreen + + // Register the drag types that this view can accept + //[self registerForDraggedTypes:[NSArray arrayWithObjects:NSFilenamesPboardType, nil]]; } @@ -341,6 +344,62 @@ m_mouseIsIn = NO; } +- (NSDragOperation)draggingEntered:(id)sender +{ + // Check if what is being dragged is a file + if ([sender.draggingPasteboard.types containsObject:NSFilenamesPboardType]) + { + // Make sure that we can get the filename (not just the contents) + if (sender.draggingSourceOperationMask & NSDragOperationLink) + { + // Return that we can accept a filename + return NSDragOperationLink; + } + } + + // Return that we can't accept this file + return NSDragOperationNone; +} + +- (BOOL)performDragOperation:(id)sender +{ + // Make sure that the dragged item is a file + if ([sender.draggingPasteboard.types containsObject:NSFilenamesPboardType]) + { + // Get the filenames from the sender + NSArray* files = [sender.draggingPasteboard propertyListForType:NSFilenamesPboardType]; + + // Convert to std::vector + + std::vector filenames; + + for (NSUInteger i = 0; i < [files count]; i++) + { + NSObject* item = files[i]; + + // Make sure that we can cast the NSObject* + if ([item isKindOfClass:[NSMutableString class]]) + { + auto* filename = reinterpret_cast(item); + + filenames.emplace_back([filename UTF8String]); + } + } + + NSPoint mousePosition = [self cursorPositionFromEvent:nil]; + + if (m_requester != nullptr) + m_requester->handleFileDroppingEvent(filenames, sf::Vector2i{int(mousePosition.x), int(mousePosition.y)}); + } + else + { + // If somehow we get a drag operation for an item we cant accept, return no + return NO; + } + + // Everything went well, return yes + return YES; +} #pragma mark #pragma mark Subclassing methods diff --git a/src/SFML/Window/macOS/SFViewController.mm b/src/SFML/Window/macOS/SFViewController.mm index b893453a4..e67235849 100644 --- a/src/SFML/Window/macOS/SFViewController.mm +++ b/src/SFML/Window/macOS/SFViewController.mm @@ -284,5 +284,17 @@ [context setView:m_oglView]; } +//////////////////////////////////////////////////////// +- (void)setFileDroppingEnabled:(bool)enabled +{ + if (enabled) + { + [m_oglView registerForDraggedTypes:[NSArray arrayWithObjects:NSFilenamesPboardType, nil]]; + } + else + { + [m_oglView unregisterDraggedTypes]; + } +} @end diff --git a/src/SFML/Window/macOS/SFWindowController.mm b/src/SFML/Window/macOS/SFWindowController.mm index 2f28d6f62..12724f6e4 100644 --- a/src/SFML/Window/macOS/SFWindowController.mm +++ b/src/SFML/Window/macOS/SFWindowController.mm @@ -628,4 +628,17 @@ return static_cast(NSHeight([m_window frame]) - NSHeight([[m_window contentView] frame])); } +//////////////////////////////////////////////////////// +- (void)setFileDroppingEnabled:(bool)enabled +{ + if (enabled) + { + [m_oglView registerForDraggedTypes:[NSArray arrayWithObjects:NSFilenamesPboardType, nil]]; + } + else + { + [m_oglView unregisterDraggedTypes]; + } +} + @end diff --git a/src/SFML/Window/macOS/WindowImplCocoa.hpp b/src/SFML/Window/macOS/WindowImplCocoa.hpp index 2ce67d484..f818cf807 100644 --- a/src/SFML/Window/macOS/WindowImplCocoa.hpp +++ b/src/SFML/Window/macOS/WindowImplCocoa.hpp @@ -228,6 +228,16 @@ public: //////////////////////////////////////////////////////////// void applyContext(NSOpenGLContextRef context) const; + //////////////////////////////////////////////////////////// + /// \brief Handle the file dropping event + /// + /// Called by SFOpenGLView to handle the file dropping event + /// + /// \param files The files that were dropped + /// + //////////////////////////////////////////////////////////// + void handleFileDroppingEvent(std::vector files, sf::Vector2i position); + //////////////////////////////////////////////////////////// /// \brief Change the type of the current process /// @@ -369,6 +379,18 @@ public: //////////////////////////////////////////////////////////// [[nodiscard]] bool hasFocus() const override; + //////////////////////////////////////////////////////////// + /// \brief Enable or disable file dropping. + /// + /// If this is disabled, then when a user drags a file on to the window + /// the file will be automatically denied. When this is enabled, the file + /// will be accepted, no matter the type + /// + /// \param enabled True to enable, false to disable + /// + //////////////////////////////////////////////////////////// + void setFileDroppingEnabled(bool enabled = true) override; + protected: //////////////////////////////////////////////////////////// /// \brief Process incoming events from the operating system diff --git a/src/SFML/Window/macOS/WindowImplCocoa.mm b/src/SFML/Window/macOS/WindowImplCocoa.mm index c09f1f60f..df2acbfe1 100644 --- a/src/SFML/Window/macOS/WindowImplCocoa.mm +++ b/src/SFML/Window/macOS/WindowImplCocoa.mm @@ -246,6 +246,11 @@ void WindowImplCocoa::windowFocusGained() pushEvent(Event::FocusGained{}); } +void WindowImplCocoa::handleFileDroppingEvent(std::vector files, sf::Vector2i position) +{ + pushEvent(Event::FilesDropped{std::move(files), position}); +} + #pragma mark #pragma mark WindowImplCocoa's mouse-event methods @@ -492,5 +497,10 @@ bool WindowImplCocoa::hasFocus() const return [m_delegate hasFocus]; } +//////////////////////////////////////////////////////////// +void WindowImplCocoa::setFileDroppingEnabled(bool enabled) +{ + [m_delegate setFileDroppingEnabled:enabled]; +} } // namespace sf::priv diff --git a/src/SFML/Window/macOS/WindowImplDelegateProtocol.h b/src/SFML/Window/macOS/WindowImplDelegateProtocol.h index a35efc0d7..00a68b4c8 100644 --- a/src/SFML/Window/macOS/WindowImplDelegateProtocol.h +++ b/src/SFML/Window/macOS/WindowImplDelegateProtocol.h @@ -251,6 +251,14 @@ class WindowImplCocoa; //////////////////////////////////////////////////////////// - (void)applyContext:(NSOpenGLContext*)context; +//////////////////////////////////////////////////////////// +/// \brief Enable or disable file dropping +/// +/// \param enable True to enable, false to disable +/// +//////////////////////////////////////////////////////////// +- (void)setFileDroppingEnabled:(bool)enabled; + @end #pragma GCC diagnostic pop