From d11779072627c40705c41d066cc1fdacde35b68a Mon Sep 17 00:00:00 2001 From: Rebecca Breu Date: Fri, 14 May 2021 21:29:58 +0200 Subject: [PATCH] Fix dropping images into BeeRef --- beeref/__main__.py | 5 ++-- beeref/fileio/__init__.py | 7 +++-- beeref/fileio/image.py | 46 +++++++++++++++++++++++++++++ beeref/view.py | 51 +++++++++++++++++++++----------- requirements/dev.txt | 5 ++-- tests/fileio/test_image.py | 49 +++++++++++++++++++++++++++++++ tests/test_view.py | 60 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 199 insertions(+), 24 deletions(-) create mode 100644 beeref/fileio/image.py create mode 100644 tests/fileio/test_image.py diff --git a/beeref/__main__.py b/beeref/__main__.py index d94d0ab..356935e 100755 --- a/beeref/__main__.py +++ b/beeref/__main__.py @@ -66,9 +66,10 @@ def handle_sigint(signum, frame): QtWidgets.QApplication.quit() -def handle_uncaught_exception(exc_type, value, traceback): +def handle_uncaught_exception(exc_type, exc, traceback): logger.critical('Unhandled exception', - exc_info=(exc_type, value, traceback)) + exc_info=(exc_type, exc, traceback)) + QtWidgets.QApplication.quit() sys.excepthook = handle_uncaught_exception diff --git a/beeref/fileio/__init__.py b/beeref/fileio/__init__.py index 866d73a..69bfd74 100644 --- a/beeref/fileio/__init__.py +++ b/beeref/fileio/__init__.py @@ -15,11 +15,11 @@ import logging - -from PyQt6 import QtCore, QtGui +from PyQt6 import QtCore from beeref import commands from beeref.fileio.errors import BeeFileIOError +from beeref.fileio.image import load_image from beeref.fileio.sql import SQLiteIO from beeref.items import BeePixmapItem @@ -59,9 +59,10 @@ def load_images(filenames, pos, scene, worker): worker.begin_processing.emit(len(filenames)) for i, filename in enumerate(filenames): logger.info(f'Loading image from file {filename}') - img = QtGui.QImage(filename) + img, filename = load_image(filename) worker.progress.emit(i) if img.isNull(): + logger.info(f'Could not load file {filename}') errors.append(filename) continue item = BeePixmapItem(img, filename) diff --git a/beeref/fileio/image.py b/beeref/fileio/image.py new file mode 100644 index 0000000..6afb181 --- /dev/null +++ b/beeref/fileio/image.py @@ -0,0 +1,46 @@ +# 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 . + +import logging +import os.path +import tempfile +from urllib.error import URLError +from urllib import request + +from PyQt6 import QtGui + + +logger = logging.getLogger(__name__) + + +def load_image(path): + if isinstance(path, str): + return (QtGui.QImage(path), path) + if path.isLocalFile(): + return (QtGui.QImage(path.path()), path.path()) + + img = QtGui.QImage() + try: + imgdata = request.urlopen(path.url()).read() + except URLError as e: + logger.debug(f'Downloading image failed: {e.reason}') + else: + with tempfile.TemporaryDirectory() as tmp: + fname = os.path.join(tmp, 'img') + with open(fname, 'wb') as f: + f.write(imgdata) + logger.debug(f'Temporarily saved in: {fname}') + img = QtGui.QImage(fname) + return (img, path.url()) diff --git a/beeref/view.py b/beeref/view.py index 38cd107..6d2b1e7 100644 --- a/beeref/view.py +++ b/beeref/view.py @@ -254,6 +254,7 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin): self.scene.selectedItems(user_only=True))) def on_items_loaded(self, value): + logger.debug('On items loded: add queued images') self.scene.add_delayed_items() def on_loading_finished(self, filename, errors): @@ -266,6 +267,7 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin): ('

Problem loading file %s

' '

Not accessible or not a proper bee file

