mirror of
https://github.com/rbreu/beeref.git
synced 2026-03-11 08:54:28 +00:00
Add action 'Arrange by Filename'
This commit is contained in:
parent
aac2d0edfc
commit
b22056cefa
9 changed files with 227 additions and 2 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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...',
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ menu_structure = [
|
|||
'arrange_optimal',
|
||||
'arrange_horizontal',
|
||||
'arrange_vertical',
|
||||
'arrange_by_filename',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
40
tests/items/test_items.py
Normal 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]
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in a new issue