Search code examples
pythonpyqt5qt5file-managerqfiledialog

Open file from QFileDialog in native file explorer via right-click in PyQt5?


In Firefox, if I download a file, there is a folder icon "Show in Folder":

Firefox Show in Folder

... which when clicked, opens the native OS file explorer in the Downloads directory, with the target download file selected:

Show in Folder opened in native File Explorer

I would like the same kind of functionality - except I want it in a PyQt5 application, when QFileDialog is opened, upon choosing an action in the right-click context menu activated when the target file is selected; e.g. with the PyQt5 example (below), I can get this Qt5 dialog:

Qt5 QFileDialog

... so, when I right-click on a target file (like test.txt in the image), I'd like a "Show in Folder" action added to the context menu, and when it is chosen, I'd like the native file explorer opened in the directory that contains the target file, and the target file selected - like what Firefox does.

How can I do that in PyQt5?

Example code:

# started from https://pythonspot.com/pyqt5-file-dialog/
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QInputDialog, QLineEdit, QFileDialog
from PyQt5.QtGui import QIcon

class App(QWidget):

    def __init__(self):
        super().__init__()
        self.title = 'PyQt5 file dialogs - pythonspot.com'
        self.left = 10
        self.top = 10
        self.width = 640
        self.height = 480
        self.initUI()

    def initUI(self):
        self.setWindowTitle(self.title)
        self.setGeometry(self.left, self.top, self.width, self.height)

        self.openFileNameDialog()

        self.show()

    def openFileNameDialog(self):
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        fileName, _ = QFileDialog.getOpenFileName(self,"QFileDialog.getOpenFileName()", "","Text Files (*.txt)", options=options)
        if fileName:
            print(fileName)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = App()
    sys.exit(app.exec_())

Solution

  • As noted in the comments, there's no built-in Qt support for this. Opening and selecting a file in the system file-manager is quite tricky and there are no perfect cross-platform solutions. However, there is a Python show-in-file-manager package that does a reasonable job if you don't want to develop your own solution. It then remains to subclass QFileDialog and reimplement the context-menu handling. (NB: this means it will no longer be possble to use static functions like getOpenFileName, which use an internal Qt instance of QFileDialog - unless, of course, you choose to reimplement those functions as well).

    Here's a basic demo (only tested on Linux):

    from PyQt5.QtCore import (
        QFile, QFileDevice,
        )
    from PyQt5.QtWidgets import (
        QApplication, QListView, QTreeView, QFileSystemModel, QToolButton,
        QWidget, QFileDialog, QAction, QMenu, QPushButton, QVBoxLayout,
        QMessageBox,
        )
    # from PyQt6.QtCore import (
    #     QFile, QFileDevice,
    #     )
    # from PyQt6.QtGui import (
    #     QAction, QFileSystemModel,
    #     )
    # from PyQt6.QtWidgets import (
    #     QApplication, QListView, QTreeView, QToolButton, QMessageBox,
    #     QWidget, QFileDialog, QMenu, QPushButton, QVBoxLayout,
    #     )
    
    class FileDialog(QFileDialog):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.setOptions(QFileDialog.Option.DontUseNativeDialog)
            self._list_view = self.findChild(QListView, 'listView')
            self._list_view.customContextMenuRequested.disconnect()
            self._list_view.customContextMenuRequested.connect(
                self.showContextMenu)
            self._tree_view = self.findChild(QTreeView, 'treeView')
            self._tree_view.customContextMenuRequested.disconnect()
            self._tree_view.customContextMenuRequested.connect(
                self.showContextMenu)
            self._rename_action = self.findChild(QAction, 'qt_rename_action')
            self._delete_action = self.findChild(QAction, 'qt_delete_action')
            self._hidden_action = self.findChild(QAction, 'qt_show_hidden_action')
            self._folder_action = self.findChild(QAction, 'qt_new_folder_action')
            self._folder_button = self.findChild(QToolButton, 'newFolderButton')
            self._show_in_action = QAction('Show In &Folder')
            self._show_in_action.triggered.connect(self.showInFolder)
            self._model = self.findChild(QFileSystemModel, 'qt_filesystem_model')
    
        def showContextMenu(self, position):
            if self.viewMode() == QFileDialog.ViewMode.Detail:
                view = self._tree_view
            else:
                view = self._list_view
            index = view.indexAt(position)
            index = index.sibling(index.row(), 0)
            if (proxy := self.proxyModel()) is not None:
                index = proxy.mapToSource(index)
            menu = QMenu(view)
            if index.isValid():
                menu.addAction(self._show_in_action)
                menu.addSeparator()
                permissions = QFileDevice.Permission(index.parent().data(
                    QFileSystemModel.Roles.FilePermissions))
                enable = bool(not self._model.isReadOnly() and
                    permissions & QFileDevice.Permission.WriteUser)
                self._rename_action.setEnabled(enable)
                menu.addAction(self._rename_action)
                self._delete_action.setEnabled(enable)
                menu.addAction(self._delete_action)
                menu.addSeparator()
            menu.addAction(self._hidden_action)
            if self._folder_button.isVisible():
                self._folder_action.setEnabled(self._folder_button.isEnabled())
                menu.addAction(self._folder_action)
            menu.exec(view.viewport().mapToGlobal(position))
    
        def showInFolder(self):
            if files := self.selectedFiles():
                try:
                    from showinfm import show_in_file_manager
                except ImportError:
                    QMessageBox.warning(self, 'Show In Folder', (
                        '<br>Please install <a href="https://pypi.org/'
                        'project/show-in-file-manager/">'
                        'show_in_file_manager</a>.<br>'
                        ))
                else:
                    show_in_file_manager(files)
    
    class Window(QWidget):
        def __init__(self):
            super().__init__()
            self.button = QPushButton('Open File')
            self.button.clicked.connect(self.openFileNameDialog)
            layout = QVBoxLayout(self)
            layout.addWidget(self.button)
            self.dialog = FileDialog(self)
    
        def openFileNameDialog(self):
            self.dialog.setFileMode(QFileDialog.FileMode.ExistingFile)
            self.dialog.setNameFilter('Text Files (*.txt);;All Files(*)')
            self.dialog.open()
    
    if __name__ == '__main__':
    
        app = QApplication(['Test'])
        window = Window()
        window.setGeometry(600, 100, 200, 50)
        window.show()
        app.exec()