PDA

View Full Version : Drag and drop per key press?



Maryu
6th September 2020, 19:52
I want to achieve a drag and drop from my application to the OS window system (Windows 10), but not by using the mouse. Instead, the content will be selected by a button press and should automatically be drag-dropped to the window under the mouse cursor.

I wrote a test class, for a regular drag-drop with the mouse, and for the same with a key press:



#include <QtCore/QList>
#include <QtCore/QUrl>
#include <QtCore/QMimeData>
#include <QtWidgets/QApplication>
#include <QtWidgets/QMainWindow>
#include <QtGui/QDrag>
#include <QtGui/QKeyEvent>
#include <QtGui/QMouseEvent>
#include <QtGui/QScreen>

class DragDropTester: public QMainWindow {
QUrl m_file;

void move_file() {
auto* mime_data = new QMimeData;
mime_data->setUrls(QList<QUrl> { } << m_file);

auto* screen = QApplication::primaryScreen();
auto mouse_pos = QCursor::pos();
QDropEvent drop_event { mouse_pos, Qt::MoveAction, mime_data, Qt::NoButton, Qt::NoModifier };
QApplication::sendEvent(screen, &drop_event);
}

protected:
void keyPressEvent(QKeyEvent* e) override {
switch (e->key()) {
case Qt::Key_M:
move_file();
break;
default:
QMainWindow::keyPressEvent(e);
}
}

void mousePressEvent(QMouseEvent*) override {
auto* mime = new QMimeData;
mime->setUrls(QList<QUrl> { } << m_file);

auto* drag = new QDrag { this };
drag->setMimeData(mime);

drag->exec();
}

public:
DragDropTester(const QString& path) : QMainWindow { } {
m_file = QUrl::fromLocalFile(path);
setFixedSize(400, 400);
show();
}

};


The drag & drop with mouse using QDrag works, but the key version does not.
Unfortunately it looks like QDrag is implemented to always listen for mouse release event to determine when to finalize the drop, instead of providing a ".drop()" function or similar ..

I looked around in the doc and I think I need to construct and handle my own QDropEvent, which I tried to do in the above code, but it does not work.

In particular I am unsure about several points:


