Repository: napari/napari

Issue:
[ipython] File > Save Screenshot... warns about replacing if extension is provided
## 🐛 Bug

If a napari viewer is launched from ipython and you use File > Save Screenshot... then everything works fine if you just enter a file name, **but if you specify the extension**—out of habit or to change the file format—then napari will warn that the file already exists, even if it does not.
Regardless of choice, the file is created and everything works.

Looking carefully, when one clicks Save the screen flashes, the file appears in the list in the dialog, and the warning pops up--the dialog remains up.
It's like the file is created and then it tries a second time when it goes to close the dialog.

Edit:
On the other hand, *if you pass a filename that exists,* **but without extension**, it will correctly warn that the file exists, but if say `no` to overwriting, the dialog closes with: `WARNING: QDialog::exec: Recursive call detected`
If you *do pass a full name with extension*, it will correctly warn that the file exists, and saying `no` will return you to the dialog to modify the name—as expected.

## To Reproduce

Steps to reproduce the behavior:

1. launch ipython and use 
```
import napari
viewer = napari.Viewer()
```
3. open any sample image or add any layer
4. File > Save Screenshot...
5. enter a file name **with an extension** but make sure it's a unique name.

## Expected behavior

No warning should be raised if the file doesn't exist, the dialog should just disappear and the file should be created.
If the warning is raised and the user clicks `no` to not overwrite, the dialog should remain open so the user can change the name.

## Environment

```
napari: 0.4.18
Platform: macOS-13.4.1-arm64-arm-64bit
System: MacOS 13.4.1
Python: 3.10.12 | packaged by conda-forge | (main, Jun 23 2023, 22:41:52) [Clang 15.0.7 ]
Qt: 5.15.6
PyQt5: 5.15.7
NumPy: 1.25.1
SciPy: 1.11.1
Dask: 2023.7.0
VisPy: 0.12.2
magicgui: 0.7.2
superqt: unknown
in-n-out: 0.1.8
app-model: 0.2.0
npe2: 0.7.0

OpenGL:
- GL version: 2.1 Metal - 83.1
- MAX_TEXTURE_SIZE: 16384

Screens:
- screen 1: resolution 1800x1169, scale 2.0

Settings path:
- /Users/sobolp/Library/Application Support/napari/napari-418_d279d6cf5d4193876ed97858e757fe322037331b/settings.yaml

```

## Additional context

Other dialogs, like Save Selected Layer work fine.
Using the native dialog (when running napari from the command line) does not have this issue.


Discussion:

Resolving patch:
diff --git a/napari/_qt/dialogs/screenshot_dialog.py b/napari/_qt/dialogs/screenshot_dialog.py
--- a/napari/_qt/dialogs/screenshot_dialog.py
+++ b/napari/_qt/dialogs/screenshot_dialog.py
@@ -62,8 +62,9 @@ def accept(self):
                     QMessageBox.No,
                 )
                 if res != QMessageBox.Yes:
-                    # standard accept return 1, reject 0. This inform that dialog should be reopened
-                    super().accept()
-                    self.exec_()
-        self.save_function(save_path)
-        return super().accept()
+                    # return in this case since a valid name for the
+                    # file is needed so the dialog needs to be visible
+                    return
+        super().accept()
+        if self.result():
+            self.save_function(save_path)


Test patch:
diff --git a/napari/_qt/dialogs/_tests/test_screenshot_dialog.py b/napari/_qt/dialogs/_tests/test_screenshot_dialog.py
new file mode 100644
--- /dev/null
+++ b/napari/_qt/dialogs/_tests/test_screenshot_dialog.py
@@ -0,0 +1,94 @@
+import pytest
+from qtpy.QtCore import QTimer
+from qtpy.QtWidgets import QApplication, QFileDialog, QLineEdit, QMessageBox
+
+from napari._qt.dialogs.screenshot_dialog import ScreenshotDialog
+
+
+@pytest.mark.parametrize("filename", ["test", "test.png", "test.tif"])
+def test_screenshot_save(qtbot, tmp_path, filename):
+    """Check passing different extensions with the filename."""
+
+    def save_function(path):
+        # check incoming path has extension event when a filename without one
+        # was provided
+        assert filename in path
+        assert "." in filename or ".png" in path
+
+        # create a file with the given path to check for
+        # non-native qt overwrite message
+        with open(path, "w") as mock_img:
+            mock_img.write("")
+
+    qt_overwrite_shown = False
+
+    def qt_overwrite_qmessagebox_warning():
+        for widget in QApplication.topLevelWidgets():
+            if isinstance(widget, QMessageBox):  # pragma: no cover
+                # test should not enter here!
+                widget.accept()
+                nonlocal qt_overwrite_shown
+                qt_overwrite_shown = True
+                break
+
+    # setup dialog
+    dialog = ScreenshotDialog(
+        save_function, directory=str(tmp_path), history=[]
+    )
+    qtbot.addWidget(dialog)
+    dialog.setOptions(QFileDialog.DontUseNativeDialog)
+    dialog.show()
+
+    # check dialog and set filename
+    assert dialog.windowTitle() == 'Save screenshot'
+    line_edit = dialog.findChild(QLineEdit)
+    line_edit.setText(filename)
+
+    # check that no warning message related with overwriting is shown
+    QTimer.singleShot(100, qt_overwrite_qmessagebox_warning)
+    dialog.accept()
+    qtbot.wait(120)
+    assert not qt_overwrite_shown, "Qt non-native overwrite message was shown!"
+
+    # check the file was created
+    save_filename = filename if '.' in filename else f'{filename}.png'
+    qtbot.waitUntil((tmp_path / save_filename).exists)
+
+
+def test_screenshot_overwrite_save(qtbot, tmp_path, monkeypatch):
+    """Check overwriting file validation."""
+    (tmp_path / "test.png").write_text("")
+
+    def save_function(path):
+        assert "test.png" in path
+        (tmp_path / "test.png").write_text("overwritten")
+
+    def overwrite_qmessagebox_warning(*args):
+        box, parent, title, text, buttons, default = args
+        assert parent == dialog
+        assert title == "Confirm overwrite"
+        assert "test.png" in text
+        assert "already exists. Do you want to replace it?" in text
+        assert buttons == QMessageBox.Yes | QMessageBox.No
+        assert default == QMessageBox.No
+
+        return QMessageBox.Yes
+
+    # monkeypath custom overwrite QMessageBox usage
+    monkeypatch.setattr(QMessageBox, "warning", overwrite_qmessagebox_warning)
+
+    dialog = ScreenshotDialog(
+        save_function, directory=str(tmp_path), history=[]
+    )
+    qtbot.addWidget(dialog)
+    dialog.setOptions(QFileDialog.DontUseNativeDialog)
+    dialog.show()
+
+    # check dialog, set filename and trigger accept logic
+    assert dialog.windowTitle() == 'Save screenshot'
+    line_edit = dialog.findChild(QLineEdit)
+    line_edit.setText("test")
+    dialog.accept()
+
+    # check the file was overwritten
+    assert (tmp_path / "test.png").read_text() == "overwritten"