Skip to content

Commit

Permalink
Merge pull request #439 from vkbo/issue438_copy-paste
Browse files Browse the repository at this point in the history
Issue #438: Editor cut/copy/paste
  • Loading branch information
vkbo committed Sep 6, 2020
2 parents 3457037 + 2c2fe12 commit 82b064f
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 36 deletions.
2 changes: 2 additions & 0 deletions nw/constants/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ class nwUnicode:
## Symbols
U_CHECK = "\u2714" # Heavy check mark
U_MULT = "\u2715" # Multiplication x
U_BULL = "\u2022" # List bullet

## Arrows
U_UTRI = "\u25b2" # Up-pointing triangle
Expand Down Expand Up @@ -294,6 +295,7 @@ class nwUnicode:
## Symbols
H_CHECK = "✔"
H_MULT = "✕"
H_BULL = "•"

## Arrows
H_UTRI = "▲"
Expand Down
117 changes: 86 additions & 31 deletions nw/gui/doceditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -732,41 +732,87 @@ def _openContextMenu(self, thePos):
"""Triggered by right click to open the context menu. Also
triggered by the Ctrl+. shortcut.
"""
if not self.spellCheck:
return
userCursor = self.textCursor()
userSelection = userCursor.hasSelection()

theCursor = self.cursorForPosition(thePos)
theCursor.select(QTextCursor.WordUnderCursor)
mnuContext = QMenu()

theWord = theCursor.selectedText().strip().strip(self.nonWord)
if theWord == "":
return
# Cut, Copy and Paste
# ===================

logger.verbose("Looking up '%s' in the dictionary" % theWord)
if self.theDict.checkWord(theWord):
return
if userSelection:
mnuCut = QAction("Cut", mnuContext)
mnuCut.triggered.connect(lambda: self.docAction(nwDocAction.CUT))
mnuContext.addAction(mnuCut)

mnuSuggest = QMenu()
mnuHead = QAction("Spelling Suggestion(s)", mnuSuggest)
mnuSuggest.addAction(mnuHead)
mnuSuggest.addSeparator()
theSuggest = self.theDict.suggestWords(theWord)
if len(theSuggest) > 0:
for aWord in theSuggest:
mnuWord = QAction(aWord, mnuSuggest)
mnuWord.triggered.connect(
lambda thePos, aWord=aWord : self._correctWord(theCursor, aWord)
)
mnuSuggest.addAction(mnuWord)
mnuSuggest.addSeparator()
mnuAdd = QAction("Add Word to Dictionary", mnuSuggest)
mnuAdd.triggered.connect(lambda thePos : self._addWord(theCursor))
mnuSuggest.addAction(mnuAdd)
else:
mnuHead = QAction("No Suggestions", mnuSuggest)
mnuSuggest.addAction(mnuHead)
mnuCopy = QAction("Copy", mnuContext)
mnuCopy.triggered.connect(lambda: self.docAction(nwDocAction.COPY))
mnuContext.addAction(mnuCopy)

mnuPaste = QAction("Paste", mnuContext)
mnuPaste.triggered.connect(lambda: self.docAction(nwDocAction.PASTE))
mnuContext.addAction(mnuPaste)

mnuContext.addSeparator()

mnuSuggest.exec_(self.viewport().mapToGlobal(thePos))
# Selections
# ==========

mnuSelAll = QAction("Select All", mnuContext)
mnuSelAll.triggered.connect(lambda: self.docAction(nwDocAction.SEL_ALL))
mnuContext.addAction(mnuSelAll)

mnuSelWord = QAction("Select Word", mnuContext)
mnuSelWord.triggered.connect(
lambda: self._makePosSelection(QTextCursor.WordUnderCursor, thePos)
)
mnuContext.addAction(mnuSelWord)

mnuSelPara = QAction("Select Paragraph", mnuContext)
mnuSelPara.triggered.connect(
lambda: self._makePosSelection(QTextCursor.BlockUnderCursor, thePos)
)
mnuContext.addAction(mnuSelPara)

# Spell Checking
# ==============

spellCheck = self.spellCheck

if spellCheck:
posCursor = self.cursorForPosition(thePos)
posCursor.select(QTextCursor.WordUnderCursor)
theWord = posCursor.selectedText().strip().strip(self.nonWord)
spellCheck &= theWord != ""

if spellCheck:
logger.verbose("Looking up '%s' in the dictionary" % theWord)
spellCheck &= not self.theDict.checkWord(theWord)

if spellCheck:
mnuContext.addSeparator()
mnuHead = QAction("Spelling Suggestion(s)", mnuContext)
mnuContext.addAction(mnuHead)

