From a8cdd79ab929b143d672cf11221dd4634a676550 Mon Sep 17 00:00:00 2001 From: Rebecca Breu Date: Sun, 28 Mar 2021 20:32:33 +0200 Subject: [PATCH] Progress Dialogs for loading and saving --- beeref/fileio/__init__.py | 8 +++---- beeref/fileio/sql.py | 28 ++++++++++++++++++---- beeref/gui.py | 11 +++++++++ beeref/items.py | 8 ++++--- beeref/scene.py | 4 ++++ beeref/view.py | 49 +++++++++++++++++++++------------------ tests/fileio/test_sql.py | 30 +++++++++++++++++++++++- tests/test_items.py | 1 + tests/test_scene.py | 16 ++++++++++++- 9 files changed, 119 insertions(+), 36 deletions(-) diff --git a/beeref/fileio/__init__.py b/beeref/fileio/__init__.py index ed2b61f..8e92ac1 100644 --- a/beeref/fileio/__init__.py +++ b/beeref/fileio/__init__.py @@ -23,15 +23,15 @@ __all__ = ['load', 'save', 'BeeFileIOError'] logger = logging.getLogger('BeeRef') -def load(filename, scene): +def load(filename, scene, progress=None): logger.info(f'Loading from file {filename}...') - io = SQLiteIO(filename, scene, readonly=True) + io = SQLiteIO(filename, scene, readonly=True, progress=progress) return io.read() -def save(filename, scene, create_new=False): +def save(filename, scene, create_new=False, progress=None): logger.info(f'Saving to file {filename}...') logger.debug(f'Create new: {create_new}') - io = SQLiteIO(filename, scene, create_new) + io = SQLiteIO(filename, scene, create_new, progress=progress) io.write() logger.debug('Saved!') diff --git a/beeref/fileio/sql.py b/beeref/fileio/sql.py index 518b794..08c1892 100644 --- a/beeref/fileio/sql.py +++ b/beeref/fileio/sql.py @@ -52,11 +52,13 @@ class SQLiteIO: USER_VERSION = 1 APPLICATION_ID = 2060242126 - def __init__(self, filename, scene, create_new=False, readonly=False): + def __init__(self, filename, scene, create_new=False, readonly=False, + progress=None): self.scene = scene self.create_new = create_new self.filename = filename self.readonly = readonly + self.progress = progress def __del__(self): self._close_connection() @@ -74,6 +76,9 @@ class SQLiteIO: and os.path.exists(self.filename)): os.remove(self.filename) + if self.create_new: + self.scene.clear_save_ids() + if self.readonly: self._connection = sqlite3.connect( f'file:{self.filename}?mode=ro') @@ -123,13 +128,20 @@ class SQLiteIO: 'SELECT pos_x, pos_y, scale, filename, sqlar.data, items.id ' 'FROM items ' 'INNER JOIN sqlar on sqlar.item_id = items.id') - for row in rows: + if self.progress: + self.progress.setMaximum(len(rows)) + + for i, row in enumerate(rows): item = BeePixmapItem(QtGui.QImage(), filename=row[3]) item.save_id = row[5] item.pixmap_from_bytes(row[4]) item.setPos(row[0], row[1]) item.setScale(row[2]) self.scene.addItem(item) + if self.progress: + self.progress.setValue(i) + if self.progress.wasCanceled(): + break @handle_sqlite_errors def write(self): @@ -149,12 +161,20 @@ class SQLiteIO: def write_data(self): to_delete = self.fetchall('SELECT id from ITEMS') - for item in self.scene.items_for_save(): - if item.save_id and not self.create_new: + to_save = list(self.scene.items_for_save()) + if self.progress: + self.progress.setMaximum(len(to_save)) + for i, item in enumerate(to_save): + logger.debug(f'Saving {item} with id {item.save_id}') + if item.save_id: self.update_item(item) to_delete.remove((item.save_id,)) else: self.insert_item(item) + if self.progress: + self.progress.setValue(i) + if self.progress.wasCanceled(): + break self.delete_items(to_delete) self.connection.commit() diff --git a/beeref/gui.py b/beeref/gui.py index 26e9915..4fd1afa 100644 --- a/beeref/gui.py +++ b/beeref/gui.py @@ -40,3 +40,14 @@ class WelcomeOverlay(QtWidgets.QWidget): layout = QtWidgets.QVBoxLayout() layout.addWidget(label) self.setLayout(layout) + + +class BeeProgressDialog(QtWidgets.QProgressDialog): + + def __init__(self, label, maximum=100, parent=None): + super().__init__(label, 'Cancel', 0, maximum - 1, parent=parent) + self.setMinimumDuration(2) + self.setWindowModality(Qt.WindowModality.WindowModal) + + def setMaximum(self, maximum): + super().setMaximum(maximum - 1) diff --git a/beeref/items.py b/beeref/items.py index 0f5e3e9..00eb8cc 100644 --- a/beeref/items.py +++ b/beeref/items.py @@ -32,17 +32,19 @@ class BeePixmapItem(QtWidgets.QGraphicsPixmapItem): def __init__(self, image, filename=None): super().__init__(QtGui.QPixmap.fromImage(image)) - logger.debug(f'Initialized image "{filename}" with dimensions: ' - f'{self.width} x {self.height} at index {self.zValue()}') - self.save_id = None self.filename = filename self.scale_factor = 1 + logger.debug(f'Initialized {self}') self.setFlags( QtWidgets.QGraphicsItem.GraphicsItemFlags.ItemIsMovable | QtWidgets.QGraphicsItem.GraphicsItemFlags.ItemIsSelectable) + def __str__(self): + return (f'Image "{self.filename}" ' + f'with dimensions {self.width} x {self.height}') + def setScale(self, factor): if factor <= 0: return diff --git a/beeref/scene.py b/beeref/scene.py index 6c6b243..5348070 100644 --- a/beeref/scene.py +++ b/beeref/scene.py @@ -119,3 +119,7 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene): """ return filter(lambda i: hasattr(i, 'save_id'), self.items()) + + def clear_save_ids(self): + for item in self.items_for_save(): + item.save_id = None diff --git a/beeref/view.py b/beeref/view.py index fba995a..fddcce9 100644 --- a/beeref/view.py +++ b/beeref/view.py @@ -20,7 +20,7 @@ from PyQt6.QtCore import Qt from beeref import commands from beeref import fileio -from beeref.gui import WelcomeOverlay +from beeref.gui import BeeProgressDialog, WelcomeOverlay from beeref.items import BeePixmapItem from beeref.scene import BeeGraphicsScene @@ -235,22 +235,13 @@ class BeeGraphicsView(QtWidgets.QGraphicsView): def on_action_normalize_size(self): self.scene.normalize_size() - def add_images(self, images): - if isinstance(images, QtGui.QImage): - images = [images] - items = [] - for img in images: - item = BeePixmapItem(img, getattr(img, 'filename', None)) - item.set_pos_center(img.pos.x(), img.pos.y()) - items.append(item) - self.undo_stack.push(commands.InsertItems(self.scene, items)) - def open_from_file(self, filename): logger.info(f'Opening file {filename}') self.scene.clear() self.undo_stack.clear() try: - fileio.load(filename, self.scene) + progress = BeeProgressDialog('Save file...', parent=self) + fileio.load(filename, self.scene, progress) self.filename = filename except fileio.BeeFileIOError: QtWidgets.QMessageBox.warning( @@ -277,7 +268,9 @@ class BeeGraphicsView(QtWidgets.QGraphicsView): if not filename.endswith('.bee'): filename = f'{filename}.bee' try: - fileio.save(filename, self.scene, create_new=True) + progress = BeeProgressDialog('Save file...', parent=self) + fileio.save(filename, self.scene, create_new=True, + progress=progress) self.filename = filename except fileio.BeeFileIOError: QtWidgets.QMessageBox.warning( @@ -290,7 +283,9 @@ class BeeGraphicsView(QtWidgets.QGraphicsView): if not self.filename: self.on_action_save_as() else: - fileio.save(self.filename, self.scene, create_new=False) + progress = BeeProgressDialog('Save file...', parent=self) + fileio.save(self.filename, self.scene, create_new=False, + progress=progress) def on_action_quit(self): logger.info('User quit. Exiting...') @@ -305,19 +300,26 @@ class BeeGraphicsView(QtWidgets.QGraphicsView): pos = self.mapToScene(self.get_view_center()) errors = [] - images = [] - for filename in filenames: + items = [] + progress = BeeProgressDialog( + 'Loading images...', len(filenames), parent=self) + + for i, filename in enumerate(filenames): logger.info(f'Loading image from file {filename}') img = QtGui.QImage(filename) if img.isNull(): errors.append(filename) continue - img.pos = pos - img.filename = filename - images.append(img) + item = BeePixmapItem(img, filename) + item.set_pos_center(pos.x(), pos.y()) + items.append(item) pos.setX(pos.x() + 50) pos.setY(pos.y() + 50) - self.add_images(images) + progress.setValue(i) + if progress.wasCanceled(): + break + + self.undo_stack.push(commands.InsertItems(self.scene, items)) if errors: errornames = [ @@ -339,9 +341,10 @@ class BeeGraphicsView(QtWidgets.QGraphicsView): logger.info('No image data in clipboard') else: self.scene.clearSelection() - img.pos = self.mapToScene( - self.mapFromGlobal(self.cursor().pos())) - self.add_images(img) + item = BeePixmapItem(img) + pos = self.mapToScene(self.mapFromGlobal(self.cursor().pos())) + item.set_pos_center(pos.x(), pos.y()) + self.undo_stack.push(commands.InsertItems(self.scene, [item])) def on_selection_changed(self): logger.debug('Currently selected items: %s', diff --git a/tests/fileio/test_sql.py b/tests/fileio/test_sql.py index 652a097..647d44f 100644 --- a/tests/fileio/test_sql.py +++ b/tests/fileio/test_sql.py @@ -15,7 +15,8 @@ from ..base import BeeTestCase class SQLiteIOTestCase(BeeTestCase): def setUp(self): - self.io = SQLiteIO(':memory:', None, create_new=True) + self.scene_mock = MagicMock() + self.io = SQLiteIO(':memory:', self.scene_mock, create_new=True) def test_แบrite_meta_application_id(self): self.io.write_meta() @@ -38,6 +39,7 @@ class SQLiteIOTestCase(BeeTestCase): 'SELECT COUNT(*) FROM sqlite_master ' 'WHERE type="table" AND name NOT LIKE "sqlite_%"') assert result[0] == 2 + self.scene_mock.clear_save_ids.assert_called_once() def test_create_schema_on_new_when_not_create_new(self): self.io.create_new = False @@ -46,6 +48,7 @@ class SQLiteIOTestCase(BeeTestCase): 'SELECT COUNT(*) FROM sqlite_master ' 'WHERE type="table" AND name NOT LIKE "sqlite_%"') assert result[0] == 0 + self.scene_mock.clear_save_ids.assert_not_called() def test_readonly_doesnt_allow_write(self): scene = BeeGraphicsScene(None) @@ -172,6 +175,16 @@ class SQLiteIOWriteTestCase(BeeTestCase): result = io.fetchone('SELECT COUNT(*) FROM items') assert result[0] == 1 + def test_updates_progress(self): + progress = MagicMock() + io = SQLiteIO(':memory:', self.scene, create_new=True, + progress=progress) + item = BeePixmapItem(QtGui.QImage()) + self.scene.addItem(item) + io.write() + progress.setMaximum.assert_called_once_with(1) + progress.setValue.assert_called_once_with(0) + class SQLiteIOReadTestCase(BeeTestCase): @@ -203,6 +216,21 @@ class SQLiteIOReadTestCase(BeeTestCase): assert item.width == 3 assert item.height == 3 + def test_updates_progress(self): + progress = MagicMock() + io = SQLiteIO(':memory:', self.scene, create_new=True, + progress=progress) + + io.create_schema_on_new() + io.ex('INSERT INTO items (type, pos_x, pos_y, scale, filename) ' + 'VALUES (?, ?, ?, ?, ?) ', + ('pixmap', 0, 0, 1, 'bee.png')) + io.ex('INSERT INTO sqlar (item_id, data) VALUES (?, ?)', (1, b'')) + io.connection.commit() + io.read() + progress.setMaximum.assert_called_once_with(1) + progress.setValue.assert_called_once_with(0) + def test_raises_error_when_file_borked(self): with tempfile.TemporaryDirectory() as dirname: fname = os.path.join(dirname, 'test.bee') diff --git a/tests/test_items.py b/tests/test_items.py index c898c75..250ff81 100644 --- a/tests/test_items.py +++ b/tests/test_items.py @@ -11,6 +11,7 @@ class BeePixmapItemTestCase(BeeTestCase): def test_init(self): item = BeePixmapItem( QtGui.QImage(self.imgfilename3x3), self.imgfilename3x3) + assert item.save_id is None assert item.width == 3 assert item.height == 3 assert item.scale_factor == 1 diff --git a/tests/test_scene.py b/tests/test_scene.py index e09ac2f..a36e1e7 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -1,8 +1,9 @@ import math from unittest.mock import patch, MagicMock -from PyQt6 import QtGui +from PyQt6 import QtGui, QtWidgets +from beeref.items import BeePixmapItem from beeref.scene import BeeGraphicsScene from .base import BeeTestCase @@ -54,3 +55,16 @@ class BeeGraphicsSceneNormalizeTestCase(BeeTestCase): def test_normalize_size_when_no_items(self): self.scene.normalize_size() + + def test_clear_save_ids(self): + item1 = BeePixmapItem(QtGui.QImage()) + item1.save_id = 5 + self.scene.addItem(item1) + item2 = BeePixmapItem(QtGui.QImage()) + self.scene.addItem(item2) + item3 = QtWidgets.QGraphicsRectItem() + self.scene.clear_save_ids() + self.scene.addItem(item3) + assert item1.save_id is None + assert item2.save_id is None + assert hasattr(item3, 'save_id') is False