diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 64fcf39..68646b8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,16 @@ 0.2.1 - (unreleased) ==================== +Added +----- + +* Show list of recent files on welcome screen + +Fixed +----- + +* Various typos (by luzpaz) + 0.2.0 - 2021-09-06 ================== diff --git a/beeref/gui.py b/beeref/gui.py index 002616f..7b50639 100644 --- a/beeref/gui.py +++ b/beeref/gui.py @@ -16,33 +16,111 @@ import logging import os.path -from PyQt6 import QtCore, QtWidgets +from PyQt6 import QtCore, QtGui, QtWidgets from PyQt6.QtCore import Qt from beeref import constants -from beeref.config import logfile_name +from beeref.config import logfile_name, BeeSettings +from beeref.main_controls import MainControlsMixin logger = logging.getLogger(__name__) -class WelcomeOverlay(QtWidgets.QWidget): +class RecentFilesModel(QtCore.QAbstractListModel): + """An entry in the 'Recent Files' list.""" + + def __init__(self, files): + super().__init__() + self.files = files + + def rowCount(self, parent): + return len(self.files) + + def data(self, index, role): + if role == QtCore.Qt.ItemDataRole.DisplayRole: + return os.path.basename(self.files[index.row()]) + if role == QtCore.Qt.ItemDataRole.FontRole: + font = QtGui.QFont() + font.setUnderline(True) + return font + + +class RecentFilesView(QtWidgets.QListView): + + def __init__(self, parent, files=None): + super().__init__(parent) + self.files = files or [] + self.clicked.connect(self.on_clicked) + self.setModel(RecentFilesModel(self.files)) + self.setMouseTracking(True) + + def on_clicked(self, index): + self.parent().parent().open_from_file(self.files[index.row()]) + + def update_files(self, files): + self.files = files + self.model().files = files + self.reset() + + def sizeHint(self): + size = QtCore.QSize() + height = sum( + (self.sizeHintForRow(i) + 2) for i in range(len(self.files))) + width = max(self.sizeHintForColumn(i) for i in range(len(self.files))) + size.setHeight(height) + size.setWidth(width + 2) + return size + + def mouseMoveEvent(self, event): + index = self.indexAt( + QtCore.QPoint(int(event.position().x()), + int(event.position().y()))) + if index.isValid(): + self.setCursor(Qt.CursorShape.PointingHandCursor) + else: + self.setCursor(Qt.CursorShape.ArrowCursor) + + super().mouseMoveEvent(event) + + +class WelcomeOverlay(MainControlsMixin, QtWidgets.QWidget): """Some basic info to be displayed when the scene is empty.""" txt = """

Paste or drop images here.

Right-click for more options.

""" - def __init__(self, parent=None): + def __init__(self, parent): super().__init__(parent) + self.control_target = parent self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground) - self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents) - label = QtWidgets.QLabel(self) - label.setText(self.txt) + self.init_main_controls() + + # Recent files + self.files_layout = QtWidgets.QVBoxLayout() + self.files_layout.addStretch(50) + self.files_layout.addWidget( + QtWidgets.QLabel('

Recent Files

