Allow drag and drop to open documents
This commit is contained in:
@@ -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
|
||||
##
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
##
|
||||
|
||||
@@ -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
|
||||
##
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user