79194465

Date: 2024-11-16 03:28:05
Score: 1.5
Natty:
Report link

Well, thanks to comments of @ekhumoro and @musicamante, I finally got to a working solution, with class and usage example - but boy, was that a doozy:

So, here is how the example below looks like on my machine:

example GUI output

Here is the code:

# started from https://stackoverflow.com/q/43890097/79194084#79194084
import sys
import os
import platform
from pathlib import Path, PureWindowsPath # python 3.4
from showinfm import show_in_file_manager, valid_file_manager, stock_file_manager # python3 -m pip install show-in-file-manager
from PyQt5.QtCore import * # QFile
from PyQt5.QtGui import *
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QTreeView, QListView, QFileDialog, QAction, QMenu, QActionGroup, QPushButton, QLabel, QMainWindow, QDialog, QFileSystemModel

class CustomShowQFileDialog(QFileDialog):
    """
    * https://forum.qt.io/topic/158937: "QFileDialog is not really meant to
      be overriden. Create your own dialog if you want something special." -
      however, there is an example there
    * https://www.qtcentre.org/threads/1827-QFileDialog-and-subclassing: "When
      you subclass QFileDialog, you are subclassing the Qt QFileDialog, not the
      native one. Calling static methods like getOpenFileName() will have no
      other effect than showing the native dialog because they are *static*,
      they will not operate on your instance.
      Therefore, you must use show() or exec() to show your instance of a
      filedialog. I recomend exec() - that way, when the dialog has been closed,
      functions like selectedFiles(), selectedFilter(), etc. will return the
      correct value."
    """
    def __init__(self, parent=None, caption="", directory="", filter=""):
        super(CustomShowQFileDialog,self).__init__(parent, caption, directory, filter)

        #print(f"{valid_file_manager()=} {stock_file_manager()=}") # both are 'explorer.exe' for MINGW64 Python

        # disable file multi-selection - ensure only a single item (file/dir)
        # selection is possible; see
        # https://www.qtcentre.org/threads/18815-Select-multiple-files-from-QFileDialog
        self.setFileMode(QFileDialog.ExistingFile)

        # must have self.show() first, so .children() are instantiated,
        # and .findChild works; but without extern call to parent .show()
        # afterwards, this self.show() on its own does not show a dialog window!
        #self.show()
        # note: QWidget::show() can call QWidget::showFullScreen(), which calls
        # ensurePolished() and setVisible(True); it turns out, to have
        # .findChild work, it is enough to just run .setVisible(True) - note
        # that this also shows the dialog upon instantiation
        #self.ensurePolished()
        self.setVisible(True)

        # fetch the QTreeView/QListView in the QFileDialog
        self.myTree = self.findChild(QTreeView, "treeView") # when QFileDialog in Detail View
        self.myList = self.findChild(QListView, "listView") # when QFileDialog in List View

        # get the actions for the original QFileDialog context menu, so
        # we can reconstruct it;
        # qt_rename_action and qt_delete_action are setEnabled(False) by default,
        # and should be enabled as per file permissions of selection (see
        # QFileDialogPrivate::showContextMenu, qtbase/src/widgets/dialogs/qfiledialog.cpp)
        # here we just enable them regardless - where app has permissions,
        # they will work:
        self.rename_act = self.findChild(QAction, "qt_rename_action")
        self.rename_act.setEnabled(True)
        self.delete_act = self.findChild(QAction, "qt_delete_action")
        self.delete_act.setEnabled(True)
        self.show_hidden_act = self.findChild(QAction, "qt_show_hidden_action")
        self.new_folder_act = self.findChild(QAction, "qt_new_folder_action")

        # Define our custom context menu action, and
        # connect this action to a slot/handler method
        self.show_in_folder_act = QAction( "Show in Folder", self )
        self.show_in_folder_act.triggered.connect(self.handle_show_in_folder_act)

        # set up context menu policy
        self.myTree.setContextMenuPolicy( Qt.CustomContextMenu )
        self.myList.setContextMenuPolicy( Qt.CustomContextMenu )
        self.myTree.customContextMenuRequested.disconnect() # OK
        self.myTree.customContextMenuRequested.connect(self.generate_context_menu)
        self.myList.customContextMenuRequested.disconnect() # OK
        self.myList.customContextMenuRequested.connect(self.generate_context_menu)

    def generate_context_menu(self, location): # https://stackoverflow.com/q/44666427
        print("generate_context_menu {}".format(self.sender().objectName())) # "treeView" or "listView"
        #
        # as in QFileDialogPrivate::showContextMenu;
        # note QFileDialogPrivate::model is QFileSystemModel*,
        # not accessible in Python - but FilePermissions are
        index = self.myList.currentIndex()
        index = index.sibling(index.row(), 0)
        #ro = (self.model) and (self.model.isReadOnly()) # cannot
        perms = index.parent().data(QFileSystemModel.FilePermissions) # no .toInt(), is int already
        print(f"    {perms=}")
        # renameAction->setEnabled(!ro && p & QFile::WriteUser);
        # deleteAction->setEnabled(!ro && p & QFile::WriteUser);
        self.rename_act.setEnabled(perms & QFile.WriteUser)
        self.delete_act.setEnabled(perms & QFile.WriteUser)
        #
        menu = QMenu()
        menu.addAction(self.rename_act)
        menu.addAction(self.delete_act)
        menu.addSeparator()
        menu.addAction(self.show_hidden_act)
        menu.addAction(self.new_folder_act)
        menu.addSeparator()
        menu.addAction(self.show_in_folder_act)
        #menu.exec_(event.globalPos()) # https://stackoverflow.com/q/65371143 -> not applicable here
        menu.exec_(self.mapToGlobal(location)) # https://stackoverflow.com/q/43820152 -> works

    def handle_show_in_folder_act(self):
        # as in QFileDialogPrivate::renameCurrent();
        # self.myList.currentIndex() works for both Detail View and List View;
        # .column() can be > 0 only for Detail view (when right-clicked over
        # e.g. file size column)
        #print(f"{self.myList.currentIndex().row()=} {self.myList.currentIndex().column()=} ")
        index = self.myList.currentIndex()
        index = index.sibling(index.row(), 0)
        # as in QFileDialogPrivate::deleteCurrent();
        # note QFileDialogPrivate::mapToSource is private, and not in Python
        #index = self.mapToSource(index.sibling(index.row(), 0))
        #print(f"{index.isValid()=} {index=}")
        fileName = index.data(QFileSystemModel.FileNameRole) # no .toString(), is str already
        filePath = index.data(QFileSystemModel.FilePathRole) # no .toString(), is str already
        perms = index.parent().data(QFileSystemModel.FilePermissions) # no .toInt(), is int already
        print(f"{fileName=} {filePath=} {perms=}")
        # NOTE: in MINGW64 Python3, filePath='C:/test/test.txt' with forward
        # slashes - this ends up as folder='/test' in launch_file_explorer of
        # showinfm, causing `folder_pidl = shell.SHILCreateFromPath(folder, 0)[0]`
        # to end up being None, causing "TypeError: None is not a valid
        # ITEMIDLIST in this context" in the desktop.BindToObject call;
        # so probably we need to ensure proper Windows line separators before
        # calling show_in_file_manager
        if "win" in platform.system().lower(): # https://stackoverflow.com/q/1387222
          # python pathlib fails to convert these forward slashes to backslashes
          # fpath = Path(filePath) # WindowsPath('C:/test/test.txt')
          # pwPath = PureWindowsPath(fpath) # PureWindowsPath('C:/test/test.txt'); same with filePath as argument
          # apparently, os.path.normpath can do forward slash to backslash? not for me
          #filePath = os.path.normpath(filePath)
          # well then, have to go manual; unfortunately show_in_file_manager
          # calls normpath itself, so we're back at forward slashes, and not
          # even allow_conversion=False helps:
          #filePath = filePath.replace(r"/", "\\")
          # what worked finally for MINGW64 python3 is to:
          # pass filePath as URI, and use allow_conversion=False
          filePath = Path(filePath).as_uri()
          print(f"{filePath=}")
        show_in_file_manager(filePath, allow_conversion=False) # verbose=True, debug=True,



