Add action 'Arrange by Filename'

This commit is contained in:
Rebecca Breu 2024-05-25 18:21:23 +02:00
parent aac2d0edfc
commit b22056cefa
9 changed files with 227 additions and 2 deletions

View file

@ -13,6 +13,7 @@ Added
* Added a confirmation dialog when attempting to close unsaved files.
The confirmation dialog can be disalbed in:
Settings -> Miscellaneous -> Confirm when closing an unsaved file
* Add option to arrange by filename (Arrange -> By Filename)
Fixed

View file

@ -260,6 +260,12 @@ actions = ActionList([
callback='on_action_arrange_vertical',
group='active_when_selection',
),
Action(
id='arrange_by_filename',
text='By &Filename',
callback='on_action_arrange_by_filename',
group='active_when_selection',
),
Action(
id='change_opacity',
text='Change &Opacity...',

View file

@ -102,6 +102,7 @@ menu_structure = [
'arrange_optimal',
'arrange_horizontal',
'arrange_vertical',
'arrange_by_filename',
],
},
{

View file

@ -41,6 +41,32 @@ def register_item(cls):
return cls
def sort_by_filename(items):
"""Order items by filename.
Items with a filename (ordered by filename) first, then items
without a filename but with a save_id follow (ordered by
save_id), then remaining items in the order that they have
been inserted into the scene.
"""
items_by_filename = []
items_by_save_id = []
items_remaining = []
for item in items:
if getattr(item, 'filename', None):
items_by_filename.append(item)
elif getattr(item, 'save_id', None):
items_by_save_id.append(item)
else:
items_remaining.append(item)
items_by_filename.sort(key=lambda x: x.filename)
items_by_save_id.sort(key=lambda x: x.save_id)
return items_by_filename + items_by_save_id + items_remaining
class BeeItemMixin(SelectableMixin):
"""Base for all items added by the user."""

View file

@ -24,7 +24,7 @@ import rpack
from beeref import commands
from beeref.config import BeeSettings
from beeref.items import item_registry, BeeErrorItem
from beeref.items import item_registry, BeeErrorItem, sort_by_filename
from beeref.selection import MultiSelectItem, RubberbandItem
@ -229,7 +229,6 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
return
gap = self.settings.valueOrDefault('Items/arrange_gap')
center = self.get_selection_center()
sizes = []
for item in items:
@ -252,12 +251,58 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
# We want the items to center around the selection's center,
# not (0, 0)
center = self.get_selection_center()
bounds = rpack.bbox_size(sizes, positions)
diff = center - QtCore.QPointF(bounds[0]/2, bounds[1]/2)
positions = [QtCore.QPointF(*pos) + diff for pos in positions]
self.undo_stack.push(commands.ArrangeItems(self, items, positions))
def arrange_by_filename(self):
"""Order items by filename.
Items with a filename (ordered by filename) first, then items
without a filename but with a save_id follow (ordered by
save_id), then remaining items in the order that they have
been inserted into the scene.
"""
self.cancel_active_modes()
max_width = 0
max_height = 0
gap = self.settings.valueOrDefault('Items/arrange_gap')
items = sort_by_filename(self.selectedItems(user_only=True))
if len(items) < 2:
return
for item in items:
rect = self.itemsBoundingRect(items=[item])
max_width = max(max_width, rect.width() + gap)
max_height = max(max_height, rect.height() + gap)
# We want the items to center around the selection's center,
# not (0, 0)
num_rows = math.ceil(math.sqrt(len(items)))
center = self.get_selection_center()
diff = center - num_rows/2 * QtCore.QPointF(max_width, max_height)
iter_items = iter(items)
positions = []
for j in range(num_rows):
for i in range(num_rows):
try:
item = next(iter_items)
rect = self.itemsBoundingRect(items=[item])
point = QtCore.QPointF(
i * max_width + (max_width - rect.width())/2,
j * max_height + (max_height - rect.height())/2)
positions.append(point + diff)
except StopIteration:
break
self.undo_stack.push(commands.ArrangeItems(self, items, positions))
def flip_items(self, vertical=False):
"""Flip selected items."""
self.cancel_active_modes()

View file

@ -324,6 +324,9 @@ class BeeGraphicsView(MainControlsMixin,
def on_action_arrange_optimal(self):
self.scene.arrange_optimal()
def on_action_arrange_by_filename(self):
self.scene.arrange_by_filename()
def on_action_change_opacity(self):
images = list(filter(
lambda item: item.is_image,

40
tests/items/test_items.py Normal file
View file

@ -0,0 +1,40 @@
from PyQt6 import QtGui
from beeref.items import sort_by_filename, BeePixmapItem
def test_sort_by_filename(view):
item1 = BeePixmapItem(QtGui.QImage())
item2 = BeePixmapItem(QtGui.QImage())
item2.filename = 'foo.png'
item2.save_id = 66
item3 = BeePixmapItem(QtGui.QImage())
item3.save_id = 33
item4 = BeePixmapItem(QtGui.QImage())
item4.filename = 'bar.png'
item4.save_id = 77
item5 = BeePixmapItem(QtGui.QImage())
item5.save_id = 22
result = sort_by_filename([item1, item2, item3, item4, item5])
assert result == [item4, item2, item5, item3, item1]
def test_sort_by_filename_when_only_by_filename(view):
item1 = BeePixmapItem(QtGui.QImage())
item1.filename = 'foo.png'
item2 = BeePixmapItem(QtGui.QImage())
item2.filename = 'bar.png'
assert sort_by_filename([item1, item2]) == [item2, item1]
def test_sort_by_filename_when_only_by_save_id(view):
item1 = BeePixmapItem(QtGui.QImage())
item1.save_id = 66
item2 = BeePixmapItem(QtGui.QImage())
item2.save_id = 33
assert sort_by_filename([item1, item2]) == [item2, item1]

View file

@ -429,6 +429,85 @@ def test_arrange_optimal_when_no_items(view):
view.scene.cancel_crop_mode.assert_called_once_with()
def test_arrange_by_filename(view):
item1 = BeePixmapItem(QtGui.QImage())
view.scene.addItem(item1)
item1.setSelected(True)
item1.crop = QtCore.QRectF(0, 0, 100, 80)
item2 = BeePixmapItem(QtGui.QImage())
item2.filename = 'foo.png'
item2.save_id = 66
view.scene.addItem(item2)
item2.setSelected(True)
item2.crop = QtCore.QRectF(0, 0, 80, 60)
item3 = BeePixmapItem(QtGui.QImage())
item3.save_id = 33
view.scene.addItem(item3)
item3.setSelected(True)
item3.crop = QtCore.QRectF(0, 0, 100, 80)
item4 = BeePixmapItem(QtGui.QImage())
item4.filename = 'bar.png'
item4.save_id = 77
view.scene.addItem(item4)
item4.setSelected(True)
item4.crop = QtCore.QRectF(0, 0, 100, 80)
view.scene.cancel_crop_mode = MagicMock()
view.scene.arrange_by_filename()
assert item4.pos() == QtCore.QPointF(-50, -40)
assert item2.pos() == QtCore.QPointF(60, -30)
assert item3.pos() == QtCore.QPointF(-50, 40)
assert item1.pos() == QtCore.QPointF(50, 40)
view.scene.cancel_crop_mode.assert_called_once_with()
def test_arrange_by_filename_with_gap(view, settings):
settings.setValue('Items/arrange_gap', 6)
item1 = BeePixmapItem(QtGui.QImage())
view.scene.addItem(item1)
item1.setSelected(True)
item1.crop = QtCore.QRectF(0, 0, 100, 80)
item2 = BeePixmapItem(QtGui.QImage())
item2.filename = 'foo.png'
item2.save_id = 66
view.scene.addItem(item2)
item2.setSelected(True)
item2.crop = QtCore.QRectF(0, 0, 80, 60)
item3 = BeePixmapItem(QtGui.QImage())
item3.save_id = 33
view.scene.addItem(item3)
item3.setSelected(True)
item3.crop = QtCore.QRectF(0, 0, 100, 80)
item4 = BeePixmapItem(QtGui.QImage())
item4.filename = 'bar.png'
item4.save_id = 77
view.scene.addItem(item4)
item4.setSelected(True)
item4.crop = QtCore.QRectF(0, 0, 100, 80)
view.scene.cancel_crop_mode = MagicMock()
view.scene.arrange_by_filename()
assert item4.pos() == QtCore.QPointF(-53, -43)
assert item2.pos() == QtCore.QPointF(63, -33)
assert item3.pos() == QtCore.QPointF(-53, 43)
assert item1.pos() == QtCore.QPointF(53, 43)
view.scene.cancel_crop_mode.assert_called_once_with()
def test_arrange_by_filename_when_no_items(view):
view.scene.cancel_crop_mode = MagicMock()
view.scene.arrange_by_filename()
view.scene.cancel_crop_mode.assert_called_once_with()
def test_flip_items(view, item):
view.scene.addItem(item)
item.setSelected(True)

View file

@ -993,6 +993,30 @@ def test_on_action_delete_items(view, item):
view.cancel_active_modes.assert_called_once()
@patch('beeref.scene.BeeGraphicsScene.arrange')
def test_on_action_arrange_horizontal(arrange_mock, view):
view.on_action_arrange_horizontal()
arrange_mock.assert_called_once_with()
@patch('beeref.scene.BeeGraphicsScene.arrange')
def test_on_action_arrange_vertical(arrange_mock, view):
view.on_action_arrange_vertical()
arrange_mock.assert_called_once_with(vertical=True)
@patch('beeref.scene.BeeGraphicsScene.arrange_optimal')
def test_on_action_arrange_optimal(arrange_mock, view):
view.on_action_arrange_optimal()
arrange_mock.assert_called_once_with()
@patch('beeref.scene.BeeGraphicsScene.arrange_by_filename')
def test_on_action_arrange_by_filename(arrange_mock, view):
view.on_action_arrange_by_filename()
arrange_mock.assert_called_once_with()
@patch('beeref.widgets.ChangeOpacityDialog.__init__',
return_value=None)
def test_on_action_change_opacity(dialog_mock, view):