Skip to content

Commit

Permalink
Add confirmation dialog when discarding an unsaved file
Browse files Browse the repository at this point in the history
  • Loading branch information
rbreu committed May 20, 2024
1 parent a89027f commit aac2d0e
Show file tree
Hide file tree
Showing 13 changed files with 270 additions and 36 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
pyqt-version: ["6.5.3", "6.7.0"]
pyqt-version: ["6.7.0"]
steps:
- uses: actions/checkout@v4
- name: Set up Python
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ Added
QT_IMAGEIO_MAXALLOC
* Display error messages when images can't be loaded from bee files
* Added option to export all images from scene (File -> Export Images)
* Added a confirmation dialog when attempting to close unsaved files.
The confirmation dialog can be disalbed in:
Settings -> Miscellaneous -> Confirm when closing an unsaved file


Fixed
Expand Down
2 changes: 1 addition & 1 deletion beeref/actions/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ def get_default_shortcut(self, index):
id='new_scene',
text='&New Scene',
shortcuts=['Ctrl+N'],
callback='clear_scene',
callback='on_action_new_scene',
),
Action(
id='fit_scene',
Expand Down
2 changes: 1 addition & 1 deletion beeref/actions/mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def _build_recent_files(self, menu=None):
qaction = QtGui.QAction(os.path.basename(filename), self)
qaction.setShortcuts(action.get_shortcuts())
qaction.triggered.connect(
partial(self.open_from_file, filename))
partial(self.on_action_open_recent_file, filename))
self.addAction(qaction)
action.qaction = qaction
self._recent_files_submenu.addAction(qaction)
Expand Down
4 changes: 4 additions & 0 deletions beeref/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ class BeeSettingsEvents(QtCore.QObject):
class BeeSettings(QtCore.QSettings):