The QDropEvent::QDropEvent(.. doc says "Constructs a drop event ... at the point specified by pos in the destination widget's coordinate system". Is the "destination" widget the "receiver" that I pass to my QDropEvent? I not, what would the correct pos be to provide the global mouse position on the OS desktop?
I am not really sure if sendEvent does what I want ?
The QCoreApplication::sendEvent(.. doc says "Sends event event directly to receiver receiver, ...". receiver is a QObject* .. I don't know if passing QScreen* to this makes sense? I assume the receiver must implement dropEvent(), but QScreen is not a widget ... This is probably wrong, but I don't know what else to pass as receiver, what would the correct receiver be in my case?


Or maybe there is completely different way and trying to construct / handle QDropEvent is wrong? Help appreciated!


Edit: A second idea: I could simulate the mouse events by sending them from the key events, but I am also stuck trying to get this to work:



// in keyPressEvent ...
case Qt::Key_M:
if (!e->isAutoRepeat()) {
QMouseEvent mouse_event { QEvent::MouseButtonPress, mapFromGlobal(QCursor::pos()),
Qt::LeftButton, Qt::NoButton, Qt::NoModifier };
QApplication::sendEvent(this, &mouse_event);
}
break;


This triggers the mouse event, but the QDrag does not show up (i.e. nothing happens and the icon that I get when I instead click the mouse does not show). Maybe because the mouse button needs to be held down, but I don't see any event setting for this :(

Maryu
7th September 2020, 04:24
My attempts did not really work out so far. With QDrag being so easy to use, it seems like an oversight that it provides so little customization.
It does not allow to set a custom button instead of left click - it does not provide any slots or signals to customize the behavior - it does not allow to manually stop/end the drag ...
I checked the Qt source but even there everything about how the QDrag is ended is hidden deeper layers and there seemed no way to reasonably reimplement a QDrag for different keys.

Therefore I decided to go with a platform dependent solution and grab my target directory with Windows functions. This works in my case since I really just want to move files, but I guess it could be expanded to handle different data similar to QMimeData by using the Clipboard. Though that would be more work.

Anyway, the reason for this post is to provide my solution (even if it has nothing to do with Qt anymore) if anyone happens to need something similar and finds this thread. Mind I have maybe written a total of 300 lines of Windows code in my life. The solution is largely almost 20 year old code taken from this blog (https://devblogs.microsoft.com/oldnewthing/20040720-00/?p=38393), slightly modified.

Most likely, this can be done better today - I checked some windows documentation and also saw another Stackoverflow post with "better/safer" code but I could not easily get it to compile on my platform so I stuck with this solution for now. Anyway:

Note: don't put the windows code together with Qt code, including various windows headers gave me many compile time errors.



// DragDropTest.cc
#include <string>

#include <windows.h>
#include <shlobj.h>
#include <exdisp.h>

#include <DragDropTest.hh>

HWND toplevel_window_under_mouse() {
const auto mouse_pos = QCursor::pos();
HWND under_mouse_hwnd = WindowFromPoint(POINT { mouse_pos.x(), mouse_pos.y() });
for (auto parent = GetParent(under_mouse_hwnd); parent != nullptr; parent = GetParent(parent))
under_mouse_hwnd = parent;

return under_mouse_hwnd;
}

std::filesystem::path path_of_window_under_mouse() {
TCHAR explorer_window_directory_path[MAX_PATH];
HWND under_mouse_hwnd = toplevel_window_under_mouse();

explorer_window_directory_path[0] = TEXT('\0');
IShellWindows* shell_windows;
if (SUCCEEDED(CoCreateInstance(CLSID_ShellWindows, NULL, CLSCTX_ALL, IID_IShellWindows, (void**)&shell_windows))) {
VARIANT v;
V_VT(&v) = VT_I4;
IDispatch* dispatch;
bool fFound = false;
for (V_I4(&v) = 0; !fFound && shell_windows->Item(v, &dispatch) == S_OK; V_I4(&v)++) {
IWebBrowserApp* webbrowser_app;
if (SUCCEEDED(dispatch->QueryInterface(IID_IWebBrowserApp, (void** )&webbrowser_app))) {
HWND webbrowser_hwnd;
if (SUCCEEDED(webbrowser_app->get_HWND((LONG_PTR* )&webbrowser_hwnd))) {
if (webbrowser_hwnd == under_mouse_hwnd) {
fFound = true;
IServiceProvider* service_provider;
if (SUCCEEDED(webbrowser_app->QueryInterface(IID_IServiceProvider, (void** )&service_provider))) {
IShellBrowser* shell_browser;
if (SUCCEEDED(
service_provider->QueryService(SID_STopLevelBrowser, IID_IShellBrowser, (void** )&shell_browser))) {
IShellView* shell_view;
if (SUCCEEDED(shell_browser->QueryActiveShellView(&shell_view))) {
IFolderView* folder_view;
if (SUCCEEDED(shell_view->QueryInterface(IID_IFolderView, (void** )&folder_view))) {
IPersistFolder2* folder;
if (SUCCEEDED(folder_view->GetFolder(IID_IPersistFolder2, (void** )&folder))) {
LPITEMIDLIST folder_item_ids;
if (SUCCEEDED(folder->GetCurFolder(&folder_item_ids))) {
SHGetPathFromIDList(folder_item_ids, explorer_window_directory_path);
CoTaskMemFree(folder_item_ids);
}
folder->Release();
}
folder_view->Release();
}
shell_view->Release();
}
shell_browser->Release();
}
service_provider->Release();
}
}
}
webbrowser_app->Release();
}
dispatch->Release();
}
shell_windows->Release();
}

return explorer_window_directory_path;
}

void DragDropTester::keyPressEvent(QKeyEvent* e) {
switch (e->key()) {
case Qt::Key_M:
if (!e->isAutoRepeat()) {
const std::filesystem::path to_move { m_file.toStdString() };
const auto destination = path_of_window_under_mouse();
if (std::filesystem::is_regular_file(to_move) && std::filesystem::is_directory(destination))
std::filesystem::rename(to_move, destination / to_move.filename());
}
break;
default:
QMainWindow::keyPressEvent(e);
}
}

DragDropTester::DragDropTester(const QString& path) :
QMainWindow { }, m_file { path } {
setFixedSize(400, 400);
show();
}



Of course the original question still remains. Even if I solved it differently, it would certainly be a welcome alternative .. so if anyone knows how to do this with Qt, please post.

d_stranz
7th September 2020, 18:47
automatically be drag-dropped to the window under the mouse cursor.

Huh? What if the window under the mouse cursor is the Recycle Bin, the Desktop, or some totally unrelated app which will mangle the files dropped onto it? Once the user selects the files, your proposed method might end up completely trashing them without any safeguards. The widget with keyboard input focus is often not the same window as the mouse location.

The whole reason for the mouse-based drag and drop system in Qt is to give both users and the app control of where files can be dragged from and dropped to. The method for creating a QDrag in the first place, the various responses to QDragEnterEvent, QDragMoveEvent, and QDragLeaveEvent, and the handling of a mouseReleaseEvent when a drag is in progress are all designed in order to give appropriate feedback to both the UI and the drag/drop system to prevent unexpected things from happening.

There are plenty of ways to use keypress actions to create something like drag and drop. A few I can think of are using a context menu, using an app level menu, using a toolbutton with a ">>" style icon, and so on to copy / move selected things from one window to another. And none of these require abusing the drag / drop system already in Qt.

Maryu
8th September 2020, 01:13
What if the window under the mouse cursor is the Recycle Bin, the Desktop, or some totally unrelated app which will mangle the files dropped onto it? Once the user selects the files, your proposed method might end up completely trashing them without any safeguards. The widget with keyboard input focus is often not the same window as the mouse location.

This is my intended functionality. The app is focused on being able to do things fast. I guess with the windows specific solution I have the benefit of being able to check the destination path myself, I didn't check what QDrag does here, but I assume it at least cannot crash, which seems enough to me. Drag-and-drop from some program to some other program, may it be part of Windows explorer, without asking if the user is really sure, seems like a common thing to me.
I just find clicking, holding, moving, releasing the mouse for every file and/or destination is too much work and takes too long. A simple key press & release is fast - the shortcut for it can be sufficiently cumbersome if needed.

I appreciate the design of QDrag for almost-always similar use, I'm quite happy with Qt so far as most common things are easy to do. But sometimes common use is not what is wanted, so I just find it unfortunate when something seemingly straightforward to implement like providing a setter for a different key doesn't exist. But maybe I'm wrong and this is hard to implement. Then again there are notes about possible deprecation of internal QDrag related classes, so maybe some extensions are planned already.


There are plenty of ways to use keypress actions to create something like drag and drop. A few I can think of are using a context menu, using an app level menu, using a toolbutton with a ">>" style icon, and so on to copy / move selected things from one window to another. And none of these require abusing the drag / drop system already in Qt.

My issue with this is again use-speed, as in the user having to navigate all this. The reason I tried drag & drop was because QDrag was able to determine the window under the mouse even if it was not part of my app, e.g. a Windows explorer window. Sure I could add a menu, add a dialog etc., but how do I get the OS window under the mouse cursor then? Manually selecting a destination is too slow. If the window is already next to my app, I just want to drop it there. The only class I found able to get an OS window under the mouse cursor is QDrag.
My posted windows-code works fine though.

d_stranz
8th September 2020, 17:30
Drag-and-drop from some program to some other program, may it be part of Windows explorer, without asking if the user is really sure, seems like a common thing to me.

What your OP proposed is not drag and drop. It is selecting something through key presses and then doing something with that selection outside of the normal drag and drop protocols. In the normal drag and drop interaction, both the drag source and potential drop targets determine what can happen and provide feedback to the user that tells them what is or is not possible. And in many cases, if the drop will be destructive, there will be a confirmation warning first. This is present in Windows Explorer and is an option that can be toggled off.


I appreciate the design of QDrag for almost-always similar use, I'm quite happy with Qt so far as most common things are easy to do. But sometimes common use is not what is wanted, so I just find it unfortunate when something seemingly straightforward to implement like providing a setter for a different key doesn't exist. But maybe I'm wrong and this is hard to implement. Then again there are notes about possible deprecation of internal QDrag related classes, so maybe some extensions are planned already.

No, not likely. What you propose is something that goes against accepted conventions for user interface interaction. Drag and drop means exactly what its name implies - you click on something, drag it to where you want it, and drop it if the destination allows.


My issue with this is again use-speed

Sorry, but I think most UI designers would tell you that you should focus on usability first, then on performance. If you implement a UI that does things in a way that is completely foreign to what users know and understand from using other, more conventional apps, then most people will not want to use your app because it requires them to remember strange patterns of interaction, or if they don't remember, could cause them to accidentally lose their files or their work because what they did caused something unexpected to happen.

It really isn't clever or innovative to force users to adapt to your style or think outside the box every time they want to use your app. People won't use your app if they have to do that, or if your app has scared them into not using it because it behaves in strange and unexpected ways. It might be boring to write code that does the usual things, but if users reject your app because it doesn't, then you really haven't succeeded.

Maryu
8th September 2020, 20:52
What your OP proposed is not drag and drop. It is selecting something through key presses and then doing something with that selection outside of the normal drag and drop protocols

Maybe I should have quoted "drag and drop" but I was not trying to refer to some standard or conventional protocol, but rather to the conceptional, abstract, action of drag & drop. That is, something like

Retrieve some object/data calculated from a screen-position
Hold on to that object while some condition is true
Retrieve some other object/data calculated from some other screen position while that condition is true or it is changed to false
Use the first and last selected objects to calculate something once the condition is false


In my use case: The first selected object is the file displayed by the app. The "condition" is simply always false. The last selected object is the OS window under the mouse cursor. The "calculate something" is moving the file to the directory of the OS window, if it is a Windows explorer window.

The underlined part is what I found only QDrag be able to do, yet locked away behind the restrictions of QDrag, and seemingly not accessible by other means. Maybe my OP question should have been "How to retrieve path of Windows explorer window under mouse cursor? QDrag can do it, how can I do it without QDrag?"


No, not likely. What you propose is something that goes against accepted conventions for user interface interaction. Drag and drop means exactly what its name implies - you click on something, drag it to where you want it, and drop it if the destination allows.

I do not see why the physical process has to have anything to do with the abstract concept. In the points I wrote above, for some reason, the reference positions must be the mouse cursor location. For some reason, the process must be started and ended by pressing and releasing the left mouse button. For some reason, the "condition" is "while left button is down". These are good defaults by convention, but I do not see any reason to intentionally not allow to change these. QDrag implements exactly the drag & drag concept I need, but for some reason does not allow me to change these arbitrary defaults.


Sorry, but I think most UI designers would tell you that you should focus on usability first, then on performance.

I am not and do not intend to be a UI designer. I need a specific solution for a specific problem. My apps whole purpose is to be efficient to use: If it can be done by a single button, it should be able to be done by a single button. If I cared about ease-of-use over efficiency-of-use, I would keep using the 3rd party program I have been using for years (minus some missing functionality). But it follows UI conventions, and over the years I am getting increasingly frustrated about unnecessary UI sugar that serves no purpose other than to be easy to use. This is great when starting out, but too cumbersome and slow in the long run. I may make my app available via some means, but this will be a side effect if anything. If someone wants to use it, great, if not, that's fine.


But as far as I take from the answers, my OP question is not possible with Qt-only. Though I still see it as "open-to-be-implemented" and not "intentionally not implemented".

d_stranz
9th September 2020, 01:21
I do not see why the physical process has to have anything to do with the abstract concept.

The physical process is the embodiment of the abstract concept. The abstract concept is not "select something and cause it to be moved somewhere else". There are any number of ways to do that that do not involve the abstract concept of dragging and dropping. The only way you could adequately implement the concept of drag and drop using the keyboard would be to simulate, maybe via arrow keys, picking the selected item(s) up from where they are, providing feedback as you move them to a new location, and feedback that the item is droppable at the new location and has been dropped.


I need a specific solution for a specific problem.

So if you are simply dong this for your own use, then have at it. I mistakenly assumed you were developing an app others would use.


"intentionally not implemented"

The likely case. The whole UI concept of drag and drop is based on the visual interaction of using the mouse (or your finger, or the claw in an arcade game simulation), picking something up, moving it somewhere else, and dropping it, all the while providing visual feedback about what is happening. QDrag and the logic around it have been designed to implement that concept, and it is a standard part of UI interaction on every graphical UI platform I've come across.