theSuggest = self.theDict.suggestWords(theWord)
if len(theSuggest) > 0:
for aWord in theSuggest:
mnuWord = QAction("%s %s" % (nwUnicode.U_ENDASH, aWord), mnuContext)
mnuWord.triggered.connect(
lambda thePos, aWord=aWord : self._correctWord(posCursor, aWord)
)
mnuContext.addAction(mnuWord)
mnuContext.addSeparator()
mnuAdd = QAction("Add Word to Dictionary", mnuContext)
mnuAdd.triggered.connect(lambda thePos : self._addWord(posCursor))
mnuContext.addAction(mnuAdd)

else:
mnuHead = QAction("No Suggestions", mnuContext)
mnuContext.addAction(mnuHead)

# Open the context menu
mnuContext.exec_(self.viewport().mapToGlobal(thePos))

return

Expand Down Expand Up @@ -1196,7 +1242,7 @@ def _formatBlock(self, docAction):
return

def _makeSelection(self, selMode):
"""Wrapper function to select a word based on a selection mode.
"""Wrapper function to select text based on a selection mode.
"""
theCursor = self.textCursor()
theCursor.clearSelection()
Expand All @@ -1216,6 +1262,15 @@ def _makeSelection(self, selMode):

return

def _makePosSelection(self, selMode, thePos):
"""Wrapper function to select text based on selection mode, but
first move cursor to given position.
"""
theCursor = self.cursorForPosition(thePos)
self.setTextCursor(theCursor)
self._makeSelection(selMode)
return

def _beginSearch(self):
"""Sets the selected text as the search text for the search bar.
"""
Expand Down
77 changes: 73 additions & 4 deletions nw/gui/docviewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@
import logging
import nw

from PyQt5.QtCore import Qt, QUrl, QSize
from PyQt5.QtCore import Qt, QUrl, QSize, pyqtSlot
from PyQt5.QtGui import (
QTextOption, QFont, QPalette, QColor, QTextCursor, QIcon
)
from PyQt5.QtWidgets import (
QTextBrowser, QWidget, QScrollArea, QLabel, QHBoxLayout, QToolButton
QTextBrowser, QWidget, QScrollArea, QLabel, QHBoxLayout, QToolButton,
QAction, QMenu
)

from nw.core import ToHtml
Expand Down Expand Up @@ -77,6 +78,10 @@ def __init__(self, theParent):
self.anchorClicked.connect(self._linkClicked)
self.setFocusPolicy(Qt.StrongFocus)

# Context Menu
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self._openContextMenu)

logger.debug("GuiDocViewer initialisation complete")

return
Expand Down Expand Up @@ -308,6 +313,7 @@ def setCursorLine(self, theLine):
# Slots
##

@pyqtSlot("QUrl")
def _linkClicked(self, theURL):
"""Slot for a link in the document being clicked.
"""
Expand All @@ -319,6 +325,49 @@ def _linkClicked(self, theURL):
self.loadFromTag(theBits[1])
return

@pyqtSlot("QPoint")
def _openContextMenu(self, thePos):
"""Triggered by right click to open the context menu.
"""
userCursor = self.textCursor()
userSelection = userCursor.hasSelection()

mnuContext = QMenu()

# Cut, Copy and Paste
# ===================

if userSelection:
mnuCopy = QAction("Copy", mnuContext)
mnuCopy.triggered.connect(lambda: self.docAction(nwDocAction.COPY))
mnuContext.addAction(mnuCopy)

mnuContext.addSeparator()

# Selections
# ==========

mnuSelAll = QAction("Select All", mnuContext)
mnuSelAll.triggered.connect(lambda: self.docAction(nwDocAction.SEL_ALL))
mnuContext.addAction(mnuSelAll)

mnuSelWord = QAction("Select Word", mnuContext)
mnuSelWord.triggered.connect(
lambda: self._makePosSelection(QTextCursor.WordUnderCursor, thePos)
)
mnuContext.addAction(mnuSelWord)

mnuSelPara = QAction("Select Paragraph", mnuContext)
mnuSelPara.triggered.connect(
lambda: self._makePosSelection(QTextCursor.BlockUnderCursor, thePos)
)
mnuContext.addAction(mnuSelPara)

# Open the context menu
mnuContext.exec_(self.viewport().mapToGlobal(thePos))

return

##
# Events
##
Expand All @@ -335,13 +384,33 @@ def resizeEvent(self, theEvent):
##

def _makeSelection(self, selMode):
"""Wrapper function for making a selection based on a specific
selection mode.
"""Wrapper function to select text based on a selection mode.
"""
theCursor = self.textCursor()
theCursor.clearSelection()
theCursor.select(selMode)