FIELDS = {
'Save/confirm_close_unsaved': {
'default': True,
'cast': bool,
},
'Items/image_storage_format': {
'default': 'best',
'validate': lambda x: x in ('png', 'jpg', 'best'),
Expand Down
2 changes: 1 addition & 1 deletion beeref/fileio/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

IMG_LOADING_ERROR_MSG = (
'Unknown format or too big?\n'
'Check Settings -> Miscellaneous -> Maximum Image Size')
'Check Settings -> Images & Items -> Maximum Image Size')


class BeeFileIOError(Exception):
Expand Down
40 changes: 38 additions & 2 deletions beeref/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,26 @@ def fit_rect(self, rect, toggle_item=None):
self.fitInView(rect, Qt.AspectRatioMode.KeepAspectRatio)
logger.trace('Fit view done')

def get_confirmation_unsaved_changes(self, msg):
confirm = self.settings.valueOrDefault('Save/confirm_close_unsaved')
if confirm and not self.undo_stack.isClean():
answer = QtWidgets.QMessageBox.question(
self,
'Discard unsaved changes?',
msg,
QtWidgets.QMessageBox.StandardButton.Yes |
QtWidgets.QMessageBox.StandardButton.Cancel)
return answer == QtWidgets.QMessageBox.StandardButton.Yes

return True

def on_action_new_scene(self):
confirm = self.get_confirmation_unsaved_changes(
'There are unsaved changes. '
'Are you sure you want to open a new scene?')
if confirm:
self.clear_scene()

def on_action_fit_scene(self):
self.fit_rect(self.scene.itemsBoundingRect())

Expand Down Expand Up @@ -388,6 +408,13 @@ def on_loading_finished(self, filename, errors):
self.scene.add_queued_items()
self.on_action_fit_scene()

def on_action_open_recent_file(self, filename):
confirm = self.get_confirmation_unsaved_changes(
'There are unsaved changes. '
'Are you sure you want to open a new scene?')
if confirm:
self.open_from_file(filename)

def open_from_file(self, filename):
logger.info(f'Opening file {filename}')
self.clear_scene()
Expand All @@ -402,6 +429,12 @@ def open_from_file(self, filename):
self.worker.start()

def on_action_open(self):
confirm = self.get_confirmation_unsaved_changes(
'There are unsaved changes. '
'Are you sure you want to open a new scene?')
if not confirm:
return

self.cancel_active_modes()
filename, f = QtWidgets.QFileDialog.getOpenFileName(
parent=self,
Expand Down Expand Up @@ -528,8 +561,11 @@ def on_export_images_file_exists(self, filename):
self.worker.start()

def on_action_quit(self):
logger.info('User quit. Exiting...')
self.app.quit()
confirm = self.get_confirmation_unsaved_changes(
'There are unsaved changes. Are you sure you want to quit?')
if confirm:
logger.info('User quit. Exiting...')
self.app.quit()

def on_action_settings(self):
widgets.settings.SettingsDialog(self)
Expand Down
80 changes: 58 additions & 22 deletions beeref/widgets/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import logging

from PyQt6 import QtWidgets
from PyQt6.QtCore import Qt

from beeref import constants
from beeref.config import BeeSettings, settings_events
Expand All @@ -26,6 +27,9 @@


class GroupBase(QtWidgets.QGroupBox):
TITLE = None
HELPTEXT = None
KEY = None

def __init__(self):
super().__init__()
Expand All @@ -50,16 +54,24 @@ def on_value_changed(self, value):
if self.ignore_value_changed:
return

value = self.convert_value_from_qt(value)
if value != self.settings.valueOrDefault(self.KEY):
logger.debug(f'Setting {self.KEY} changed to: {value}')
self.settings.setValue(self.KEY, value)
self.update_title()

def convert_value_from_qt(self, value):
return value

def on_restore_defaults(self):
new_value = self.settings.valueOrDefault(self.KEY)
self.ignore_value_changed = True
self.set_value(new_value)
self.ignore_value_changed = False
self.update_title()


class RadioGroup(GroupBase):
TITLE = None
HELPTEXT = None
KEY = None
OPTIONS = None

def __init__(self):
Expand All @@ -79,38 +91,46 @@ def __init__(self):
self.ignore_value_changed = False
self.layout.addStretch(100)

def on_restore_defaults(self):
new_value = self.settings.valueOrDefault(self.KEY)
self.ignore_value_changed = True
for value, btn in self.buttons.items():
btn.setChecked(value == new_value)
self.ignore_value_changed = False
self.update_title()
def set_value(self, value):
for old_value, btn in self.buttons.items():
btn.setChecked(old_value == value)


class IntegerGroup(GroupBase):
TITLE = None
HELPTEXT = None
KEY = None
MIN = None
MAX = None

def __init__(self):
super().__init__()
self.input = QtWidgets.QSpinBox()
self.input.setRange(self.MIN, self.MAX)
self.input.setValue(self.settings.valueOrDefault(self.KEY))
self.set_value(self.settings.valueOrDefault(self.KEY))
self.input.valueChanged.connect(self.on_value_changed)
self.layout.addWidget(self.input)
self.layout.addStretch(100)
self.ignore_value_changed = False

def on_restore_defaults(self):
new_value = self.settings.valueOrDefault(self.KEY)
self.ignore_value_changed = True
self.input.setValue(new_value)
def set_value(self, value):
self.input.setValue(value)


class SingleCheckboxGroup(GroupBase):
LABEL = None

def __init__(self):
super().__init__()
self.input = QtWidgets.QCheckBox(self.LABEL)
self.set_value(self.settings.valueOrDefault(self.KEY))
self.input.checkStateChanged.connect(self.on_value_changed)
self.layout.addWidget(self.input)
self.layout.addStretch(100)
self.ignore_value_changed = False
self.update_title()

def set_value(self, value):
self.input.setChecked(value)

def convert_value_from_qt(self, value):
return value == Qt.CheckState.Checked


class ImageStorageFormatWidget(RadioGroup):
Expand Down Expand Up @@ -144,6 +164,15 @@ class AllocationLimitWidget(IntegerGroup):
MAX = 10000


class ConfirmCloseUnsavedWidget(SingleCheckboxGroup):
TITLE = 'Confirm when closing an unsaved file:'
HELPTEXT = (
'When about to close an unsaved file, should BeeRef ask for '
'confirmation?')
LABEL = 'Confirm when closing'
KEY = 'Save/confirm_close_unsaved'


class SettingsDialog(QtWidgets.QDialog):
def __init__(self, parent):
super().__init__(parent)
Expand All @@ -154,11 +183,18 @@ def __init__(self, parent):
misc = QtWidgets.QWidget()
misc_layout = QtWidgets.QGridLayout()
misc.setLayout(misc_layout)
misc_layout.addWidget(ImageStorageFormatWidget(), 0, 0)
misc_layout.addWidget(ArrangeGapWidget(), 0, 1)
misc_layout.addWidget(AllocationLimitWidget(), 1, 0)
misc_layout.addWidget(ConfirmCloseUnsavedWidget(), 0, 0)
tabs.addTab(misc, '&Miscellaneous')

# Images & Items
items = QtWidgets.QWidget()
items_layout = QtWidgets.QGridLayout()
items.setLayout(items_layout)
items_layout.addWidget(ImageStorageFormatWidget(), 0, 0)
items_layout.addWidget(ArrangeGapWidget(), 0, 1)
items_layout.addWidget(AllocationLimitWidget(), 1, 0)
tabs.addTab(items, '&Images && Items')

layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
layout.addWidget(tabs)
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ requires-python = ">=3.9,<3.13"
dependencies = [
"exif>=1.3.5,<=1.6.0",
"lxml==5.1.0",
"pyQt6-Qt6>=6.5.3,<=6.7.0",
"pyQt6>=6.5.0,<=6.7.0",
"pyQt6-Qt6>=6.7.0,<=6.7.0",
"pyQt6>=6.7.0,<=6.7.0",
"rectangle-packer>=2.0.1,<=2.0.2",
]

Expand Down
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from setuptools import setup

setup()
2 changes: 1 addition & 1 deletion tests/actions/test_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def on_foo(self):
def on_bar(self):
pass

def open_from_file(self):
def on_action_open_recent_file(self, filename):
pass


Expand Down
Loading

0 comments on commit aac2d0e

Please sign in to comment.