Fix dropping images into BeeRef

This commit is contained in:
Rebecca Breu 2021-05-14 21:29:58 +02:00
parent 6118acedc5
commit d117790726
7 changed files with 199 additions and 24 deletions

View file

@ -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

View file

@ -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)

46
beeref/fileio/image.py Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
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())

View file

@ -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):
('<p>Problem loading file %s</p>'
'<p>Not accessible or not a proper bee file</p>') % 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'<li>{fn}</li>' 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')

View file

@ -1,3 +1,4 @@
flake8
pytest
coverage
flake8
httpretty
pytest

View file

@ -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

View file

@ -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