PyQT5 doesn't prompt to overwrite when a default suffix is used

33 Views Asked by At

I'm using PyQT5 on Ubuntu 23.10 (GNOME 45 on Wayland).

I would like QFileDialog to prompt for overwrite if the user typed a file name without suffix, which would exist if the default suffix is applied to it (e.g.: image turns into image.png which would overwrite the existing image.png file).

The current behavior prompts only if the user selected an existing file or typed it with a suffix. If the user clicks Replace on the popup, then both the popup as well as the dialog are closed and the dialog is accepted. If the user clicks Cancel on the popup, only the popup is closed.

This behavior is even more problematic if I have to deal with multiple filters (e.g.: Images (*.png);; Text (*.txt)), for which I think I'd need to add a listener to QFileDialog.setNameFilter that updates the default suffix with QFileDialog.setDefaultSuffix, but I couldn't find an event/signal like filterChanged.

How can I change QFileDialog to:

  1. Prompt the user if they would select a file that would overwrite another one with its suffix?
  2. Automatically change the default suffix.

I tried subclassing QFileDialog, but if setDefaultSuffix was called, then the suffix is already appended as well as the dialog is already closed by the time dialog.accept() is called, so I can only reject it.

import os, re

from PyQt5 import QtWidgets


class QFileDialog(QtWidgets.QFileDialog):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def accept(self):
        """This is run after dialog is closed.
        """
        path = next(iter(self.selectedFiles()), "")
        filter = self.selectedNameFilter()
        root, ext = os.path.splitext(path)

        if path and not ext:
            match = re.search(r'\*(\.[\w\.]+)', filter)
            if match:
                ext = match[1]
                path += ext

                if os.path.exists(path) and not self.askOverwrite(path):
                    return super().reject()

        return super().accept()

    def askOverwrite(self, path):
        """Replicate the popup asking to overwrite a file."""
        dirname, filename = os.path.split(path)

        msg_box = QtWidgets.QMessageBox(self)
        msg_box.setText(f'A file named "{filename}" already exists. Do you want to replace it?')
        msg_box.setInformativeText(f'The file already exists in "{os.path.basename(dirname)}". Replacing it will overwrite its contents.')
        msg_box.setStandardButtons(QtWidgets.QMessageBox.Cancel | QtWidgets.QMessageBox.Yes)
        msg_box.setDefaultButton(QtWidgets.QMessageBox.Cancel)
        return msg_box.exec_() == QtWidgets.QMessageBox.Yes


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("File exporter")
        button = QtWidgets.QPushButton("Export!")
        button.clicked.connect(self.onClicked)
        self.setCentralWidget(button)

    def save_file(self, filter):
        dialog = QFileDialog(self, "Save as...", "", filter=filter)
        dialog.setAcceptMode(QFileDialog.AcceptSave)
        dialog.setFileMode(QFileDialog.AnyFile)

        match = re.search(r'\*(\.[\w\.]+)', filter)
        if match:
            dialog.setDefaultSuffix(match[1])

        if dialog.exec_() == QFileDialog.Accepted:
            file = next(iter(dialog.selectedFiles()), None)
        else:
            print("rejected")
            file = None
        return file


    def onClicked(self):
        print("\nclicked!")
        file = self.save_file("Image files (*.png)")
        print("Export this", file)


if __name__ == "__main__":
    app = QtWidgets.QApplication([])
    window = MainWindow()
    window.show()
    app.exec_()

Edit: added minimal reproducible example.

0

There are 0 best solutions below