') % filename) else: + self.scene.add_delayed_items() self.on_action_fit_scene() def open_from_file(self, filename): @@ -348,6 +350,7 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin): gui.DebugLogDialog(self) def on_insert_images_finished(self, filename, errors): + logger.debug('Insert images finished') if errors: errornames = [ f'
  • {fn}
  • ' for fn in errors] @@ -359,22 +362,19 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin): self, 'Problem loading images', msg + errornames) + self.scene.add_delayed_items() self.scene.arrange_optimal() self.undo_stack.endMacro() - 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})') - + def do_insert_images(self, filenames, pos=None): + if not pos: + pos = self.get_view_center() self.scene.clearSelection() self.undo_stack.beginMacro('Insert Images') self.worker = fileio.ThreadedIO( fileio.load_images, filenames, - self.mapToScene(self.get_view_center()), + self.mapToScene(pos), self.scene) self.worker.progress.connect(self.on_items_loaded) self.worker.finished.connect(self.on_insert_images_finished) @@ -384,6 +384,14 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin): parent=self) self.worker.start() + 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})') + self.do_insert_images(filenames) + def on_action_paste(self): logger.info('Pasting from clipboard...') clipboard = QtWidgets.QApplication.clipboard() @@ -515,12 +523,11 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin): self.welcome_overlay.resize(self.size()) def dragEnterEvent(self, event): - logger.debug('Received drag enter event') - - # tbd: always empty??? - print(event.mimeData().formats()) - print(dir(event)) - if event.mimeData().hasImage(): + mimedata = event.mimeData() + logger.debug(f'Drag enter event: {mimedata.formats()}') + if mimedata.hasUrls(): + event.acceptProposedAction() + elif mimedata.hasImage(): event.acceptProposedAction() else: logger.info('Attempted drop not an image') @@ -529,6 +536,16 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin): event.acceptProposedAction() def dropEvent(self, event): - logger.info('Handling file drop...') - print(event.mimeData().formats()) - # tbd + mimedata = event.mimeData() + logger.debug(f'Handling file drop: {mimedata.formats()}') + pos = QtCore.QPoint(round(event.position().x()), + round(event.position().y())) + if mimedata.hasUrls(): + self.do_insert_images(mimedata.urls(), pos) + elif mimedata.hasImage(): + img = QtGui.QImage(mimedata.imageData()) + item = BeePixmapItem(img) + pos = self.mapToScene(pos) + item.set_pos_center(pos) + self.undo_stack.push(commands.InsertItems(self.scene, [item])) + logger.info('Drop not an image') diff --git a/requirements/dev.txt b/requirements/dev.txt index 092a599..7e7258f 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,3 +1,4 @@ -flake8 -pytest coverage +flake8 +httpretty +pytest diff --git a/tests/fileio/test_image.py b/tests/fileio/test_image.py new file mode 100644 index 0000000..2e067e8 --- /dev/null +++ b/tests/fileio/test_image.py @@ -0,0 +1,49 @@ +import httpretty + +from PyQt6 import QtCore + +from beeref.fileio.image import load_image +from ..base import BeeTestCase + + +class LoadImageTestCase(BeeTestCase): + + def test_loads_from_filename(self): + img, filename = load_image(self.imgfilename3x3) + assert img.isNull() is False + assert filename == self.imgfilename3x3 + + def test_loads_from_nonexisting_filename(self): + img, filename = load_image('foo.png') + assert img.isNull() is True + assert filename == 'foo.png' + + def test_loads_from_existing_local_url(self): + url = QtCore.QUrl.fromLocalFile(self.imgfilename3x3) + img, filename = load_image(url) + assert img.isNull() is False + assert filename == self.imgfilename3x3 + + @httpretty.activate + def test_loads_from_existing_web_url(self): + url = 'http://example.com/foo.png' + httpretty.register_uri( + httpretty.GET, + url, + body=self.imgdata3x3, + ) + img, filename = load_image(QtCore.QUrl(url)) + assert img.isNull() is False + assert filename == url + + @httpretty.activate + def test_loads_from_web_url_errors(self): + url = 'http://example.com/foo.png' + httpretty.register_uri( + httpretty.GET, + url, + status=500, + ) + img, filename = load_image(QtCore.QUrl(url)) + assert img.isNull() is True + assert filename == url diff --git a/tests/test_view.py b/tests/test_view.py index 90d8916..648ba37 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -364,3 +364,63 @@ class UpdateWindowTitleTestCase(ViewBaseTestCase): self.view.filename = 'test.bee' self.view.update_window_title() assert self.parent.windowTitle() == 'test.bee* - BeeRef' + + +class DragDropTestCase(ViewBaseTestCase): + + def test_drag_enter_when_url(self): + url = QtCore.QUrl() + url.fromLocalFile(self.imgfilename3x3) + mimedata = QtCore.QMimeData() + mimedata.setUrls([url]) + event = MagicMock() + event.mimeData.return_value = mimedata + + self.view.dragEnterEvent(event) + event.acceptProposedAction.assert_called_once() + + def test_drag_enter_when_img(self): + mimedata = QtCore.QMimeData() + mimedata.setImageData(QtGui.QImage(self.imgfilename3x3)) + event = MagicMock() + event.mimeData.return_value = mimedata + + self.view.dragEnterEvent(event) + event.acceptProposedAction.assert_called_once() + + def test_drag_enter_when_unsupported(self): + mimedata = QtCore.QMimeData() + event = MagicMock() + event.mimeData.return_value = mimedata + + self.view.dragEnterEvent(event) + event.acceptProposedAction.assert_not_called() + + def test_drag_move(self): + event = MagicMock() + self.view.dragMoveEvent(event) + event.acceptProposedAction.assert_called_once() + + @patch('beeref.view.BeeGraphicsView.do_insert_images') + def test_drop_when_url(self, insert_mock): + url = QtCore.QUrl() + url.fromLocalFile(self.imgfilename3x3) + mimedata = QtCore.QMimeData() + mimedata.setUrls([url]) + event = MagicMock() + event.mimeData.return_value = mimedata + event.position.return_value = QtCore.QPointF(10, 20) + + self.view.dropEvent(event) + insert_mock.assert_called_once_with([url], QtCore.QPoint(10, 20)) + + def test_drop_when_img(self): + mimedata = QtCore.QMimeData() + mimedata.setImageData(QtGui.QImage(self.imgfilename3x3)) + event = MagicMock() + event.mimeData.return_value = mimedata + event.position.return_value = QtCore.QPointF(10, 20) + + self.view.dropEvent(event) + assert len(self.view.scene.items()) == 1 + assert self.view.scene.items()[0].isSelected() is True