#class MainWidget(QWidget):
class MainWindow(QMainWindow):

    def __init__(self, parent=None):
        #super(MainWidget,self).__init__(parent)
        super(MainWindow,self).__init__(parent)

        central_widget = QWidget(self)        # for MainWindow(QMainWindow)
        self.setCentralWidget(central_widget) # for MainWindow(QMainWindow)

        #creation of main layout
        mainLayout = QVBoxLayout()

        self.button = QPushButton("Click for file dialog")
        self.button.clicked.connect(self.on_button_click)
        mainLayout.addWidget( self.button )

        self.label = QLabel("[path]")
        mainLayout.addWidget( self.label )

        # creation of a widget inside
        #self.monWidget = CustomShowQFileDialog()
        #mainLayout.addWidget( self.monWidget )

        #self.setLayout( mainLayout ) # for MainWidget(QWidget)
        central_widget.setLayout( mainLayout ) # for MainWindow(QMainWindow)

    def on_button_click(self):
        print("on_button_click")
        """
        Note: the "tradidional" use with the static .getOpenFileName(...) is
        like this:

        options = QFileDialog.Options() # https://stackoverflow.com/q/63012420
        options |= QFileDialog.DontUseNativeDialog
        load_file_path, _ = QFileDialog.getOpenFileName(self, "Open .txt File", filter="Text Files(*.txt)", options=options)
        if load_file_path: # user has made a choice
            print(f"{load_file_path=}")

        However, as per https://www.qtcentre.org/threads/1827-QFileDialog-and-subclassing,
        we cannot use that idiom for a custom subclass; instead we must block
        with .exec(), which will return an int depending on OK/Cancel button
        pressed:
          0: Cancel (QDialog::Rejected),
          1: Open/file double-click) (QDialog::Accepted)
        ... and afterwards use .selectedFiles()
        """

        # instantiation alone raises dialog for now;
        self.myqfd = CustomShowQFileDialog(self, "Open .txt File", filter="Text Files(*.txt)")
        retint = self.myqfd.exec_() # exec blocks; however here returns int
        print(f"{retint=}")
        if retint == QDialog.Accepted:
          print(f"{self.myqfd.selectedFiles()}")
          firstfile = self.myqfd.selectedFiles()[0]
          self.label.setText(firstfile)


app = QApplication(sys.argv)
#window = MainWidget()
window=MainWindow()
window.show()
window.resize(180, 160)
sys.exit(app.exec_())
Reasons:
  • Blacklisted phrase (0.5): thanks
  • Blacklisted phrase (1): to comment
  • Blacklisted phrase (1): stackoverflow
  • Long answer (-1):
  • Has code block (-0.5):
  • User mentioned (1): @ekhumoro
  • User mentioned (0): @musicamante
  • Self-answer (0.5):
  • High reputation (-1):
Posted by: sdbbs