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:
show_in_file_manager
, however since I use MINGW64 Python3 on Windows 10, os.path.normpath
etc all convert paths to forwards slashes (even if you manually supply a Windows path with backslashes) which breaks show_in_file_manager
- thankfully, there is a workaround here: supply path as_uri()
, and use allow_conversion=False
in show_in_file_manager
So, here is how the example below looks like on my machine:
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_())