diff --git a/beeref/commands.py b/beeref/commands.py
index d112a98..2383bb0 100644
--- a/beeref/commands.py
+++ b/beeref/commands.py
@@ -18,12 +18,16 @@ from PyQt6 import QtCore, QtGui
class InsertItems(QtGui.QUndoCommand):
- def __init__(self, scene, items):
+ def __init__(self, scene, items, ignore_first_redo=False):
super().__init__('Insert items')
self.scene = scene
self.items = items
+ self.ignore_first_redo = ignore_first_redo
def redo(self):
+ if self.ignore_first_redo:
+ self.ignore_first_redo = False
+ return
self.scene.clearSelection()
for item in self.items:
self.scene.addItem(item)
diff --git a/beeref/fileio/__init__.py b/beeref/fileio/__init__.py
index 52955bf..ccf7637 100644
--- a/beeref/fileio/__init__.py
+++ b/beeref/fileio/__init__.py
@@ -15,23 +15,86 @@
import logging
+
+from PyQt6 import QtCore, QtGui
+
+from beeref import commands
from beeref.fileio.errors import BeeFileIOError
from beeref.fileio.sql import SQLiteIO
+from beeref.items import BeePixmapItem
-__all__ = ['load', 'save', 'BeeFileIOError']
+__all__ = [
+ 'load_bee',
+ 'save_bee',
+ 'load_images',
+ 'ThreadedLoader',
+ 'BeeFileIOError',
+]
+
logger = logging.getLogger('BeeRef')
-def load(filename, scene, progress=None):
+def load_bee(filename, scene, worker=None):
+ """Load BeeRef native file."""
logger.info(f'Loading from file {filename}...')
- io = SQLiteIO(filename, scene, readonly=True, progress=progress)
+ io = SQLiteIO(filename, scene, readonly=True, worker=worker)
return io.read()
-def save(filename, scene, create_new=False, progress=None):
+def save_bee(filename, scene, create_new=False, worker=None):
+ """Save BeeRef native file."""
logger.info(f'Saving to file {filename}...')
logger.debug(f'Create new: {create_new}')
- io = SQLiteIO(filename, scene, create_new, progress=progress)
+ io = SQLiteIO(filename, scene, create_new, worker=worker)
io.write()
logger.info('Saved!')
+
+
+def load_images(filenames, pos, scene, worker):
+ """Add images to existing scene."""
+
+ errors = []
+ items = []
+ worker.begin_processing.emit(len(filenames))
+ for i, filename in enumerate(filenames):
+ logger.info(f'Loading image from file {filename}')
+ img = QtGui.QImage(filename)
+ worker.progress.emit(i)
+ if img.isNull():
+ errors.append(filename)
+ continue
+ item = BeePixmapItem(img, filename)
+ item.set_pos_center(pos)
+ pos.setX(pos.x() + 50)
+ pos.setY(pos.y() + 50)
+ scene.add_item_later(item, selected=True)
+ items.append(item)
+ if worker.canceled:
+ break
+
+ scene.undo_stack.push(
+ commands.InsertItems(scene, items, ignore_first_redo=True))
+ worker.finished.emit('', errors)
+
+
+class ThreadedIO(QtCore.QThread):
+ """Dedicated thread for loading and saving."""
+
+ progress = QtCore.pyqtSignal(int)
+ finished = QtCore.pyqtSignal(str, list)
+ begin_processing = QtCore.pyqtSignal(int)
+
+ def __init__(self, func, *args, **kwargs):
+ super().__init__()
+ self.func = func
+ self.args = args
+ self.kwargs = kwargs
+ self.kwargs['worker'] = self
+ self.canceled = False
+
+ def run(self):
+ self.func(*self.args, **self.kwargs)
+
+ def on_canceled(self):
+ self.canceled = True
diff --git a/beeref/fileio/sql.py b/beeref/fileio/sql.py
index a3de084..8a105fc 100644
--- a/beeref/fileio/sql.py
+++ b/beeref/fileio/sql.py
@@ -43,9 +43,10 @@ def handle_sqlite_errors(func):
func(self, *args, **kwargs)
except sqlite3.Error as e:
logger.exception(f'Error while reading/writing {self.filename}')
- if self.progress:
- self.progress.setValue(self.progress.maximum())
- raise BeeFileIOError(msg=str(e), filename=self.filename) from e
+ if self.worker:
+ self.worker.finished.emit('', [str(e)])
+ else:
+ raise BeeFileIOError(msg=str(e), filename=self.filename) from e
return wrapper
@@ -55,12 +56,12 @@ class SQLiteIO:
APPLICATION_ID = 2060242126
def __init__(self, filename, scene, create_new=False, readonly=False,
- progress=None):
+ worker=None):
self.scene = scene
self.create_new = create_new
self.filename = filename
self.readonly = readonly
- self.progress = progress
+ self.worker = worker
def __del__(self):
self._close_connection()
@@ -130,24 +131,27 @@ class SQLiteIO:
'SELECT items.id, x, y, z, scale, rotation, flip, filename, '
'sqlar.data '
'FROM items INNER JOIN sqlar on sqlar.item_id = items.id')
- if self.progress:
- self.progress.setMaximum(len(rows))
+ if self.worker:
+ self.worker.begin_processing.emit(len(rows))
for i, row in enumerate(rows):
item = BeePixmapItem(QtGui.QImage(), filename=row[7])
item.save_id = row[0]
item.pixmap_from_bytes(row[8])
item.setPos(row[1], row[2])
- self.scene.addItem(item)
+ self.scene.add_item_later(item)
item.setZValue(row[3])
item.setScale(row[4])
item.setRotation(row[5])
if row[6] == -1:
item.do_flip()
- if self.progress:
- self.progress.setValue(i)
- if self.progress.wasCanceled():
- break
+ if self.worker:
+ self.worker.progress.emit(i)
+ if self.worker.canceled:
+ self.worker.finished.emit('', [])
+ return
+ if self.worker:
+ self.worker.finished.emit(self.filename, [])
@handle_sqlite_errors
def write(self):
@@ -168,8 +172,8 @@ class SQLiteIO:
def write_data(self):
to_delete = self.fetchall('SELECT id from ITEMS')
to_save = list(self.scene.items_for_save())
- if self.progress:
- self.progress.setMaximum(len(to_save))
+ if self.worker:
+ self.worker.begin_processing.emit(len(to_save))
for i, item in enumerate(to_save):
logger.debug(f'Saving {item} with id {item.save_id}')
if item.save_id:
@@ -177,12 +181,14 @@ class SQLiteIO:
to_delete.remove((item.save_id,))
else:
self.insert_item(item)
- if self.progress:
- self.progress.setValue(i)
- if self.progress.wasCanceled():
+ if self.worker:
+ self.worker.progress.emit(i)
+ if self.worker.canceled:
break
self.delete_items(to_delete)
self.connection.commit()
+ if self.worker:
+ self.worker.finished.emit(self.filename, [])
def delete_items(self, to_delete):
self.exmany('DELETE FROM items WHERE id=?', to_delete)
diff --git a/beeref/gui.py b/beeref/gui.py
index 7abf2e7..f24e90d 100644
--- a/beeref/gui.py
+++ b/beeref/gui.py
@@ -44,10 +44,20 @@ class WelcomeOverlay(QtWidgets.QWidget):
class BeeProgressDialog(QtWidgets.QProgressDialog):
- def __init__(self, label, maximum=100, parent=None):
+ def __init__(self, label, worker, maximum=100, parent=None):
super().__init__(label, 'Cancel', 0, maximum, parent=parent)
- self.setMinimumDuration(2)
+ self.setMinimumDuration(0)
self.setWindowModality(Qt.WindowModality.WindowModal)
+ worker.begin_processing.connect(self.on_begin_processing)
+ worker.progress.connect(self.on_progress)
+ worker.finished.connect(self.on_finished)
+ self.canceled.connect(worker.on_canceled)
- def setValue(self, value):
- super().setValue(value + 1)
+ def on_progress(self, value):
+ self.setValue(value + 1)
+
+ def on_begin_processing(self, value):
+ self.setMaximum(value)
+
+ def on_finished(self, filename, errors):
+ self.close()
diff --git a/beeref/scene.py b/beeref/scene.py
index 0c5688b..5e0f9e6 100644
--- a/beeref/scene.py
+++ b/beeref/scene.py
@@ -13,6 +13,7 @@
# You should have received a copy of the GNU General Public License
# along with BeeRef. If not, see .
+from queue import Queue
import logging
import math
@@ -38,6 +39,7 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
self.rubberband_item = RubberbandItem()
self.selectionChanged.connect(self.on_selection_change)
self.changed.connect(self.on_change)
+ self.items_to_add = Queue()
def normalize_width_or_height(self, mode):
"""Scale the selected images to have the same width or height, as
@@ -205,3 +207,19 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
and not self.multi_select_item.rotate_active):
self.multi_select_item.fit_selection_area(
self.get_selection_rect())
+
+ def add_item_later(self, item, selected=False):
+ """Keep an item for adding later via ``add_delayed_items``"""
+
+ self.items_to_add.put((item, selected))
+
+ def add_delayed_items(self):
+ """Adds items added via ``add_items_later``"""
+
+ while not self.items_to_add.empty():
+ item, selected = self.items_to_add.get()
+ self.addItem(item)
+ if selected:
+ item.setSelected(True)
+ item.bring_to_front()
+ self.max_z = max(self.max_z, item.zValue())
diff --git a/beeref/selection.py b/beeref/selection.py
index 99ae1dc..38e5220 100644
--- a/beeref/selection.py
+++ b/beeref/selection.py
@@ -72,7 +72,8 @@ class BaseItemMixin:
def setZValue(self, value):
logger.debug(f'Setting z-value for {self} to {value}')
super().setZValue(value)
- self.scene().max_z = max(self.scene().max_z, value)
+ if self.scene():
+ self.scene().max_z = max(self.scene().max_z, value)
def bring_to_front(self):
self.setZValue(self.scene().max_z + 0.001)
@@ -536,7 +537,7 @@ class MultiSelectItem(SelectableMixin,
def fit_selection_area(self, rect):
"""Updates itself to fit the given selection area."""
- logging.debug(f'Fit selection area to {rect}')
+ logger.debug(f'Fit selection area to {rect}')
# Only update when values have changed, otherwise we end up in an
# infinite event loop sceneChange -> itemChange -> sceneChange ...
diff --git a/beeref/view.py b/beeref/view.py
index 15d83aa..fd365dc 100644
--- a/beeref/view.py
+++ b/beeref/view.py
@@ -78,7 +78,7 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
def on_scene_changed(self, region):
if not self.scene.items():
- logger.info('No items in scene')
+ logger.debug('No items in scene')
self.setTransform(QtGui.QTransform())
self.welcome_overlay.show()
else:
@@ -138,23 +138,32 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
def on_action_flip_vertically(self):
self.scene.flip_items(vertical=True)
- def open_from_file(self, filename):
- logger.info(f'Opening file {filename}')
- self.clear_scene()
- try:
- progress = BeeProgressDialog(
- 'Loading %s' % filename,
- parent=self)
- fileio.load(filename, self.scene, progress)
+ def on_items_loaded(self, value):
+ self.scene.add_delayed_items()
+
+ def on_loading_finished(self, filename, errors):
+ if filename:
self.filename = filename
- progress.close()
- except fileio.BeeFileIOError:
+ if errors:
QtWidgets.QMessageBox.warning(
self,
'Problem loading file',
('
Problem loading file %s
'
'Not accessible or not a proper bee file
') % filename)
+ def open_from_file(self, filename):
+ logger.info(f'Opening file {filename}')
+ self.clear_scene()
+ self.worker = fileio.ThreadedIO(
+ fileio.load_bee, filename, self.scene)
+ self.worker.progress.connect(self.on_items_loaded)
+ self.worker.finished.connect(self.on_loading_finished)
+ self.progress = BeeProgressDialog(
+ 'Loading %s' % filename,
+ worker=self.worker,
+ parent=self)
+ self.worker.start()
+
def on_action_open(self):
filename, f = QtWidgets.QFileDialog.getOpenFileName(
parent=self,
@@ -164,44 +173,59 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
self.open_from_file(filename)
self.filename = filename
+ def on_saving_finished(self, filename, errors):
+ if filename:
+ self.filename = filename
+ else:
+ QtWidgets.QMessageBox.warning(
+ self,
+ 'Problem saving file',
+ ('Problem saving file %s
'
+ 'File/directory not accessible
') % filename)
+
+ def do_save(self, filename, create_new):
+ if not filename.endswith('.bee'):
+ filename = f'{filename}.bee'
+ self.worker = fileio.ThreadedIO(
+ fileio.save_bee, filename, self.scene, create_new=create_new)
+ self.worker.finished.connect(self.on_saving_finished)
+ self.progress = BeeProgressDialog(
+ 'Saving %s' % filename,
+ worker=self.worker,
+ parent=self)
+ self.worker.start()
+
def on_action_save_as(self):
filename, f = QtWidgets.QFileDialog.getSaveFileName(
parent=self,
caption='Save file',
filter='BeeRef File (*.bee)')
if filename:
- if not filename.endswith('.bee'):
- filename = f'{filename}.bee'
- try:
- progress = BeeProgressDialog(
- 'Saving %s' % filename,
- parent=self)
- fileio.save(filename, self.scene, create_new=True,
- progress=progress)
- self.filename = filename
- progress.close()
- except fileio.BeeFileIOError:
- QtWidgets.QMessageBox.warning(
- self,
- 'Problem saving file',
- ('Problem saving file %s
'
- 'File/directory not accessible
') % filename)
+ self.do_save(filename, create_new=True)
def on_action_save(self):
if not self.filename:
self.on_action_save_as()
else:
- progress = BeeProgressDialog(
- 'Saving %s' % self.filename,
- parent=self)
- fileio.save(self.filename, self.scene, create_new=False,
- progress=progress)
- progress.close()
+ self.do_save(self.filename, create_new=False)
def on_action_quit(self):
logger.info('User quit. Exiting...')
self.app.quit()
+ def on_insert_images_finished(self, filename, errors):
+ if errors:
+ errornames = [
+ f'{fn}' for fn in errors]
+ errornames = '' % '\n'.join(errornames)
+ msg = ('{errors} image(s) out of {total} '
+ 'could not be opened:'.format(
+ errors=len(errors), total=len(errors)))
+ QtWidgets.QMessageBox.warning(
+ self,
+ 'Problem loading images',
+ msg + errornames)
+
def on_action_insert_images(self):
formats = self.get_supported_image_formats(QtGui.QImageReader)
filenames, f = QtWidgets.QFileDialog.getOpenFileNames(
@@ -209,42 +233,19 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
caption='Select one ore more images to open',
filter=f'Images ({formats})')
- pos = self.mapToScene(self.get_view_center())
- errors = []
- 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)
- progress.setValue(i)
- if progress.wasCanceled():
- break
- if img.isNull():
- errors.append(filename)
- continue
- item = BeePixmapItem(img, filename)
- item.set_pos_center(pos)
- items.append(item)
- pos.setX(pos.x() + 50)
- pos.setY(pos.y() + 50)
-
- progress.close()
- self.undo_stack.push(commands.InsertItems(self.scene, items))
-
- if errors:
- errornames = [
- f'{fn}' for fn in errors]
- errornames = '' % '\n'.join(errornames)
- msg = ('{errors} image(s) out of {total} '
- 'could not be opened:'.format(
- errors=len(errors), total=len(filenames)))
- QtWidgets.QMessageBox.warning(
- self,
- 'Problem loading images',
- msg + errornames)
+ self.scene.clearSelection()
+ self.worker = fileio.ThreadedIO(
+ fileio.load_images,
+ filenames,
+ self.mapToScene(self.get_view_center()),
+ self.scene)
+ self.worker.progress.connect(self.on_items_loaded)
+ self.worker.finished.connect(self.on_insert_images_finished)
+ self.progress = BeeProgressDialog(
+ 'Loading images',
+ worker=self.worker,
+ parent=self)
+ self.worker.start()
def on_action_paste(self):
logger.info('Pasting from clipboard...')
diff --git a/tests/base.py b/tests/base.py
index 19880ad..d00d486 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -20,6 +20,12 @@ class BeeTestCase(TestCase):
inst = QtWidgets.QApplication.instance()
cls.app = inst if inst else QtWidgets.QApplication([])
+ def queue2list(self, queue):
+ qlist = []
+ while not queue.empty():
+ qlist.append(queue.get())
+ return qlist
+
# @classmethod
# def tearDownClass(cls):
# cls.app.quit()
diff --git a/tests/fileio/test_init.py b/tests/fileio/test_init.py
index 7b553f6..2c3126d 100644
--- a/tests/fileio/test_init.py
+++ b/tests/fileio/test_init.py
@@ -1,21 +1,87 @@
import os.path
import tempfile
-from unittest.mock import patch
+from unittest.mock import MagicMock, patch
+
+from PyQt6 import QtCore
from beeref import fileio
+from beeref import commands
+from beeref.scene import BeeGraphicsScene
+from ..base import BeeTestCase
@patch('beeref.fileio.sql.SQLiteIO.write')
-def test_save_create_new_false(write_mock):
+def test_save_bee_create_new_false(write_mock):
with tempfile.TemporaryDirectory() as dirname:
fname = os.path.join(dirname, 'test.bee')
- fileio.save(fname, 'myscene', create_new=False)
+ fileio.save_bee(fname, 'myscene', create_new=False)
write_mock.assert_called_once()
@patch('beeref.fileio.sql.SQLiteIO.read')
-def test_write(read_mock):
+def test_read_bee(read_mock):
with tempfile.TemporaryDirectory() as dirname:
fname = os.path.join(dirname, 'test.bee')
- fileio.load(fname, 'myscene')
+ fileio.load_bee(fname, 'myscene')
read_mock.assert_called_once()
+
+
+class LoadImagesTestCase(BeeTestCase):
+
+ def setUp(self):
+ self.scene = BeeGraphicsScene(MagicMock())
+
+ def test_loads(self):
+ worker = MagicMock(canceled=False)
+ fileio.load_images([self.imgfilename3x3],
+ QtCore.QPointF(5, 6), self.scene, worker)
+ worker.begin_processing.emit.assert_called_once_with(1)
+ worker.progress.emit.assert_called_once_with(0)
+ worker.finished.emit.assert_called_once_with('', [])
+ items = self.queue2list(self.scene.items_to_add)
+ assert len(items) == 1
+ item = items[0][0]
+ args = self.scene.undo_stack.push.call_args_list[0][0]
+ cmd = args[0]
+ assert isinstance(cmd, commands.InsertItems)
+ assert cmd.items == [item]
+ assert cmd.scene == self.scene
+ assert cmd.ignore_first_redo is True
+ assert item.pos() == QtCore.QPointF(3.5, 4.5)
+
+ def test_canceled(self):
+ worker = MagicMock(canceled=True)
+ fileio.load_images([self.imgfilename3x3, self.imgfilename3x3],
+ QtCore.QPointF(5, 6), self.scene, worker)
+ worker.begin_processing.emit.assert_called_once_with(2)
+ worker.progress.emit.assert_called_once_with(0)
+ worker.finished.emit.assert_called_once_with('', [])
+ items = self.queue2list(self.scene.items_to_add)
+ assert len(items) == 1
+ item = items[0][0]
+ args = self.scene.undo_stack.push.call_args_list[0][0]
+ cmd = args[0]
+ assert isinstance(cmd, commands.InsertItems)
+ assert cmd.items == [item]
+ assert cmd.scene == self.scene
+ assert cmd.ignore_first_redo is True
+ assert item.pos() == QtCore.QPointF(3.5, 4.5)
+
+ def test_error(self):
+ worker = MagicMock(canceled=False)
+ fileio.load_images(['foo.jpg', self.imgfilename3x3],
+ QtCore.QPointF(5, 6), self.scene, worker)
+ worker.begin_processing.emit.assert_called_once_with(2)
+ worker.progress.emit.assert_any_call(0)
+ worker.progress.emit.assert_any_call(1)
+ worker.finished.emit.assert_called_once_with('', ['foo.jpg'])
+ items = self.queue2list(self.scene.items_to_add)
+ assert len(items) == 1
+ item = items[0][0]
+ args = self.scene.undo_stack.push.call_args_list[0][0]
+ cmd = args[0]
+ assert isinstance(cmd, commands.InsertItems)
+ assert cmd.items == [item]
+ assert cmd.scene == self.scene
+ assert cmd.ignore_first_redo is True
+ assert item.pos() == QtCore.QPointF(3.5, 4.5)
diff --git a/tests/fileio/test_sql.py b/tests/fileio/test_sql.py
index 42804d9..e4b24fb 100644
--- a/tests/fileio/test_sql.py
+++ b/tests/fileio/test_sql.py
@@ -189,14 +189,28 @@ class SQLiteIOWriteTestCase(BeeTestCase):
assert result[0] == 1
def test_updates_progress(self):
- progress = MagicMock()
+ worker = MagicMock(canceled=False)
io = SQLiteIO(':memory:', self.scene, create_new=True,
- progress=progress)
+ worker=worker)
item = BeePixmapItem(QtGui.QImage())
self.scene.addItem(item)
io.write()
- progress.setMaximum.assert_called_once_with(1)
- progress.setValue.assert_called_once_with(0)
+ worker.begin_processing.emit.assert_called_once_with(1)
+ worker.progress.emit.assert_called_once_with(0)
+ worker.finished.emit.assert_called_once_with(':memory:', [])
+
+ def test_canceled(self):
+ worker = MagicMock(canceled=True)
+ io = SQLiteIO(':memory:', self.scene, create_new=True,
+ worker=worker)
+ item = BeePixmapItem(QtGui.QImage())
+ self.scene.addItem(item)
+ item = BeePixmapItem(QtGui.QImage())
+ self.scene.addItem(item)
+ io.write()
+ worker.begin_processing.emit.assert_called_once_with(2)
+ worker.progress.emit.assert_called_once_with(0)
+ worker.finished.emit.assert_called_once_with(':memory:', [])
class SQLiteIOReadTestCase(BeeTestCase):
@@ -220,8 +234,8 @@ class SQLiteIOReadTestCase(BeeTestCase):
io = SQLiteIO(fname, self.scene, readonly=True)
io.read()
- assert len(self.scene.items()) == 1
- item = self.scene.items()[0]
+ item, selected = self.scene.items_to_add.get()
+ assert selected is False
assert item.save_id == 1
assert item.pos().x() == 22.2
assert item.pos().y() == 33.3
@@ -232,11 +246,12 @@ class SQLiteIOReadTestCase(BeeTestCase):
assert item.filename == 'bee.png'
assert item.width == 3
assert item.height == 3
+ assert self.scene.items_to_add.empty() is True
def test_updates_progress(self):
- progress = MagicMock()
+ worker = MagicMock(canceled=False)
io = SQLiteIO(':memory:', self.scene, create_new=True,
- progress=progress)
+ worker=worker)
io.create_schema_on_new()
io.ex('INSERT INTO items (type, x, y, z, scale, filename) '
@@ -245,8 +260,29 @@ class SQLiteIOReadTestCase(BeeTestCase):
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)
+ worker.begin_processing.emit.assert_called_once_with(1)
+ worker.progress.emit.assert_called_once_with(0)
+ worker.finished.emit.assert_called_once_with(':memory:', [])
+
+ def test_canceled(self):
+ worker = MagicMock(canceled=True)
+ io = SQLiteIO(':memory:', self.scene, create_new=True,
+ worker=worker)
+
+ io.create_schema_on_new()
+ io.ex('INSERT INTO items (type, x, y, z, scale, filename) '
+ 'VALUES (?, ?, ?, ?, ?, ?) ',
+ ('pixmap', 0, 0, 0, 1, 'bee.png'))
+ io.ex('INSERT INTO sqlar (item_id, data) VALUES (?, ?)', (1, b''))
+ io.ex('INSERT INTO items (type, x, y, z, scale, filename) '
+ 'VALUES (?, ?, ?, ?, ?, ?) ',
+ ('pixmap', 50, 50, 0, 1, 'bee2.png'))
+ io.ex('INSERT INTO sqlar (item_id, data) VALUES (?, ?)', (1, b''))
+ io.connection.commit()
+ io.read()
+ worker.begin_processing.emit.assert_called_once_with(2)
+ worker.progress.emit.assert_called_once_with(0)
+ worker.finished.emit.assert_called_once_with('', [])
def test_raises_error_when_file_borked(self):
with tempfile.TemporaryDirectory() as dirname:
@@ -259,6 +295,20 @@ class SQLiteIOReadTestCase(BeeTestCase):
io.read()
assert exinfo.value.filename == fname
+ def test_emits_error_message_when_file_borked(self):
+ with tempfile.TemporaryDirectory() as dirname:
+ fname = os.path.join(dirname, 'test.bee')
+ with open(fname, 'w') as f:
+ f.write('foobar')
+
+ worker = MagicMock()
+ io = SQLiteIO(fname, self.scene, readonly=True, worker=worker)
+ io.read()
+ worker.finished.emit.assert_called_once()
+ args = worker.finished.emit.call_args_list[0][0]
+ assert args[0] == ''
+ assert len(args[1]) == 1
+
def test_reads_raises_error_when_file_empty(self):
with tempfile.TemporaryDirectory() as dirname:
fname = os.path.join(dirname, 'test.bee')
diff --git a/tests/test_commands.py b/tests/test_commands.py
index ca76b00..9e9b9b5 100644
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -30,6 +30,26 @@ class InsertItemsTestCase(BeeTestCase):
assert list(scene.items_for_save()) == [item1]
assert item1.isSelected() is False
+ @patch('beeref.scene.BeeGraphicsScene.views')
+ def test_ignore_first_redo(self, views_mock):
+ scene = BeeGraphicsScene(None)
+ view = MagicMock(get_scale=MagicMock(return_value=1))
+ views_mock.return_value = [view]
+ scene.update_selection = MagicMock()
+ scene.max_z = 5
+ item1 = BeePixmapItem(QtGui.QImage())
+ scene.addItem(item1)
+ item2 = BeePixmapItem(QtGui.QImage())
+ command = commands.InsertItems(scene, [item2], ignore_first_redo=True)
+ command.redo()
+ assert list(scene.items_for_save()) == [item1]
+ assert item1.isSelected() is False
+ command.redo()
+ assert list(scene.items_for_save()) == [item1, item2]
+ assert item1.isSelected() is False
+ assert item2.isSelected() is True
+ item2.zValue() > 5
+
class DeleteItemsTestCase(BeeTestCase):
diff --git a/tests/test_scene.py b/tests/test_scene.py
index cd8cce0..317a4d0 100644
--- a/tests/test_scene.py
+++ b/tests/test_scene.py
@@ -77,7 +77,7 @@ class BeeGraphicsSceneTestCase(BeeTestCase):
self.scene.flip_items(vertical=True)
args = self.scene.undo_stack.push.call_args_list[0][0]
cmd = args[0]
- isinstance(cmd, commands.FlipItems)
+ assert isinstance(cmd, commands.FlipItems)
assert cmd.items == [item]
assert cmd.anchor == QtCore.QPointF(60, 50)
assert cmd.vertical is True
@@ -272,7 +272,7 @@ class BeeGraphicsSceneTestCase(BeeTestCase):
self.scene.undo_stack.push.assert_called_once()
args = self.scene.undo_stack.push.call_args_list[0][0]
cmd = args[0]
- isinstance(cmd, commands.MoveItemsBy)
+ assert isinstance(cmd, commands.MoveItemsBy)
assert cmd.items == [item]
assert cmd.ignore_first_redo is True
assert cmd.delta.x() == 10
@@ -473,3 +473,26 @@ class BeeGraphicsSceneTestCase(BeeTestCase):
self.scene.multi_select_item.rotate_active = True
self.scene.on_change(None)
self.scene.multi_select_item.fit_selection_area.assert_not_called()
+
+ def test_add_delayed_items_unselected(self):
+ item = BeePixmapItem(QtGui.QImage())
+ item.setZValue(0.33)
+ self.scene.add_item_later(item, selected=False)
+ self.scene.add_delayed_items()
+ assert self.scene.items() == [item]
+ assert item.isSelected() is False
+ assert self.scene.max_z == 0.33
+
+ def test_add_delayed_items_selected(self):
+ self.scene.max_z = 0.6
+ item = BeePixmapItem(QtGui.QImage())
+ item.setZValue(0.33)
+ self.scene.add_item_later(item, selected=True)
+ self.scene.add_delayed_items()
+ assert self.scene.items() == [item]
+ assert item.isSelected() is True
+ assert item.zValue() > 0.6
+
+ def test_add_delayed_items_when_no_items(self):
+ self.scene.add_delayed_items()
+ assert self.scene.items() == []
diff --git a/tests/test_view.py b/tests/test_view.py
index 3f01c34..ec511bc 100644
--- a/tests/test_view.py
+++ b/tests/test_view.py
@@ -2,6 +2,8 @@ import os.path
import tempfile
from unittest.mock import MagicMock, patch
+from pytest import mark
+
from PyQt6 import QtGui, QtWidgets
from beeref.items import BeePixmapItem
@@ -87,29 +89,44 @@ class BeeGraphicsViewTestCase(BeeTestCase):
root = os.path.dirname(__file__)
filename = os.path.join(root, 'assets', 'test1item.bee')
self.view.open_from_file(filename)
- assert len(self.view.scene.items()) == 1
+ self.view.worker.wait()
+ items = self.queue2list(self.view.scene.items_to_add)
+ assert len(items) == 1
+ item, selected = items[0]
+ assert items[0][0].pixmap()
+ assert items[0][1] is False
clear_mock.assert_called_once_with()
@patch('PyQt6.QtWidgets.QMessageBox.warning')
def test_open_from_file_when_error(self, warn_mock):
+ # FIXME: #1
+ # Can't check signal handling currently
self.view.open_from_file('uieauiae')
+ self.view.worker.wait()
+ assert self.view.scene.items_to_add.empty() is True
assert len(self.view.scene.items()) == 0
- warn_mock.assert_called_once()
@patch('PyQt6.QtWidgets.QFileDialog.getOpenFileName')
def test_on_action_open(self, dialog_mock):
+ # FIXME: #1
+ # Can't check signal handling currently
root = os.path.dirname(__file__)
dialog_mock.return_value = (
os.path.join(root, 'assets', 'test1item.bee'),
None)
self.view.on_action_open()
- assert len(self.view.scene.items()) == 1
+ self.view.worker.wait()
+ items = self.queue2list(self.view.scene.items_to_add)
+ assert len(items) == 1
+ assert items[0][0].pixmap()
+ assert items[0][1] is False
@patch('PyQt6.QtWidgets.QFileDialog.getOpenFileName')
- def test_on_action_open_when_no_filename(self, dialog_mock):
+ @patch('beeref.view.BeeGraphicsView.on_action_open')
+ def test_on_action_open_when_no_filename(self, dialog_mock, open_mock):
dialog_mock.return_value = (None, None)
self.view.on_action_open()
- assert len(self.view.scene.items()) == 0
+ open_mock.assert_not_called()
@patch('PyQt6.QtWidgets.QFileDialog.getSaveFileName')
def test_on_action_save_as(self, dialog_mock):
@@ -120,10 +137,11 @@ class BeeGraphicsViewTestCase(BeeTestCase):
assert os.path.exists(filename) is False
dialog_mock.return_value = (filename, None)
self.view.on_action_save_as()
+ self.view.worker.wait()
assert os.path.exists(filename) is True
@patch('PyQt6.QtWidgets.QFileDialog.getSaveFileName')
- @patch('beeref.fileio.save')
+ @patch('beeref.view.BeeGraphicsView.do_save')
def test_on_action_save_as_when_no_filename(self, save_mock, dialog_mock):
item = BeePixmapItem(QtGui.QImage(self.imgfilename3x3))
self.view.scene.addItem(item)
@@ -140,8 +158,10 @@ class BeeGraphicsViewTestCase(BeeTestCase):
assert os.path.exists(filename) is False
dialog_mock.return_value = (filename, None)
self.view.on_action_save_as()
+ self.view.worker.wait()
assert os.path.exists(f'{filename}.bee') is True
+ @mark.skip('needs pytest-qt')
@patch('PyQt6.QtWidgets.QMessageBox.warning')
@patch('PyQt6.QtWidgets.QFileDialog.getSaveFileName')
@patch('beeref.fileio.save')
@@ -165,6 +185,7 @@ class BeeGraphicsViewTestCase(BeeTestCase):
self.view.filename = os.path.join(tmpdir, 'test.bee')
assert os.path.exists(self.view.filename) is False
self.view.on_action_save()
+ self.view.worker.wait()
assert os.path.exists(self.view.filename) is True
@patch('beeref.view.BeeGraphicsView.on_action_save_as')
@@ -178,23 +199,30 @@ class BeeGraphicsViewTestCase(BeeTestCase):
@patch('beeref.scene.BeeGraphicsScene.clearSelection')
@patch('PyQt6.QtWidgets.QFileDialog.getOpenFileNames')
def test_on_action_insert_images(self, dialog_mock, clear_mock):
+ # FIXME: #1
+ # Can't check signal handling currently
dialog_mock.return_value = ([self.imgfilename3x3], None)
self.view.on_action_insert_images()
- assert len(self.view.scene.items()) == 1
- assert self.view.scene.items()[0].isSelected() is True
+ self.view.worker.wait()
+ items = self.queue2list(self.view.scene.items_to_add)
+ assert len(items) == 1
+ assert items[0][0].pixmap()
+ assert items[0][1] is True
clear_mock.assert_called_once_with()
@patch('beeref.scene.BeeGraphicsScene.clearSelection')
- @patch('PyQt6.QtWidgets.QMessageBox.warning')
@patch('PyQt6.QtWidgets.QFileDialog.getOpenFileNames')
- def test_on_action_insert_images_when_error(
- self, dialog_mock, warn_mock, clear_mock):
+ def test_on_action_insert_images_when_error(self, dialog_mock, clear_mock):
+ # FIXME: #1
+ # Can't check signal handling currently
dialog_mock.return_value = (
[self.imgfilename3x3, 'iaeiae', 'trntrn'], None)
self.view.on_action_insert_images()
- assert len(self.view.scene.items()) == 1
- assert self.view.scene.items()[0].isSelected() is True
- warn_mock.assert_called_once()
+ self.view.worker.wait()
+ items = self.queue2list(self.view.scene.items_to_add)
+ assert len(items) == 1
+ assert items[0][0].pixmap()
+ assert items[0][1] is True
clear_mock.assert_called_once_with()
@patch('beeref.scene.BeeGraphicsScene.clearSelection')