mirror of
https://github.com/rbreu/beeref.git
synced 2026-03-11 08:54:28 +00:00
Added configurable gap between images when arranging
This commit is contained in:
parent
781c49fddd
commit
9c506aadaa
8 changed files with 212 additions and 14 deletions
|
|
@ -1,7 +1,14 @@
|
|||
0.3.2 - (unreleased)
|
||||
====================
|
||||
|
||||
Added
|
||||
-----
|
||||
|
||||
* For arranging, a gap between images can now be configured in the
|
||||
settings.
|
||||
|
||||
Fixed:
|
||||
------
|
||||
|
||||
* Scene Export: Fix output image size and margins when scene had been
|
||||
scaled or moved
|
||||
|
|
|
|||
|
|
@ -106,8 +106,16 @@ settings_events = BeeSettingsEvents()
|
|||
|
||||
class BeeSettings(QtCore.QSettings):
|
||||
|
||||
DEFAULTS = {
|
||||
'Items/image_storage_format': 'best',
|
||||
FIELDS = {
|
||||
'Items/image_storage_format': {
|
||||
'default': 'best',
|
||||
'validate': lambda x: x in ('png', 'jpg', 'best'),
|
||||
},
|
||||
'Items/arrange_gap': {
|
||||
'default': 0,
|
||||
'cast': int,
|
||||
'validate': lambda x: 0 <= x <= 200,
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
|
|
@ -123,15 +131,40 @@ class BeeSettings(QtCore.QSettings):
|
|||
constants.APPNAME,
|
||||
constants.APPNAME)
|
||||
|
||||
def valueOrDefault(self, key, type=None):
|
||||
val = self.value(key, type)
|
||||
def valueOrDefault(self, key):
|
||||
"""Get the value for key, or the default value specified in FIELDS.
|
||||
|
||||
This is the method to be used for configurable settings (as
|
||||
opposed to settings that BeeRef stores on its own.)
|
||||
|
||||
This will validate and type cast the given value if 'cast' and
|
||||
'validate' are specified in the FIELDS entry for the given
|
||||
key. The default value will be returned if validation or type
|
||||
casting fails.
|
||||
|
||||
"""
|
||||
|
||||
val = self.value(key)
|
||||
conf = self.FIELDS[key]
|
||||
if val is None:
|
||||
val = self.DEFAULTS.get(key)
|
||||
val = conf['default']
|
||||
if 'cast' in conf:
|
||||
try:
|
||||
val = conf['cast'](val)
|
||||
except (ValueError, TypeError):
|
||||
val = conf['default']
|
||||
if 'validate' in conf:
|
||||
if not conf['validate'](val):
|
||||
val = conf['default']
|
||||
return val
|
||||
|
||||
def restore_defaults(self):
|
||||
"""Restore all the values specified in FILEDS to their default values
|
||||
by removing them from the settings file.
|
||||
"""
|
||||
|
||||
logger.debug('Restoring settings to defaults')
|
||||
for key in self.DEFAULTS.keys():
|
||||
for key in self.FIELDS.keys():
|
||||
self.remove(key)
|
||||
settings_events.restore_defaults.emit()
|
||||
|
||||
|
|
|
|||
|
|
@ -135,8 +135,6 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem):
|
|||
"""Determines the format for storing this image."""
|
||||
|
||||
formt = self.settings.valueOrDefault('Items/image_storage_format')
|
||||
if formt not in ('png', 'jpg', 'best'):
|
||||
formt = 'best'
|
||||
|
||||
if formt == 'best':
|
||||
# Images with alpha channel and small images are stored as png
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from PyQt6.QtCore import Qt
|
|||
import rpack
|
||||
|
||||
from beeref import commands
|
||||
from beeref.config import BeeSettings
|
||||
from beeref.items import item_registry
|
||||
from beeref.selection import MultiSelectItem, RubberbandItem
|
||||
|
||||
|
|
@ -48,6 +49,7 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
self.internal_clipboard = []
|
||||
self.edit_item = None
|
||||
self.crop_item = None
|
||||
self.settings = BeeSettings()
|
||||
|
||||
def clear(self):
|
||||
super().clear()
|
||||
|
|
@ -166,6 +168,7 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
if len(items) < 2:
|
||||
return
|
||||
|
||||
gap = self.settings.valueOrDefault('Items/arrange_gap')
|
||||
center = self.get_selection_center()
|
||||
positions = []
|
||||
rects = []
|
||||
|
|
@ -182,7 +185,7 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
positions.append(
|
||||
QtCore.QPointF(
|
||||
round(center.x() - rect['rect'].width()/2), y))
|
||||
y += rect['rect'].height()
|
||||
y += rect['rect'].height() + gap
|
||||
|
||||
else:
|
||||
rects.sort(key=lambda r: r['rect'].topLeft().x())
|
||||
|
|
@ -192,7 +195,7 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
positions.append(
|
||||
QtCore.QPointF(
|
||||
x, round(center.y() - rect['rect'].height()/2)))
|
||||
x += rect['rect'].width()
|
||||
x += rect['rect'].width() + gap
|
||||
|
||||
self.undo_stack.push(
|
||||
commands.ArrangeItems(self,
|
||||
|
|
@ -206,12 +209,14 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
if len(items) < 2:
|
||||
return
|
||||
|
||||
gap = self.settings.valueOrDefault('Items/arrange_gap')
|
||||
center = self.get_selection_center()
|
||||
|
||||
sizes = []
|
||||
for item in items:
|
||||
rect = self.itemsBoundingRect(items=[item])
|
||||
sizes.append((round(rect.width()), round(rect.height())))
|
||||
|
||||
center = self.get_selection_center()
|
||||
sizes.append((round(rect.width() + gap),
|
||||
round(rect.height() + gap)))
|
||||
|
||||
# The minimal area the items need if they could be packed optimally;
|
||||
# we use this as a starting shape for the packing algorithm
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ class RadioGroup(QtWidgets.QGroupBox):
|
|||
layout.addWidget(btn)
|
||||
|
||||
self.ignore_values_changed = False
|
||||
layout.addStretch(100)
|
||||
|
||||
def on_values_changed(self, value, button):
|
||||
if self.ignore_values_changed:
|
||||
|
|
@ -73,6 +74,48 @@ class RadioGroup(QtWidgets.QGroupBox):
|
|||
self.ignore_values_changed = False
|
||||
|
||||
|
||||
class IntegerGroup(QtWidgets.QGroupBox):
|
||||
TITLE = None
|
||||
HELPTEXT = None
|
||||
KEY = None
|
||||
MIN = None
|
||||
MAX = None
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(self.TITLE)
|
||||
self.settings = BeeSettings()
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
settings_events.restore_defaults.connect(self.on_restore_defaults)
|
||||
|
||||
if self.HELPTEXT:
|
||||
helptxt = QtWidgets.QLabel(self.HELPTEXT)
|
||||
helptxt.setWordWrap(True)
|
||||
layout.addWidget(helptxt)
|
||||
|
||||
self.input = QtWidgets.QSpinBox()
|
||||
self.input.setValue(self.settings.valueOrDefault(self.KEY))
|
||||
self.input.setRange(self.MIN, self.MAX)
|
||||
self.input.valueChanged.connect(self.on_value_changed)
|
||||
layout.addWidget(self.input)
|
||||
layout.addStretch(100)
|
||||
self.ignore_values_changed = False
|
||||
|
||||
def on_value_changed(self, value):
|
||||
if self.ignore_values_changed:
|
||||
return
|
||||
|
||||
if value != self.settings.valueOrDefault(self.KEY):
|
||||
logger.debug(f'Setting {self.KEY} changed to: {value}')
|
||||
self.settings.setValue(self.KEY, value)
|
||||
|
||||
def on_restore_defaults(self):
|
||||
new_value = self.settings.valueOrDefault(self.KEY)
|
||||
self.ignore_values_changed = True
|
||||
self.input.setValue(new_value)
|
||||
self.ignore_values_changed = False
|
||||
|
||||
|
||||
class ImageStorageFormatWidget(RadioGroup):
|
||||
TITLE = 'Image Storage Format:'
|
||||
HELPTEXT = ('How images are stored inside bee files.'
|
||||
|
|
@ -87,6 +130,14 @@ class ImageStorageFormatWidget(RadioGroup):
|
|||
'Small bee file, but lossy and no transparency support'))
|
||||
|
||||
|
||||
class ArrangeGapWidget(IntegerGroup):
|
||||
TITLE = 'Arrange Gap:'
|
||||
HELPTEXT = ('The gap between images when using arrange actions.')
|
||||
KEY = 'Items/arrange_gap'
|
||||
MIN = 0
|
||||
MAX = 200
|
||||
|
||||
|
||||
class SettingsDialog(QtWidgets.QDialog):
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
|
|
@ -98,6 +149,7 @@ class SettingsDialog(QtWidgets.QDialog):
|
|||
misc_layout = QtWidgets.QGridLayout()
|
||||
misc.setLayout(misc_layout)
|
||||
misc_layout.addWidget(ImageStorageFormatWidget(), 0, 0)
|
||||
misc_layout.addWidget(ArrangeGapWidget(), 0, 1)
|
||||
tabs.addTab(misc, '&Miscellaneous')
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
|
|
|
|||
|
|
@ -44,6 +44,21 @@ def test_settings_value_or_default_gets_overriden_value(settings):
|
|||
assert settings.valueOrDefault('Items/image_storage_format') == 'png'
|
||||
|
||||
|
||||
def test_settings_value_or_default_gets_default_when_invalid(settings):
|
||||
settings.setValue('Items/image_storage_format', 'foo')
|
||||
assert settings.valueOrDefault('Items/image_storage_format') == 'best'
|
||||
|
||||
|
||||
def test_settings_value_or_default_casts_value(settings):
|
||||
settings.setValue('Items/arrange_gap', '5')
|
||||
assert settings.valueOrDefault('Items/arrange_gap') == 5
|
||||
|
||||
|
||||
def test_settings_value_or_default_gets_default_when_cast_error(settings):
|
||||
settings.setValue('Items/arrange_gap', 'foo')
|
||||
assert settings.valueOrDefault('Items/arrange_gap') == 0
|
||||
|
||||
|
||||
def test_restore_defaults_restores(settings):
|
||||
settings.setValue('Items/image_storage_format', 'png')
|
||||
settings.restore_defaults()
|
||||
|
|
|
|||
|
|
@ -271,6 +271,29 @@ def test_arrange_horizontal(view):
|
|||
view.scene.cancel_crop_mode.assert_called_once_with()
|
||||
|
||||
|
||||
def test_arrange_horizontal_with_gap(view, settings):
|
||||
settings.setValue('Items/arrange_gap', 6)
|
||||
item1 = BeePixmapItem(QtGui.QImage())
|
||||
view.scene.addItem(item1)
|
||||
item1.setSelected(True)
|
||||
item1.setPos(10, -100)
|
||||
item2 = BeePixmapItem(QtGui.QImage())
|
||||
view.scene.addItem(item2)
|
||||
item2.setSelected(True)
|
||||
item2.setPos(-10, 40)
|
||||
view.scene.cancel_crop_mode = MagicMock()
|
||||
|
||||
with patch.object(item1, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
with patch.object(item2, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
view.scene.arrange()
|
||||
|
||||
assert item2.pos() == QtCore.QPointF(-50, -30)
|
||||
assert item1.pos() == QtCore.QPointF(56, -30)
|
||||
view.scene.cancel_crop_mode.assert_called_once_with()
|
||||
|
||||
|
||||
def test_arrange_vertical(view):
|
||||
item1 = BeePixmapItem(QtGui.QImage())
|
||||
view.scene.addItem(item1)
|
||||
|
|
@ -293,6 +316,29 @@ def test_arrange_vertical(view):
|
|||
view.scene.cancel_crop_mode = MagicMock()
|
||||
|
||||
|
||||
def test_arrange_vertical_with_gap(view, settings):
|
||||
settings.setValue('Items/arrange_gap', 6)
|
||||
item1 = BeePixmapItem(QtGui.QImage())
|
||||
view.scene.addItem(item1)
|
||||
item1.setSelected(True)
|
||||
item1.setPos(10, -100)
|
||||
item2 = BeePixmapItem(QtGui.QImage())
|
||||
view.scene.addItem(item2)
|
||||
item2.setSelected(True)
|
||||
item2.setPos(-10, 40)
|
||||
view.scene.cancel_crop_mode = MagicMock()
|
||||
|
||||
with patch.object(item1, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
with patch.object(item2, 'bounding_rect_unselected',
|
||||
return_value=QtCore.QRectF(0, 0, 100, 80)):
|
||||
view.scene.arrange(vertical=True)
|
||||
|
||||
assert item1.pos() == QtCore.QPointF(0, -70)
|
||||
assert item2.pos() == QtCore.QPointF(0, 16)
|
||||
view.scene.cancel_crop_mode = MagicMock()
|
||||
|
||||
|
||||
def test_arrange_when_rotated(view):
|
||||
item1 = BeePixmapItem(QtGui.QImage())
|
||||
view.scene.addItem(item1)
|
||||
|
|
@ -340,6 +386,24 @@ def test_arrange_optimal(view):
|
|||
view.scene.cancel_crop_mode.assert_called_once_with()
|
||||
|
||||
|
||||
def test_arrange_optimal_with_gap(view, settings):
|
||||
settings.setValue('Items/arrange_gap', 6)
|
||||
for i in range(4):
|
||||
item = BeePixmapItem(QtGui.QImage())
|
||||
view.scene.addItem(item)
|
||||
item.setSelected(True)
|
||||
item.crop = QtCore.QRectF(0, 0, 100, 80)
|
||||
|
||||
view.scene.cancel_crop_mode = MagicMock()
|
||||
view.scene.arrange_optimal()
|
||||
expected_positions = {(-56, -46), (50, -46), (-56, 40), (50, 40)}
|
||||
actual_positions = {
|
||||
(i.pos().x(), i.pos().y())
|
||||
for i in view.scene.selectedItems(user_only=True)}
|
||||
assert expected_positions == actual_positions
|
||||
view.scene.cancel_crop_mode.assert_called_once_with()
|
||||
|
||||
|
||||
def test_arrange_optimal_when_rotated(view):
|
||||
for i in range(4):
|
||||
item = BeePixmapItem(QtGui.QImage())
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from unittest.mock import patch
|
|||
|
||||
from PyQt6 import QtWidgets
|
||||
from beeref.widgets.settings import (
|
||||
ArrangeGapWidget,
|
||||
ImageStorageFormatWidget,
|
||||
SettingsDialog,
|
||||
)
|
||||
|
|
@ -22,7 +23,7 @@ def test_image_storage_format_saves_change(settings, view):
|
|||
assert widget.buttons['best'].isChecked() is False
|
||||
assert widget.buttons['png'].isChecked() is False
|
||||
assert widget.buttons['jpg'].isChecked() is True
|
||||
assert settings.valueOrDefault('Items/image_storage_format', 'jpg')
|
||||
assert settings.valueOrDefault('Items/image_storage_format') == 'jpg'
|
||||
|
||||
|
||||
def test_image_storage_format_on_restore_defaults(settings, view):
|
||||
|
|
@ -35,11 +36,34 @@ def test_image_storage_format_on_restore_defaults(settings, view):
|
|||
assert widget.buttons['jpg'].isChecked() is False
|
||||
|
||||
|
||||
def test_arrange_gap_initialises_input_from_settings(settings, view):
|
||||
settings.setValue('Items/arrange_gap', 6)
|
||||
widget = ArrangeGapWidget()
|
||||
assert widget.input.value() == 6
|
||||
|
||||
|
||||
def test_arrange_gap_saves_change(settings, view):
|
||||
settings.setValue('Items/arrange_gap', 6)
|
||||
widget = ArrangeGapWidget()
|
||||
widget.input.setValue(8)
|
||||
assert settings.valueOrDefault('Items/arrange_gap') == 8
|
||||
|
||||
|
||||
def test_arrange_gap_on_restore_defaults(settings, view):
|
||||
widget = ArrangeGapWidget()
|
||||
widget.input.setValue(7)
|
||||
settings.setValue('Items/arrange_gap', 0)
|
||||
widget.on_restore_defaults()
|
||||
assert widget.input.value() == 0
|
||||
|
||||
|
||||
@patch('PyQt6.QtWidgets.QMessageBox.question',
|
||||
return_value=QtWidgets.QMessageBox.StandardButton.Yes)
|
||||
def test_settings_dialog_on_restore_defaults(msg_mock, settings, view):
|
||||
dialog = SettingsDialog(view)
|
||||
settings.setValue('Items/image_storage_format', 'jpg')
|
||||
settings.setValue('Items/arrange_gap', 10)
|
||||
dialog.on_restore_defaults()
|
||||
msg_mock.assert_called_once()
|
||||
assert settings.valueOrDefault('Items/image_storage_format') == 'best'
|
||||
assert settings.valueOrDefault('Items/arrange_gap') == 0
|
||||
|
|
|
|||
Loading…
Reference in a new issue