mirror of
https://github.com/rbreu/beeref.git
synced 2026-03-11 08:54:28 +00:00
Add text item
This commit is contained in:
parent
849de6197a
commit
5d115b9391
23 changed files with 1022 additions and 137 deletions
|
|
@ -1,6 +1,11 @@
|
|||
0.2.0 - unreleased
|
||||
==================
|
||||
|
||||
Added
|
||||
-----
|
||||
|
||||
* You can now add plain text notes
|
||||
|
||||
Changed
|
||||
-------
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ actions = [
|
|||
},
|
||||
{
|
||||
'id': 'quit',
|
||||
'text': '&Quit...',
|
||||
'text': '&Quit',
|
||||
'shortcuts': ['Ctrl+Q'],
|
||||
'callback': 'on_action_quit',
|
||||
},
|
||||
|
|
@ -44,6 +44,12 @@ actions = [
|
|||
'shortcuts': ['Ctrl+I'],
|
||||
'callback': 'on_action_insert_images',
|
||||
},
|
||||
{
|
||||
'id': 'insert_text',
|
||||
'text': '&Text',
|
||||
'shortcuts': ['Ctrl+T'],
|
||||
'callback': 'on_action_insert_text',
|
||||
},
|
||||
{
|
||||
'id': 'undo',
|
||||
'text': '&Undo',
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ menu_structure = [
|
|||
'menu': '&Insert',
|
||||
'items': [
|
||||
'insert_images',
|
||||
'insert_text',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -36,4 +36,5 @@ COLORS = {
|
|||
# BeeRef specific:
|
||||
'Scene:Selection': (116, 234, 231),
|
||||
'Scene:Canvas': (60, 60, 60),
|
||||
'Scene:Text': (200, 200, 200)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,5 +42,14 @@
|
|||
<h4>Move Window</h4>
|
||||
|
||||
<p>
|
||||
Ctrl + Alt + Left Cick + Drag
|
||||
Ctrl + Alt + Left Click + Drag
|
||||
</p>
|
||||
|
||||
|
||||
<h4>Edit Text</h4>
|
||||
|
||||
<p>
|
||||
Double Click on Text<br/>
|
||||
Press Enter or click elsewhere to stop editing<br/>
|
||||
Shift + Enter for newline
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -65,12 +65,15 @@ def load_images(filenames, pos, scene, worker):
|
|||
logger.info(f'Could not load file {filename}')
|
||||
errors.append(filename)
|
||||
continue
|
||||
|
||||
item = BeePixmapItem(img, filename)
|
||||
item.set_pos_center(pos)
|
||||
scene.add_item_later(item, selected=True)
|
||||
scene.add_item_later({'item': item, 'type': 'pixmap'}, selected=True)
|
||||
items.append(item)
|
||||
if worker.canceled:
|
||||
break
|
||||
# Give main thread time to process items:
|
||||
worker.msleep(10)
|
||||
|
||||
scene.undo_stack.push(
|
||||
commands.InsertItems(scene, items, ignore_first_redo=True))
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ def exif_rotated_image(path=None):
|
|||
with open(path, 'rb') as f:
|
||||
try:
|
||||
exifimg = exif.Image(f)
|
||||
except plum.exceptions.UnpackError:
|
||||
logger.info(f'Exif parser failed on image: {path}')
|
||||
except (plum.exceptions.UnpackError, NotImplementedError):
|
||||
logger.exception(f'Exif parser failed on image: {path}')
|
||||
return img
|
||||
|
||||
if 'orientation' in exifimg.list_all():
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
USER_VERSION = 2
|
||||
APPLICATION_ID = 2060242126
|
||||
|
||||
|
||||
SCHEMA = [
|
||||
"""
|
||||
CREATE TABLE items (
|
||||
|
|
@ -9,7 +13,7 @@ SCHEMA = [
|
|||
scale REAL DEFAULT 1,
|
||||
rotation REAL DEFAULT 0,
|
||||
flip INTEGER DEFAULT 1,
|
||||
filename TEXT
|
||||
data JSON
|
||||
)
|
||||
""",
|
||||
"""
|
||||
|
|
@ -27,3 +31,11 @@ SCHEMA = [
|
|||
)
|
||||
""",
|
||||
]
|
||||
|
||||
|
||||
MIGRATIONS = {
|
||||
2: [
|
||||
"ALTER TABLE items ADD COLUMN data JSON",
|
||||
"UPDATE items SET data = json_object('filename', filename)",
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,16 +23,20 @@ https://www.sqlite.org/appfileformat.html
|
|||
https://www.sqlite.org/sqlar.html
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import sqlite3
|
||||
import tempfile
|
||||
|
||||
from PyQt6 import QtGui
|
||||
|
||||
from beeref import constants
|
||||
from beeref.items import BeePixmapItem
|
||||
from .errors import BeeFileIOError
|
||||
from .schema import SCHEMA
|
||||
from .schema import SCHEMA, USER_VERSION, MIGRATIONS, APPLICATION_ID
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -54,8 +58,6 @@ def handle_sqlite_errors(func):
|
|||
|
||||
|
||||
class SQLiteIO:
|
||||
USER_VERSION = 1
|
||||
APPLICATION_ID = 2060242126
|
||||
|
||||
def __init__(self, filename, scene, create_new=False, readonly=False,
|
||||
worker=None):
|
||||
|
|
@ -74,6 +76,9 @@ class SQLiteIO:
|
|||
delattr(self, '_connection')
|
||||
if hasattr(self, '_cursor'):
|
||||
delattr(self, '_cursor')
|
||||
if hasattr(self, '_tmpdir'):
|
||||
self._tmpdir.cleanup()
|
||||
delattr(self, '_tmpdir')
|
||||
|
||||
def _establish_connection(self):
|
||||
if (self.create_new
|
||||
|
|
@ -86,9 +91,42 @@ class SQLiteIO:
|
|||
|
||||
uri = pathlib.Path(self.filename).resolve().as_uri()
|
||||
if self.readonly:
|
||||
uri = f'{uri}?mode=ro'
|
||||
uri = f'{uri}?mode=rw'
|
||||
self._connection = sqlite3.connect(uri, uri=True)
|
||||
self._cursor = self.connection.cursor()
|
||||
if not self.create_new:
|
||||
self._migrate()
|
||||
|
||||
def _migrate(self):
|
||||
"""Migrate database if necessary."""
|
||||
|
||||
version = self.fetchone('PRAGMA user_version')[0]
|
||||
logger.debug(f'Found bee file version: {version}')
|
||||
if version == USER_VERSION:
|
||||
logger.debug('Version ok; no migrations necessary')
|
||||
return
|
||||
|
||||
if self.readonly:
|
||||
try:
|
||||
# See whether file is writable so we can migrate it directly
|
||||
self.ex('PRAGMA application_id=%s' % APPLICATION_ID)
|
||||
except sqlite3.Error:
|
||||
logger.debug('File not writable; use temporary copy instead')
|
||||
self._connection.close()
|
||||
self._tmpdir = tempfile.TemporaryDirectory(
|
||||
prefix=constants.APPNAME)
|
||||
tmpname = os.path.join(self._tmpdir.name, 'mig.bee')
|
||||
shutil.copyfile(self.filename, tmpname)
|
||||
self._connection = sqlite3.connect(tmpname)
|
||||
self._cursor = self.connection.cursor()
|
||||
|
||||
for i in range(version, USER_VERSION):
|
||||
logger.debug(f'Migrating from version {i} to {i + 1}...')
|
||||
for migration in MIGRATIONS[i + 1]:
|
||||
self.ex(migration)
|
||||
self.write_meta()
|
||||
self.connection.commit()
|
||||
logger.debug('Migration finished')
|
||||
|
||||
@property
|
||||
def connection(self):
|
||||
|
|
@ -117,47 +155,61 @@ class SQLiteIO:
|
|||
return self.cursor.fetchall()
|
||||
|
||||
def write_meta(self):
|
||||
self.ex('PRAGMA application_id=%s' % self.APPLICATION_ID)
|
||||
self.ex('PRAGMA user_version=%s' % self.USER_VERSION)
|
||||
self.ex('PRAGMA application_id=%s' % APPLICATION_ID)
|
||||
self.ex('PRAGMA user_version=%s' % USER_VERSION)
|
||||
self.ex('PRAGMA foreign_keys=1')
|
||||
|
||||
def create_schema_on_new(self):
|
||||
if self.create_new:
|
||||
self.write_meta()
|
||||
for schema in SCHEMA:
|
||||
self.ex(schema)
|
||||
|
||||
@handle_sqlite_errors
|
||||
def read(self):
|
||||
rows = self.fetchall(
|
||||
'SELECT items.id, x, y, z, scale, rotation, flip, filename, '
|
||||
'sqlar.data '
|
||||
'FROM items INNER JOIN sqlar on sqlar.item_id = items.id')
|
||||
'SELECT items.id, type, x, y, z, scale, rotation, flip, '
|
||||
'items.data, sqlar.data '
|
||||
'FROM items LEFT OUTER JOIN sqlar on sqlar.item_id = items.id')
|
||||
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])
|
||||
item.setZValue(row[3])
|
||||
item.setScale(row[4])
|
||||
item.setRotation(row[5])
|
||||
if row[6] == -1:
|
||||
item.do_flip()
|
||||
self.scene.add_item_later(item)
|
||||
data = {
|
||||
'save_id': row[0],
|
||||
'type': row[1],
|
||||
'x': row[2],
|
||||
'y': row[3],
|
||||
'z': row[4],
|
||||
'scale': row[5],
|
||||
'rotation': row[6],
|
||||
'flip': row[7],
|
||||
'data': json.loads(row[8]),
|
||||
}
|
||||
|
||||
if data['type'] == 'pixmap':
|
||||
data['item'] = BeePixmapItem(QtGui.QImage(), **data['data'])
|
||||
data['item'].pixmap_from_bytes(row[9])
|
||||
|
||||
self.scene.add_item_later(data)
|
||||
|
||||
if self.worker:
|
||||
logger.trace(f'Emit progress: {i}')
|
||||
self.worker.progress.emit(i)
|
||||
if self.worker.canceled:
|
||||
self.worker.finished.emit('', [])
|
||||
return
|
||||
# Give main thread time to process items:
|
||||
self.worker.msleep(10)
|
||||
if self.worker:
|
||||
self.worker.finished.emit(self.filename, [])
|
||||
|
||||
@handle_sqlite_errors
|
||||
def write(self):
|
||||
if self.readonly:
|
||||
raise sqlite3.OperationalError(
|
||||
'attempt to write a readonly database')
|
||||
try:
|
||||
self.write_meta()
|
||||
self.create_schema_on_new()
|
||||
self.write_data()
|
||||
except sqlite3.Error:
|
||||
|
|
@ -198,23 +250,26 @@ class SQLiteIO:
|
|||
def insert_item(self, item):
|
||||
self.ex(
|
||||
'INSERT INTO items (type, x, y, z, scale, rotation, flip, '
|
||||
'filename) '
|
||||
'data) '
|
||||
'VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
('pixmap', item.pos().x(), item.pos().y(), item.zValue(),
|
||||
item.scale(), item.rotation(), item.flip(), item.filename))
|
||||
(item.TYPE, item.pos().x(), item.pos().y(), item.zValue(),
|
||||
item.scale(), item.rotation(), item.flip(),
|
||||
json.dumps(item.get_extra_save_data())))
|
||||
item.save_id = self.cursor.lastrowid
|
||||
pixmap = item.pixmap_to_bytes()
|
||||
|
||||
if item.filename:
|
||||
basename = os.path.splitext(os.path.basename(item.filename))[0]
|
||||
name = '%04d-%s.png' % (item.save_id, basename)
|
||||
else:
|
||||
name = '%04d.png' % item.save_id
|
||||
if hasattr(item, 'pixmap_to_bytes'):
|
||||
pixmap = item.pixmap_to_bytes()
|
||||
|
||||
self.ex(
|
||||
'INSERT INTO sqlar (item_id, name, mode, sz, data) '
|
||||
'VALUES (?, ?, ?, ?, ?)',
|
||||
(item.save_id, name, 0o644, len(pixmap), pixmap))
|
||||
if item.filename:
|
||||
basename = os.path.splitext(os.path.basename(item.filename))[0]
|
||||
name = '%04d-%s.png' % (item.save_id, basename)
|
||||
else:
|
||||
name = '%04d.png' % item.save_id
|
||||
|
||||
self.ex(
|
||||
'INSERT INTO sqlar (item_id, name, mode, sz, data) '
|
||||
'VALUES (?, ?, ?, ?, ?)',
|
||||
(item.save_id, name, 0o644, len(pixmap), pixmap))
|
||||
self.connection.commit()
|
||||
|
||||
def update_item(self, item):
|
||||
|
|
@ -225,8 +280,10 @@ class SQLiteIO:
|
|||
"""
|
||||
self.ex(
|
||||
'UPDATE items SET x=?, y=?, z=?, scale=?, rotation=?, flip=?, '
|
||||
'filename=? '
|
||||
'data=? '
|
||||
'WHERE id=?',
|
||||
(item.pos().x(), item.pos().y(), item.zValue(), item.scale(),
|
||||
item.rotation(), item.flip(), item.filename, item.save_id))
|
||||
item.rotation(), item.flip(),
|
||||
json.dumps(item.get_extra_save_data()),
|
||||
item.save_id))
|
||||
self.connection.commit()
|
||||
|
|
|
|||
166
beeref/items.py
166
beeref/items.py
|
|
@ -20,16 +20,64 @@ text).
|
|||
import logging
|
||||
|
||||
from PyQt6 import QtCore, QtGui, QtWidgets
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
from beeref.constants import COLORS
|
||||
from beeref.selection import SelectableMixin
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
item_registry = {}
|
||||
|
||||
class BeePixmapItem(SelectableMixin, QtWidgets.QGraphicsPixmapItem):
|
||||
|
||||
def register_item(cls):
|
||||
item_registry[cls.TYPE] = cls
|
||||
return cls
|
||||
|
||||
|
||||
class BeeItemMixin(SelectableMixin):
|
||||
"""Base for all items added by the user."""
|
||||
|
||||
def set_pos_center(self, pos):
|
||||
"""Sets the position using the item's center as the origin point."""
|
||||
|
||||
self.setPos(pos - self.center_scene_coords)
|
||||
|
||||
def has_selection_outline(self):
|
||||
return self.isSelected()
|
||||
|
||||
def has_selection_handles(self):
|
||||
return self.isSelected() and self.scene().has_single_selection()
|
||||
|
||||
def selection_action_items(self):
|
||||
"""The items affected by selection actions like scaling and rotating.
|
||||
"""
|
||||
return [self]
|
||||
|
||||
def on_selected_change(self, value):
|
||||
if (value and self.scene()
|
||||
and not self.scene().has_selection()
|
||||
and not self.scene().rubberband_active):
|
||||
self.bring_to_front()
|
||||
|
||||
def update_from_data(self, **kwargs):
|
||||
self.save_id = kwargs.get('save_id', self.save_id)
|
||||
self.setPos(kwargs.get('x', self.pos().x()),
|
||||
kwargs.get('y', self.pos().y()))
|
||||
self.setZValue(kwargs.get('z', self.zValue()))
|
||||
self.setScale(kwargs.get('scale', self.scale()))
|
||||
self.setRotation(kwargs.get('rotation', self.rotation()))
|
||||
if kwargs.get('flip') != self.flip():
|
||||
self.do_flip()
|
||||
|
||||
|
||||
@register_item
|
||||
class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem):
|
||||
"""Class for images added by the user."""
|
||||
|
||||
TYPE = 'pixmap'
|
||||
|
||||
def __init__(self, image, filename=None):
|
||||
super().__init__(QtGui.QPixmap.fromImage(image))
|
||||
self.save_id = None
|
||||
|
|
@ -37,14 +85,16 @@ class BeePixmapItem(SelectableMixin, QtWidgets.QGraphicsPixmapItem):
|
|||
logger.debug(f'Initialized {self}')
|
||||
self.init_selectable()
|
||||
|
||||
@classmethod
|
||||
def create_from_data(self, **kwargs):
|
||||
item = kwargs.pop('item')
|
||||
data = kwargs.pop('data', {})
|
||||
item.filename = item.filename or data.get('filename')
|
||||
return item
|
||||
|
||||
def __str__(self):
|
||||
return (f'Image "{self.filename}" {self.width} x {self.height}')
|
||||
|
||||
def set_pos_center(self, pos):
|
||||
"""Sets the position using the item's center as the origin point."""
|
||||
|
||||
self.setPos(pos - self.center_scene_coords)
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self.pixmap().size().width()
|
||||
|
|
@ -53,6 +103,9 @@ class BeePixmapItem(SelectableMixin, QtWidgets.QGraphicsPixmapItem):
|
|||
def height(self):
|
||||
return self.pixmap().size().height()
|
||||
|
||||
def get_extra_save_data(self):
|
||||
return {'filename': self.filename}
|
||||
|
||||
def pixmap_to_bytes(self):
|
||||
"""Convert the pixmap data to PNG bytestring."""
|
||||
barray = QtCore.QByteArray()
|
||||
|
|
@ -72,23 +125,6 @@ class BeePixmapItem(SelectableMixin, QtWidgets.QGraphicsPixmapItem):
|
|||
painter.drawPixmap(0, 0, self.pixmap())
|
||||
self.paint_selectable(painter, option, widget)
|
||||
|
||||
def has_selection_outline(self):
|
||||
return self.isSelected()
|
||||
|
||||
def has_selection_handles(self):
|
||||
return self.isSelected() and self.scene().has_single_selection()
|
||||
|
||||
def selection_action_items(self):
|
||||
"""The items affected by selection actions like scaling and rotating.
|
||||
"""
|
||||
return [self]
|
||||
|
||||
def on_selected_change(self, value):
|
||||
if (value and self.scene()
|
||||
and not self.scene().has_selection()
|
||||
and not self.scene().rubberband_active):
|
||||
self.bring_to_front()
|
||||
|
||||
def create_copy(self):
|
||||
item = BeePixmapItem(QtGui.QImage(), self.filename)
|
||||
item.setPixmap(self.pixmap())
|
||||
|
|
@ -99,3 +135,87 @@ class BeePixmapItem(SelectableMixin, QtWidgets.QGraphicsPixmapItem):
|
|||
if self.flip() == -1:
|
||||
item.do_flip()
|
||||
return item
|
||||
|
||||
|
||||
@register_item
|
||||
class BeeTextItem(BeeItemMixin, QtWidgets.QGraphicsTextItem):
|
||||
"""Class for text added by the user."""
|
||||
|
||||
TYPE = 'text'
|
||||
|
||||
def __init__(self, text=None):
|
||||
super().__init__(text or "Text")
|
||||
self.save_id = None
|
||||
logger.debug(f'Initialized {self}')
|
||||
self.init_selectable()
|
||||
self.is_editable = True
|
||||
self.edit_mode = False
|
||||
self.setDefaultTextColor(QtGui.QColor(*COLORS['Scene:Text']))
|
||||
|
||||
@classmethod
|
||||
def create_from_data(cls, **kwargs):
|
||||
data = kwargs.get('data', {})
|
||||
item = cls(**data)
|
||||
return item
|
||||
|
||||
def __str__(self):
|
||||
txt = self.toPlainText()[:40]
|
||||
return (f'Text "{txt}"')
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return QtWidgets.QGraphicsTextItem.boundingRect(self).width()
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return QtWidgets.QGraphicsTextItem.boundingRect(self).height()
|
||||
|
||||
def get_extra_save_data(self):
|
||||
return {'text': self.toPlainText()}
|
||||
|
||||
def contains(self, point):
|
||||
return self.boundingRect().contains(point)
|
||||
|
||||
def paint(self, painter, option, widget):
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
color = QtGui.QColor(0, 0, 0)
|
||||
color.setAlpha(40)
|
||||
brush = QtGui.QBrush(color)
|
||||
painter.setBrush(brush)
|
||||
painter.drawRect(QtWidgets.QGraphicsTextItem.boundingRect(self))
|
||||
option.state = QtWidgets.QStyle.StateFlag.State_Enabled
|
||||
super().paint(painter, option, widget)
|
||||
self.paint_selectable(painter, option, widget)
|
||||
|
||||
def create_copy(self):
|
||||
item = BeeTextItem(self.toPlainText())
|
||||
item.setPos(self.pos())
|
||||
item.setZValue(self.zValue())
|
||||
item.setScale(self.scale())
|
||||
item.setRotation(self.rotation())
|
||||
if self.flip() == -1:
|
||||
item.do_flip()
|
||||
return item
|
||||
|
||||
def enter_edit_mode(self):
|
||||
self.edit_mode = True
|
||||
self.setTextInteractionFlags(
|
||||
Qt.TextInteractionFlag.TextEditorInteraction)
|
||||
|
||||
def exit_edit_mode(self):
|
||||
self.edit_mode = False
|
||||
# reset selection:
|
||||
self.setTextCursor(QtGui.QTextCursor(self.document()))
|
||||
self.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
|
||||
|
||||
def has_selection_handles(self):
|
||||
return super().has_selection_handles() and not self.edit_mode
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if (event.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return)
|
||||
and event.modifiers() == Qt.KeyboardModifier.NoModifier):
|
||||
self.exit_edit_mode()
|
||||
self.scene().edit_item = None
|
||||
event.accept()
|
||||
return
|
||||
super().keyPressEvent(event)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from PyQt6.QtCore import Qt
|
|||
import rpack
|
||||
|
||||
from beeref import commands
|
||||
from beeref.items import item_registry
|
||||
from beeref.selection import MultiSelectItem, RubberbandItem
|
||||
|
||||
|
||||
|
|
@ -45,6 +46,7 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
self.changed.connect(self.on_change)
|
||||
self.items_to_add = Queue()
|
||||
self.internal_clipboard = []
|
||||
self.edit_item = None
|
||||
|
||||
def addItem(self, item):
|
||||
logger.debug(f'Adding item {item}')
|
||||
|
|
@ -249,7 +251,17 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self.event_start = event.scenePos()
|
||||
if self.itemAt(event.scenePos(), self.views()[0].transform()):
|
||||
item_at_pos = self.itemAt(
|
||||
event.scenePos(), self.views()[0].transform())
|
||||
|
||||
if self.edit_item:
|
||||
if item_at_pos != self.edit_item:
|
||||
self.edit_item.exit_edit_mode()
|
||||
self.edit_item = None
|
||||
else:
|
||||
super().mousePressEvent(event)
|
||||
return
|
||||
if item_at_pos:
|
||||
self.move_active = True
|
||||
elif self.items():
|
||||
self.rubberband_active = True
|
||||
|
|
@ -262,9 +274,14 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
self.move_active = False
|
||||
if not item.isSelected():
|
||||
item.setSelected(True)
|
||||
self.views()[0].fit_rect(
|
||||
self.itemsBoundingRect(items=[item]),
|
||||
toggle_item=item)
|
||||
if item.is_editable:
|
||||
self.edit_item = item
|
||||
item.enter_edit_mode()
|
||||
self.mousePressEvent(event)
|
||||
else:
|
||||
self.views()[0].fit_rect(
|
||||
self.itemsBoundingRect(items=[item]),
|
||||
toggle_item=item)
|
||||
return
|
||||
super().mouseDoubleClickEvent(event)
|
||||
|
||||
|
|
@ -381,19 +398,26 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
self.multi_select_item.fit_selection_area(
|
||||
self.itemsBoundingRect(selection_only=True))
|
||||
|
||||
def add_item_later(self, item, selected=False):
|
||||
"""Keep an item for adding later via ``add_queued_items``"""
|
||||
def add_item_later(self, itemdata, selected=False):
|
||||
"""Keep an item for adding later via ``add_queued_items``
|
||||
|
||||
self.items_to_add.put((item, selected))
|
||||
:param dict itemdata: Defines the item's data
|
||||
:param bool selected: Whether the item is initialised as selected
|
||||
"""
|
||||
|
||||
self.items_to_add.put((itemdata, selected))
|
||||
|
||||
def add_queued_items(self):
|
||||
"""Adds items added via ``add_items_later``"""
|
||||
|
||||
while not self.items_to_add.empty():
|
||||
item, selected = self.items_to_add.get()
|
||||
data, selected = self.items_to_add.get()
|
||||
cls = item_registry[data.pop('type')]
|
||||
item = cls.create_from_data(**data)
|
||||
item.update_from_data(**data)
|
||||
self.addItem(item)
|
||||
# Force recalculation of min/max z values:
|
||||
item.setZValue(item.zValue())
|
||||
if selected:
|
||||
item.setSelected(True)
|
||||
item.bring_to_front()
|
||||
self.max_z = max(self.max_z, item.zValue())
|
||||
self.min_z = min(self.min_z, item.zValue())
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ class SelectableMixin(BaseItemMixin):
|
|||
|
||||
self.viewport_scale = 1
|
||||
self.reset_actions()
|
||||
self.is_editable = False
|
||||
|
||||
def reset_actions(self):
|
||||
self.scale_active = False
|
||||
|
|
|
|||
|
|
@ -13,10 +13,6 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import logging.handlers
|
||||
import os
|
||||
import os.path
|
||||
|
||||
from PyQt6 import QtCore, QtGui
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ from beeref.config import CommandlineArgs, BeeSettings
|
|||
from beeref import constants
|
||||
from beeref import fileio
|
||||
from beeref import gui
|
||||
from beeref.items import BeePixmapItem
|
||||
from beeref.items import BeePixmapItem, BeeTextItem
|
||||
from beeref.scene import BeeGraphicsScene
|
||||
|
||||
|
||||
|
|
@ -120,8 +120,12 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
|
|||
self.context_menu.exec(self.mapToGlobal(point))
|
||||
|
||||
def get_supported_image_formats(self, cls):
|
||||
formats = map(lambda f: f'*.{f.data().decode()}',
|
||||
cls.supportedImageFormats())
|
||||
formats = []
|
||||
|
||||
for f in cls.supportedImageFormats():
|
||||
string = f'*.{f.data().decode()}'
|
||||
formats.extend((string, string.upper()))
|
||||
print(formats)
|
||||
return ' '.join(formats)
|
||||
|
||||
def get_view_center(self):
|
||||
|
|
@ -162,6 +166,7 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
|
|||
# It seems to be more reliable when we fit a second time
|
||||
# Sometimes a changing scene rect can mess up the fitting
|
||||
self.fitInView(rect, Qt.AspectRatioMode.KeepAspectRatio)
|
||||
logger.trace('Fit view done')
|
||||
|
||||
def on_action_fit_scene(self):
|
||||
self.fit_rect(self.scene.itemsBoundingRect())
|
||||
|
|
@ -281,7 +286,7 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
|
|||
self.scene.selectedItems(user_only=True)))
|
||||
|
||||
def on_items_loaded(self, value):
|
||||
logger.debug('On items loded: add queued images')
|
||||
logger.debug('On items loaded: add queued items')
|
||||
self.scene.add_queued_items()
|
||||
|
||||
def on_loading_finished(self, filename, errors):
|
||||
|
|
@ -413,12 +418,19 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
|
|||
|
||||
def on_action_insert_images(self):
|
||||
formats = self.get_supported_image_formats(QtGui.QImageReader)
|
||||
logger.debug(f'Supported image types for reading: {formats}')
|
||||
filenames, f = QtWidgets.QFileDialog.getOpenFileNames(
|
||||
parent=self,
|
||||
caption='Select one ore more images to open',
|
||||
filter=f'Images ({formats})')
|
||||
self.do_insert_images(filenames)
|
||||
|
||||
def on_action_insert_text(self):
|
||||
item = BeeTextItem()
|
||||
pos = self.mapToScene(self.mapFromGlobal(self.cursor().pos()))
|
||||
item.setScale(1 / self.get_scale())
|
||||
self.undo_stack.push(commands.InsertItems(self.scene, [item], pos))
|
||||
|
||||
def on_action_copy(self):
|
||||
logger.debug('Copying to clipboard...')
|
||||
clipboard = QtWidgets.QApplication.clipboard()
|
||||
|
|
@ -454,7 +466,13 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
|
|||
item = BeePixmapItem(img)
|
||||
self.undo_stack.push(commands.InsertItems(self.scene, [item], pos))
|
||||
return
|
||||
logger.info('No image data in clipboard')
|
||||
text = clipboard.text()
|
||||
if text:
|
||||
item = BeeTextItem(text)
|
||||
item.setScale(1 / self.get_scale())
|
||||
self.undo_stack.push(commands.InsertItems(self.scene, [item], pos))
|
||||
return
|
||||
logger.info('No image data or text in clipboard')
|
||||
|
||||
def on_selection_changed(self):
|
||||
logger.debug('Currently selected items: %s',
|
||||
|
|
@ -497,7 +515,7 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
|
|||
much (causing overflow errors).
|
||||
|
||||
:param func: Function which takes the width and height as
|
||||
arguments and turns it into a number, for ex. ``min`` or ``max``.
|
||||
arguments and turns it into a number, for ex. ``min`` or ``max``.
|
||||
"""
|
||||
|
||||
topleft = self.mapFromScene(
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,5 +1,7 @@
|
|||
import os.path
|
||||
import pytest
|
||||
import uuid
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from PyQt6 import QtGui, QtWidgets
|
||||
|
|
@ -66,7 +68,7 @@ def imgdata3x3(imgfilename3x3):
|
|||
|
||||
@pytest.fixture
|
||||
def tmpfile(tmpdir):
|
||||
yield os.path.join(tmpdir, 'test')
|
||||
yield os.path.join(tmpdir, str(uuid.uuid4()))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ def test_exif_rotated_image_not_a_file(qapp):
|
|||
|
||||
|
||||
def test_exif_rotated_image_exif_unpack_error(qapp, imgfilename3x3):
|
||||
with patch('beeref.fileio.image.exif.Image') as exif_mock:
|
||||
exif_mock.raise_error = plum.exceptions.UnpackError()
|
||||
with patch('beeref.fileio.image.exif.Image',
|
||||
side_effect=plum.exceptions.UnpackError()):
|
||||
img = exif_rotated_image(imgfilename3x3)
|
||||
assert img.isNull() is False
|
||||
|
||||
|
|
|
|||
|
|
@ -33,9 +33,9 @@ def test_load_images_loads(view, imgfilename3x3):
|
|||
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 = queue2list(view.scene.items_to_add)
|
||||
assert len(items) == 1
|
||||
item = items[0][0]
|
||||
itemdata = queue2list(view.scene.items_to_add)
|
||||
assert len(itemdata) == 1
|
||||
item = itemdata[0][0]['item']
|
||||
args = view.scene.undo_stack.push.call_args_list[0][0]
|
||||
cmd = args[0]
|
||||
assert isinstance(cmd, commands.InsertItems)
|
||||
|
|
@ -53,9 +53,9 @@ def test_load_images_canceled(view, imgfilename3x3):
|
|||
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 = queue2list(view.scene.items_to_add)
|
||||
assert len(items) == 1
|
||||
item = items[0][0]
|
||||
itemdata = queue2list(view.scene.items_to_add)
|
||||
assert len(itemdata) == 1
|
||||
item = itemdata[0][0]['item']
|
||||
args = view.scene.undo_stack.push.call_args_list[0][0]
|
||||
cmd = args[0]
|
||||
assert isinstance(cmd, commands.InsertItems)
|
||||
|
|
@ -74,9 +74,9 @@ def test_load_images_error(view, imgfilename3x3):
|
|||
worker.progress.emit.assert_any_call(0)
|
||||
worker.progress.emit.assert_any_call(1)
|
||||
worker.finished.emit.assert_called_once_with('', ['foo.jpg'])
|
||||
items = queue2list(view.scene.items_to_add)
|
||||
assert len(items) == 1
|
||||
item = items[0][0]
|
||||
itemdata = queue2list(view.scene.items_to_add)
|
||||
assert len(itemdata) == 1
|
||||
item = itemdata[0][0]['item']
|
||||
args = view.scene.undo_stack.push.call_args_list[0][0]
|
||||
cmd = args[0]
|
||||
assert isinstance(cmd, commands.InsertItems)
|
||||
|
|
|
|||
|
|
@ -1,26 +1,80 @@
|
|||
import json
|
||||
import os
|
||||
import os.path
|
||||
import stat
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from PyQt6 import QtGui
|
||||
import pytest
|
||||
|
||||
from beeref.fileio import schema
|
||||
from beeref.fileio.errors import BeeFileIOError
|
||||
from beeref.fileio.sql import SQLiteIO
|
||||
from beeref.items import BeePixmapItem
|
||||
from beeref.items import BeePixmapItem, BeeTextItem
|
||||
|
||||
|
||||
def test_sqliteio_migrate_does_nothing_when_version_ok(tmpfile):
|
||||
io = SQLiteIO(tmpfile, MagicMock(), create_new=True)
|
||||
io.ex('PRAGMA user_version=%s' % schema.USER_VERSION)
|
||||
io.connection.commit()
|
||||
del(io)
|
||||
with patch('beeref.fileio.sql.SQLiteIO.ex') as ex_mock:
|
||||
SQLiteIO(tmpfile, MagicMock())
|
||||
ex_mock.assert_not_called()
|
||||
|
||||
|
||||
@patch('beeref.fileio.sql.USER_VERSION', 3)
|
||||
@patch('beeref.fileio.sql.MIGRATIONS', {
|
||||
2: ['CREATE TABLE foo (col1 INT)',
|
||||
'CREATE TABLE bar (baz INT)'],
|
||||
3: ['ALTER TABLE foo ADD COLUMN col2 TEXT']})
|
||||
def test_sqliteio_migrate_migrates(tmpfile):
|
||||
io = SQLiteIO(tmpfile, MagicMock(), create_new=True)
|
||||
io.ex('PRAGMA user_version=1')
|
||||
io.connection.commit()
|
||||
del(io)
|
||||
io = SQLiteIO(tmpfile, MagicMock())
|
||||
io.ex('INSERT INTO foo (col1, col2) VALUES (22, "hello world")')
|
||||
io.ex('INSERT INTO bar (baz) VALUES (55)')
|
||||
result = io.fetchone('PRAGMA user_version')
|
||||
assert result[0] == 3
|
||||
|
||||
|
||||
@patch('beeref.fileio.sql.USER_VERSION', 3)
|
||||
@patch('beeref.fileio.sql.MIGRATIONS', {
|
||||
2: ['CREATE TABLE foo (col1 INT)',
|
||||
'CREATE TABLE bar (baz INT)'],
|
||||
3: ['ALTER TABLE foo ADD COLUMN col2 TEXT']})
|
||||
def test_sqliteio_migrate_migrates_when_file_not_writable(tmpfile):
|
||||
io = SQLiteIO(tmpfile, MagicMock(), create_new=True)
|
||||
io.ex('PRAGMA user_version=1')
|
||||
io.connection.commit()
|
||||
del(io)
|
||||
os.chmod(tmpfile, stat.S_IREAD)
|
||||
with pytest.raises(PermissionError):
|
||||
open(tmpfile, 'w')
|
||||
io = SQLiteIO(tmpfile, MagicMock(), readonly=True)
|
||||
io.ex('INSERT INTO foo (col1, col2) VALUES (22, "hello world")')
|
||||
io.ex('INSERT INTO bar (baz) VALUES (55)')
|
||||
result = io.fetchone('PRAGMA user_version')
|
||||
assert result[0] == 3
|
||||
newdir = io._tmpdir.name
|
||||
del(io)
|
||||
assert os.path.exists(newdir) is False
|
||||
|
||||
|
||||
def test_sqliteio_ẁrite_meta_application_id(tmpfile):
|
||||
io = SQLiteIO(tmpfile, MagicMock(), create_new=True)
|
||||
io.write_meta()
|
||||
result = io.fetchone('PRAGMA application_id')
|
||||
assert result[0] == SQLiteIO.APPLICATION_ID
|
||||
assert result[0] == schema.APPLICATION_ID
|
||||
|
||||
|
||||
def test_sqliteio_ẁrite_meta_user_version(tmpfile):
|
||||
io = SQLiteIO(tmpfile, MagicMock(), create_new=True)
|
||||
io.write_meta()
|
||||
result = io.fetchone('PRAGMA user_version')
|
||||
assert result[0] == SQLiteIO.USER_VERSION
|
||||
assert result[0] == schema.USER_VERSION
|
||||
|
||||
|
||||
def test_sqliteio_ẁrite_meta_foreign_keys(tmpfile):
|
||||
|
|
@ -41,7 +95,9 @@ def test_sqliteio_create_schema_on_new_when_create_new(tmpfile):
|
|||
scene_mock.clear_save_ids.assert_called_once()
|
||||
|
||||
|
||||
def test_sqliteio_create_schema_on_new_when_not_create_new(tmpfile):
|
||||
@patch('beeref.fileio.sql.SQLiteIO._migrate')
|
||||
def test_sqliteio_create_schema_on_new_when_not_create_new(
|
||||
migrate_mock, tmpfile):
|
||||
scene_mock = MagicMock()
|
||||
io = SQLiteIO(tmpfile, scene_mock, create_new=False)
|
||||
io.create_schema_on_new()
|
||||
|
|
@ -83,7 +139,36 @@ def test_sqliteio_write_calls_write_meta(tmpfile, view):
|
|||
metamock.assert_called_once()
|
||||
|
||||
|
||||
def test_sqliteio_write_inserts_new_item(tmpfile, view):
|
||||
def test_sqliteio_write_inserts_new_text_item(tmpfile, view):
|
||||
item = BeeTextItem(text='foo bar')
|
||||
view.scene.addItem(item)
|
||||
item.setScale(1.3)
|
||||
item.setPos(44, 55)
|
||||
item.setZValue(0.22)
|
||||
item.setRotation(33)
|
||||
item.do_flip()
|
||||
io = SQLiteIO(tmpfile, view.scene, create_new=True)
|
||||
io.write()
|
||||
|
||||
assert item.save_id == 1
|
||||
result = io.fetchone(
|
||||
'SELECT x, y, z, scale, rotation, flip, items.data, type, '
|
||||
'sqlar.data, sqlar.name '
|
||||
'FROM items '
|
||||
'LEFT OUTER JOIN sqlar on sqlar.item_id = items.id')
|
||||
assert result[0] == 44.0
|
||||
assert result[1] == 55.0
|
||||
assert result[2] == 0.22
|
||||
assert result[3] == 1.3
|
||||
assert result[4] == 33
|
||||
assert result[5] == -1
|
||||
assert json.loads(result[6]) == {'text': 'foo bar'}
|
||||
assert result[7] == 'text'
|
||||
assert result[8] is None
|
||||
assert result[9] is None
|
||||
|
||||
|
||||
def test_sqliteio_write_inserts_new_pixmap_item(tmpfile, view):
|
||||
item = BeePixmapItem(QtGui.QImage(), filename='bee.jpg')
|
||||
view.scene.addItem(item)
|
||||
item.setScale(1.3)
|
||||
|
|
@ -97,7 +182,7 @@ def test_sqliteio_write_inserts_new_item(tmpfile, view):
|
|||
|
||||
assert item.save_id == 1
|
||||
result = io.fetchone(
|
||||
'SELECT x, y, z, scale, rotation, flip, filename, type, '
|
||||
'SELECT x, y, z, scale, rotation, flip, items.data, type, '
|
||||
'sqlar.data, sqlar.name '
|
||||
'FROM items '
|
||||
'INNER JOIN sqlar on sqlar.item_id = items.id')
|
||||
|
|
@ -107,26 +192,61 @@ def test_sqliteio_write_inserts_new_item(tmpfile, view):
|
|||
assert result[3] == 1.3
|
||||
assert result[4] == 33
|
||||
assert result[5] == -1
|
||||
assert result[6] == 'bee.jpg'
|
||||
assert json.loads(result[6]) == {'filename': 'bee.jpg'}
|
||||
assert result[7] == 'pixmap'
|
||||
assert result[8] == b'abc'
|
||||
assert result[9] == '0001-bee.png'
|
||||
|
||||
|
||||
def test_sqliteio_write_inserts_new_item_without_filename(tmpfile, view, item):
|
||||
def test_sqliteio_write_inserts_new_pixmap_item_without_filename(
|
||||
tmpfile, view, item):
|
||||
view.scene.addItem(item)
|
||||
io = SQLiteIO(tmpfile, view.scene, create_new=True)
|
||||
io.write()
|
||||
|
||||
assert item.save_id == 1
|
||||
result = io.fetchone(
|
||||
'SELECT filename, sqlar.name FROM items '
|
||||
'SELECT items.data, sqlar.name FROM items '
|
||||
'INNER JOIN sqlar on sqlar.item_id = items.id')
|
||||
assert result[0] is None
|
||||
assert json.loads(result[0]) == {'filename': None}
|
||||
assert result[1] == '0001.png'
|
||||
|
||||
|
||||
def test_sqliteio_write_updates_existing_item(tmpfile, view):
|
||||
def test_sqliteio_write_updates_existing_text_item(tmpfile, view):
|
||||
item = BeeTextItem(text='foo bar')
|
||||
view.scene.addItem(item)
|
||||
item.setScale(1.3)
|
||||
item.setPos(44, 55)
|
||||
item.setZValue(0.22)
|
||||
item.setRotation(33)
|
||||
item.save_id = 1
|
||||
io = SQLiteIO(tmpfile, view.scene, create_new=True)
|
||||
io.write()
|
||||
item.setScale(0.7)
|
||||
item.setPos(20, 30)
|
||||
item.setZValue(0.33)
|
||||
item.setRotation(100)
|
||||
item.do_flip()
|
||||
item.setPlainText('updated')
|
||||
io.create_new = False
|
||||
io.write()
|
||||
|
||||
assert io.fetchone('SELECT COUNT(*) from items') == (1,)
|
||||
result = io.fetchone(
|
||||
'SELECT x, y, z, scale, rotation, flip, items.data, sqlar.data '
|
||||
'FROM items '
|
||||
'LEFT OUTER JOIN sqlar on sqlar.item_id = items.id')
|
||||
assert result[0] == 20
|
||||
assert result[1] == 30
|
||||
assert result[2] == 0.33
|
||||
assert result[3] == 0.7
|
||||
assert result[4] == 100
|
||||
assert result[5] == -1
|
||||
assert json.loads(result[6]) == {'text': 'updated'}
|
||||
assert result[7] is None
|
||||
|
||||
|
||||
def test_sqliteio_write_updates_existing_pixmap_item(tmpfile, view):
|
||||
item = BeePixmapItem(QtGui.QImage(), filename='bee.png')
|
||||
view.scene.addItem(item)
|
||||
item.setScale(1.3)
|
||||
|
|
@ -149,7 +269,7 @@ def test_sqliteio_write_updates_existing_item(tmpfile, view):
|
|||
|
||||
assert io.fetchone('SELECT COUNT(*) from items') == (1,)
|
||||
result = io.fetchone(
|
||||
'SELECT x, y, z, scale, rotation, flip, filename, sqlar.data '
|
||||
'SELECT x, y, z, scale, rotation, flip, items.data, sqlar.data '
|
||||
'FROM items '
|
||||
'INNER JOIN sqlar on sqlar.item_id = items.id')
|
||||
assert result[0] == 20
|
||||
|
|
@ -158,11 +278,27 @@ def test_sqliteio_write_updates_existing_item(tmpfile, view):
|
|||
assert result[3] == 0.7
|
||||
assert result[4] == 100
|
||||
assert result[5] == -1
|
||||
assert result[6] == 'new.png'
|
||||
assert json.loads(result[6]) == {'filename': 'new.png'}
|
||||
assert result[7] == b'abc'
|
||||
|
||||
|
||||
def test_sqliteio_write_removes_nonexisting_item(tmpfile, view):
|
||||
def test_sqliteio_write_removes_nonexisting_text_item(tmpfile, view):
|
||||
item = BeeTextItem('foo bar')
|
||||
item.setScale(1.3)
|
||||
item.setPos(44, 55)
|
||||
view.scene.addItem(item)
|
||||
io = SQLiteIO(tmpfile, view.scene, create_new=True)
|
||||
io.write()
|
||||
|
||||
view.scene.removeItem(item)
|
||||
io.create_new = False
|
||||
io.write()
|
||||
|
||||
assert io.fetchone('SELECT COUNT(*) from items') == (0,)
|
||||
assert io.fetchone('SELECT COUNT(*) from sqlar') == (0,)
|
||||
|
||||
|
||||
def test_sqliteio_write_removes_nonexisting_pixmap_item(tmpfile, view):
|
||||
item = BeePixmapItem(QtGui.QImage(), filename='bee.png')
|
||||
item.setScale(1.3)
|
||||
item.setPos(44, 55)
|
||||
|
|
@ -215,13 +351,42 @@ def test_sqliteio_write_canceled(tmpfile, view):
|
|||
worker.finished.emit.assert_called_once_with(tmpfile, [])
|
||||
|
||||
|
||||
def test_sqliteio_read_reads_readonly(tmpfile, view, imgdata3x3):
|
||||
def test_sqliteio_read_reads_readonly_text_item(tmpfile, view):
|
||||
io = SQLiteIO(tmpfile, view.scene, create_new=True)
|
||||
io.create_schema_on_new()
|
||||
io.ex('INSERT INTO items '
|
||||
'(type, x, y, z, scale, rotation, flip, filename) '
|
||||
'(type, x, y, z, scale, rotation, flip, data) '
|
||||
'VALUES (?, ?, ?, ?, ?, ?, ?, ?) ',
|
||||
('pixmap', 22.2, 33.3, 0.22, 3.4, 45, -1, 'bee.png'))
|
||||
('text', 22.2, 33.3, 0.22, 3.4, 45, -1,
|
||||
json.dumps({'text': 'foo bar'})))
|
||||
io.connection.commit()
|
||||
del(io)
|
||||
|
||||
io = SQLiteIO(tmpfile, view.scene, readonly=True)
|
||||
io.read()
|
||||
view.scene.add_queued_items()
|
||||
assert len(view.scene.items()) == 1
|
||||
item = view.scene.items()[0]
|
||||
assert item.isSelected() is False
|
||||
assert item.save_id == 1
|
||||
assert item.pos().x() == 22.2
|
||||
assert item.pos().y() == 33.3
|
||||
assert item.zValue() == 0.22
|
||||
assert item.scale() == 3.4
|
||||
assert item.rotation() == 45
|
||||
assert item.flip() == -1
|
||||
assert item.toPlainText() == 'foo bar'
|
||||
assert view.scene.items_to_add.empty() is True
|
||||
|
||||
|
||||
def test_sqliteio_read_reads_readonly_pixmap_item(tmpfile, view, imgdata3x3):
|
||||
io = SQLiteIO(tmpfile, view.scene, create_new=True)
|
||||
io.create_schema_on_new()
|
||||
io.ex('INSERT INTO items '
|
||||
'(type, x, y, z, scale, rotation, flip, data) '
|
||||
'VALUES (?, ?, ?, ?, ?, ?, ?, ?) ',
|
||||
('pixmap', 22.2, 33.3, 0.22, 3.4, 45, -1,
|
||||
json.dumps({'filename': 'bee.png'})))
|
||||
io.ex('INSERT INTO sqlar (item_id, data) VALUES (?, ?)',
|
||||
(1, imgdata3x3))
|
||||
io.connection.commit()
|
||||
|
|
@ -229,8 +394,10 @@ def test_sqliteio_read_reads_readonly(tmpfile, view, imgdata3x3):
|
|||
|
||||
io = SQLiteIO(tmpfile, view.scene, readonly=True)
|
||||
io.read()
|
||||
item, selected = view.scene.items_to_add.get()
|
||||
assert selected is False
|
||||
view.scene.add_queued_items()
|
||||
assert len(view.scene.items()) == 1
|
||||
item = view.scene.items()[0]
|
||||
assert item.isSelected() is False
|
||||
assert item.save_id == 1
|
||||
assert item.pos().x() == 22.2
|
||||
assert item.pos().y() == 33.3
|
||||
|
|
@ -250,11 +417,12 @@ def test_sqliteio_read_updates_progress(tmpfile, view):
|
|||
worker=worker)
|
||||
|
||||
io.create_schema_on_new()
|
||||
io.ex('INSERT INTO items (type, x, y, z, scale, filename) '
|
||||
io.ex('INSERT INTO items (type, x, y, z, scale, data) '
|
||||
'VALUES (?, ?, ?, ?, ?, ?) ',
|
||||
('pixmap', 0, 0, 0, 1, 'bee.png'))
|
||||
('pixmap', 0, 0, 0, 1, json.dumps({'filename': 'bee.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(1)
|
||||
worker.progress.emit.assert_called_once_with(0)
|
||||
|
|
@ -264,17 +432,17 @@ def test_sqliteio_read_updates_progress(tmpfile, view):
|
|||
def test_sqliteio_read_canceled(tmpfile, view):
|
||||
worker = MagicMock(canceled=True)
|
||||
io = SQLiteIO(tmpfile, view.scene, create_new=True, worker=worker)
|
||||
|
||||
io.create_schema_on_new()
|
||||
io.ex('INSERT INTO items (type, x, y, z, scale, filename) '
|
||||
io.ex('INSERT INTO items (type, x, y, z, scale, data) '
|
||||
'VALUES (?, ?, ?, ?, ?, ?) ',
|
||||
('pixmap', 0, 0, 0, 1, 'bee.png'))
|
||||
('pixmap', 0, 0, 0, 1, json.dumps({'filename': 'bee.png'})))
|
||||
io.ex('INSERT INTO sqlar (item_id, data) VALUES (?, ?)', (1, b''))
|
||||
io.ex('INSERT INTO items (type, x, y, z, scale, filename) '
|
||||
io.ex('INSERT INTO items (type, x, y, z, scale, data) '
|
||||
'VALUES (?, ?, ?, ?, ?, ?) ',
|
||||
('pixmap', 50, 50, 0, 1, 'bee2.png'))
|
||||
io.ex('INSERT INTO sqlar (item_id, data) VALUES (?, ?)', (1, b''))
|
||||
('pixmap', 50, 50, 0, 1, json.dumps({'filename': 'bee2.png'})))
|
||||
io.ex('INSERT INTO sqlar (item_id, data) VALUES (?, ?)', (2, b''))
|
||||
io.connection.commit()
|
||||
|
||||
io.read()
|
||||
worker.begin_processing.emit.assert_called_once_with(2)
|
||||
worker.progress.emit.assert_called_once_with(0)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@ from unittest.mock import patch, MagicMock, PropertyMock
|
|||
|
||||
from PyQt6 import QtCore, QtGui
|
||||
|
||||
from beeref.items import BeePixmapItem
|
||||
from beeref.items import BeePixmapItem, item_registry
|
||||
|
||||
|
||||
def test_in_item_registry():
|
||||
assert item_registry['pixmap'] == BeePixmapItem
|
||||
|
||||
|
||||
@patch('beeref.selection.SelectableMixin.init_selectable')
|
||||
|
|
@ -48,6 +52,11 @@ def test_set_pos_center_when_rotated(qapp, item):
|
|||
assert item.pos().y() == -100
|
||||
|
||||
|
||||
def test_get_extra_save_data(item):
|
||||
item.filename = 'foobar.png'
|
||||
assert item.get_extra_save_data() == {'filename': 'foobar.png'}
|
||||
|
||||
|
||||
def test_pixmap_to_bytes(qapp, imgfilename3x3):
|
||||
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
assert item.pixmap_to_bytes().startswith(b'\x89PNG')
|
||||
|
|
@ -114,6 +123,41 @@ def test_selection_action_items(qapp):
|
|||
assert item.selection_action_items() == [item]
|
||||
|
||||
|
||||
def test_update_from_data(item):
|
||||
item.update_from_data(
|
||||
save_id=3,
|
||||
x=11,
|
||||
y=22,
|
||||
z=1.2,
|
||||
scale=2.5,
|
||||
rotation=45,
|
||||
flip=-1)
|
||||
assert item.save_id == 3
|
||||
assert item.pos() == QtCore.QPointF(11, 22)
|
||||
assert item.zValue() == 1.2
|
||||
assert item.rotation() == 45
|
||||
assert item.flip() == -1
|
||||
|
||||
|
||||
def test_update_from_data_keeps_flip(item):
|
||||
item.do_flip()
|
||||
item.update_from_data(flip=-1)
|
||||
assert item.flip() == -1
|
||||
|
||||
|
||||
def test_update_from_data_keeps_unset_values(item):
|
||||
item.setScale(3)
|
||||
item.update_from_data(rotation=45)
|
||||
assert item.scale() == 3
|
||||
|
||||
|
||||
def test_create_from_data(item):
|
||||
new_item = BeePixmapItem.create_from_data(
|
||||
item=item, data={'filename': 'foobar.png'})
|
||||
assert new_item is item
|
||||
assert item.filename == 'foobar.png'
|
||||
|
||||
|
||||
def test_create_copy(qapp, imgfilename3x3):
|
||||
item = BeePixmapItem(QtGui.QImage(imgfilename3x3), 'foo.png')
|
||||
item.setPos(20, 30)
|
||||
301
tests/items/test_textitem.py
Normal file
301
tests/items/test_textitem.py
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
from unittest.mock import patch, MagicMock, PropertyMock
|
||||
|
||||
from PyQt6 import QtCore, QtWidgets
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
from beeref.items import BeeTextItem, item_registry
|
||||
|
||||
|
||||
def test_in_items_registry():
|
||||
assert item_registry['text'] == BeeTextItem
|
||||
|
||||
|
||||
@patch('beeref.selection.SelectableMixin.init_selectable')
|
||||
def test_init(selectable_mock, qapp):
|
||||
item = BeeTextItem('foo bar')
|
||||
assert item.save_id is None
|
||||
assert item.width
|
||||
assert item.height
|
||||
assert item.scale() == 1
|
||||
assert item.toPlainText() == 'foo bar'
|
||||
assert item.is_editable is True
|
||||
assert item.edit_mode is False
|
||||
selectable_mock.assert_called_once()
|
||||
|
||||
|
||||
def test_set_pos_center(qapp):
|
||||
item = BeeTextItem('foo bar')
|
||||
with patch('beeref.items.BeeTextItem.width',
|
||||
new_callable=PropertyMock, return_value=200):
|
||||
with patch('beeref.items.BeeTextItem.height',
|
||||
new_callable=PropertyMock, return_value=100):
|
||||
item.set_pos_center(QtCore.QPointF(0, 0))
|
||||
assert item.pos().x() == -100
|
||||
assert item.pos().y() == -50
|
||||
|
||||
|
||||
def test_set_pos_center_when_scaled(qapp):
|
||||
item = BeeTextItem('foo bar')
|
||||
item.setScale(2)
|
||||
with patch('beeref.items.BeeTextItem.width',
|
||||
new_callable=PropertyMock, return_value=200):
|
||||
with patch('beeref.items.BeeTextItem.height',
|
||||
new_callable=PropertyMock, return_value=100):
|
||||
item.set_pos_center(QtCore.QPointF(0, 0))
|
||||
assert item.pos().x() == -200
|
||||
assert item.pos().y() == -100
|
||||
|
||||
|
||||
def test_set_pos_center_when_rotated(qapp):
|
||||
item = BeeTextItem('foo bar')
|
||||
item.setRotation(90)
|
||||
with patch('beeref.items.BeeTextItem.width',
|
||||
new_callable=PropertyMock, return_value=200):
|
||||
with patch('beeref.items.BeeTextItem.height',
|
||||
new_callable=PropertyMock, return_value=100):
|
||||
item.set_pos_center(QtCore.QPointF(0, 0))
|
||||
assert item.pos().x() == 50
|
||||
assert item.pos().y() == -100
|
||||
|
||||
|
||||
def test_get_extra_save_data(qapp):
|
||||
item = BeeTextItem('foo bar')
|
||||
assert item.get_extra_save_data() == {'text': 'foo bar'}
|
||||
|
||||
|
||||
@patch('beeref.items.BeeTextItem.boundingRect')
|
||||
def test_contains_when_inside_bounds(brect_mock, qapp):
|
||||
brect_mock.return_value = QtCore.QRectF(20, 30, 50, 50)
|
||||
item = BeeTextItem('foo bar')
|
||||
item.contains(QtCore.QPointF(33, 45)) is True
|
||||
brect_mock.assert_called_once_with()
|
||||
|
||||
|
||||
@patch('beeref.items.BeeTextItem.boundingRect')
|
||||
def test_contains_when_outside_bounds(brect_mock, qapp):
|
||||
brect_mock.return_value = QtCore.QRectF(20, 30, 50, 50)
|
||||
item = BeeTextItem('foo bar')
|
||||
item.contains(QtCore.QPointF(19, 29)) is False
|
||||
brect_mock.assert_called_once_with()
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsTextItem.paint')
|
||||
def test_paint(paint_mock, qapp):
|
||||
item = BeeTextItem('foo bar')
|
||||
item.paint_selectable = MagicMock()
|
||||
painter = MagicMock()
|
||||
option = MagicMock()
|
||||
item.paint(painter, option, 'widget')
|
||||
item.paint_selectable.assert_called_once()
|
||||
painter.drawRect.assert_called_once()
|
||||
assert option.state == QtWidgets.QStyle.StateFlag.State_Enabled
|
||||
paint_mock.assert_called_once_with(painter, option, 'widget')
|
||||
|
||||
|
||||
def test_has_selection_outline_when_not_selected(view):
|
||||
item = BeeTextItem('foo bar')
|
||||
view.scene.addItem(item)
|
||||
item.setSelected(False)
|
||||
item.has_selection_outline() is False
|
||||
|
||||
|
||||
def test_has_selection_outline_when_selected(view):
|
||||
item = BeeTextItem('foo bar')
|
||||
view.scene.addItem(item)
|
||||
item.setSelected(True)
|
||||
item.has_selection_outline() is True
|
||||
|
||||
|
||||
def test_has_selection_handles_when_not_selected(view):
|
||||
item = BeeTextItem('foo bar')
|
||||
view.scene.addItem(item)
|
||||
item.setSelected(False)
|
||||
item2 = BeeTextItem('baz')
|
||||
view.scene.addItem(item2)
|
||||
item2.setSelected(False)
|
||||
item.has_selection_handles() is False
|
||||
|
||||
|
||||
def test_has_selection_handles_when_selected_single(view):
|
||||
item = BeeTextItem('foo bar')
|
||||
view.scene.addItem(item)
|
||||
item.setSelected(True)
|
||||
item2 = BeeTextItem('baz')
|
||||
view.scene.addItem(item2)
|
||||
item2.setSelected(False)
|
||||
item.has_selection_handles() is True
|
||||
|
||||
|
||||
def test_has_selection_handles_when_selected_multi(view):
|
||||
item = BeeTextItem('foo bar')
|
||||
view.scene.addItem(item)
|
||||
item.setSelected(True)
|
||||
item2 = BeeTextItem('baz')
|
||||
view.scene.addItem(item2)
|
||||
item2.setSelected(True)
|
||||
item.has_selection_handles() is False
|
||||
|
||||
|
||||
def test_has_selection_handles_when_selected_single_and_edit_mode(view):
|
||||
item = BeeTextItem('foo bar')
|
||||
item.edit_mode = False
|
||||
view.scene.addItem(item)
|
||||
item.setSelected(True)
|
||||
item2 = BeeTextItem('baz')
|
||||
view.scene.addItem(item2)
|
||||
item2.setSelected(False)
|
||||
item.has_selection_handles() is False
|
||||
|
||||
|
||||
def test_selection_action_items(qapp):
|
||||
item = BeeTextItem('foo bar')
|
||||
assert item.selection_action_items() == [item]
|
||||
|
||||
|
||||
def test_update_from_data(qapp):
|
||||
item = BeeTextItem('foo bar')
|
||||
item.update_from_data(
|
||||
save_id=3,
|
||||
x=11,
|
||||
y=22,
|
||||
z=1.2,
|
||||
scale=2.5,
|
||||
rotation=45,
|
||||
flip=-1)
|
||||
assert item.save_id == 3
|
||||
assert item.pos() == QtCore.QPointF(11, 22)
|
||||
assert item.zValue() == 1.2
|
||||
assert item.rotation() == 45
|
||||
assert item.flip() == -1
|
||||
|
||||
|
||||
def test_update_from_data_keeps_flip(qapp):
|
||||
item = BeeTextItem('foo bar')
|
||||
item.do_flip()
|
||||
item.update_from_data(flip=-1)
|
||||
assert item.flip() == -1
|
||||
|
||||
|
||||
def test_update_from_data_keeps_unset_values(qapp):
|
||||
item = BeeTextItem('foo bar')
|
||||
item.setScale(3)
|
||||
item.update_from_data(rotation=45)
|
||||
assert item.scale() == 3
|
||||
|
||||
|
||||
def test_create_from_data(qapp):
|
||||
item = BeeTextItem.create_from_data(data={'text': 'hello world'})
|
||||
item.toPlainText() == 'hello world'
|
||||
|
||||
|
||||
def test_create_copy(qapp):
|
||||
item = BeeTextItem('foo bar')
|
||||
item.setPos(20, 30)
|
||||
item.setRotation(33)
|
||||
item.do_flip()
|
||||
item.setZValue(0.5)
|
||||
item.setScale(2.2)
|
||||
|
||||
copy = item.create_copy()
|
||||
assert copy.toPlainText() == 'foo bar'
|
||||
assert copy.pos() == QtCore.QPointF(20, 30)
|
||||
assert copy.rotation() == 33
|
||||
assert item.flip() == -1
|
||||
assert item.zValue() == 0.5
|
||||
assert item.scale() == 2.2
|
||||
|
||||
|
||||
def test_enter_edit_mode(qapp):
|
||||
item = BeeTextItem('foo bar')
|
||||
item.enter_edit_mode()
|
||||
assert item.edit_mode is True
|
||||
flags = item.textInteractionFlags()
|
||||
assert flags == Qt.TextInteractionFlag.TextEditorInteraction
|
||||
|
||||
|
||||
@patch('PyQt6.QtGui.QTextCursor')
|
||||
@patch('beeref.items.BeeTextItem.setTextCursor')
|
||||
def test_exit_edit_mode(setcursor_mock, cursor_mock, qapp):
|
||||
item = BeeTextItem('foo bar')
|
||||
item.edit_mode = True
|
||||
item.exit_edit_mode()
|
||||
assert item.edit_mode is False
|
||||
flags = item.textInteractionFlags()
|
||||
assert flags == Qt.TextInteractionFlag.NoTextInteraction
|
||||
cursor_mock.assert_called_once_with(item.document())
|
||||
setcursor_mock.assert_called_once_with(cursor_mock.return_value)
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsTextItem.keyPressEvent')
|
||||
@patch('beeref.items.BeeTextItem.exit_edit_mode')
|
||||
def test_key_press_event_any_key(exit_mock, key_press_mock, view):
|
||||
item = BeeTextItem('foo bar')
|
||||
view.scene.addItem(item)
|
||||
view.scene.edit_item = item
|
||||
event = MagicMock()
|
||||
event.key.return_value = Qt.Key.Key_T
|
||||
event.modifiers.return_value = Qt.KeyboardModifier.NoModifier
|
||||
item.keyPressEvent(event)
|
||||
key_press_mock.assert_called_once_with(event)
|
||||
exit_mock.assert_not_called()
|
||||
assert view.scene.edit_item == item
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsTextItem.keyPressEvent')
|
||||
@patch('beeref.items.BeeTextItem.exit_edit_mode')
|
||||
def test_key_press_event_shift_return(exit_mock, key_press_mock, view):
|
||||
item = BeeTextItem('foo bar')
|
||||
view.scene.addItem(item)
|
||||
view.scene.edit_item = item
|
||||
event = MagicMock()
|
||||
event.key.return_value = Qt.Key.Key_Return
|
||||
event.modifiers.return_value = Qt.KeyboardModifier.ShiftModifier
|
||||
item.keyPressEvent(event)
|
||||
key_press_mock.assert_called_once_with(event)
|
||||
exit_mock.assert_not_called()
|
||||
assert view.scene.edit_item == item
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsTextItem.keyPressEvent')
|
||||
@patch('beeref.items.BeeTextItem.exit_edit_mode')
|
||||
def test_key_press_event_shift_enter(exit_mock, key_press_mock, view):
|
||||
item = BeeTextItem('foo bar')
|
||||
view.scene.addItem(item)
|
||||
view.scene.edit_item = item
|
||||
event = MagicMock()
|
||||
event.key.return_value = Qt.Key.Key_Enter
|
||||
event.modifiers.return_value = Qt.KeyboardModifier.ShiftModifier
|
||||
item.keyPressEvent(event)
|
||||
key_press_mock.assert_called_once_with(event)
|
||||
exit_mock.assert_not_called()
|
||||
assert view.scene.edit_item == item
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsTextItem.keyPressEvent')
|
||||
@patch('beeref.items.BeeTextItem.exit_edit_mode')
|
||||
def test_key_press_event_return(exit_mock, key_press_mock, view):
|
||||
item = BeeTextItem('foo bar')
|
||||
view.scene.addItem(item)
|
||||
view.scene.edit_item = item
|
||||
event = MagicMock()
|
||||
event.key.return_value = Qt.Key.Key_Return
|
||||
event.modifiers.return_value = Qt.KeyboardModifier.NoModifier
|
||||
item.keyPressEvent(event)
|
||||
key_press_mock.assert_not_called()
|
||||
exit_mock.assert_called_once_with()
|
||||
assert view.scene.edit_item is None
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsTextItem.keyPressEvent')
|
||||
@patch('beeref.items.BeeTextItem.exit_edit_mode')
|
||||
def test_key_press_event_enter(exit_mock, key_press_mock, view):
|
||||
item = BeeTextItem('foo bar')
|
||||
view.scene.addItem(item)
|
||||
view.scene.edit_item = item
|
||||
event = MagicMock()
|
||||
event.key.return_value = Qt.Key.Key_Enter
|
||||
event.modifiers.return_value = Qt.KeyboardModifier.NoModifier
|
||||
item.keyPressEvent(event)
|
||||
key_press_mock.assert_not_called()
|
||||
exit_mock.assert_called_once_with()
|
||||
assert view.scene.edit_item is None
|
||||
|
|
@ -7,7 +7,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets
|
|||
from PyQt6.QtCore import Qt
|
||||
|
||||
from beeref import commands
|
||||
from beeref.items import BeePixmapItem
|
||||
from beeref.items import BeePixmapItem, BeeTextItem
|
||||
|
||||
|
||||
def test_add_remove_item(view, item):
|
||||
|
|
@ -447,6 +447,66 @@ def test_mouse_press_event_when_left_click_over_item(mouse_mock, view, item):
|
|||
assert view.scene.event_start == QtCore.QPointF(10, 20)
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsScene.mousePressEvent')
|
||||
def test_mouse_press_event_when_left_click_over_item_in_edit_mode(
|
||||
mouse_mock, view):
|
||||
item = BeeTextItem('foo bar')
|
||||
item.exit_edit_mode = MagicMock()
|
||||
view.scene.addItem(item)
|
||||
view.scene.edit_item = item
|
||||
view.scene.itemAt = MagicMock(return_value=item)
|
||||
event = MagicMock(
|
||||
button=MagicMock(return_value=Qt.MouseButton.LeftButton),
|
||||
)
|
||||
view.scene.mousePressEvent(event)
|
||||
event.accept.assert_not_called()
|
||||
mouse_mock.assert_called_once_with(event)
|
||||
assert view.scene.edit_item == item
|
||||
item.exit_edit_mode.assert_not_called()
|
||||
assert view.scene.move_active is False
|
||||
assert view.scene.rubberband_active is False
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsScene.mousePressEvent')
|
||||
def test_mouse_press_event_when_left_click_over_diff_item_in_edit_mode(
|
||||
mouse_mock, view, item):
|
||||
txtitem = BeeTextItem('foo bar')
|
||||
txtitem.exit_edit_mode = MagicMock()
|
||||
view.scene.addItem(txtitem)
|
||||
view.scene.edit_item = txtitem
|
||||
view.scene.itemAt = MagicMock(return_value=item)
|
||||
event = MagicMock(
|
||||
button=MagicMock(return_value=Qt.MouseButton.LeftButton),
|
||||
)
|
||||
view.scene.mousePressEvent(event)
|
||||
event.accept.assert_not_called()
|
||||
mouse_mock.assert_called_once_with(event)
|
||||
assert view.scene.edit_item is None
|
||||
txtitem.exit_edit_mode.assert_called_once_with()
|
||||
assert view.scene.move_active is True
|
||||
assert view.scene.rubberband_active is False
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsScene.mousePressEvent')
|
||||
def test_mouse_press_event_when_left_click_over_no_item_in_edit_mode(
|
||||
mouse_mock, view):
|
||||
item = BeeTextItem('foo bar')
|
||||
item.exit_edit_mode = MagicMock()
|
||||
view.scene.addItem(item)
|
||||
view.scene.edit_item = item
|
||||
view.scene.itemAt = MagicMock(return_value=None)
|
||||
event = MagicMock(
|
||||
button=MagicMock(return_value=Qt.MouseButton.LeftButton),
|
||||
)
|
||||
view.scene.mousePressEvent(event)
|
||||
event.accept.assert_not_called()
|
||||
mouse_mock.assert_called_once_with(event)
|
||||
assert view.scene.edit_item is None
|
||||
item.exit_edit_mode.assert_called_once_with()
|
||||
assert view.scene.move_active is False
|
||||
assert view.scene.rubberband_active is True
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsScene.mousePressEvent')
|
||||
def test_mouse_press_event_when_left_click_not_over_item(
|
||||
mouse_mock, view, item):
|
||||
|
|
@ -501,6 +561,33 @@ def test_mouse_doubleclick_event_when_over_item(mouse_mock, view, item):
|
|||
mouse_mock.assert_not_called()
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsScene.mousePressEvent')
|
||||
@patch('PyQt6.QtWidgets.QGraphicsScene.mouseDoubleClickEvent')
|
||||
def test_mouse_doubleclick_event_when_over_editable_item(
|
||||
double_mock, press_mock, view):
|
||||
item = BeeTextItem('foo bar')
|
||||
item.enter_edit_mode = MagicMock()
|
||||
event = MagicMock()
|
||||
view.scene.move_active = True
|
||||
view.scene.addItem(item)
|
||||
item.setPos(30, 40)
|
||||
item.setSelected(True)
|
||||
view.scene.itemAt = MagicMock(return_value=item)
|
||||
view.fit_rect = MagicMock()
|
||||
|
||||
with patch('beeref.items.BeePixmapItem.width',
|
||||
new_callable=PropertyMock, return_value=100):
|
||||
with patch('beeref.items.BeePixmapItem.height',
|
||||
new_callable=PropertyMock, return_value=100):
|
||||
view.scene.mouseDoubleClickEvent(event)
|
||||
|
||||
assert view.scene.move_active is False
|
||||
item.enter_edit_mode.assert_called_once_with()
|
||||
view.scene.edit_item == item
|
||||
double_mock.assert_not_called()
|
||||
press_mock.assert_called_once_with(event)
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QGraphicsScene.mouseDoubleClickEvent')
|
||||
def test_mouse_doubleclick_event_when_item_not_selected(
|
||||
mouse_mock, view, item):
|
||||
|
|
@ -944,21 +1031,24 @@ def test_on_change_when_no_multi_select(view):
|
|||
view.scene.multi_select_item.fit_selection_area.assert_not_called()
|
||||
|
||||
|
||||
def test_add_queued_items_unselected(view, item):
|
||||
item.setZValue(0.33)
|
||||
view.scene.add_item_later(item, selected=False)
|
||||
def test_add_queued_items_unselected(view):
|
||||
data = {'type': 'text', 'z': 0.33, 'data': {'text': 'foo'}}
|
||||
view.scene.add_item_later(data, selected=False)
|
||||
view.scene.add_queued_items()
|
||||
assert view.scene.items() == [item]
|
||||
assert len(view.scene.items()) == 1
|
||||
item = view.scene.items()[0]
|
||||
assert item.isSelected() is False
|
||||
assert view.scene.max_z == 0.33
|
||||
assert item.toPlainText() == 'foo'
|
||||
|
||||
|
||||
def test_add_queued_items_selected(view, item):
|
||||
def test_add_queued_items_selected(view):
|
||||
view.scene.max_z = 0.6
|
||||
item.setZValue(0.33)
|
||||
view.scene.add_item_later(item, selected=True)
|
||||
data = {'type': 'text', 'z': 0.33, 'data': {'text': 'foo'}}
|
||||
view.scene.add_item_later(data, selected=True)
|
||||
view.scene.add_queued_items()
|
||||
assert view.scene.items() == [item]
|
||||
assert len(view.scene.items()) == 1
|
||||
item = view.scene.items()[0]
|
||||
assert item.isSelected() is True
|
||||
assert item.zValue() > 0.6
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import os.path
|
||||
import shutil
|
||||
import sqlite3
|
||||
from unittest.mock import MagicMock, patch, mock_open
|
||||
|
||||
|
|
@ -244,7 +245,6 @@ def test_on_action_save_as_when_error(
|
|||
view.scene.addItem(item)
|
||||
view.on_saving_finished = MagicMock()
|
||||
filename = os.path.join(tmpdir, 'test.bee')
|
||||
assert os.path.exists(filename) is False
|
||||
dialog_mock.return_value = (filename, None)
|
||||
save_mock.side_effect = sqlite3.Error('foo')
|
||||
view.on_action_save_as()
|
||||
|
|
@ -256,8 +256,10 @@ def test_on_action_save(view, qtbot, imgfilename3x3, tmpdir):
|
|||
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
view.scene.addItem(item)
|
||||
view.filename = os.path.join(tmpdir, 'test.bee')
|
||||
root = os.path.dirname(__file__)
|
||||
shutil.copyfile(os.path.join(root, 'assets', 'test1item.bee'),
|
||||
view.filename)
|
||||
view.on_saving_finished = MagicMock()
|
||||
assert os.path.exists(view.filename) is False
|
||||
view.on_action_save()
|
||||
qtbot.waitUntil(lambda: view.on_saving_finished.called is True)
|
||||
assert os.path.exists(view.filename) is True
|
||||
|
|
@ -320,6 +322,16 @@ def test_on_action_insert_images_when_error(
|
|||
'', ['iaeiae', 'trntrn'])
|
||||
|
||||
|
||||
@patch('beeref.scene.BeeGraphicsScene.clearSelection')
|
||||
def test_on_action_insert_text(clear_mock, view):
|
||||
view.on_action_insert_text()
|
||||
clear_mock.assert_called_once_with()
|
||||
assert len(view.scene.items()) == 1
|
||||
item = view.scene.items()[0]
|
||||
assert item.toPlainText() == 'Text'
|
||||
assert item.isSelected() is True
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QApplication.clipboard')
|
||||
def test_on_action_copy(clipboard_mock, view, imgfilename3x3):
|
||||
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
|
|
@ -359,9 +371,24 @@ def test_on_action_paste_internal(mimedata_mock, clear_mock, view):
|
|||
|
||||
|
||||
@patch('beeref.scene.BeeGraphicsScene.clearSelection')
|
||||
@patch('PyQt6.QtGui.QClipboard.text')
|
||||
@patch('PyQt6.QtGui.QClipboard.image')
|
||||
def test_on_action_paste_when_empty(clipboard_mock, clear_mock, view):
|
||||
clipboard_mock.return_value = QtGui.QImage()
|
||||
def test_on_action_paste_when_text(img_mock, text_mock, clear_mock, view):
|
||||
img_mock.return_value = QtGui.QImage()
|
||||
text_mock.return_value = 'foo bar'
|
||||
view.on_action_paste()
|
||||
assert len(view.scene.items()) == 1
|
||||
assert view.scene.items()[0].isSelected() is True
|
||||
assert view.scene.items()[0].toPlainText() == 'foo bar'
|
||||
clear_mock.assert_called_once_with()
|
||||
|
||||
|
||||
@patch('beeref.scene.BeeGraphicsScene.clearSelection')
|
||||
@patch('PyQt6.QtGui.QClipboard.text')
|
||||
@patch('PyQt6.QtGui.QClipboard.image')
|
||||
def test_on_action_paste_when_empty(img_mock, text_mock, clear_mock, view):
|
||||
img_mock.return_value = QtGui.QImage()
|
||||
text_mock.return_value = ''
|
||||
view.on_action_paste()
|
||||
assert len(view.scene.items()) == 0
|
||||
clear_mock.assert_not_called()
|
||||
|
|
|
|||
Loading…
Reference in a new issue