', self)) + self.files_view = RecentFilesView(self) + self.files_layout.addWidget(self.files_view) + self.files_layout.addStretch(50) + + # Help text + label = QtWidgets.QLabel(self.txt, self) label.setAlignment(Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignCenter) - layout = QtWidgets.QVBoxLayout() - layout.addWidget(label) - self.setLayout(layout) + self.layout = QtWidgets.QHBoxLayout() + self.layout.addStretch(50) + self.layout.addWidget(label) + self.layout.addStretch(50) + self.setLayout(self.layout) + + def show(self): + files = BeeSettings().get_recent_files(existing_only=True) + self.files_view.update_files(files) + if files and self.layout.indexOf(self.files_layout) < 0: + self.layout.insertLayout(0, self.files_layout) + super().show() class BeeProgressDialog(QtWidgets.QProgressDialog): diff --git a/beeref/main_controls.py b/beeref/main_controls.py new file mode 100644 index 0000000..4626e34 --- /dev/null +++ b/beeref/main_controls.py @@ -0,0 +1,78 @@ +# 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 + +from PyQt6 import QtCore, QtGui +from PyQt6.QtCore import Qt + +from beeref import commands +from beeref.items import BeePixmapItem +from beeref import fileio + + +logger = logging.getLogger(__name__) + + +class MainControlsMixin: + """Basic controls shared by the main view and the welcome overlay: + + * Right-click menu + * Dropping files + """ + + def init_main_controls(self): + self.setContextMenuPolicy( + Qt.ContextMenuPolicy.CustomContextMenu) + self.customContextMenuRequested.connect( + self.control_target.on_context_menu) + self.setAcceptDrops(True) + + def dragEnterEvent(self, event): + 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') + + def dragMoveEvent(self, event): + event.acceptProposedAction() + + def dropEvent(self, event): + 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(): + logger.debug(f'Found dropped urls: {mimedata.urls()}') + if not self.control_target.scene.items(): + # Check if we have a bee file we can open directly + path = mimedata.urls()[0] + if (path.isLocalFile() + and fileio.is_bee_file(path.toLocalFile())): + self.control_target.open_from_file(path.toLocalFile()) + return + self.control_target.do_insert_images(mimedata.urls(), pos) + elif mimedata.hasImage(): + img = QtGui.QImage(mimedata.imageData()) + item = BeePixmapItem(img) + pos = self.control_target.mapToScene(pos) + self.control_target.undo_stack.push( + commands.InsertItems(self.control_target.scene, [item], pos)) + else: + logger.info('Drop not an image') diff --git a/beeref/view.py b/beeref/view.py index a750070..46f46c3 100644 --- a/beeref/view.py +++ b/beeref/view.py @@ -28,6 +28,7 @@ from beeref import constants from beeref import fileio from beeref import gui from beeref.items import BeePixmapItem, BeeTextItem +from beeref.main_controls import MainControlsMixin from beeref.scene import BeeGraphicsScene @@ -35,7 +36,9 @@ commandline_args = CommandlineArgs() logger = logging.getLogger(__name__) -class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin): +class BeeGraphicsView(MainControlsMixin, + QtWidgets.QGraphicsView, + ActionsMixin): def __init__(self, app, parent=None): super().__init__(parent) @@ -47,7 +50,6 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin): self.setBackgroundBrush( QtGui.QBrush(QtGui.QColor(*constants.COLORS['Scene:Canvas']))) self.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) - self.setAcceptDrops(True) self.undo_stack = QtGui.QUndoStack(self) self.undo_stack.setUndoLimit(100) @@ -68,9 +70,8 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin): # Context menu and actions self.build_menu_and_actions() - self.setContextMenuPolicy( - Qt.ContextMenuPolicy.CustomContextMenu) - self.customContextMenuRequested.connect(self.on_context_menu) + self.control_target = self + self.init_main_controls() # Load file given via command line if commandline_args.filename: @@ -671,39 +672,3 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin): super().resizeEvent(event) self.recalc_scene_rect() self.welcome_overlay.resize(self.size()) - - def dragEnterEvent(self, event): - 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') - - def dragMoveEvent(self, event): - event.acceptProposedAction() - - def dropEvent(self, event): - 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(): - logger.debug(f'Found dropped urls: {mimedata.urls()}') - if not self.scene.items(): - # Check if we have a bee file we can open directly - path = mimedata.urls()[0] - if (path.isLocalFile() - and fileio.is_bee_file(path.toLocalFile())): - self.open_from_file(path.toLocalFile()) - return - self.do_insert_images(mimedata.urls(), pos) - elif mimedata.hasImage(): - img = QtGui.QImage(mimedata.imageData()) - item = BeePixmapItem(img) - pos = self.mapToScene(pos) - self.undo_stack.push(commands.InsertItems(self.scene, [item], pos)) - else: - logger.info('Drop not an image') diff --git a/tests/test_gui.py b/tests/test_gui.py index 855683d..3cd72c7 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -1,8 +1,10 @@ -from PyQt6 import QtWidgets +from unittest.mock import MagicMock + +from PyQt6 import QtCore, QtWidgets from PyQt6.QtCore import Qt from beeref.config import logfile_name -from beeref.gui import DebugLogDialog +from beeref.gui import DebugLogDialog, RecentFilesModel def test_debug_log_dialog(qtbot, settings, view): @@ -16,3 +18,23 @@ def test_debug_log_dialog(qtbot, settings, view): qtbot.mouseClick(dialog.copy_button, Qt.MouseButton.LeftButton) clipboard = QtWidgets.QApplication.clipboard() assert clipboard.text() == 'my log output' + + +def test_recent_files_model_rowcount(view): + model = RecentFilesModel(['foo.png', 'bar.png']) + assert model.rowCount(None) == 2 + + +def test_recent_files_model_data_diplayrole(view): + model = RecentFilesModel(['foo.png', 'bar.png']) + index = MagicMock() + index.row.return_value = 1 + assert model.data(index, QtCore.Qt.ItemDataRole.DisplayRole) == 'bar.png' + + +def test_recent_files_model_data_fontrole(view): + model = RecentFilesModel(['foo.png', 'bar.png']) + index = MagicMock() + index.row.return_value = 1 + font = model.data(index, QtCore.Qt.ItemDataRole.FontRole) + assert font.underline() is True