From b3a322ebd76f02e608691dd11dfa7197d730da09 Mon Sep 17 00:00:00 2001 From: Rebecca Breu Date: Mon, 22 Mar 2021 21:48:05 +0100 Subject: [PATCH] Missing lib directory --- beeref.py | 2 +- beeref/__init__.py | 0 beeref/bee_json.py | 38 +++ beeref/gui.py | 24 ++ beeref/items.py | 109 +++++++++ beeref/scene.py | 71 ++++++ beeref/selection.py | 92 ++++++++ beeref/tests/__init__.py | 0 beeref/tests/assets/test3x3.png | Bin 0 -> 158 bytes beeref/tests/base.py | 14 ++ beeref/tests/test_bee_json.py | 30 +++ beeref/tests/test_items.py | 80 +++++++ beeref/tests/test_scene.py | 47 ++++ beeref/view.py | 397 ++++++++++++++++++++++++++++++++ 14 files changed, 903 insertions(+), 1 deletion(-) create mode 100644 beeref/__init__.py create mode 100644 beeref/bee_json.py create mode 100644 beeref/gui.py create mode 100644 beeref/items.py create mode 100644 beeref/scene.py create mode 100644 beeref/selection.py create mode 100644 beeref/tests/__init__.py create mode 100644 beeref/tests/assets/test3x3.png create mode 100644 beeref/tests/base.py create mode 100644 beeref/tests/test_bee_json.py create mode 100644 beeref/tests/test_items.py create mode 100644 beeref/tests/test_scene.py create mode 100644 beeref/view.py diff --git a/beeref.py b/beeref.py index b59151d..d83c98e 100755 --- a/beeref.py +++ b/beeref.py @@ -7,7 +7,7 @@ import sys from PyQt6 import QtCore, QtGui, QtWidgets -from lib.view import BeeGraphicsView +from beeref.view import BeeGraphicsView logger = logging.getLogger('BeeRef') diff --git a/beeref/__init__.py b/beeref/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/beeref/bee_json.py b/beeref/bee_json.py new file mode 100644 index 0000000..330b389 --- /dev/null +++ b/beeref/bee_json.py @@ -0,0 +1,38 @@ +import json + +from beeref.items import BeePixmapItem + + +class BeeJSONEncoder(json.JSONEncoder): + + def default(self, obj): + if hasattr(obj, 'to_bee_json'): + return obj.to_bee_json() + return json.JSONEncoder.default(self, obj) + + +def dumps(obj): + return json.dumps(obj, cls=BeeJSONEncoder) + + +class BeeJSONDecoder(json.JSONDecoder): + + bee_classes = [BeePixmapItem] + + def __init__(self, *args, **kwargs): + super().__init__(object_hook=self.object_hook, *args, **kwargs) + + def get_bee_class(self, cls): + for bee_class in self.bee_classes: + if bee_class.__name__ == cls: + return bee_class + + def object_hook(self, obj): + if 'cls' in obj: + bee_class = self.get_bee_class(obj['cls']) + return bee_class.from_bee_json(obj) + return obj + + +def loads(obj): + return json.loads(obj, cls=BeeJSONDecoder) diff --git a/beeref/gui.py b/beeref/gui.py new file mode 100644 index 0000000..fbaf4f9 --- /dev/null +++ b/beeref/gui.py @@ -0,0 +1,24 @@ +import logging + +from PyQt6 import QtCore, QtGui, QtWidgets +from PyQt6.QtCore import Qt + + +logger = logging.getLogger('BeeRef') + + +class WelcomeOverlay(QtWidgets.QWidget): + + txt = """

Paste or drop images here.

+

Right-click for more options.

""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground) + self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents) + label = QtWidgets.QLabel(self) + label.setText(self.txt) + label.setAlignment(Qt.Alignment.AlignVCenter | Qt.Alignment.AlignCenter) + layout = QtWidgets.QVBoxLayout() + layout.addWidget(label) + self.setLayout(layout) diff --git a/beeref/items.py b/beeref/items.py new file mode 100644 index 0000000..a5b923c --- /dev/null +++ b/beeref/items.py @@ -0,0 +1,109 @@ +import base64 +import logging + +from PyQt6 import QtCore, QtGui, QtWidgets + +from beeref.selection import SelectionItem + + +logger = logging.getLogger('BeeRef') + + +class BeePixmapItem(QtWidgets.QGraphicsPixmapItem): + + def __init__(self, image, filename=None): + super().__init__(QtGui.QPixmap.fromImage(image)) + logger.debug(f'Initialized image "{filename}" with dimensions: ' + f'{self.width} x {self.height} at index {self.zValue()}') + + self.filename = filename + self.scale_factor = 1 + self.setFlags( + QtWidgets.QGraphicsItem.GraphicsItemFlags.ItemIsMovable + | QtWidgets.QGraphicsItem.GraphicsItemFlags.ItemIsSelectable) + + def setScale(self, factor): + self.scale_factor = factor + logger.debug(f'Setting scale for image "{self.filename}" to {factor}') + super().setScale(factor) + + def set_pos_center(self, x, y): + """Sets the position using the item's center as the origin point.""" + + self.setPos(x - self.width * self.scale_factor / 2, + y - self.height * self.scale_factor / 2) + + @property + def width(self): + return self.pixmap().size().width() + + @property + def height(self): + return self.pixmap().size().height() + + def pixmap_to_str(self): + barray = QtCore.QByteArray() + buffer = QtCore.QBuffer(barray) + buffer.open(QtCore.QIODevice.OpenMode.WriteOnly) + img = self.pixmap().toImage() + img.save(buffer, 'PNG') + data = base64.b64encode(barray.data()) + return data.decode('ascii') + + @classmethod + def qimage_from_str(self, data): + img = QtGui.QImage() + img.loadFromData(base64.b64decode(data)) + return img + + def to_bee_json(self): + return { + 'cls': self.__class__.__name__, + 'scale': self.scale_factor, + 'pixmap': self.pixmap_to_str(), + 'pos': [self.pos().x(), self.pos().y()], + 'z': self.zValue(), + 'filename': self.filename, + } + + @classmethod + def from_bee_json(cls, obj): + img = cls.qimage_from_str(obj['pixmap']) + item = cls(img, filename=obj.get('filename')) + if 'scale' in obj: + item.setScale(obj['scale']) + if 'pos' in obj: + item.setPos(*obj['pos']) + if 'z' in obj: + item.setZValue(obj['z']) + + return item + + def create_selection(self): + SelectionItem(self) + + def clear_selection(self): + for child in self.childItems(): + logger.debug('Removing child item...') + self.scene().removeItem(child) + logger.debug('Child item removed') + + def itemChange(self, change, value): + ret = super().itemChange(change, value) + + if change == self.GraphicsItemChange.ItemSelectedChange: + if value: + logger.debug('Item selected') + self.create_selection() + else: + logger.debug('Item deselected') + self.clear_selection() + + return ret + + def mousePressEvent(self, event): + print('????') + + def sceneEvent(self, event): + print('********', event) + return super().sceneEvent(event) diff --git a/beeref/scene.py b/beeref/scene.py new file mode 100644 index 0000000..1697fa0 --- /dev/null +++ b/beeref/scene.py @@ -0,0 +1,71 @@ +import logging +import math + +from PyQt6 import QtCore, QtWidgets +from PyQt6.QtCore import Qt + + +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): + values = [getattr(i, mode) for i in self.selectedItems()] + if not values: + return + avg = sum(values) / len(values) + + logger.debug(f'Calculated average {mode} {avg}') + + for item in self.selectedItems(): + factor = avg / getattr(item, mode) + item.setScale(factor) + + def normalize_height(self): + return self.normalize_width_or_height('height') + + def normalize_width(self): + return self.normalize_width_or_height('width') + + def normalize_size(self): + sizes = [i.width * i.height for i in self.selectedItems()] + + if not sizes: + return + + avg = sum(sizes) / len(sizes) + logger.debug(f'Calculated average size {avg}') + + for item in self.selectedItems(): + factor = math.sqrt(avg / item.width / item.height) + item.setScale(factor) + + def has_selection(self): + """Checks whether there are currently items selected.""" + + return bool(self.selectedItems()) + + def has_single_selection(self): + """Checks whether there's currently exactly one item selected.""" + + return len(self.selectedItems()) == 1 + + def mousePressEvent(self, event): + if event.button() == Qt.MouseButtons.RightButton: + # Right-click invokes the context menu on the + # GraphicsView. We don't need it here. + return + super().mousePressEvent(event) + + def items_for_export(self): + """Returns the items that are to be exported.""" + + # self.items() holds items in reverse order of addition + return list(filter(lambda i: hasattr(i, 'to_bee_json'), + reversed(self.items()))) diff --git a/beeref/selection.py b/beeref/selection.py new file mode 100644 index 0000000..fafc534 --- /dev/null +++ b/beeref/selection.py @@ -0,0 +1,92 @@ +import logging + +from PyQt6 import QtCore, QtGui, QtWidgets +from PyQt6.QtCore import Qt + + +logger = logging.getLogger('BeeRef') + + +class SelectionItem(QtWidgets.QGraphicsItem): + + color = QtGui.QColor(116, 234, 231, 255) + handle_size = 30 # scaling handles + resize_size = 50 # area for scaling hover events + margin = 200 # margin for bounding box etc + + debug = False + + def __init__(self, item): + super().__init__(parent=item) + self.single_select_mode = False + + bounds = self.parentItem().boundingRect() + pos = bounds.bottomRight() + self.bottom_right_scale_bounds = QtCore.QRectF( + pos.x() - self.resize_size/2, + pos.y() - self.resize_size/2, + self.resize_size, + self.resize_size) + + def boundingRect(self): + bounds = self.parentItem().boundingRect() + return QtCore.QRectF( + bounds.topLeft().x() - self.margin, + bounds.topLeft().y() - self.margin, + bounds.bottomRight().x() + self.margin * 2, + bounds.bottomRight().y() + self.margin * 2) + + def shape(self): + path = QtGui.QPainterPath() + path.addRect(self.boundingRect()) + return path + + def draw_debug_rect(self, painter, rect): + pen = QtGui.QPen(QtGui.QColor('red')) + pen.setWidth(3) + painter.setPen(pen) + painter.drawRect(rect) + + def paint(self, painter, option, widget): + pen = QtGui.QPen(self.color) + pen.setWidth(10) + painter.setPen(pen) + + # Draw the main selection rectangle + bounds = self.parentItem().boundingRect() + painter.drawRect(bounds) + + self.single_select_mode = self.parentItem()\ + .scene().has_single_selection() + + self.setAcceptHoverEvents(self.single_select_mode) + self.setAcceptDrops(self.single_select_mode) + + + # If it's a single selection, draw the handles: + if self.single_select_mode: + self.setFlags( + QtWidgets.QGraphicsItem.GraphicsItemFlags.ItemIsSelectable) + pos = bounds.bottomRight() + painter.fillRect(pos.x() - self.handle_size/2, + pos.y() - self.handle_size/2, + self.handle_size, + self.handle_size, + self.color) + + if self.debug: + self.draw_debug_rect(painter, self.boundingRect()) + self.draw_debug_rect(painter, self.bottom_right_scale_bounds) + + def hoverMoveEvent(self, event): + if not self.single_select_mode: + return + + # In bottomright scale area? + if self.bottom_right_scale_bounds.contains(event.pos()): + self.setCursor(Qt.CursorShape.SizeFDiagCursor) + else: + self.setCursor(Qt.CursorShape.ArrowCursor) + + def dragEnterEvent(self, event): + print('OOOOOOOOOOOOOOOdrag enter') diff --git a/beeref/tests/__init__.py b/beeref/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/beeref/tests/assets/test3x3.png b/beeref/tests/assets/test3x3.png new file mode 100644 index 0000000000000000000000000000000000000000..0009dacb8646bbefc0b803d325953c140bd483db GIT binary patch literal 158 zcmeAS@N?(olHy`uVBq!ia0vp^%plCc1SD^IDZKzvoCO|{#S9F3${@^GvDCf{D9B#o z>Fdh=lv$KllEto{c^^!MP|ku_QG`p**uBL&4qCHz2%`PaLR7 y!qdeugyVX0Lc)*#|Nqysu}SnkFfjPT&T!w3@6*fUYbOKMFnGH9xvX 0 + + +class BeePixmapItemFromBeeJsonTestCase(BeeTestCase): + + def test_basic(self): + bee_json = { + 'cls': 'BeePixmapItem', + 'scale': 2, + 'pos': [100.0, 200.0], + 'z': 3.0, + 'pixmap': '', + 'filename': 'bee.png', + } + + item = BeePixmapItem.from_bee_json(bee_json) + + assert isinstance(item, BeePixmapItem) + assert item.scale_factor == 2 + assert item.pos().x() == 100.0 + assert item.pos().y() == 200.0 + assert item.zValue() == 3.0 + assert item.filename == 'bee.png' diff --git a/beeref/tests/test_scene.py b/beeref/tests/test_scene.py new file mode 100644 index 0000000..4be81de --- /dev/null +++ b/beeref/tests/test_scene.py @@ -0,0 +1,47 @@ +import math +from unittest.mock import patch, MagicMock + +from PyQt6 import QtGui + +from beeref.items import BeePixmapItem +from beeref.scene import BeeGraphicsScene +from beeref.tests.base import BeeTestCase + + +class BeeGraphicsSceneNormalizeTestCase(BeeTestCase): + + def test_normalize_height(self): + item1 = MagicMock(height=100) + item2 = MagicMock(height=200) + scene = BeeGraphicsScene() + + with patch.object(scene, 'selectedItems', + return_value=[item1, item2]): + scene.normalize_height() + + item1.setScale.assert_called_once_with(1.5) + item2.setScale.assert_called_once_with(0.75) + + def test_normalize_width(self): + item1 = MagicMock(width=100) + item2 = MagicMock(width=200) + scene = BeeGraphicsScene() + + with patch.object(scene, 'selectedItems', + return_value=[item1, item2]): + scene.normalize_width() + + item1.setScale.assert_called_once_with(1.5) + item2.setScale.assert_called_once_with(0.75) + + def test_normalize_size(self): + item1 = MagicMock(width=100, height=200) + item2 = MagicMock(width=400, height=100) + scene = BeeGraphicsScene() + + with patch.object(scene, 'selectedItems', + return_value=[item1, item2]): + scene.normalize_size() + + item1.setScale.assert_called_once_with(math.sqrt(1.5)) + item2.setScale.assert_called_once_with(math.sqrt(0.75)) diff --git a/beeref/view.py b/beeref/view.py new file mode 100644 index 0000000..5725ffe --- /dev/null +++ b/beeref/view.py @@ -0,0 +1,397 @@ +import logging + +from PyQt6 import QtCore, QtGui, QtWidgets +from PyQt6.QtCore import Qt + +from beeref import bee_json +from beeref.gui import WelcomeOverlay +from beeref.items import BeePixmapItem +from beeref.scene import BeeGraphicsScene + + +logger = logging.getLogger('BeeRef') + + +class BeeGraphicsView(QtWidgets.QGraphicsView): + + def __init__(self, app, parent=None, filename=None): + super().__init__(parent) + self.app = app + + self.scene = BeeGraphicsScene() + self.filename = None + + # TBD: make scrollbar configurable + self.setHorizontalScrollBarPolicy( + Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy( + Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) + self.setAcceptDrops(True) + + # TBD: fix zoom anchor - why does this not work? + # self.setTransformationAnchor( + # QtWidgets.QGraphicsView.ViewportAnchor.AnchorUnderMouse) + + self.pan_active = False + self.scene.changed.connect(self.on_scene_changed) + self.scene.selectionChanged.connect(self.on_selection_changed) + + self.setScene(self.scene) + + # Context menu and actions + self.setContextMenuPolicy( + Qt.ContextMenuPolicy.CustomContextMenu) + self.customContextMenuRequested.connect(self.on_context_menu) + self.context_menu = QtWidgets.QMenu(self) + self.build_actions() + + self.welcome_overlay = WelcomeOverlay(self) + + # Load file given via command line + if filename: + self.open_from_file(filename) + + def on_scene_changed(self, region): + if not self.scene.items(): + logger.info('No items in scene') + self.welcome_overlay.show() + else: + self.welcome_overlay.hide() + self.recalc_scene_rect() + + def build_actions(self): + + self.actions_active_when_selection = [] + + def add_to_menu(menu, actions): + for action in actions: + qaction = QtGui.QAction(action['text'], self) + if 'shortcuts' in action: + qaction.setShortcuts(action['shortcuts']) + qaction.triggered.connect(action['callback']) + self.addAction(qaction) + menu.addAction(qaction) + if 'group' in action: + action['group'].append(qaction) + qaction.setEnabled(action.get('enabled', True)) + + # File menu + actions = [ + { + 'text': '&Open', + 'shortcuts': ['Ctrl+O'], + 'callback': self.on_action_open, + }, + { + 'text': '&Save', + 'shortcuts': ['Ctrl+S'], + 'callback': self.on_action_save, + }, + { + 'text': 'Save &As...', + 'shortcuts': ['Ctrl+Shift+S'], + 'callback': self.on_action_save_as, + }, + { + 'text': '&Quit...', + 'shortcuts': ['Ctrl+Q'], + 'callback': self.on_action_quit, + }, + ] + add_to_menu(self.context_menu.addMenu('&File'), actions) + + # Add menu + actions = [ + { + 'text': 'Images from &files...', + 'shortcuts': ['Ctrl+I'], + 'callback': self.on_action_load_images, + }, + { + 'text': 'Image from &clipboard...', + 'shortcuts': ['Ctrl+V'], + 'callback': self.on_action_paste_images, + }, + + ] + add_to_menu(self.context_menu.addMenu('&Add'), actions) + + # Edit menu + actions = [ + { + 'text': '&Delete', + 'shortcuts': ['Del'], + 'callback': self.on_action_delete_images, + 'group': self.actions_active_when_selection, + 'enabled': False, + }, + ] + items_menu = self.context_menu.addMenu('&Items') + add_to_menu(items_menu, actions) + + actions = [ + { + 'text': '&Height', + 'shortcuts': ['Shift+H'], + 'callback': self.on_action_normalize_height, + 'group': self.actions_active_when_selection, + 'enabled': False, + }, + { + 'text': '&Width', + 'shortcuts': ['Shift+W'], + 'callback': self.on_action_normalize_width, + 'group': self.actions_active_when_selection, + 'enabled': False, + }, + { + 'text': '&Size', + 'shortcuts': ['Shift+S'], + 'callback': self.on_action_normalize_size, + 'group': self.actions_active_when_selection, + 'enabled': False, + }, + ] + add_to_menu(items_menu.addMenu('&Normalize'), actions) + + def on_context_menu(self, point): + self.context_menu.exec(self.mapToGlobal(point)) + + def get_supported_image_formats(self, cls): + formats = map(lambda f: f'*.{f.data().decode()}', + cls.supportedImageFormats()) + return ' '.join(formats) + + def get_view_center(self): + 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_normalize_height(self): + self.scene.normalize_height() + + def on_action_normalize_width(self): + self.scene.normalize_width() + + 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 save_to_file(self, filename): + logging.info(f'Saving to file {filename}') + with open(filename, 'w') as f: + f.write(bee_json.dumps({'items': self.scene.items_for_export()})) + self.filename = filename + + def open_from_file(self, filename): + logging.info(f'Opening file {filename}') + with open(filename, 'r') as f: + items = bee_json.loads(f.read())['items'] + self.scene.clear() + for item in items: + self.scene.addItem(item) + self.filename = filename + + def on_action_open(self): + filename, f = QtWidgets.QFileDialog.getOpenFileName( + parent=self, + caption='Open file', + filter='BeeRef File (*.bee)') + if filename: + self.open_from_file(filename) + + def on_action_save_as(self): + filename, f = QtWidgets.QFileDialog.getSaveFileName( + parent=self, + caption='Save file', + filter='BeeRef File (*.bee)') + if filename: + if not filename.endswith('.bee'): + filename = f'{filename}.bee' + self.save_to_file(filename) + + def on_action_save(self): + if not self.filename: + self.on_action_save_as() + else: + self.save_to_file(self.filename) + + def on_action_quit(self): + logger.info('User quit. Exiting...') + self.app.quit() + + def on_action_load_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 = [] + 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) + pos.setX(pos.x() + 50) + pos.setY(pos.y() + 50) + + if errors: + errornames = [ + f'
  • {fn}
  • ' for fn in errors] + errornames = '
      %s
    ' % '\n'.join(errornames) + msg = ('{errors} image(s) out of {total} ' + 'could not be opened:'.format( + errors=len(errors), total=len(filenames))) + QtWidgets.QMessageBox.warning( + self, + 'Problem loading images', + msg + errornames) + + def on_action_paste_images(self): + logging.info('Pasting image from clipboard...') + clipboard = QtWidgets.QApplication.clipboard() + img = clipboard.image() + if img.isNull(): + logging.info('No image data in clipboard') + else: + self.scene.clearSelection() + pos = self.mapToScene( + self.mapFromGlobal(self.cursor().pos())) + self.add_image(img, pos) + + def on_selection_changed(self): + for action in self.actions_active_when_selection: + action.setEnabled(self.scene.has_selection()) + self.viewport().repaint() + + def recalc_scene_rect(self): + """Resize the scene rectangle so that it is always one view width + wider than all items' bounding box at each side and one view + width higher on top and bottom. This gives the impression of + an infinite canvas.""" + + logger.debug('Recalculating scene rectangle...') + try: + topleft = self.mapFromScene( + self.scene.itemsBoundingRect().topLeft()) + topleft = self.mapToScene(QtCore.QPoint( + topleft.x() - self.size().width(), + topleft.y() - self.size().height())) + bottomright = self.mapFromScene( + self.scene.itemsBoundingRect().bottomRight()) + bottomright = self.mapToScene(QtCore.QPoint( + bottomright.x() + self.size().width(), + bottomright.y() + self.size().height())) + self.setSceneRect(QtCore.QRectF(topleft, bottomright)) + except OverflowError: + logger.info('Maximum scene size reached') + + def get_zoom_size(self, func): + """Calculates the size of the current items' bounding box + in the view's coordinates. + + This helps ensure that we never zoom out too much (scene + becomes so tiny that they become invisible) or zoom in too + much (causing overflow errors). + + `func` is a function witch takes the width and height as + arguments and turns it into a number, for ex. `min` or `max`. + """ + + topleft = self.mapFromScene( + self.scene.itemsBoundingRect().topLeft()) + bottomright = self.mapFromScene( + self.scene.itemsBoundingRect().bottomRight()) + return func(bottomright.x() - topleft.x(), + bottomright.y() - topleft.y()) + + def scale(self, *args, **kwargs): + super().scale(*args, **kwargs) + self.recalc_scene_rect() + + def wheelEvent(self, event): + factor = 1.2 + if event.angleDelta().y() > 0: + if self.get_zoom_size(max) < 10000000: + self.scale(factor, factor) + else: + logging.debug('Maximum zoom size reached') + else: + if self.get_zoom_size(min) > 50: + self.scale(1/factor, 1/factor) + else: + logging.debug('Minimum zoom size reached') + event.accept() + + def mouseMoveEvent(self, event): + if self.pan_active: + point = event.position() + hscroll = self.horizontalScrollBar() + hscroll.setValue(hscroll.value() + self.pan_start.x() - point.x()) + vscroll = self.verticalScrollBar() + vscroll.setValue(vscroll.value() + self.pan_start.y() - point.y()) + self.pan_start = point + event.accept() + return + + super().mouseMoveEvent(event) + + def mousePressEvent(self, event): + if (event.button() == Qt.MouseButtons.MiddleButton + or (event.button() == Qt.MouseButtons.LeftButton + and event.modifiers() == Qt.KeyboardModifiers.AltModifier)): + self.pan_active = True + self.pan_start = event.position() + self.setCursor(Qt.CursorShape.ClosedHandCursor) + event.accept() + return + + super().mousePressEvent(event) + + def mouseReleaseEvent(self, event): + if self.pan_active: + self.setCursor(Qt.CursorShape.ArrowCursor) + self.pan_active = False + event.accept() + return + + super().mouseReleaseEvent(event) + + def resizeEvent(self, event): + super().resizeEvent(event) + self.recalc_scene_rect() + self.welcome_overlay.resize(self.size()) + + def dragEnterEvent(self, event): + logging.debug('Received drag enter event') + + # tbd: always empty??? + print(event.mimeData().formats()) + print(dir(event)) + if event.mimeData().hasImage(): + event.acceptProposedAction() + else: + logging.info('Attempted drop not an image') + + def dragMoveEvent(self, event): + event.acceptProposedAction() + + def dropEvent(self, event): + logging.info('Handling file drop...') + print(event.mimeData().formats()) + # tbd