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