diff --git a/beeref/commands.py b/beeref/commands.py new file mode 100644 index 0000000..ae53b7b --- /dev/null +++ b/beeref/commands.py @@ -0,0 +1,54 @@ +# This file is part of BeeRef. +# +# BeeRef is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# BeeRef is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with BeeRef. If not, see . + +from PyQt6 import QtGui + + +class InsertImages(QtGui.QUndoCommand): + + def __init__(self, scene, items): + super().__init__('Insert images') + self.scene = scene + self.items = items + + def redo(self): + self.scene.clearSelection() + for item in self.items: + item.setSelected(True) + self.scene.addItem(item) + + def undo(self): + self.scene.clearSelection() + for item in self.items: + self.scene.removeItem(item) + + +class DeleteSelectedItems(QtGui.QUndoCommand): + + def __init__(self, scene): + super().__init__('Delete images') + self.scene = scene + self.items = [] + + def redo(self): + for item in self.scene.selectedItems(): + self.items.append(item) + self.scene.removeItem(item) + + def undo(self): + self.scene.clearSelection() + for item in self.items: + item.setSelected(True) + self.scene.addItem(item) diff --git a/beeref/scene.py b/beeref/scene.py index 40c53dc..1c16090 100644 --- a/beeref/scene.py +++ b/beeref/scene.py @@ -25,11 +25,6 @@ logger = logging.getLogger('BeeRef') class BeeGraphicsScene(QtWidgets.QGraphicsScene): - def delete_selected_items(self): - for item in self.selectedItems(): - logger.debug('Deleting item...') - self.removeItem(item) - def normalize_width_or_height(self, mode): """Scale the selected images to have the same width or height, as specified by ``mode``. diff --git a/beeref/view.py b/beeref/view.py index b599614..8f5a617 100644 --- a/beeref/view.py +++ b/beeref/view.py @@ -19,6 +19,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets from PyQt6.QtCore import Qt from beeref import bee_json +from beeref import commands from beeref.gui import WelcomeOverlay from beeref.items import BeePixmapItem from beeref.scene import BeeGraphicsScene @@ -62,6 +63,8 @@ class BeeGraphicsView(QtWidgets.QGraphicsView): self.build_actions() self.welcome_overlay = WelcomeOverlay(self) + self.undo_stack = QtGui.QUndoStack(self) + self.undo_stack.setUndoLimit(100) # Load file given via command line if filename: @@ -116,35 +119,45 @@ class BeeGraphicsView(QtWidgets.QGraphicsView): ] add_to_menu(self.context_menu.addMenu('&File'), actions) - # Add menu + # Main menu actions = [ { - 'text': 'Images from &files...', + 'text': '&Insert Images...', 'shortcuts': ['Ctrl+I'], - 'callback': self.on_action_load_images, - }, - { - 'text': 'Image from &clipboard...', - 'shortcuts': ['Ctrl+V'], - 'callback': self.on_action_paste_images, + 'callback': self.on_action_insert_images, }, ] - add_to_menu(self.context_menu.addMenu('&Add'), actions) + add_to_menu(self.context_menu, actions) # Edit menu actions = [ + { + 'text': '&Undo', + 'shortcuts': ['Ctrl+Z'], + 'callback': self.on_action_undo, + }, + { + 'text': '&Redo', + 'shortcuts': ['Ctrl+Shift+Z'], + 'callback': self.on_action_redo, + }, + { + 'text': '&Paste', + 'shortcuts': ['Ctrl+V'], + 'callback': self.on_action_paste, + }, { 'text': '&Delete', 'shortcuts': ['Del'], - 'callback': self.on_action_delete_images, + 'callback': self.on_action_delete_items, 'group': self.actions_active_when_selection, 'enabled': False, }, ] - items_menu = self.context_menu.addMenu('&Items') - add_to_menu(items_menu, actions) + add_to_menu(self.context_menu.addMenu('&Edit'), actions) + items_menu = self.context_menu.addMenu('&Items') actions = [ { 'text': '&Height', @@ -182,8 +195,17 @@ class BeeGraphicsView(QtWidgets.QGraphicsView): return QtCore.QPoint(self.size().width() / 2, self.size().height() / 2) - def on_action_delete_images(self): - self.scene.delete_selected_items() + def on_action_undo(self): + logger.debug('Undo: %s' % self.undo_stack.undoText()) + self.undo_stack.undo() + + def on_action_redo(self): + logger.debug('Redo: %s' % self.undo_stack.redoText()) + self.undo_stack.redo() + + def on_action_delete_items(self): + logger.debug('Deleting items...') + self.undo_stack.push(commands.DeleteSelectedItems(self.scene)) def on_action_normalize_height(self): self.scene.normalize_height() @@ -194,11 +216,15 @@ class BeeGraphicsView(QtWidgets.QGraphicsView): def on_action_normalize_size(self): self.scene.normalize_size() - def add_image(self, img, pos, filename=None): - item = BeePixmapItem(img, filename) - item.set_pos_center(pos.x(), pos.y()) - item.setSelected(True) - self.scene.addItem(item) + def add_images(self, images): + if isinstance(images, QtGui.QImage): + images = [images] + items = [] + for img in images: + item = BeePixmapItem(img, getattr(img, 'filename', None)) + item.set_pos_center(img.pos.x(), img.pos.y()) + items.append(item) + self.undo_stack.push(commands.InsertImages(self.scene, items)) def save_to_file(self, filename): logger.info(f'Saving to file {filename}') @@ -244,27 +270,28 @@ class BeeGraphicsView(QtWidgets.QGraphicsView): logger.info('User quit. Exiting...') self.app.quit() - def on_action_load_images(self): + def on_action_insert_images(self): formats = self.get_supported_image_formats(QtGui.QImageReader) filenames, f = QtWidgets.QFileDialog.getOpenFileNames( parent=self, caption='Select one ore more images to open', filter=f'Images ({formats})') - if filenames: - self.scene.clearSelection() - pos = self.mapToScene(self.get_view_center()) errors = [] + images = [] for filename in filenames: logger.info(f'Loading image from file {filename}') img = QtGui.QImage(filename) if img.isNull(): errors.append(filename) continue - self.add_image(img, pos, filename) + img.pos = pos + img.filename = filename + images.append(img) pos.setX(pos.x() + 50) pos.setY(pos.y() + 50) + self.add_images(images) if errors: errornames = [ @@ -278,17 +305,17 @@ class BeeGraphicsView(QtWidgets.QGraphicsView): 'Problem loading images', msg + errornames) - def on_action_paste_images(self): - logger.info('Pasting image from clipboard...') + def on_action_paste(self): + logger.info('Pasting from clipboard...') clipboard = QtWidgets.QApplication.clipboard() img = clipboard.image() if img.isNull(): logger.info('No image data in clipboard') else: self.scene.clearSelection() - pos = self.mapToScene( + img.pos = self.mapToScene( self.mapFromGlobal(self.cursor().pos())) - self.add_image(img, pos) + self.add_images(img) def on_selection_changed(self): logger.debug('Currently selected items: %s',