How to get the drop target when the drop occurs outside the application?

Viewed 489

I have a python qt5 program with a QTreeWidget. Each QTreeWidgetItem represents files. I have the logic working correctly within mouseMoveEvent so that QDrag CopyAction passes the file mime data successfully to windows when the user drags it to windows explorer or the desktop. However, I need to know what that target path is. The QDrag target() function only returns a widget if the user drags within the application. If the user drags outside the application (like the desktop), then target() returns None.

Is there anyway to know the path the user dragged to in windows?

The reason I need to know the target path is because the file will actually be generated at runtime and the generation can take as long as a minute to complete. In this mvce example I am using touch command to instantly create an empty file so the user doesn't have to wait or see a long delay. Then after the actual file is generated a minute later, I want to replace the empty file with the actual file. My application can then tell the user that the file is now available. I can not pre-generate the file because there will be many in the list and each will take minutes to generate.

Here is my mvce (minimum reproducible example):

example.py

#!python3

import sys
from PyQt5.QtWidgets import QMainWindow, QApplication, QTreeWidgetItem
from main_ui import Ui_MainWindow   

class MainWindow(QMainWindow, Ui_MainWindow):

    def __init__(self, parent = None):
        super(MainWindow, self).__init__(parent)   # initialization of the superclass
        self.setupUi(self)   # setup the GUI --> function generated by pyuic4

        self.tree1.blockSignals(True)  

        self.tree1.clear()
        self.tree1.setHeaderHidden(True)
        self.tree1.setColumnCount(2)
        self.tree1.setRootIsDecorated(True)  # show expansion icon

        level1 = QTreeWidgetItem(self.tree1)

        level1.setText(0, "root")
        level1.setExpanded(True)

        level2 = QTreeWidgetItem(level1)
        level2.setExpanded(False)
        level2.setText(0, "dragme to desktop")
        level2.setExpanded(True)

        self.tree1.show()

        self.tree1.resizeColumnToContents(0)

        self.tree1.blockSignals(False)  

        self.show()

if __name__ == "__main__":

    app = QApplication(sys.argv)

    myapp = MainWindow()      # instantiate the main window
    rc = app.exec_()
    sys.exit(rc)   # exit with the same return code of Qt application

main.ui

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>415</width>
    <height>451</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <layout class="QHBoxLayout" name="horizontalLayout">
    <item>
     <widget class="mytree" name="tree1">
      <property name="baseSize">
       <size>
        <width>0</width>
        <height>0</height>
       </size>
      </property>
      <property name="styleSheet">
       <string notr="true"/>
      </property>
      <property name="verticalScrollBarPolicy">
       <enum>Qt::ScrollBarAlwaysOff</enum>
      </property>
      <property name="horizontalScrollBarPolicy">
       <enum>Qt::ScrollBarAlwaysOff</enum>
      </property>
      <property name="autoScrollMargin">
       <number>16</number>
      </property>
      <property name="showDropIndicator" stdset="0">
       <bool>false</bool>
      </property>
      <property name="dragEnabled">
       <bool>true</bool>
      </property>
      <property name="dragDropMode">
       <enum>QAbstractItemView::DragDrop</enum>
      </property>
      <property name="alternatingRowColors">
       <bool>false</bool>
      </property>
      <property name="verticalScrollMode">
       <enum>QAbstractItemView::ScrollPerPixel</enum>
      </property>
      <property name="uniformRowHeights">
       <bool>true</bool>
      </property>
      <property name="columnCount">
       <number>0</number>
      </property>
      <attribute name="headerVisible">
       <bool>true</bool>
      </attribute>
      <attribute name="headerCascadingSectionResizes">
       <bool>false</bool>
      </attribute>
      <attribute name="headerDefaultSectionSize">
       <number>100</number>
      </attribute>
      <attribute name="headerStretchLastSection">
       <bool>false</bool>
      </attribute>
     </widget>
    </item>
   </layout>
  </widget>
 </widget>
 <customwidgets>
  <customwidget>
   <class>mytree</class>
   <extends>QTreeWidget</extends>
   <header>mytree.h</header>
  </customwidget>
 </customwidgets>
 <resources/>
 <connections/>
</ui>

mytree.py

#!python3

import tempfile
from pathlib import Path
from PyQt5 import QtGui 
from PyQt5 import QtCore
from PyQt5 import QtWidgets
from PyQt5.QtCore import Qt

class mytree(QtWidgets.QTreeWidget):   # mytree is now a subclass of qlineedit class

    def __init__(self, parent=None):
        super(mytree, self).__init__(parent)  # The use of "super" is a slightly better method of calling the parent for initialization
        self.setDragEnabled(True)

    def mouseMoveEvent(self, event):  # is called whenever the mouse moves while a mouse button is held down
        super(mytree, self).mouseMoveEvent(event)   #propagate

        tempdir = tempfile.gettempdir() # temp directory aka %temp%
        realfile = tempdir + "/empty.txt"
        Path(realfile).touch()  # create empty file

        fileurl = "file:///%s" % realfile  # build url
        url = QtCore.QUrl(fileurl)
        urllist = []
        urllist.append(url)  # only 1 file 

        drag = QtGui.QDrag(self)

        md = QtCore.QMimeData()
        md.setUrls(urllist)
        drag.setMimeData(md)    
        result = drag.exec_(Qt.CopyAction)     

        source = drag.source()
        print("source = %s" % source)
        target = drag.target()   # returns widget if inside the application, returns None if windows desktop ... etc.
        print("target = %s" % target)
3 Answers

Cause

QDrag::target returns a pointer to QObject. Within a Qt widgets application this makes sense, because all possible drop targets are some kind of QWidgets, i.e. QObjects. When the drop action is outside the app, it does not make sense to return anything than a null pointer, because:

  1. If not, a pointer to which QObject should target return instead?

  2. If you somehow manage to get some kind of a pointer, to which type would you cast it to access the information you need, because QObject definitely does not contain any file URLs?

I admit the documentation of QDrag::target is not as explicit on this, as the documentation of QDropEvent::source:

Returns the target of the drag and drop operation. This is the widget where the drag object was dropped.

compared to

If the source of the drag operation is a widget in this application, this function returns that source; otherwise it returns nullptr.

However, the This is the widget part gives a hint about this fact, that the target should be a widget.

Solution

Instead of a drag functionality use QFileDialog::getSaveFileUrl to get the url of the file.

Workaround: QFileSystemModel

Depending on your requirements, a solution may be to include a file system tree inside your application by rendering a QFileSystemModel inside a QTreeView.

Using this approach, the drag and drop operation is completely handled inside your application, which allows you to extract the destination path and, if desired, to directly generate the file in the background, instead of working with an empty file first.

Track moved empty file

A possible solution is to let the drag and drop operation move an empty file and track the move (rename) operation of this file. However, I'm not sure if this is possible using Python and/or in a cross platform way.

Using a QFileSystemWatcher to perform this tracking is not possible as it doesn't report the new file location. This thread discusses possible implementations on the principal platforms:

Related