Add file dropping support

This commit is contained in:
justanobdy 2024-09-01 13:27:54 -07:00
parent 74dfd76b25
commit 89a23ea996
16 changed files with 528 additions and 1 deletions

View File

@ -35,6 +35,7 @@
#include <SFML/System/Vector2.hpp>
#include <variant>
#include <vector>
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<sf::String> 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
////////////////////////////////////////////////////////////

View File

@ -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

View File

@ -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<long>(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<long>(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<Atom*>(data);
// Go through all of them and check if we support any of them
for (int i = 0; i < static_cast<int>(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<Atom>(windowEvent.xclient.data.l[i])))
{
m_acceptedFileType = static_cast<Atom>(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<long>(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<long>(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<char*>(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<sf::String> by the new lines
std::vector<String> 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<unsigned char*>(&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<Atom, 1> acceptableFileTypes({
getAtom("text/uri-list"),
});
return std::any_of(acceptableFileTypes.begin(),
acceptableFileTypes.end(),
[fileType](const Atom& atom) { return atom == fileType; });
}
} // namespace sf::priv

View File

@ -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

View File

@ -38,6 +38,7 @@
// or mingw-w64 addresses files in a case insensitive manner.
#include <dbt.h>
#include <ostream>
#include <shellapi.h>
#include <vector>
#include <cstddef>
@ -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<HDROP>(wParam);
const unsigned int count = DragQueryFileW(hDrop, 0xFFFFFFFF, nullptr, 0);
if (count == 0)
break;
std::vector<String> files;
// Get the filenames as wchar_t then add it to the files vector
for (unsigned int i = 0; i < count; i++)
{
std::vector<wchar_t> buffer(DragQueryFileW(hDrop, i, nullptr, 0) + 1);
DragQueryFileW(hDrop, i, buffer.data(), static_cast<UINT>(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;
}
}
}

View File

@ -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

View File

@ -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()
{

View File

@ -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

View File

@ -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

View File

@ -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<NSDraggingInfo>)sender;
// Function called by MacOS when a file is dropped on top of the window
- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender;
@end
@interface SFOpenGLView (keyboard)

View File

@ -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<NSDraggingInfo>)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<NSDraggingInfo>)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::string>
std::vector<sf::String> 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<NSMutableString*>(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

View File

@ -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

View File

@ -628,4 +628,17 @@
return static_cast<float>(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

View File

@ -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<sf::String> 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

View File

@ -246,6 +246,11 @@ void WindowImplCocoa::windowFocusGained()
pushEvent(Event::FocusGained{});
}
void WindowImplCocoa::handleFileDroppingEvent(std::vector<sf::String> 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

View File

@ -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