if selMode == QTextCursor.BlockUnderCursor:
# This selection mode also selects the preceding oaragraph
# separator, which we want to avoid.
posS = theCursor.selectionStart()
posE = theCursor.selectionEnd()
selTxt = theCursor.selectedText()
if selTxt.startswith(nwUnicode.U_PSEP):
theCursor.setPosition(posS+1, QTextCursor.MoveAnchor)
theCursor.setPosition(posE, QTextCursor.KeepAnchor)

self.setTextCursor(theCursor)

return

def _makePosSelection(self, selMode, thePos):
"""Wrapper function to select text based on selection mode, but
first move cursor to given position.
"""
theCursor = self.cursorForPosition(thePos)
self.setTextCursor(theCursor)
self._makeSelection(selMode)
return

def _makeStyleSheet(self):
Expand Down
55 changes: 54 additions & 1 deletion tests/test_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from os import path
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtGui import QTextCursor
from PyQt5.QtWidgets import QAction, QDialogButtonBox, QTreeWidgetItem

from nw.gui import (
Expand Down Expand Up @@ -397,7 +398,7 @@ def testProjectEditor(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
testFile = path.join(nwTempGUI, "2_nwProject.nwx")
refFile = path.join(nwRef, "gui", "2_nwProject.nwx")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
assert cmpFiles(testFile, refFile, [2, 8, 9, 10])

# qtbot.stopForInteraction()
nwGUI.closeMain()
Expand Down Expand Up @@ -1110,6 +1111,58 @@ def testDocAction(qtbot, nwLipsum, nwTemp):
assert nwGUI.docEditor.getText()[27:74] == cleanText
qtbot.wait(stepDelay)

# Editor Context Menu
theCursor = nwGUI.docEditor.textCursor()
theCursor.setPosition(100)
nwGUI.docEditor.setTextCursor(theCursor)
theRect = nwGUI.docEditor.cursorRect()

nwGUI.docEditor._openContextMenu(theRect.bottomRight())
qtbot.mouseClick(nwGUI.docEditor, Qt.LeftButton, pos=theRect.topLeft())

nwGUI.docEditor._makePosSelection(QTextCursor.WordUnderCursor, theRect.center())
theCursor = nwGUI.docEditor.textCursor()
assert theCursor.selectedText() == "imperdiet"

nwGUI.docEditor._makePosSelection(QTextCursor.BlockUnderCursor, theRect.center())
theCursor = nwGUI.docEditor.textCursor()
assert theCursor.selectedText() == (
"Pellentesque nec erat ut nulla posuere commodo. Curabitur nisi augue, imperdiet et porta "
"imperdiet, efficitur id leo. Cras finibus arcu at nibh commodo congue. Proin suscipit "
"placerat condimentum. Aenean ante enim, cursus id lorem a, blandit venenatis nibh. "
"Maecenas suscipit porta elit, sit amet porta felis porttitor eu. Sed a dui nibh. "
"Phasellus sed faucibus dui. Pellentesque felis nulla, ultrices non efficitur quis, "
"rutrum id mi. Mauris tempus auctor nisl, in bibendum enim pellentesque sit amet. Proin "
"nunc lacus, imperdiet nec posuere ac, interdum non lectus."
)

# Viewer Context Menu
assert nwGUI.viewDocument("4c4f28287af27")

theCursor = nwGUI.docViewer.textCursor()
theCursor.setPosition(100)
nwGUI.docViewer.setTextCursor(theCursor)
theRect = nwGUI.docViewer.cursorRect()

nwGUI.docViewer._openContextMenu(theRect.bottomRight())
qtbot.mouseClick(nwGUI.docViewer, Qt.LeftButton, pos=theRect.topLeft())

nwGUI.docViewer._makePosSelection(QTextCursor.WordUnderCursor, theRect.center())
theCursor = nwGUI.docViewer.textCursor()
assert theCursor.selectedText() == "imperdiet"

nwGUI.docEditor._makePosSelection(QTextCursor.BlockUnderCursor, theRect.center())
theCursor = nwGUI.docEditor.textCursor()
assert theCursor.selectedText() == (
"Pellentesque nec erat ut nulla posuere commodo. Curabitur nisi augue, imperdiet et porta "
"imperdiet, efficitur id leo. Cras finibus arcu at nibh commodo congue. Proin suscipit "
"placerat condimentum. Aenean ante enim, cursus id lorem a, blandit venenatis nibh. "
"Maecenas suscipit porta elit, sit amet porta felis porttitor eu. Sed a dui nibh. "
"Phasellus sed faucibus dui. Pellentesque felis nulla, ultrices non efficitur quis, "
"rutrum id mi. Mauris tempus auctor nisl, in bibendum enim pellentesque sit amet. Proin "
"nunc lacus, imperdiet nec posuere ac, interdum non lectus."
)

# qtbot.stopForInteraction()
nwGUI.closeMain()

Expand Down

0 comments on commit 82b064f

Please sign in to comment.