mirror of
https://github.com/rbreu/beeref.git
synced 2026-03-11 08:54:28 +00:00
Add ability to toggle grayscale
This commit is contained in:
parent
3769531493
commit
371e2e19a1
10 changed files with 126 additions and 3 deletions
|
|
@ -6,7 +6,8 @@ Added
|
|||
|
||||
* For arranging, a gap between images can now be configured in the
|
||||
settings.
|
||||
* The opacity of images can now be changed (Images -> Change Opacity).
|
||||
* The opacity of images can be changed (Images -> Change Opacity).
|
||||
* Images can be set to display as grayscale (Images -> Grayscale).
|
||||
|
||||
|
||||
Fixed:
|
||||
|
|
|
|||
|
|
@ -160,6 +160,14 @@ actions = [
|
|||
'callback': 'on_action_change_opacity',
|
||||
'group': 'active_when_selection',
|
||||
},
|
||||
{
|
||||
'id': 'grayscale',
|
||||
'text': '&Grayscale',
|
||||
'shortcuts': ['G'],
|
||||
'checkable': True,
|
||||
'callback': 'on_action_grayscale',
|
||||
'group': 'active_when_selection',
|
||||
},
|
||||
{
|
||||
'id': 'crop',
|
||||
'text': '&Crop',
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ menu_structure = [
|
|||
'menu': '&Images',
|
||||
'items': [
|
||||
'change_opacity',
|
||||
'grayscale',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -325,7 +325,7 @@ class ChangeText(QtGui.QUndoCommand):
|
|||
|
||||
|
||||
class ChangeOpacity(QtGui.QUndoCommand):
|
||||
"""Change Opacity."""
|
||||
"""Change opacity on images."""
|
||||
|
||||
def __init__(self, items, opacity, ignore_first_redo=False):
|
||||
super().__init__('Change Opacity')
|
||||
|
|
@ -345,3 +345,21 @@ class ChangeOpacity(QtGui.QUndoCommand):
|
|||
def undo(self):
|
||||
for item, opacity in zip(self.items, self.old_opacities):
|
||||
item.setOpacity(opacity)
|
||||
|
||||
|
||||
class ToggleGrayscale(QtGui.QUndoCommand):
|
||||
"""Toggle grayscale mode on images."""
|
||||
|
||||
def __init__(self, items, grayscale):
|
||||
super().__init__('Toggle Grayscale')
|
||||
self.items = list(filter(lambda item: item.is_image, items))
|
||||
self.grayscale = grayscale
|
||||
self.old_grayscales = [item.grayscale for item in items]
|
||||
|
||||
def redo(self):
|
||||
for item in self.items:
|
||||
item.grayscale = self.grayscale
|
||||
|
||||
def undo(self):
|
||||
for item, grayscale in zip(self.items, self.old_grayscales):
|
||||
item.grayscale = grayscale
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem):
|
|||
self.crop_mode = False
|
||||
self.init_selectable()
|
||||
self.settings = BeeSettings()
|
||||
self.grayscale = False
|
||||
|
||||
@classmethod
|
||||
def create_from_data(self, **kwargs):
|
||||
|
|
@ -102,6 +103,7 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem):
|
|||
if 'crop' in data:
|
||||
item.crop = QtCore.QRectF(*data['crop'])
|
||||
item.setOpacity(data.get('opacity', 1))
|
||||
item.grayscale = data.get('grayscale', False)
|
||||
return item
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -119,6 +121,23 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem):
|
|||
self._crop = value
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def grayscale(self):
|
||||
return self._grayscale
|
||||
|
||||
@grayscale.setter
|
||||
def grayscale(self, value):
|
||||
logger.debug('Setting grayscale for {self} to {value}')
|
||||
self._grayscale = value
|
||||
if value is True:
|
||||
img = self.pixmap().toImage()
|
||||
img = img.convertToFormat(QtGui.QImage.Format.Format_Grayscale8)
|
||||
self._grayscale_pixmap = QtGui.QPixmap.fromImage(img)
|
||||
else:
|
||||
self._grayscale_pixmap = None
|
||||
|
||||
self.update()
|
||||
|
||||
def bounding_rect_unselected(self):
|
||||
if self.crop_mode:
|
||||
return QtWidgets.QGraphicsPixmapItem.boundingRect(self)
|
||||
|
|
@ -128,6 +147,7 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem):
|
|||
def get_extra_save_data(self):
|
||||
return {'filename': self.filename,
|
||||
'opacity': self.opacity(),
|
||||
'grayscale': self.grayscale,
|
||||
'crop': [self.crop.topLeft().x(),
|
||||
self.crop.topLeft().y(),
|
||||
self.crop.width(),
|
||||
|
|
@ -177,6 +197,7 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem):
|
|||
item.setScale(self.scale())
|
||||
item.setRotation(self.rotation())
|
||||
item.setOpacity(self.opacity())
|
||||
item.grayscale = self.grayscale
|
||||
if self.flip() == -1:
|
||||
item.do_flip()
|
||||
item.crop = self.crop
|
||||
|
|
@ -326,7 +347,8 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem):
|
|||
self.draw_crop_rect(painter, handle())
|
||||
self.draw_crop_rect(painter, self.crop_temp)
|
||||
else:
|
||||
painter.drawPixmap(self.crop, self.pixmap(), self.crop)
|
||||
pm = self._grayscale_pixmap if self.grayscale else self.pixmap()
|
||||
painter.drawPixmap(self.crop, pm, self.crop)
|
||||
self.paint_selectable(painter, option, widget)
|
||||
|
||||
def enter_crop_mode(self):
|
||||
|
|
|
|||
|
|
@ -287,6 +287,14 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
self.scene.selectedItems(user_only=True)))
|
||||
widgets.ChangeOpacityDialog(self, images, self.undo_stack)
|
||||
|
||||
def on_action_grayscale(self, checked):
|
||||
images = list(filter(
|
||||
lambda item: item.is_image,
|
||||
self.scene.selectedItems(user_only=True)))
|
||||
if images:
|
||||
self.undo_stack.push(
|
||||
commands.ToggleGrayscale(images, checked))
|
||||
|
||||
def on_action_crop(self):
|
||||
self.scene.crop_items()
|
||||
|
||||
|
|
|
|||
|
|
@ -235,6 +235,7 @@ def test_sqliteio_write_inserts_new_pixmap_item_png(tmpfile, view):
|
|||
item.setRotation(33)
|
||||
item.do_flip()
|
||||
item.crop = QtCore.QRectF(5, 5, 100, 80)
|
||||
item.grayscale = True
|
||||
item.pixmap_to_bytes = MagicMock(return_value=(b'abc', 'png'))
|
||||
io = SQLiteIO(tmpfile, view.scene, create_new=True)
|
||||
io.write()
|
||||
|
|
@ -255,6 +256,7 @@ def test_sqliteio_write_inserts_new_pixmap_item_png(tmpfile, view):
|
|||
'filename': 'bee.jpg',
|
||||
'crop': [5, 5, 100, 80],
|
||||
'opacity': 0.66,
|
||||
'grayscale': True,
|
||||
}
|
||||
assert result[7] == 'pixmap'
|
||||
assert result[8] == b'abc'
|
||||
|
|
@ -346,6 +348,7 @@ def test_sqliteio_write_updates_existing_pixmap_item(tmpfile, view):
|
|||
item.setOpacity(0.75)
|
||||
item.do_flip()
|
||||
item.crop = QtCore.QRectF(1, 2, 30, 40)
|
||||
item.grayscale = True
|
||||
item.filename = 'new.png'
|
||||
item.pixmap_to_bytes.return_value = b'updated'
|
||||
io.create_new = False
|
||||
|
|
@ -366,6 +369,7 @@ def test_sqliteio_write_updates_existing_pixmap_item(tmpfile, view):
|
|||
'filename': 'new.png',
|
||||
'crop': [1, 2, 30, 40],
|
||||
'opacity': 0.75,
|
||||
'grayscale': True,
|
||||
}
|
||||
assert result[7] == b'abc'
|
||||
|
||||
|
|
@ -502,6 +506,7 @@ def test_sqliteio_read_reads_readonly_pixmap_item(tmpfile, view, imgdata3x3):
|
|||
assert item.height == 3
|
||||
assert item.crop == QtCore.QRectF(0, 0, 3, 3)
|
||||
assert item.opacity() == 1
|
||||
assert item.grayscale is False
|
||||
assert view.scene.items_to_add.empty() is True
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,19 @@ def test_set_crop(qapp, item):
|
|||
item.prepareGeometryChange.assert_called_once_with()
|
||||
|
||||
|
||||
def test_set_grayscale_true(qapp, item):
|
||||
item.grayscale = True
|
||||
assert item.grayscale is True
|
||||
assert item._grayscale_pixmap is not None
|
||||
|
||||
|
||||
def test_set_grayscale_false(qapp, item):
|
||||
item._grayscale_pixmap = QtGui.QPixmap()
|
||||
item.grayscale = False
|
||||
assert item.grayscale is False
|
||||
assert item._grayscale_pixmap is None
|
||||
|
||||
|
||||
def test_bounding_rect_unselected(qapp, imgfilename3x3):
|
||||
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
item.crop = QtCore.QRectF(1, 1, 2, 2)
|
||||
|
|
@ -76,10 +89,12 @@ def test_get_extra_save_data(item):
|
|||
item.filename = 'foobar.png'
|
||||
item.crop = QtCore.QRectF(10, 20, 30, 40)
|
||||
item.setOpacity(0.75)
|
||||
item.grayscale = True
|
||||
assert item.get_extra_save_data() == {
|
||||
'filename': 'foobar.png',
|
||||
'crop': [10, 20, 30, 40],
|
||||
'opacity': 0.75,
|
||||
'grayscale': True
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -255,6 +270,7 @@ def test_create_from_minimal_data(qapp, item, imgfilename3x3):
|
|||
assert item.filename == 'foobar.png'
|
||||
assert item.crop == QtCore.QRectF(0, 0, 3, 3)
|
||||
assert item.opacity() == 1
|
||||
assert item.grayscale is False
|
||||
|
||||
|
||||
def test_create_from_data_with_crop(item):
|
||||
|
|
@ -273,6 +289,14 @@ def test_create_from_data_with_opacity(item):
|
|||
assert item.opacity() == 0.7
|
||||
|
||||
|
||||
def test_create_from_data_with_grayscale(item):
|
||||
new_item = BeePixmapItem.create_from_data(
|
||||
item=item, data={'filename': 'foobar.png', 'grayscale': True})
|
||||
assert new_item is item
|
||||
assert item.filename == 'foobar.png'
|
||||
assert item.grayscale is True
|
||||
|
||||
|
||||
def test_create_copy(qapp, imgfilename3x3):
|
||||
item = BeePixmapItem(QtGui.QImage(imgfilename3x3), 'foo.png')
|
||||
item.setPos(20, 30)
|
||||
|
|
@ -282,6 +306,7 @@ def test_create_copy(qapp, imgfilename3x3):
|
|||
item.setScale(2.2)
|
||||
item.crop = QtCore.QRectF(10, 20, 30, 40)
|
||||
item.setOpacity(0.7)
|
||||
item.grayscale = True
|
||||
|
||||
copy = item.create_copy()
|
||||
assert copy.pixmap_to_bytes() == item.pixmap_to_bytes()
|
||||
|
|
@ -293,6 +318,7 @@ def test_create_copy(qapp, imgfilename3x3):
|
|||
assert copy.scale() == 2.2
|
||||
assert copy.crop == QtCore.QRectF(10, 20, 30, 40)
|
||||
assert copy.opacity() == 0.7
|
||||
assert copy.grayscale is True
|
||||
|
||||
|
||||
def test_copy_to_clipboard(qapp, imgfilename3x3):
|
||||
|
|
|
|||
|
|
@ -514,3 +514,18 @@ def test_change_opacity_ignore_first_redo(view):
|
|||
command.redo()
|
||||
assert item1.opacity() == 0.7
|
||||
assert item2.opacity() == 0.7
|
||||
|
||||
|
||||
def test_toggle_grayscale(view):
|
||||
item1 = BeePixmapItem(QtGui.QImage())
|
||||
item1.grayscale = True
|
||||
view.scene.addItem(item1)
|
||||
item2 = BeePixmapItem(QtGui.QImage())
|
||||
item2.grayscale = False
|
||||
command = commands.ToggleGrayscale([item1, item2], True)
|
||||
command.redo()
|
||||
assert item1.grayscale is True
|
||||
assert item2.grayscale is True
|
||||
command.undo()
|
||||
assert item1.grayscale is True
|
||||
assert item2.grayscale is False
|
||||
|
|
|
|||
|
|
@ -735,6 +735,25 @@ def test_on_action_change_opacity(dialog_mock, view):
|
|||
dialog_mock.assert_called_once_with(view, [pixmapitem1], view.undo_stack)
|
||||
|
||||
|
||||
def test_on_action_grayscale(view):
|
||||
pixmapitem1 = BeePixmapItem(QtGui.QImage())
|
||||
view.scene.addItem(pixmapitem1)
|
||||
pixmapitem1.setSelected(True)
|
||||
|
||||
pixmapitem2 = BeePixmapItem(QtGui.QImage())
|
||||
view.scene.addItem(pixmapitem2)
|
||||
pixmapitem2.setSelected(False)
|
||||
|
||||
textitem = BeeTextItem('foo')
|
||||
view.scene.addItem(textitem)
|
||||
textitem.setSelected(True)
|
||||
|
||||
view.on_action_grayscale(True)
|
||||
assert len(view.undo_stack) == 1
|
||||
assert pixmapitem1.grayscale is True
|
||||
assert pixmapitem2.grayscale is False
|
||||
|
||||
|
||||
@patch('PyQt6.QtGui.QUndoStack.isClean', return_value=True)
|
||||
def test_update_window_title_no_changes_no_filename(clear_mock, view):
|
||||
view.filename = None
|
||||
|
|
|
|||
Loading…
Reference in a new issue