From 5dcfe5c874dc8474434d41c1a5c9e0fcd8117557 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 20 Nov 2024 02:12:28 +0100 Subject: [PATCH] Allow drag and drop to open documents --- novelwriter/common.py | 7 +++- novelwriter/core/itemmodel.py | 5 ++- novelwriter/gui/doceditor.py | 35 ++++++++++++++-- novelwriter/gui/docviewer.py | 35 ++++++++++++++-- novelwriter/gui/projtree.py | 76 ----------------------------------- novelwriter/guimain.py | 1 + 6 files changed, 74 insertions(+), 85 deletions(-) diff --git a/novelwriter/common.py b/novelwriter/common.py index 92b304e7..64a4de1b 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -37,7 +37,7 @@ from typing import TYPE_CHECKING, Any, Literal, TypeVar from urllib.parse import urljoin from urllib.request import pathname2url -from PyQt5.QtCore import QCoreApplication, QUrl +from PyQt5.QtCore import QCoreApplication, QMimeData, QUrl from PyQt5.QtGui import QColor, QDesktopServices, QFont, QFontInfo from novelwriter.constants import nwConst, nwLabels, nwUnicode, trConst @@ -441,6 +441,11 @@ def qtLambda(func: Callable, *args: Any, **kwargs: Any) -> Callable: return wrapper +def decodeMimeHandles(mimeData: QMimeData) -> list[str]: + """Decode and split a mime data object with handles.""" + return mimeData.data(nwConst.MIME_HANDLE).data().decode().split("|") + + ## # Encoder Functions ## diff --git a/novelwriter/core/itemmodel.py b/novelwriter/core/itemmodel.py index c29abbc3..87795cb2 100644 --- a/novelwriter/core/itemmodel.py +++ b/novelwriter/core/itemmodel.py @@ -32,7 +32,7 @@ from PyQt5.QtCore import QAbstractItemModel, QMimeData, QModelIndex, Qt from PyQt5.QtGui import QIcon from novelwriter import SHARED -from novelwriter.common import minmax +from novelwriter.common import decodeMimeHandles, minmax from novelwriter.constants import nwConst from novelwriter.core.item import NWItem from novelwriter.enum import nwItemClass @@ -191,6 +191,7 @@ class ProjectNode: if 0 <= pos < len(self._children): node = self._children.pop(pos) self._refreshChildrenPos() + self.updateCount() return node return None @@ -330,7 +331,7 @@ class ProjectModel(QAbstractItemModel): """Process mime data drop.""" if self.canDropMimeData(data, action, row, column, parent): items = [] - for handle in data.data(nwConst.MIME_HANDLE).data().decode().split("|"): + for handle in decodeMimeHandles(data): if (index := self.indexFromHandle(handle)).isValid(): items.append(index) self.multiMove(items, parent, row) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 3ddd7731..c0b9a4c6 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -43,8 +43,9 @@ from PyQt5.QtCore import ( pyqtSlot ) from PyQt5.QtGui import ( - QColor, QCursor, QKeyEvent, QKeySequence, QMouseEvent, QPalette, QPixmap, - QResizeEvent, QTextBlock, QTextCursor, QTextDocument, QTextOption + QColor, QCursor, QDragEnterEvent, QDragMoveEvent, QDropEvent, QKeyEvent, + QKeySequence, QMouseEvent, QPalette, QPixmap, QResizeEvent, QTextBlock, + QTextCursor, QTextDocument, QTextOption ) from PyQt5.QtWidgets import ( QAction, QApplication, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit, @@ -52,7 +53,7 @@ from PyQt5.QtWidgets import ( ) from novelwriter import CONFIG, SHARED -from novelwriter.common import minmax, qtLambda, transferCase +from novelwriter.common import decodeMimeHandles, minmax, qtLambda, transferCase from novelwriter.constants import nwConst, nwKeyWords, nwShortcode, nwUnicode from novelwriter.core.document import NWDocument from novelwriter.enum import ( @@ -116,6 +117,7 @@ class GuiDocEditor(QPlainTextEdit): loadDocumentTagRequest = pyqtSignal(str, Enum) novelItemMetaChanged = pyqtSignal(str) novelStructureChanged = pyqtSignal() + openDocumentRequest = pyqtSignal(str, Enum, str, bool) requestNewNoteCreation = pyqtSignal(str, nwItemClass) requestNextDocument = pyqtSignal(str, bool) requestProjectItemRenamed = pyqtSignal(str, str) @@ -191,6 +193,7 @@ class GuiDocEditor(QPlainTextEdit): self.setMinimumWidth(CONFIG.pxInt(300)) self.setAutoFillBackground(True) self.setFrameStyle(QFrame.Shape.NoFrame) + self.setAcceptDrops(True) # Custom Shortcuts self.keyContext = QShortcut(self) @@ -997,6 +1000,32 @@ class GuiDocEditor(QPlainTextEdit): return + def dragEnterEvent(self, event: QDragEnterEvent) -> None: + """Overload drag enter event to handle dragged items.""" + if event.mimeData().hasFormat(nwConst.MIME_HANDLE): + event.acceptProposedAction() + else: + super().dragEnterEvent(event) + return + + def dragMoveEvent(self, event: QDragMoveEvent) -> None: + """Overload drag move event to handle dragged items.""" + if event.mimeData().hasFormat(nwConst.MIME_HANDLE): + event.acceptProposedAction() + else: + super().dragMoveEvent(event) + return + + def dropEvent(self, event: QDropEvent) -> None: + """Overload drop event to handle dragged items.""" + if event.mimeData().hasFormat(nwConst.MIME_HANDLE): + if handles := decodeMimeHandles(event.mimeData()): + if SHARED.project.tree.checkType(handles[0], nwItemType.FILE): + self.openDocumentRequest.emit(handles[0], nwDocMode.EDIT, "", True) + else: + super().dropEvent(event) + return + def focusNextPrevChild(self, next: bool) -> bool: """Capture the focus request from the tab key on the text editor. If the editor has focus, we do not change focus and diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index 6745db9d..2675eaa1 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -31,15 +31,18 @@ import logging from enum import Enum from PyQt5.QtCore import QPoint, Qt, QUrl, pyqtSignal, pyqtSlot -from PyQt5.QtGui import QCursor, QDesktopServices, QMouseEvent, QPalette, QResizeEvent, QTextCursor +from PyQt5.QtGui import ( + QCursor, QDesktopServices, QDragEnterEvent, QDragMoveEvent, QDropEvent, + QMouseEvent, QPalette, QResizeEvent, QTextCursor +) from PyQt5.QtWidgets import ( QAction, QApplication, QFrame, QHBoxLayout, QMenu, QTextBrowser, QToolButton, QWidget ) from novelwriter import CONFIG, SHARED -from novelwriter.common import qtLambda -from novelwriter.constants import nwStyles, nwUnicode +from novelwriter.common import decodeMimeHandles, qtLambda +from novelwriter.constants import nwConst, nwStyles, nwUnicode from novelwriter.enum import nwDocAction, nwDocMode, nwItemType from novelwriter.error import logException from novelwriter.extensions.configlayout import NColourLabel @@ -445,6 +448,32 @@ class GuiDocViewer(QTextBrowser): super().mouseReleaseEvent(event) return + def dragEnterEvent(self, event: QDragEnterEvent) -> None: + """Overload drag enter event to handle dragged items.""" + if event.mimeData().hasFormat(nwConst.MIME_HANDLE): + event.acceptProposedAction() + else: + super().dragEnterEvent(event) + return + + def dragMoveEvent(self, event: QDragMoveEvent) -> None: + """Overload drag move event to handle dragged items.""" + if event.mimeData().hasFormat(nwConst.MIME_HANDLE): + event.acceptProposedAction() + else: + super().dragMoveEvent(event) + return + + def dropEvent(self, event: QDropEvent) -> None: + """Overload drop event to handle dragged items.""" + if event.mimeData().hasFormat(nwConst.MIME_HANDLE): + if handles := decodeMimeHandles(event.mimeData()): + if SHARED.project.tree.checkType(handles[0], nwItemType.FILE): + self.openDocumentRequest.emit(handles[0], nwDocMode.VIEW, "", True) + else: + super().dropEvent(event) + return + ## # Internal Functions ## diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index f029647e..ab723d43 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -1111,82 +1111,6 @@ class GuiProjectTree(QTreeView): # self._scrollTimer.stop() return - ## - # Events - ## - - # def startDrag(self, dropAction: Qt.DropAction) -> None: - # """Capture the drag and drop handling to pop alerts.""" - # super().startDrag(dropAction) - # if self._popAlert: - # SHARED.error(self._popAlert) - # self._popAlert = None - # return - - # def dragEnterEvent(self, event: QDragEnterEvent) -> None: - # """Check that we're only dragging items that are siblings, and - # not a root level item. - # """ - # items = self.selectedItems() - # if items and (parent := items[0].parent()) and all(x.parent() is parent for x in items): - # super().dragEnterEvent(event) - # else: - # logger.warning("Drag action is not allowed and has been cancelled") - # self._popAlert = self.tr( - # "Drag and drop is only allowed for single items, non-root " - # "items, or multiple items with the same parent." - # ) - # event.mimeData().clear() - # event.ignore() - # return - - # def dragMoveEvent(self, event: QDragMoveEvent) -> None: - # """Capture the drag move event to enable edge auto scroll.""" - # y = event.pos().y() - # if y < self._scrollMargin: - # if not self._scrollTimer.isActive(): - # self._scrollDirection = -1 - # self._scrollTimer.start() - # elif y > self.height() - self._scrollMargin: - # if not self._scrollTimer.isActive(): - # self._scrollDirection = 1 - # self._scrollTimer.start() - # super().dragMoveEvent(event) - # return - - # def dropEvent(self, event: QDropEvent) -> None: - # """Overload the drop item event to ensure the drag and drop - # action is allowed, and update relevant data. - # """ - # tItem = self.itemAt(event.pos()) - # onItem = self.dropIndicatorPosition() == QAbstractItemView.DropIndicatorPosition.OnItem - # onView = self.dropIndicatorPosition() == QAbstractItemView.DropIndicatorPosition.OnViewport - # # Make sure nothing can be dropped on invisible root (see #1569) - # # Make sure nothing can be dropped on the viewport (see #2108) - # if tItem is None or (tItem.parent() is None and not onItem) or onView: - # logger.error("Invalid drop location") - # event.ignore() - # return - - # mItems: dict[str, tuple[QTreeWidgetItem, bool]] = {} - # sItems = self.selectedItems() - # if sItems and (parent := sItems[0].parent()) and all(x.parent() is parent for x in sItems): - # for sItem in sItems: - # mHandle = str(sItem.data(self.C_DATA, self.D_HANDLE)) - # mItems[mHandle] = (sItem, sItem.isExpanded()) - # self.propagateCount(mHandle, 0) - - # super().dropEvent(event) - - # for mHandle, (sItem, isExpanded) in mItems.items(): - # self._postItemMove(mHandle) - # sItem.setExpanded(isExpanded) - # self._alertTreeChange(mHandle, flush=False) - - # self.saveTreeOrder() - - # return - ## # Internal Functions ## diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index fc7cd532..adda022d 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -259,6 +259,7 @@ class GuiMain(QMainWindow): self.docEditor.loadDocumentTagRequest.connect(self._followTag) self.docEditor.novelItemMetaChanged.connect(self.novelView.updateNovelItemMeta) self.docEditor.novelStructureChanged.connect(self.novelView.refreshTree) + self.docEditor.openDocumentRequest.connect(self._openDocument) self.docEditor.requestNewNoteCreation.connect(self.projView.createNewNote) self.docEditor.requestNextDocument.connect(self.openNextDocument) self.docEditor.requestProjectItemRenamed.connect(self.projView.renameTreeItem)