0

What I want to do

I am trying to make an interactive plot for a Jupyter Notebook. The functions are all written in different files, but their intended use is in interactive notebook sessions. I have a Button widget on a matplotlib figure, which, when clicked, I want to open a file dialog where a user can enter a filename to save the figure to. I am on Mac OSX (Mojave 10.14.6) and Tkinter is giving me major problems (complete system crashes), so I am trying to implement this with PyQt5.

The code

-----------
plotting.py
-----------
from . import file_dialog as fdo
import matplotlib.pyplot as plt
import matplotlib.widgets as wdgts

def plot_stack(stack):
    fig, ax = plt.subplots(figsize=(8, 6))
    plt.subplots_adjust(bottom=0.25, left=-0.1)

    ...  # plotting happens here

    # button for saving
    def dosaveframe(event):
        fname = fdo.save()
        fig.savefig(fname) # to be changed to something more appropriate

    savea = plt.axes([0.65, 0.8, 0.15, 0.05], facecolor=axcolor)
    saveb = Button(savea, "save frame", hovercolor="yellow")
    saveb.on_clicked(dosaveframe)
    savea._button = saveb  # for persistence

    plt.show()

--------------
file_dialog.py
--------------
import sys
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import (QWidget, QFileDialog)

class SaveFileDialog(QWidget):

    def __init__(self, text="Save file", types="All Files (*)"):
        super().__init__()
        self.title = text
        self.setWindowTitle(self.title)
        self.types = types
        self.filename = self.saveFileDialog()
        self.show()

    def saveFileDialog(self):
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        filename, _ = (
            QFileDialog.getSaveFileName(self, "Enter filename",
                                        self.types, options=options))
        return filename

def save(directory='./', filters="All files (*)"):
    """Open a save file dialog"""
    app = QApplication([directory])
    ex = SaveFileDialog(types=filters)
    return ex.filename
    sys.exit(app.exec_())

What is not working

The save dialog opens and it responds to the mouse, but not to the keyboard. The keyboard stays connected to the notebook no matter if I select the little window, so when I press "s" it saves the notebook. As such, the user can not enter a file path. How can I make this work? I have Anaconda, PyQt 5.9.2, matplotlib 3.1.1, jupyter 1.0.0.

DIN14970
  • 341
  • 2
  • 8
  • Another user just posted [this question](https://stackoverflow.com/questions/60977801) with a very similar issue (the question was different, but the provided code was wrong as this is). I don't use Jupyter, so I can't promise that it will solve your issue, but despite that you definitely should **not** create a QWidget to show a QFileDialog, nor you should wait for a blocking function like `getSaveFileName` in an `__init__` (which might be the source of your problem): just call that function to get the path. Also, the `sys.exit` at the end is completely useless, since it's after the `return`. – musicamante Apr 01 '20 at 19:56
  • Thanks for taking the time to reply to my question. I should have mentioned that my solution with the widget is already a workaround: the direct simple way as described in your link I tried earlier. In this case, I thought the file dialog didn't open at all. Then I discovered the file dialog is opened behind the browser window. In both methods, I can not type in the file dialog. At least I already discovered that it's not behavior unique to jupyter notebook: if I call `save` from the command line it also opens a file dialog in which I can't type. Could it be a Qt bug on Mac? – DIN14970 Apr 02 '20 at 08:12
  • Jupyter is a webapp. tkinter and Qt are stand alone applications and they don't really work together with Jupyter very well. I think you can do what you are trying to do inside Jupyter using ipywidgets. There is no reason to launch a subprocess from Jupyter to run a separate stand alone graphical interface in either QT or tkinter. – Aaron Watters Apr 19 '20 at 12:12

1 Answers1

0

I found a very crappy, non-clean solution but it seems to work. For some reason, opening a QFileDialog directly does not allow me to activate it. It opens up behind the active window from where it was called (terminal window or browser in Jupyter Notebook) and does not respond to the keyboard. So the save function in the following block does NOT work as expected on Mac:

from PyQt5.QtWidgets import QApplication, QFileDialog

def save(directory='./', filters="All files (*)"):
    app = QApplication([directory])
    path, _ = QFileDialog.getSaveFileName(caption="Save to file",
                                          filter=filters,
                                          options=options)
    return path

What does work is if the file dialog is opened from a widget. So working with a dummy widget that never shows up on the screen does work for me, at least from the command line:

from PyQt5.QtWidgets import (QApplication, QFileDialog, QWidget)


class DummySaveFileDialogWidget(QWidget):

    def __init__(self, title="Save file", filters="All Files (*)"):
        super().__init__()
        self.title = title
        self.filters = filters
        self.fname = self.savefiledialog()

    def savefiledialog(self):
        filename, _ = QFileDialog.getSaveFileName(caption=self.title,
                                                  filter=self.filters,
                                                  options=options)
        return filename

def save(directory='./', filters="All files (*)"):
    app = QApplication([directory])
    form = DummySaveFileDialogWidget()
    return form.fname

If anyone finds a more elegant solution that works let me know


EDIT: this works when it is called from the command line, but still not from a Jupyter Notebook. Also tried this, no success. The file dialog stays behind the browser window and does not respond to the keyboard.

DIN14970
  • 341
  • 2
  • 8