Merge release 2.7.5
This commit is contained in:
+108
-180
@@ -29,7 +29,7 @@ General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
""" # noqa
|
||||
from __future__ import annotations
|
||||
|
||||
import bisect
|
||||
@@ -46,11 +46,11 @@ from PyQt6.QtGui import (
|
||||
QAction, QCursor, QDragEnterEvent, QDragMoveEvent, QDropEvent,
|
||||
QInputMethodEvent, QKeyEvent, QKeySequence, QMouseEvent, QPalette, QPixmap,
|
||||
QResizeEvent, QShortcut, QTextBlock, QTextCursor, QTextDocument,
|
||||
QTextOption
|
||||
QTextFormat, QTextOption
|
||||
)
|
||||
from PyQt6.QtWidgets import (
|
||||
QApplication, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QMenu,
|
||||
QPlainTextEdit, QToolBar, QVBoxLayout, QWidget
|
||||
QPlainTextEdit, QTextEdit, QToolBar, QVBoxLayout, QWidget
|
||||
)
|
||||
|
||||
from novelwriter import CONFIG, SHARED
|
||||
@@ -77,7 +77,7 @@ from novelwriter.types import (
|
||||
QtAlignCenterTop, QtAlignJustify, QtAlignLeft, QtAlignLeftTop,
|
||||
QtAlignRight, QtImCursorRectangle, QtKeepAnchor, QtModCtrl, QtModNone,
|
||||
QtModShift, QtMouseLeft, QtMoveAnchor, QtMoveLeft, QtMoveRight,
|
||||
QtScrollAlwaysOff, QtScrollAsNeeded
|
||||
QtScrollAlwaysOff, QtScrollAsNeeded, QtTransparent
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -99,7 +99,7 @@ class _TagAction(IntFlag):
|
||||
|
||||
|
||||
class GuiDocEditor(QPlainTextEdit):
|
||||
"""Gui Widget: Main Document Editor"""
|
||||
"""Gui Widget: Main Document Editor."""
|
||||
|
||||
__slots__ = (
|
||||
"_autoReplace", "_completer", "_doReplace", "_docChanged", "_docHandle", "_followTag1",
|
||||
@@ -147,6 +147,8 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
self._lastActive = 0.0 # Timestamp of last activity
|
||||
self._lastFind = None # Position of the last found search word
|
||||
self._doReplace = False # Switch to temporarily disable auto-replace
|
||||
self._lineColor = QtTransparent
|
||||
self._selection = QTextEdit.ExtraSelection()
|
||||
|
||||
# Auto-Replace
|
||||
self._autoReplace = TextAutoReplace()
|
||||
@@ -235,8 +237,6 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
|
||||
logger.debug("Ready: GuiDocEditor")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Properties
|
||||
##
|
||||
@@ -285,18 +285,16 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
self.docHeader.clearHeader()
|
||||
self.docFooter.setHandle(self._docHandle)
|
||||
self.docToolBar.setVisible(False)
|
||||
self.setExtraSelections([])
|
||||
|
||||
self.itemHandleChanged.emit("")
|
||||
|
||||
return
|
||||
|
||||
def updateTheme(self) -> None:
|
||||
"""Update theme elements."""
|
||||
self.docSearch.updateTheme()
|
||||
self.docHeader.updateTheme()
|
||||
self.docFooter.updateTheme()
|
||||
self.docToolBar.updateTheme()
|
||||
return
|
||||
|
||||
def updateSyntaxColors(self) -> None:
|
||||
"""Update the syntax highlighting theme."""
|
||||
@@ -317,7 +315,9 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
self.docHeader.matchColors()
|
||||
self.docFooter.matchColors()
|
||||
|
||||
return
|
||||
self._lineColor = syntax.line
|
||||
self._selection.format.setBackground(self._lineColor)
|
||||
self._selection.format.setProperty(QTextFormat.Property.FullWidthSelection, True)
|
||||
|
||||
def initEditor(self) -> None:
|
||||
"""Initialise or re-initialise the editor with the user's
|
||||
@@ -374,6 +374,8 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
# Refresh sizes
|
||||
self.setTabStopDistance(CONFIG.tabWidth)
|
||||
self.setCursorWidth(CONFIG.cursorWidth)
|
||||
self.setExtraSelections([])
|
||||
self._cursorMoved()
|
||||
|
||||
# If we have a document open, we should refresh it in case the
|
||||
# font changed, otherwise we just clear the editor entirely,
|
||||
@@ -384,8 +386,6 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
else:
|
||||
self.clearEditor()
|
||||
|
||||
return
|
||||
|
||||
def loadText(self, tHandle: str, tLine: int | None = None) -> bool:
|
||||
"""Load text from a document into the editor. If we have an I/O
|
||||
error, we must handle this and clear the editor so that we don't
|
||||
@@ -463,7 +463,6 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
self.updateDocMargins()
|
||||
self.setDocumentChanged(True)
|
||||
QApplication.restoreOverrideCursor()
|
||||
return
|
||||
|
||||
def saveText(self) -> bool:
|
||||
"""Save the text currently in the editor to the NWDocument
|
||||
@@ -531,7 +530,6 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
vBar.setValue(vBar.value() + 1)
|
||||
count += 1
|
||||
QApplication.processEvents()
|
||||
return
|
||||
|
||||
def updateDocMargins(self) -> None:
|
||||
"""Automatically adjust the margins so the text is centred if
|
||||
@@ -572,8 +570,6 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
lM = max(self._vpMargin, fH)
|
||||
self.setViewportMargins(tM, uM, tM, lM)
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Getters
|
||||
##
|
||||
@@ -583,20 +579,19 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
QTextDocument->toRawText instead of toPlainText. The former preserves
|
||||
non-breaking spaces, the latter does not. We still want to get rid of
|
||||
paragraph and line separators though.
|
||||
|
||||
See: https://doc.qt.io/qt-6/qtextdocument.html#toPlainText
|
||||
"""
|
||||
text = self._qDocument.toRawText()
|
||||
text = text.replace(nwUnicode.U_LSEP, "\n") # Line separators
|
||||
text = text.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators
|
||||
return text
|
||||
return text.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators
|
||||
|
||||
def getSelectedText(self) -> str:
|
||||
"""Get currently selected text."""
|
||||
if (cursor := self.textCursor()).hasSelection():
|
||||
text = cursor.selectedText()
|
||||
text = text.replace(nwUnicode.U_LSEP, "\n") # Line separators
|
||||
text = text.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators
|
||||
return text
|
||||
return text.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators
|
||||
return ""
|
||||
|
||||
def getCursorPosition(self) -> int:
|
||||
@@ -617,7 +612,6 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
logger.debug("Document changed status is '%s'", state)
|
||||
self._docChanged = state
|
||||
self.editedStatusChanged.emit(self._docChanged)
|
||||
return
|
||||
|
||||
def setCursorPosition(self, position: int) -> None:
|
||||
"""Move the cursor to a given position in the document."""
|
||||
@@ -626,14 +620,12 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
cursor.setPosition(minmax(position, 0, chars-1))
|
||||
self.setTextCursor(cursor)
|
||||
self.centerCursor()
|
||||
return
|
||||
|
||||
def saveCursorPosition(self) -> None:
|
||||
"""Save the cursor position to the current project item."""
|
||||
if self._nwItem is not None:
|
||||
cursPos = self.getCursorPosition()
|
||||
self._nwItem.setCursorPos(cursPos)
|
||||
return
|
||||
|
||||
def setCursorLine(self, line: int | None) -> None:
|
||||
"""Move the cursor to a given line in the document."""
|
||||
@@ -642,7 +634,6 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
if block:
|
||||
self.setCursorPosition(block.position())
|
||||
logger.debug("Cursor moved to line %d", line)
|
||||
return
|
||||
|
||||
def setCursorSelection(self, start: int, length: int) -> None:
|
||||
"""Make a text selection."""
|
||||
@@ -651,14 +642,15 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
cursor.setPosition(start, QtMoveAnchor)
|
||||
cursor.setPosition(start + length, QtKeepAnchor)
|
||||
self.setTextCursor(cursor)
|
||||
return
|
||||
|
||||
##
|
||||
# Spell Checking
|
||||
##
|
||||
|
||||
def toggleSpellCheck(self, state: bool | None) -> None:
|
||||
"""This is the main spell check setting function, and this one
|
||||
"""Toggle spell checking.
|
||||
|
||||
This is the main spell check setting function, and this one
|
||||
should call all other setSpellCheck functions in other classes.
|
||||
If the spell check state is not defined (None), then toggle the
|
||||
current status saved in this class.
|
||||
@@ -682,8 +674,6 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
|
||||
logger.debug("Spell check is set to '%s'", str(state))
|
||||
|
||||
return
|
||||
|
||||
def spellCheckDocument(self) -> None:
|
||||
"""Rerun the highlighter to update spell checking status of the
|
||||
currently loaded text.
|
||||
@@ -695,7 +685,6 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
QApplication.restoreOverrideCursor()
|
||||
logger.debug("Document highlighted in %.3f ms", 1000*(time() - start))
|
||||
self.updateStatusMessage.emit(self.tr("Spell check complete"))
|
||||
return
|
||||
|
||||
##
|
||||
# General Class Methods
|
||||
@@ -735,6 +724,8 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
self._toggleFormat(2, "*")
|
||||
elif action == nwDocAction.MD_STRIKE:
|
||||
self._toggleFormat(2, "~")
|
||||
elif action == nwDocAction.MD_MARK:
|
||||
self._toggleFormat(2, "=")
|
||||
elif action == nwDocAction.S_QUOTE:
|
||||
self._wrapSelection(CONFIG.fmtSQuoteOpen, CONFIG.fmtSQuoteClose)
|
||||
elif action == nwDocAction.D_QUOTE:
|
||||
@@ -822,7 +813,6 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
details=self.tr("File Location: {0}").format(self._nwDocument.fileLocation),
|
||||
log=False
|
||||
)
|
||||
return
|
||||
|
||||
def insertText(self, insert: str | nwDocInsert) -> None:
|
||||
"""Insert a specific type of text at the cursor position."""
|
||||
@@ -964,7 +954,6 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
event.acceptProposedAction()
|
||||
else:
|
||||
super().dragEnterEvent(event)
|
||||
return
|
||||
|
||||
def dragMoveEvent(self, event: QDragMoveEvent) -> None:
|
||||
"""Overload drag move event to handle dragged items."""
|
||||
@@ -972,7 +961,6 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
event.acceptProposedAction()
|
||||
else:
|
||||
super().dragMoveEvent(event)
|
||||
return
|
||||
|
||||
def dropEvent(self, event: QDropEvent) -> None:
|
||||
"""Overload drop event to handle dragged items."""
|
||||
@@ -982,7 +970,6 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
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
|
||||
@@ -1009,7 +996,6 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
else:
|
||||
self._processTag(cursor)
|
||||
super().mouseReleaseEvent(event)
|
||||
return
|
||||
|
||||
def resizeEvent(self, event: QResizeEvent) -> None:
|
||||
"""If the text editor is resized, we must make sure the document
|
||||
@@ -1017,7 +1003,6 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
"""
|
||||
self.updateDocMargins()
|
||||
super().resizeEvent(event)
|
||||
return
|
||||
|
||||
def inputMethodEvent(self, event: QInputMethodEvent) -> None:
|
||||
"""Handle text being input from CJK input methods."""
|
||||
@@ -1045,14 +1030,13 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
|
||||
@pyqtSlot(str, Enum)
|
||||
def onProjectItemChanged(self, tHandle: str, change: nwChange) -> None:
|
||||
"""Called when an item label is changed to check if the document
|
||||
title bar needs updating,
|
||||
"""Process project item change. Called when an item label is
|
||||
changed to check if the document title bar needs updating.
|
||||
"""
|
||||
if tHandle == self._docHandle and change == nwChange.UPDATE:
|
||||
self.docHeader.setHandle(tHandle)
|
||||
self.docFooter.updateInfo()
|
||||
self.updateDocMargins()
|
||||
return
|
||||
|
||||
@pyqtSlot(str)
|
||||
def insertKeyWord(self, keyword: str) -> bool:
|
||||
@@ -1063,8 +1047,7 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
logger.error("Invalid keyword '%s'", keyword)
|
||||
return False
|
||||
logger.debug("Inserting keyword '%s'", keyword)
|
||||
state = self.insertNewBlock(f"{keyword}: ")
|
||||
return state
|
||||
return self.insertNewBlock(f"{keyword}: ")
|
||||
|
||||
@pyqtSlot()
|
||||
def toggleSearch(self) -> None:
|
||||
@@ -1073,14 +1056,12 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
self.closeSearch()
|
||||
else:
|
||||
self.beginSearch()
|
||||
return
|
||||
|
||||
@pyqtSlot(list, list)
|
||||
def updateChangedTags(self, updated: list[str], deleted: list[str]) -> None:
|
||||
"""Tags have changed, so just in case we rehighlight them."""
|
||||
if updated or deleted:
|
||||
self._qDocument.syntaxHighlighter.rehighlightByType(BLOCK_META)
|
||||
return
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
@@ -1122,13 +1103,14 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
if self._autoReplace.process(text, cursor):
|
||||
self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
|
||||
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _cursorMoved(self) -> None:
|
||||
"""Triggered when the cursor moved in the editor."""
|
||||
self.docFooter.updateLineCount(self.textCursor())
|
||||
return
|
||||
if CONFIG.lineHighlight:
|
||||
self._selection.cursor = self.textCursor()
|
||||
self._selection.cursor.clearSelection()
|
||||
self.setExtraSelections([self._selection])
|
||||
|
||||
@pyqtSlot(int, int, str)
|
||||
def _insertCompletion(self, pos: int, length: int, text: str) -> None:
|
||||
@@ -1140,13 +1122,11 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
cursor.setPosition(check + length, QtKeepAnchor)
|
||||
cursor.insertText(text)
|
||||
self._completer.close()
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _openContextFromCursor(self) -> None:
|
||||
"""Open the spell check context menu at the cursor."""
|
||||
self._openContextMenu(self.cursorRect().center())
|
||||
return
|
||||
|
||||
@pyqtSlot("QPoint")
|
||||
def _openContextMenu(self, pos: QPoint) -> None:
|
||||
@@ -1204,13 +1184,13 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
|
||||
# Spell Checking
|
||||
if SHARED.project.data.spellCheck:
|
||||
word, cPos, cLen, suggest = self._qDocument.spellErrorAtPos(pCursor.position())
|
||||
if word and cPos >= 0 and cLen > 0:
|
||||
word, offset, suggest = self._qDocument.spellErrorAtPos(pCursor.position())
|
||||
if word and offset >= 0:
|
||||
logger.debug("Word '%s' is misspelled", word)
|
||||
block = pCursor.block()
|
||||
sCursor = self.textCursor()
|
||||
sCursor.setPosition(block.position() + cPos)
|
||||
sCursor.movePosition(QtMoveRight, QtKeepAnchor, cLen)
|
||||
sCursor.setPosition(block.position() + offset)
|
||||
sCursor.movePosition(QtMoveRight, QtKeepAnchor, len(word))
|
||||
if suggest:
|
||||
ctxMenu.addSeparator()
|
||||
qtAddAction(ctxMenu, self.tr("Spelling Suggestion(s)"))
|
||||
@@ -1233,8 +1213,6 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
|
||||
ctxMenu.setParent(None)
|
||||
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _runDocumentTasks(self) -> None:
|
||||
"""Run timer document tasks."""
|
||||
@@ -1271,11 +1249,10 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
if not self.textCursor().hasSelection():
|
||||
# Selection counter should take precedence (#2155)
|
||||
self.docFooter.updateMainCount(mCount, False)
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _updateSelectedStatus(self) -> None:
|
||||
"""The user made a change in text selection. Forward this
|
||||
"""Process user change in text selection. Forward this
|
||||
information to the footer, and start the selection word counter.
|
||||
"""
|
||||
if self.textCursor().hasSelection():
|
||||
@@ -1284,7 +1261,6 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
else:
|
||||
self._timerSel.stop()
|
||||
self.docFooter.updateMainCount(0, False)
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _runSelCounter(self) -> None:
|
||||
@@ -1302,14 +1278,12 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
if self._docHandle and self._nwItem:
|
||||
self.docFooter.updateMainCount(cCount if CONFIG.useCharCount else wCount, True)
|
||||
self._timerSel.stop()
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _closeCurrentDocument(self) -> None:
|
||||
"""Close the document. Forwarded to the main Gui."""
|
||||
self.closeEditorRequest.emit()
|
||||
self.docToolBar.setVisible(False)
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _toggleToolBarVisibility(self) -> None:
|
||||
@@ -1317,7 +1291,6 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
state = not self.docToolBar.isVisible()
|
||||
self.docToolBar.setVisible(state)
|
||||
CONFIG.showEditToolBar = state
|
||||
return
|
||||
|
||||
##
|
||||
# Search & Replace
|
||||
@@ -1328,14 +1301,12 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
self.docSearch.setSearchText(self.getSelectedText() or None)
|
||||
resS, _ = self.findAllOccurences()
|
||||
self.docSearch.setResultCount(None, len(resS))
|
||||
return
|
||||
|
||||
def beginReplace(self) -> None:
|
||||
"""Initialise the search box and reset the replace text box."""
|
||||
self.beginSearch()
|
||||
self.docSearch.setReplaceText("")
|
||||
self.updateDocMargins()
|
||||
return
|
||||
|
||||
def findNext(self, goBack: bool = False) -> None:
|
||||
"""Search for the next or previous occurrence of the search bar
|
||||
@@ -1623,8 +1594,6 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
|
||||
self.setTextCursor(cursor)
|
||||
|
||||
return
|
||||
|
||||
def _replaceQuotes(self, sQuote: str, oQuote: str, cQuote: str) -> None:
|
||||
"""Replace all straight quotes in the selected text."""
|
||||
cursor = self.textCursor()
|
||||
@@ -1884,8 +1853,6 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
cursor.insertText(cleanText.rstrip() + "\n")
|
||||
cursor.endEditBlock()
|
||||
|
||||
return
|
||||
|
||||
def _insertCommentStructure(self, style: nwComment) -> None:
|
||||
"""Insert a shortcut/comment combo."""
|
||||
if self._docHandle and style == nwComment.FOOTNOTE:
|
||||
@@ -1933,7 +1900,6 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
cursor.endEditBlock()
|
||||
cursor.setPosition(pos)
|
||||
self.setTextCursor(cursor)
|
||||
return
|
||||
|
||||
def _addWord(self, word: str, block: QTextBlock, save: bool) -> None:
|
||||
"""Slot for the spell check context menu triggered when the user
|
||||
@@ -1942,7 +1908,6 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
logger.debug("Added '%s' to project dictionary, %s", word, "saved" if save else "unsaved")
|
||||
SHARED.spelling.addWord(word, save=save)
|
||||
self._qDocument.syntaxHighlighter.rehighlightBlock(block)
|
||||
return
|
||||
|
||||
def _processTag(
|
||||
self, cursor: QTextCursor | None = None, follow: bool = True, create: bool = False
|
||||
@@ -2016,7 +1981,6 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
if self._docHandle:
|
||||
text = block.text().lstrip("#").lstrip("!").strip()
|
||||
self.requestProjectItemRenamed.emit(self._docHandle, text)
|
||||
return
|
||||
|
||||
def _autoSelect(self) -> QTextCursor:
|
||||
"""Return a cursor which may or may not have a selection based
|
||||
@@ -2086,14 +2050,11 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
|
||||
self.setTextCursor(cursor)
|
||||
|
||||
return
|
||||
|
||||
def _makePosSelection(self, mode: QTextCursor.SelectionType, pos: QPoint) -> None:
|
||||
"""Select text based on selection mode, but first move cursor."""
|
||||
cursor = self.cursorForPosition(pos)
|
||||
self.setTextCursor(cursor)
|
||||
self._makeSelection(mode)
|
||||
return
|
||||
|
||||
def _allowAutoReplace(self, state: bool) -> None:
|
||||
"""Enable/disable the auto-replace feature temporarily."""
|
||||
@@ -2101,11 +2062,10 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
self._doReplace = CONFIG.doReplace
|
||||
else:
|
||||
self._doReplace = False
|
||||
return
|
||||
|
||||
|
||||
class CommandCompleter(QMenu):
|
||||
"""GuiWidget: Command Completer Menu
|
||||
"""GuiWidget: Command Completer Menu.
|
||||
|
||||
This is a context menu with options populated from the user's
|
||||
defined tags and keys. It also helps to type the meta data keyword
|
||||
@@ -2119,8 +2079,6 @@ class CommandCompleter(QMenu):
|
||||
|
||||
def __init__(self, parent: QWidget) -> None:
|
||||
super().__init__(parent=parent)
|
||||
self._parent = parent
|
||||
return
|
||||
|
||||
def updateMetaText(self, text: str, pos: int) -> bool:
|
||||
"""Update the menu options based on the line of text."""
|
||||
@@ -2214,7 +2172,6 @@ class CommandCompleter(QMenu):
|
||||
else:
|
||||
self.close() # Close to release the event lock before forwarding the key press (#2510)
|
||||
self._parent.keyPressEvent(event)
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
@@ -2223,11 +2180,10 @@ class CommandCompleter(QMenu):
|
||||
def _emitComplete(self, pos: int, length: int, value: str) -> None:
|
||||
"""Emit the signal to indicate a selection has been made."""
|
||||
self.insertText.emit(pos, length, value)
|
||||
return
|
||||
|
||||
|
||||
class BackgroundWordCounter(QRunnable):
|
||||
"""The Off-GUI Thread Word Counter
|
||||
"""The Off-GUI Thread Word Counter.
|
||||
|
||||
A runnable for the word counter to be run in the thread pool off the
|
||||
main GUI thread.
|
||||
@@ -2239,9 +2195,9 @@ class BackgroundWordCounter(QRunnable):
|
||||
self._forSelection = forSelection
|
||||
self._isRunning = False
|
||||
self.signals = BackgroundWordCounterSignals()
|
||||
return
|
||||
|
||||
def isRunning(self) -> bool:
|
||||
"""Return True if the word counter is already running."""
|
||||
return self._isRunning
|
||||
|
||||
@pyqtSlot()
|
||||
@@ -2259,17 +2215,17 @@ class BackgroundWordCounter(QRunnable):
|
||||
self.signals.countsReady.emit(cC, wC, pC)
|
||||
self._isRunning = False
|
||||
|
||||
return
|
||||
|
||||
|
||||
class BackgroundWordCounterSignals(QObject):
|
||||
"""The QRunnable cannot emit a signal, so we need a simple QObject
|
||||
to hold the word counter signal.
|
||||
"""
|
||||
|
||||
countsReady = pyqtSignal(int, int, int)
|
||||
|
||||
|
||||
class TextAutoReplace:
|
||||
"""Encapsulates the editor auto replace feature."""
|
||||
|
||||
__slots__ = (
|
||||
"_doPadAfter", "_doPadBefore", "_padAfter", "_padBefore", "_padChar",
|
||||
@@ -2279,7 +2235,6 @@ class TextAutoReplace:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.initSettings()
|
||||
return
|
||||
|
||||
def initSettings(self) -> None:
|
||||
"""Initialise the auto-replace settings from config."""
|
||||
@@ -2298,27 +2253,28 @@ class TextAutoReplace:
|
||||
self._padAfter = CONFIG.fmtPadAfter
|
||||
self._doPadBefore = bool(CONFIG.fmtPadBefore)
|
||||
self._doPadAfter = bool(CONFIG.fmtPadAfter)
|
||||
return
|
||||
|
||||
def process(self, text: str, cursor: QTextCursor) -> bool:
|
||||
"""Auto-replace text elements based on main configuration.
|
||||
Returns True if anything was changed.
|
||||
"""
|
||||
pos = cursor.positionInBlock()
|
||||
length = len(text)
|
||||
if length < 1 or pos-1 > length:
|
||||
aPos = cursor.position()
|
||||
bPos = cursor.positionInBlock()
|
||||
block = cursor.block()
|
||||
length = block.length() - 1
|
||||
if length < 1 or bPos-1 > length:
|
||||
return False
|
||||
|
||||
delete, insert = self._determine(text, pos)
|
||||
if insert == "":
|
||||
return False
|
||||
cursor.movePosition(QtMoveLeft, QtKeepAnchor, min(4, bPos))
|
||||
last = cursor.selectedText()
|
||||
delete, insert = self._determine(last, bPos)
|
||||
|
||||
check = insert
|
||||
if self._doPadBefore and check in self._padBefore:
|
||||
if not (check == ":" and length > 1 and text[0] == "@"):
|
||||
delete = max(delete, 1)
|
||||
chkPos = pos - delete - 1
|
||||
if chkPos >= 0 and text[chkPos].isspace():
|
||||
chkPos = len(last) - delete - 1
|
||||
if chkPos >= 0 and last[chkPos].isspace():
|
||||
# Strip existing space before inserting a new (#1061)
|
||||
delete += 1
|
||||
insert = self._padChar + insert
|
||||
@@ -2329,6 +2285,7 @@ class TextAutoReplace:
|
||||
insert = insert + self._padChar
|
||||
|
||||
if delete > 0:
|
||||
cursor.setPosition(aPos)
|
||||
cursor.movePosition(QtMoveLeft, QtKeepAnchor, delete)
|
||||
cursor.insertText(insert)
|
||||
return True
|
||||
@@ -2337,42 +2294,55 @@ class TextAutoReplace:
|
||||
|
||||
def _determine(self, text: str, pos: int) -> tuple[int, str]:
|
||||
"""Determine what to replace, if anything."""
|
||||
t1 = text[pos-1:pos]
|
||||
t2 = text[pos-2:pos]
|
||||
t3 = text[pos-3:pos]
|
||||
t4 = text[pos-4:pos]
|
||||
if t1 == "":
|
||||
# Return early if there is nothing to check
|
||||
return 0, ""
|
||||
t1 = text[-1:]
|
||||
t2 = text[-2:]
|
||||
t3 = text[-3:]
|
||||
t4 = text[-4:]
|
||||
|
||||
leading = t2[:1].isspace()
|
||||
if self._replaceDQuote:
|
||||
if leading and t2.endswith('"'):
|
||||
if self._replaceDQuote and t1 == '"':
|
||||
# Process Double Quote
|
||||
if pos == 1:
|
||||
return 1, self._quoteDO
|
||||
elif t1 == '"':
|
||||
if pos == 1:
|
||||
return 1, self._quoteDO
|
||||
elif pos == 2 and t2 == '>"':
|
||||
return 1, self._quoteDO
|
||||
elif pos == 3 and t3 == '>>"':
|
||||
return 1, self._quoteDO
|
||||
else:
|
||||
return 1, self._quoteDC
|
||||
elif t2[:1].isspace() and t2.endswith('"'):
|
||||
return 1, self._quoteDO
|
||||
elif pos == 2 and t2 == '>"':
|
||||
return 1, self._quoteDO
|
||||
elif pos == 3 and t3 == '>>"':
|
||||
return 1, self._quoteDO
|
||||
elif pos == 2 and t2 == '_"':
|
||||
return 1, self._quoteDO
|
||||
elif t3[:1].isspace() and t3.endswith('_"'):
|
||||
return 1, self._quoteDO
|
||||
elif pos == 3 and t3 in ('**"', '=="', '~~"'):
|
||||
return 1, self._quoteDO
|
||||
elif t4[:1].isspace() and t4.endswith(('**"', '=="', '~~"')):
|
||||
return 1, self._quoteDO
|
||||
else:
|
||||
return 1, self._quoteDC
|
||||
|
||||
if self._replaceSQuote:
|
||||
if leading and t2.endswith("'"):
|
||||
if self._replaceSQuote and t1 == "'":
|
||||
# Process Single Quote
|
||||
if pos == 1:
|
||||
return 1, self._quoteSO
|
||||
elif t1 == "'":
|
||||
if pos == 1:
|
||||
return 1, self._quoteSO
|
||||
elif pos == 2 and t2 == ">'":
|
||||
return 1, self._quoteSO
|
||||
elif pos == 3 and t3 == ">>'":
|
||||
return 1, self._quoteSO
|
||||
else:
|
||||
return 1, self._quoteSC
|
||||
elif t2[:1].isspace() and t2.endswith("'"):
|
||||
return 1, self._quoteSO
|
||||
elif pos == 2 and t2 == ">'":
|
||||
return 1, self._quoteSO
|
||||
elif pos == 3 and t3 == ">>'":
|
||||
return 1, self._quoteSO
|
||||
elif pos == 2 and t2 == "_'":
|
||||
return 1, self._quoteSO
|
||||
elif t3[:1].isspace() and t3.endswith("_'"):
|
||||
return 1, self._quoteSO
|
||||
elif pos == 3 and t3 in ("**'", "=='", "~~'"):
|
||||
return 1, self._quoteSO
|
||||
elif t4[:1].isspace() and t4.endswith(("**'", "=='", "~~'")):
|
||||
return 1, self._quoteSO
|
||||
else:
|
||||
return 1, self._quoteSC
|
||||
|
||||
if self._replaceDash:
|
||||
if self._replaceDash and t1 == "-":
|
||||
# Process Dashes
|
||||
if t4 == "----":
|
||||
return 4, "\u2015" # Horizontal bar
|
||||
elif t3 == "---":
|
||||
@@ -2385,6 +2355,7 @@ class TextAutoReplace:
|
||||
return 2, "\u2015" # Horizontal bar
|
||||
|
||||
if self._replaceDots and t3 == "...":
|
||||
# Process Dots
|
||||
return 3, "\u2026" # Ellipsis
|
||||
|
||||
if t1 == "\u2028": # Line separator
|
||||
@@ -2395,7 +2366,7 @@ class TextAutoReplace:
|
||||
|
||||
|
||||
class GuiDocToolBar(QWidget):
|
||||
"""The Formatting and Options Fold Out Menu
|
||||
"""The Formatting and Options Fold Out Menu.
|
||||
|
||||
Only used by DocEditor, and is opened by the first button in the
|
||||
header.
|
||||
@@ -2432,6 +2403,12 @@ class GuiDocToolBar(QWidget):
|
||||
qtLambda(self.requestDocAction.emit, nwDocAction.MD_STRIKE)
|
||||
)
|
||||
|
||||
self.tbMarkMD = NIconToolButton(self, iSz)
|
||||
self.tbMarkMD.setToolTip(self.tr("Markdown Highlight"))
|
||||
self.tbMarkMD.clicked.connect(
|
||||
qtLambda(self.requestDocAction.emit, nwDocAction.MD_MARK)
|
||||
)
|
||||
|
||||
self.tbBold = NIconToolButton(self, iSz)
|
||||
self.tbBold.setToolTip(self.tr("Shortcode Bold"))
|
||||
self.tbBold.clicked.connect(
|
||||
@@ -2481,6 +2458,7 @@ class GuiDocToolBar(QWidget):
|
||||
self.outerBox.addWidget(self.tbBoldMD)
|
||||
self.outerBox.addWidget(self.tbItalicMD)
|
||||
self.outerBox.addWidget(self.tbStrikeMD)
|
||||
self.outerBox.addWidget(self.tbMarkMD)
|
||||
self.outerBox.addSpacing(4)
|
||||
self.outerBox.addWidget(self.tbBold)
|
||||
self.outerBox.addWidget(self.tbItalic)
|
||||
@@ -2500,8 +2478,6 @@ class GuiDocToolBar(QWidget):
|
||||
|
||||
logger.debug("Ready: GuiDocToolBar")
|
||||
|
||||
return
|
||||
|
||||
def updateTheme(self) -> None:
|
||||
"""Initialise GUI elements that depend on specific settings."""
|
||||
syntax = SHARED.theme.syntaxTheme
|
||||
@@ -2515,6 +2491,7 @@ class GuiDocToolBar(QWidget):
|
||||
self.tbBoldMD.setThemeIcon("fmt_bold", "orange")
|
||||
self.tbItalicMD.setThemeIcon("fmt_italic", "orange")
|
||||
self.tbStrikeMD.setThemeIcon("fmt_strike", "orange")
|
||||
self.tbMarkMD.setThemeIcon("fmt_mark", "orange")
|
||||
self.tbBold.setThemeIcon("fmt_bold")
|
||||
self.tbItalic.setThemeIcon("fmt_italic")
|
||||
self.tbStrike.setThemeIcon("fmt_strike")
|
||||
@@ -2523,11 +2500,9 @@ class GuiDocToolBar(QWidget):
|
||||
self.tbSuperscript.setThemeIcon("fmt_superscript")
|
||||
self.tbSubscript.setThemeIcon("fmt_subscript")
|
||||
|
||||
return
|
||||
|
||||
|
||||
class GuiDocEditSearch(QFrame):
|
||||
"""The Embedded Document Search/Replace Feature
|
||||
"""The Embedded Document Search/Replace Feature.
|
||||
|
||||
Only used by DocEditor, and is at a fixed position in the
|
||||
QTextEdit's viewport.
|
||||
@@ -2657,8 +2632,6 @@ class GuiDocEditSearch(QFrame):
|
||||
|
||||
logger.debug("Ready: GuiDocEditSearch")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Properties
|
||||
##
|
||||
@@ -2707,14 +2680,12 @@ class GuiDocEditSearch(QFrame):
|
||||
self.searchBox.selectAll()
|
||||
if CONFIG.searchRegEx:
|
||||
self._alertSearchValid(True)
|
||||
return
|
||||
|
||||
def setReplaceText(self, text: str) -> None:
|
||||
"""Set the replace text."""
|
||||
self.showReplace.setChecked(True)
|
||||
self.replaceBox.setFocus()
|
||||
self.replaceBox.setText(text)
|
||||
return
|
||||
|
||||
def setResultCount(self, currRes: int | None, resCount: int | None) -> None:
|
||||
"""Set the count values for the current search."""
|
||||
@@ -2729,7 +2700,6 @@ class GuiDocEditSearch(QFrame):
|
||||
self.resultLabel.setMinimumWidth(minWidth)
|
||||
self.adjustSize()
|
||||
self.docEditor.updateDocMargins()
|
||||
return
|
||||
|
||||
##
|
||||
# Methods
|
||||
@@ -2745,7 +2715,6 @@ class GuiDocEditSearch(QFrame):
|
||||
self.resultLabel.setMinimumWidth(
|
||||
SHARED.theme.getTextWidth("?/?", SHARED.theme.guiFontSmall)
|
||||
)
|
||||
return
|
||||
|
||||
def updateTheme(self) -> None:
|
||||
"""Update theme elements."""
|
||||
@@ -2770,11 +2739,9 @@ class GuiDocEditSearch(QFrame):
|
||||
self.searchOpt.setStyleSheet("QToolBar {padding: 0;}")
|
||||
self.showReplace.setStyleSheet("QToolButton {border: none; background: transparent;}")
|
||||
|
||||
return
|
||||
|
||||
def cycleFocus(self) -> bool:
|
||||
"""The tab key just alternates focus between the two input
|
||||
boxes, if the replace box is visible.
|
||||
"""Cycle focus on tab key press. This just alternates focus
|
||||
between the two input boxes, if the replace box is visible.
|
||||
"""
|
||||
if self.searchBox.hasFocus():
|
||||
self.replaceBox.setFocus()
|
||||
@@ -2799,7 +2766,6 @@ class GuiDocEditSearch(QFrame):
|
||||
self.setVisible(False)
|
||||
self.docEditor.updateDocMargins()
|
||||
self.docEditor.setFocus()
|
||||
return
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
@@ -2809,13 +2775,11 @@ class GuiDocEditSearch(QFrame):
|
||||
def _doSearch(self) -> None:
|
||||
"""Call the search action function for the document editor."""
|
||||
self.docEditor.findNext(goBack=(QApplication.keyboardModifiers() == QtModShift))
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _doReplace(self) -> None:
|
||||
"""Call the replace action function for the document editor."""
|
||||
self.docEditor.replaceNext()
|
||||
return
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def _doToggleReplace(self, state: bool) -> None:
|
||||
@@ -2824,43 +2788,36 @@ class GuiDocEditSearch(QFrame):
|
||||
self.replaceButton.setVisible(state)
|
||||
self.adjustSize()
|
||||
self.docEditor.updateDocMargins()
|
||||
return
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def _doToggleCase(self, state: bool) -> None:
|
||||
"""Enable/disable case sensitive mode."""
|
||||
CONFIG.searchCase = state
|
||||
return
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def _doToggleWord(self, state: bool) -> None:
|
||||
"""Enable/disable whole word search mode."""
|
||||
CONFIG.searchWord = state
|
||||
return
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def _doToggleRegEx(self, state: bool) -> None:
|
||||
"""Enable/disable regular expression search mode."""
|
||||
CONFIG.searchRegEx = state
|
||||
return
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def _doToggleLoop(self, state: bool) -> None:
|
||||
"""Enable/disable looping the search."""
|
||||
CONFIG.searchLoop = state
|
||||
return
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def _doToggleProject(self, state: bool) -> None:
|
||||
"""Enable/disable continuing search in next project file."""
|
||||
CONFIG.searchNextFile = state
|
||||
return
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def _doToggleMatchCap(self, state: bool) -> None:
|
||||
"""Enable/disable preserving capitalisation when replacing."""
|
||||
CONFIG.searchMatchCap = state
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
@@ -2876,11 +2833,10 @@ class GuiDocEditSearch(QFrame):
|
||||
palette.text().color() if isValid else SHARED.theme.errorText
|
||||
)
|
||||
self.searchBox.setPalette(palette)
|
||||
return
|
||||
|
||||
|
||||
class GuiDocEditHeader(QWidget):
|
||||
"""The Embedded Document Header
|
||||
"""The Embedded Document Header.
|
||||
|
||||
Only used by DocEditor, and is at a fixed position in the
|
||||
QTextEdit's viewport.
|
||||
@@ -2971,8 +2927,6 @@ class GuiDocEditHeader(QWidget):
|
||||
|
||||
logger.debug("Ready: GuiDocEditHeader")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
@@ -2989,7 +2943,6 @@ class GuiDocEditHeader(QWidget):
|
||||
self.searchButton.setVisible(False)
|
||||
self.closeButton.setVisible(False)
|
||||
self.minmaxButton.setVisible(False)
|
||||
return
|
||||
|
||||
def setOutline(self, data: dict[int, str]) -> None:
|
||||
"""Set the document outline dataset."""
|
||||
@@ -3001,13 +2954,11 @@ class GuiDocEditHeader(QWidget):
|
||||
action.triggered.connect(qtLambda(self._gotoBlock, number))
|
||||
self._docOutline = data
|
||||
logger.debug("Document outline updated in %.3f ms", 1000*(time() - tStart))
|
||||
return
|
||||
|
||||
def updateFont(self) -> None:
|
||||
"""Update the font settings."""
|
||||
self.setFont(SHARED.theme.guiFont)
|
||||
self.itemTitle.setFont(SHARED.theme.guiFontSmall)
|
||||
return
|
||||
|
||||
def updateTheme(self) -> None:
|
||||
"""Update theme elements."""
|
||||
@@ -3026,8 +2977,6 @@ class GuiDocEditHeader(QWidget):
|
||||
|
||||
self.matchColors()
|
||||
|
||||
return
|
||||
|
||||
def matchColors(self) -> None:
|
||||
"""Update the colours of the widget to match those of the syntax
|
||||
theme rather than the main GUI.
|
||||
@@ -3041,12 +2990,10 @@ class GuiDocEditHeader(QWidget):
|
||||
self.itemTitle.setTextColors(
|
||||
color=palette.windowText().color(), faded=SHARED.theme.fadedText
|
||||
)
|
||||
return
|
||||
|
||||
def changeFocusState(self, state: bool) -> None:
|
||||
"""Toggle focus state."""
|
||||
self.itemTitle.setColorState(state)
|
||||
return
|
||||
|
||||
def setHandle(self, tHandle: str) -> None:
|
||||
"""Set the document title from the handle, or alternatively, set
|
||||
@@ -3067,8 +3014,6 @@ class GuiDocEditHeader(QWidget):
|
||||
self.closeButton.setVisible(True)
|
||||
self.minmaxButton.setVisible(True)
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
##
|
||||
@@ -3078,19 +3023,16 @@ class GuiDocEditHeader(QWidget):
|
||||
"""Trigger the close editor on the main window."""
|
||||
self.clearHeader()
|
||||
self.closeDocumentRequest.emit()
|
||||
return
|
||||
|
||||
@pyqtSlot(int)
|
||||
def _gotoBlock(self, blockNumber: int) -> None:
|
||||
"""Move cursor to a specific heading."""
|
||||
self.docEditor.setCursorLine(blockNumber + 1)
|
||||
return
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def _focusModeChanged(self, focusMode: bool) -> None:
|
||||
"""Update minimise/maximise icon of the Focus Mode button."""
|
||||
self.minmaxButton.setThemeIcon("minimise" if focusMode else "maximise", "blue")
|
||||
return
|
||||
|
||||
##
|
||||
# Events
|
||||
@@ -3102,11 +3044,10 @@ class GuiDocEditHeader(QWidget):
|
||||
"""
|
||||
if event.button() == QtMouseLeft:
|
||||
self.docEditor.requestProjectItemSelected.emit(self._docHandle or "", True)
|
||||
return
|
||||
|
||||
|
||||
class GuiDocEditFooter(QWidget):
|
||||
"""The Embedded Document Footer
|
||||
"""The Embedded Document Footer.
|
||||
|
||||
Only used by DocEditor, and is at a fixed position in the
|
||||
QTextEdit's viewport.
|
||||
@@ -3191,8 +3132,6 @@ class GuiDocEditFooter(QWidget):
|
||||
|
||||
logger.debug("Ready: GuiDocEditFooter")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
@@ -3202,7 +3141,6 @@ class GuiDocEditFooter(QWidget):
|
||||
self._trMainCount = trStats(nwLabels.STATS_DISPLAY[
|
||||
nwStats.CHARS if CONFIG.useCharCount else nwStats.WORDS
|
||||
])
|
||||
return
|
||||
|
||||
def updateFont(self) -> None:
|
||||
"""Update the font settings."""
|
||||
@@ -3210,7 +3148,6 @@ class GuiDocEditFooter(QWidget):
|
||||
self.statusText.setFont(SHARED.theme.guiFontSmall)
|
||||
self.linesText.setFont(SHARED.theme.guiFontSmall)
|
||||
self.wordsText.setFont(SHARED.theme.guiFontSmall)
|
||||
return
|
||||
|
||||
def updateTheme(self) -> None:
|
||||
"""Update theme elements."""
|
||||
@@ -3218,7 +3155,6 @@ class GuiDocEditFooter(QWidget):
|
||||
self.linesIcon.setPixmap(SHARED.theme.getPixmap("lines", (iPx, iPx)))
|
||||
self.wordsIcon.setPixmap(SHARED.theme.getPixmap("stats", (iPx, iPx)))
|
||||
self.matchColors()
|
||||
return
|
||||
|
||||
def matchColors(self) -> None:
|
||||
"""Update the colours of the widget to match those of the syntax
|
||||
@@ -3236,8 +3172,6 @@ class GuiDocEditFooter(QWidget):
|
||||
self.linesText.setPalette(palette)
|
||||
self.wordsText.setPalette(palette)
|
||||
|
||||
return
|
||||
|
||||
def setHandle(self, tHandle: str | None) -> None:
|
||||
"""Set the handle that will populate the footer's data."""
|
||||
self._docHandle = tHandle
|
||||
@@ -3250,8 +3184,6 @@ class GuiDocEditFooter(QWidget):
|
||||
self.updateInfo()
|
||||
self.updateMainCount(0, False)
|
||||
|
||||
return
|
||||
|
||||
def updateInfo(self) -> None:
|
||||
"""Update the content of text labels."""
|
||||
if self._tItem is None:
|
||||
@@ -3266,8 +3198,6 @@ class GuiDocEditFooter(QWidget):
|
||||
self.statusIcon.setPixmap(sIcon)
|
||||
self.statusText.setText(sText)
|
||||
|
||||
return
|
||||
|
||||
def updateLineCount(self, cursor: QTextCursor) -> None:
|
||||
"""Update the line and document position counter."""
|
||||
if document := cursor.document():
|
||||
@@ -3277,7 +3207,6 @@ class GuiDocEditFooter(QWidget):
|
||||
self.linesText.setText(
|
||||
self._trLineCount.format(f"{cLine:n}", f"{100*cPos//cCount:d} %")
|
||||
)
|
||||
return
|
||||
|
||||
def updateMainCount(self, count: int, selection: bool) -> None:
|
||||
"""Update main counter information."""
|
||||
@@ -3290,4 +3219,3 @@ class GuiDocEditFooter(QWidget):
|
||||
else:
|
||||
text = self._trMainCount.format("0", "+0")
|
||||
self.wordsText.setText(text)
|
||||
return
|
||||
|
||||
+139
-96
@@ -21,7 +21,7 @@ General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
""" # noqa
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
@@ -36,11 +36,12 @@ from PyQt6.QtGui import (
|
||||
)
|
||||
|
||||
from novelwriter import CONFIG, SHARED
|
||||
from novelwriter.common import checkInt
|
||||
from novelwriter.common import checkInt, utf16CharMap
|
||||
from novelwriter.constants import nwStyles, nwUnicode
|
||||
from novelwriter.enum import nwComment
|
||||
from novelwriter.text.comments import processComment
|
||||
from novelwriter.text.patterns import REGEX_PATTERNS, DialogParser
|
||||
from novelwriter.types import QtTextUserProperty
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -56,6 +57,7 @@ BLOCK_TITLE = 4
|
||||
|
||||
|
||||
class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
"""GUI: Editor Syntax Highlighter."""
|
||||
|
||||
__slots__ = (
|
||||
"_cmnRules", "_dialogParser", "_hStyles", "_isInactive", "_isNovel",
|
||||
@@ -84,8 +86,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
|
||||
logger.debug("Ready: GuiDocHighlighter")
|
||||
|
||||
return
|
||||
|
||||
def initHighlighter(self) -> None:
|
||||
"""Initialise the syntax highlighter, setting all the colour
|
||||
rules and building the RegExes.
|
||||
@@ -110,6 +110,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
self._addCharFormat("bold", colEmph, "b")
|
||||
self._addCharFormat("italic", colEmph, "i")
|
||||
self._addCharFormat("strike", syntax.hidden, "s")
|
||||
self._addCharFormat("mark", syntax.mark, "bg")
|
||||
self._addCharFormat("mspaces", syntax.error, "err")
|
||||
self._addCharFormat("nobreak", colBreak, "bg")
|
||||
self._addCharFormat("altdialog", syntax.dialA)
|
||||
@@ -139,7 +140,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
|
||||
# Multiple or Trailing Spaces
|
||||
if CONFIG.showMultiSpaces:
|
||||
rxRule = re.compile(r"[ ]{2,}|[ ]*$", re.UNICODE)
|
||||
rxRule = re.compile(r"\s{2,}")
|
||||
hlRule = {
|
||||
0: self._hStyles["mspaces"],
|
||||
}
|
||||
@@ -148,7 +149,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
self._cmnRules.append((rxRule, hlRule))
|
||||
|
||||
# Non-Breaking Spaces
|
||||
rxRule = re.compile(f"[{nwUnicode.U_NBSP}{nwUnicode.U_THNBSP}]+", re.UNICODE)
|
||||
rxRule = re.compile(f"[{nwUnicode.U_NBSP}{nwUnicode.U_THNBSP}]+")
|
||||
hlRule = {
|
||||
0: self._hStyles["nobreak"],
|
||||
}
|
||||
@@ -196,6 +197,17 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
self._txtRules.append((rxRule, hlRule))
|
||||
self._cmnRules.append((rxRule, hlRule))
|
||||
|
||||
# Markdown Highlight
|
||||
rxRule = REGEX_PATTERNS.markdownMark
|
||||
hlRule = {
|
||||
1: self._hStyles["markup"],
|
||||
2: self._hStyles["mark"],
|
||||
3: self._hStyles["markup"],
|
||||
}
|
||||
self._minRules.append((rxRule, hlRule))
|
||||
self._txtRules.append((rxRule, hlRule))
|
||||
self._cmnRules.append((rxRule, hlRule))
|
||||
|
||||
# Shortcodes
|
||||
rxRule = REGEX_PATTERNS.shortcodePlain
|
||||
hlRule = {
|
||||
@@ -226,7 +238,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
self._cmnRules.append((rxRule, hlRule))
|
||||
|
||||
# Alignment Tags
|
||||
rxRule = re.compile(r"(^>{1,2}|<{1,2}$)", re.UNICODE)
|
||||
rxRule = re.compile(r"(^>{1,2}|<{1,2}$)")
|
||||
hlRule = {
|
||||
1: self._hStyles["markup"],
|
||||
}
|
||||
@@ -234,7 +246,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
self._txtRules.append((rxRule, hlRule))
|
||||
|
||||
# Auto-Replace Tags
|
||||
rxRule = re.compile(r"<(\S+?)>", re.UNICODE)
|
||||
rxRule = re.compile(r"<(\S+?)>")
|
||||
hlRule = {
|
||||
0: self._hStyles["replace"],
|
||||
}
|
||||
@@ -242,8 +254,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
self._txtRules.append((rxRule, hlRule))
|
||||
self._cmnRules.append((rxRule, hlRule))
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Setters
|
||||
##
|
||||
@@ -251,7 +261,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
def setSpellCheck(self, state: bool) -> None:
|
||||
"""Enable/disable the real time spell checker."""
|
||||
self._spellCheck = state
|
||||
return
|
||||
|
||||
def setHandle(self, tHandle: str) -> None:
|
||||
"""Set the handle of the currently highlighted document."""
|
||||
@@ -262,7 +271,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
self._isNovel = item.isDocumentLayout()
|
||||
self._isInactive = item.isInactiveClass()
|
||||
logger.debug("Syntax highlighter enabled for item '%s'", tHandle)
|
||||
return
|
||||
|
||||
##
|
||||
# Methods
|
||||
@@ -280,7 +288,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
if block.userState() & cType > 0:
|
||||
self.rehighlightBlock(block)
|
||||
logger.debug("Document highlighted in %.3f ms" % (1000*(time() - tStart)))
|
||||
return
|
||||
|
||||
##
|
||||
# Highlight Block
|
||||
@@ -296,27 +303,37 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
if self._tHandle is None or not text:
|
||||
return
|
||||
|
||||
xOff = 0
|
||||
hRules = None
|
||||
blockLen = self.currentBlock().length()
|
||||
utf16Map = None
|
||||
if blockLen > len(text) + 1:
|
||||
# If the lengths are different, the line contains 4 byte
|
||||
# Unicode characters, and we must use a map between Python
|
||||
# string indices and the UTF-16 indices used by Qt, where a
|
||||
# 4 byte character occupies two slots. See #2449.
|
||||
utf16Map = utf16CharMap(text)
|
||||
|
||||
offset = 0
|
||||
rules = None
|
||||
if text.startswith("@"): # Keywords and commands
|
||||
self.setCurrentBlockState(BLOCK_META)
|
||||
index = SHARED.project.index
|
||||
isValid, bits, pos = index.scanThis(text)
|
||||
isValid, bits, loc = index.scanThis(text)
|
||||
isGood = index.checkThese(bits, self._tHandle)
|
||||
if isValid:
|
||||
for n, bit in enumerate(bits):
|
||||
xPos = pos[n]
|
||||
xLen = len(bit)
|
||||
pos = utf16Map[loc[n]] if utf16Map else loc[n]
|
||||
length = utf16Map[loc[n] + len(bit)] - pos if utf16Map else len(bit)
|
||||
if n == 0 and isGood[n]:
|
||||
self.setFormat(xPos, xLen, self._hStyles["keyword"])
|
||||
self.setFormat(pos, length, self._hStyles["keyword"])
|
||||
elif isGood[n] and not self._isInactive:
|
||||
one, two = index.parseValue(bit)
|
||||
self.setFormat(xPos, len(one), self._hStyles["tag"])
|
||||
if two:
|
||||
yPos = xPos + len(bit) - len(two)
|
||||
self.setFormat(yPos, len(two), self._hStyles["optional"])
|
||||
a, b = index.parseValue(bit)
|
||||
aLen = utf16Map[loc[n] + len(a)] - pos if utf16Map else len(a)
|
||||
self.setFormat(pos, aLen, self._hStyles["tag"])
|
||||
if b:
|
||||
bLen = utf16Map[loc[n] + len(b)] - pos if utf16Map else len(b)
|
||||
self.setFormat(pos + length - bLen, bLen, self._hStyles["optional"])
|
||||
elif not self._isInactive:
|
||||
self.setFormat(xPos, xLen, self._hStyles["invalid"])
|
||||
self.setFormat(pos, length, self._hStyles["invalid"])
|
||||
|
||||
# We never want to run the spell checker on keyword/values,
|
||||
# so we force a return here
|
||||
@@ -327,98 +344,118 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
|
||||
if text.startswith("# "): # Heading 1
|
||||
self.setFormat(0, 1, self._hStyles["head1h"])
|
||||
self.setFormat(1, len(text), self._hStyles["header1"])
|
||||
self.setFormat(1, blockLen, self._hStyles["header1"])
|
||||
|
||||
elif text.startswith("## "): # Heading 2
|
||||
self.setFormat(0, 2, self._hStyles["head2h"])
|
||||
self.setFormat(2, len(text), self._hStyles["header2"])
|
||||
self.setFormat(2, blockLen, self._hStyles["header2"])
|
||||
|
||||
elif text.startswith("### "): # Heading 3
|
||||
self.setFormat(0, 3, self._hStyles["head3h"])
|
||||
self.setFormat(3, len(text), self._hStyles["header3"])
|
||||
self.setFormat(3, blockLen, self._hStyles["header3"])
|
||||
|
||||
elif text.startswith("#### "): # Heading 4
|
||||
self.setFormat(0, 4, self._hStyles["head4h"])
|
||||
self.setFormat(4, len(text), self._hStyles["header4"])
|
||||
self.setFormat(4, blockLen, self._hStyles["header4"])
|
||||
|
||||
elif text.startswith("#! "): # Title
|
||||
self.setFormat(0, 2, self._hStyles["head1h"])
|
||||
self.setFormat(2, len(text), self._hStyles["header1"])
|
||||
self.setFormat(2, blockLen, self._hStyles["header1"])
|
||||
|
||||
elif text.startswith("##! "): # Unnumbered
|
||||
self.setFormat(0, 3, self._hStyles["head2h"])
|
||||
self.setFormat(3, len(text), self._hStyles["header2"])
|
||||
self.setFormat(3, blockLen, self._hStyles["header2"])
|
||||
|
||||
elif text.startswith("###! "): # Alternative Scene
|
||||
self.setFormat(0, 4, self._hStyles["head3h"])
|
||||
self.setFormat(4, len(text), self._hStyles["header3"])
|
||||
self.setFormat(4, blockLen, self._hStyles["header3"])
|
||||
|
||||
elif text.startswith("%"): # Comments
|
||||
self.setCurrentBlockState(BLOCK_TEXT)
|
||||
hRules = self._cmnRules
|
||||
rules = self._cmnRules
|
||||
|
||||
cStyle, cMod, _, cDot, cPos = processComment(text)
|
||||
cLen = len(text) - cPos
|
||||
xOff = cPos
|
||||
if cStyle == nwComment.PLAIN:
|
||||
self.setFormat(0, cLen, self._hStyles["hidden"])
|
||||
elif cStyle == nwComment.IGNORE:
|
||||
self.setFormat(0, cLen, self._hStyles["strike"])
|
||||
style, mod, _, dot, pos = processComment(text)
|
||||
offset = pos
|
||||
if utf16Map:
|
||||
dot = utf16Map[dot]
|
||||
pos = utf16Map[pos]
|
||||
length = blockLen - pos
|
||||
if style == nwComment.PLAIN:
|
||||
self.setFormat(0, length, self._hStyles["hidden"])
|
||||
elif style == nwComment.IGNORE:
|
||||
self.setFormat(0, length, self._hStyles["strike"])
|
||||
return # No more processing for these
|
||||
elif cMod:
|
||||
self.setFormat(0, cDot, self._hStyles["modifier"])
|
||||
self.setFormat(cDot, cPos - cDot, self._hStyles["value"])
|
||||
self.setFormat(cPos, cLen, self._hStyles["note"])
|
||||
elif mod:
|
||||
self.setFormat(0, dot, self._hStyles["modifier"])
|
||||
self.setFormat(dot, pos - dot, self._hStyles["value"])
|
||||
self.setFormat(pos, length, self._hStyles["note"])
|
||||
else:
|
||||
self.setFormat(0, cPos, self._hStyles["modifier"])
|
||||
self.setFormat(cPos, cLen, self._hStyles["note"])
|
||||
self.setFormat(0, pos, self._hStyles["modifier"])
|
||||
self.setFormat(pos, length, self._hStyles["note"])
|
||||
|
||||
elif text.startswith("["): # Special Command
|
||||
self.setCurrentBlockState(BLOCK_TEXT)
|
||||
hRules = self._txtRules if self._isNovel else self._minRules
|
||||
rules = self._txtRules if self._isNovel else self._minRules
|
||||
|
||||
sText = text.rstrip().lower()
|
||||
if sText in ("[newpage]", "[new page]", "[vspace]"):
|
||||
self.setFormat(0, len(text), self._hStyles["code"])
|
||||
check = text.rstrip().lower()
|
||||
if check in ("[newpage]", "[new page]", "[vspace]"):
|
||||
self.setFormat(0, blockLen, self._hStyles["code"])
|
||||
return
|
||||
elif sText.startswith("[vspace:") and sText.endswith("]"):
|
||||
tLen = len(sText)
|
||||
tVal = checkInt(sText[8:-1], 0)
|
||||
cVal = "value" if tVal > 0 else "invalid"
|
||||
elif check.startswith("[vspace:") and check.endswith("]"):
|
||||
value = checkInt(check[8:-1], 0)
|
||||
style = "value" if value > 0 else "invalid"
|
||||
self.setFormat(0, 8, self._hStyles["code"])
|
||||
self.setFormat(8, tLen-9, self._hStyles[cVal])
|
||||
self.setFormat(tLen-1, tLen, self._hStyles["code"])
|
||||
self.setFormat(8, blockLen-10, self._hStyles[style])
|
||||
self.setFormat(blockLen-2, blockLen, self._hStyles["code"])
|
||||
return
|
||||
|
||||
else: # Text Paragraph
|
||||
self.setCurrentBlockState(BLOCK_TEXT)
|
||||
hRules = self._txtRules if self._isNovel else self._minRules
|
||||
rules = self._txtRules if self._isNovel else self._minRules
|
||||
if self._isNovel and self._dialogParser.enabled:
|
||||
for pos, end in self._dialogParser(text):
|
||||
length = end - pos
|
||||
self.setFormat(pos, length, self._hStyles["dialog"])
|
||||
if utf16Map:
|
||||
for pos, end in self._dialogParser(text):
|
||||
pos = utf16Map[pos]
|
||||
end = utf16Map[end]
|
||||
self.setFormat(pos, end - pos, self._hStyles["dialog"])
|
||||
else:
|
||||
for pos, end in self._dialogParser(text):
|
||||
self.setFormat(pos, end - pos, self._hStyles["dialog"])
|
||||
|
||||
if hRules:
|
||||
for rX, hRule in hRules:
|
||||
for res in re.finditer(rX, text[xOff:]):
|
||||
for xM, hFmt in hRule.items():
|
||||
xPos = res.start(xM) + xOff
|
||||
xEnd = res.end(xM) + xOff
|
||||
for x in range(xPos, xEnd):
|
||||
cFmt = self.format(x)
|
||||
if cFmt.fontStyleName() != "markup":
|
||||
cFmt.merge(hFmt)
|
||||
self.setFormat(x, 1, cFmt)
|
||||
if rules:
|
||||
if utf16Map:
|
||||
for rX, hRule in rules:
|
||||
for res in re.finditer(rX, text[offset:]):
|
||||
for x, hFmt in hRule.items():
|
||||
pos = res.start(x) + offset
|
||||
end = res.end(x) + offset
|
||||
for x in range(pos, end):
|
||||
m = utf16Map[x]
|
||||
cFmt = self.format(m)
|
||||
if not cFmt.property(QtTextUserProperty):
|
||||
cFmt.merge(hFmt)
|
||||
self.setFormat(m, utf16Map[x+1] - m, cFmt)
|
||||
else:
|
||||
for rX, hRule in rules:
|
||||
for res in re.finditer(rX, text[offset:]):
|
||||
for x, hFmt in hRule.items():
|
||||
pos = res.start(x) + offset
|
||||
end = res.end(x) + offset
|
||||
for x in range(pos, end):
|
||||
cFmt = self.format(x)
|
||||
if not cFmt.property(QtTextUserProperty):
|
||||
cFmt.merge(hFmt)
|
||||
self.setFormat(x, 1, cFmt)
|
||||
|
||||
data = self.currentBlockUserData()
|
||||
if not isinstance(data, TextBlockData):
|
||||
data = TextBlockData()
|
||||
self.setCurrentBlockUserData(data)
|
||||
|
||||
data.processText(text, xOff)
|
||||
data.processText(text, offset)
|
||||
if self._spellCheck:
|
||||
for xPos, xEnd in data.spellCheck():
|
||||
for x in range(xPos, xEnd):
|
||||
for pos, end, _ in data.spellCheck(utf16Map):
|
||||
for x in range(pos, end):
|
||||
cFmt = self.format(x)
|
||||
cFmt.merge(self._spellErr)
|
||||
self.setFormat(x, 1, cFmt)
|
||||
@@ -435,10 +472,8 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
) -> None:
|
||||
"""Generate a highlighter character format."""
|
||||
charFormat = QTextCharFormat()
|
||||
charFormat.setFontStyleName(name)
|
||||
|
||||
if color:
|
||||
charFormat.setForeground(color)
|
||||
blockMerge = name == "markup"
|
||||
charFormat.setProperty(QtTextUserProperty, blockMerge)
|
||||
|
||||
if style:
|
||||
styles = style.split(",")
|
||||
@@ -455,16 +490,23 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
charFormat.setUnderlineStyle(QTextCharFormat.UnderlineStyle.SpellCheckUnderline)
|
||||
if "bg" in styles and color is not None:
|
||||
charFormat.setBackground(QBrush(color, Qt.BrushStyle.SolidPattern))
|
||||
color = None
|
||||
|
||||
if color:
|
||||
charFormat.setForeground(color)
|
||||
|
||||
if size:
|
||||
charFormat.setFontPointSize(round(size*CONFIG.textFont.pointSize()))
|
||||
|
||||
self._hStyles[name] = charFormat
|
||||
|
||||
return
|
||||
|
||||
|
||||
class TextBlockData(QTextBlockUserData):
|
||||
"""Custom QTextBlock Data.
|
||||
|
||||
Custom data stored in a single text block. The spell check state is
|
||||
cached here and used when correcting misspelled text.
|
||||
"""
|
||||
|
||||
__slots__ = ("_metaData", "_offset", "_spellErrors", "_text")
|
||||
|
||||
@@ -473,8 +515,7 @@ class TextBlockData(QTextBlockUserData):
|
||||
self._text = ""
|
||||
self._offset = 0
|
||||
self._metaData: list[tuple[int, int, str, str]] = []
|
||||
self._spellErrors: list[tuple[int, int]] = []
|
||||
return
|
||||
self._spellErrors: list[tuple[int, int, str]] = []
|
||||
|
||||
@property
|
||||
def metaData(self) -> list[tuple[int, int, str, str]]:
|
||||
@@ -482,7 +523,7 @@ class TextBlockData(QTextBlockUserData):
|
||||
return self._metaData
|
||||
|
||||
@property
|
||||
def spellErrors(self) -> list[tuple[int, int]]:
|
||||
def spellErrors(self) -> list[tuple[int, int, str]]:
|
||||
"""Return spell error data from last check."""
|
||||
return self._spellErrors
|
||||
|
||||
@@ -505,22 +546,24 @@ class TextBlockData(QTextBlockUserData):
|
||||
text = f"{text[:s]}{pad}{text[e:]}"
|
||||
self._metaData.append((s, e, res.group(0), "url"))
|
||||
|
||||
self._text = text.replace("\u02bc", "'")
|
||||
self._text = text.replace("\u02bc", "'").replace("_", " ")
|
||||
self._offset = offset
|
||||
|
||||
return
|
||||
|
||||
def spellCheck(self) -> list[tuple[int, int]]:
|
||||
def spellCheck(self, utf16Map: list[int] | None) -> list[tuple[int, int, str]]:
|
||||
"""Run the spell checker and cache the result, and return the
|
||||
list of spell check errors.
|
||||
"""
|
||||
self._spellErrors = []
|
||||
checker = SHARED.spelling
|
||||
for res in RX_WORDS.finditer(self._text.replace("_", " "), self._offset):
|
||||
if (
|
||||
(word := res.group(0))
|
||||
and not (word.isnumeric() or word.isupper() or checker.checkWord(word))
|
||||
):
|
||||
self._spellErrors.append((res.start(0), res.end(0)))
|
||||
|
||||
spell = SHARED.spelling
|
||||
if utf16Map:
|
||||
self._spellErrors = [
|
||||
(utf16Map[r.start(0)], utf16Map[r.end(0)], w)
|
||||
for r in RX_WORDS.finditer(self._text, self._offset)
|
||||
if (w := r.group(0)) and not (w.isnumeric() or w.isupper() or spell.checkWord(w))
|
||||
]
|
||||
else:
|
||||
self._spellErrors = [
|
||||
(r.start(0), r.end(0), w)
|
||||
for r in RX_WORDS.finditer(self._text, self._offset)
|
||||
if (w := r.group(0)) and not (w.isnumeric() or w.isupper() or spell.checkWord(w))
|
||||
]
|
||||
return self._spellErrors
|
||||
|
||||
@@ -23,7 +23,7 @@ General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
""" # noqa
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
@@ -60,6 +60,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiDocViewer(QTextBrowser):
|
||||
"""GUI: Document Viewer."""
|
||||
|
||||
closeDocumentRequest = pyqtSignal()
|
||||
documentLoaded = pyqtSignal(str)
|
||||
@@ -110,8 +111,6 @@ class GuiDocViewer(QTextBrowser):
|
||||
|
||||
logger.debug("Ready: GuiDocViewer")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Properties
|
||||
##
|
||||
@@ -138,13 +137,11 @@ class GuiDocViewer(QTextBrowser):
|
||||
self.setSearchPaths([""])
|
||||
self._docHandle = None
|
||||
self.docHeader.clearHeader()
|
||||
return
|
||||
|
||||
def updateTheme(self) -> None:
|
||||
"""Update theme elements."""
|
||||
self.docHeader.updateTheme()
|
||||
self.docFooter.updateTheme()
|
||||
return
|
||||
|
||||
def initViewer(self) -> None:
|
||||
"""Set editor settings from main config."""
|
||||
@@ -206,8 +203,6 @@ class GuiDocViewer(QTextBrowser):
|
||||
# If we have a document open, we should reload it in case the font changed
|
||||
self.reloadText()
|
||||
|
||||
return
|
||||
|
||||
def loadText(self, tHandle: str, updateHistory: bool = True) -> bool:
|
||||
"""Load text into the viewer from an item handle."""
|
||||
if not SHARED.project.tree.checkType(tHandle, nwItemType.FILE):
|
||||
@@ -228,11 +223,11 @@ class GuiDocViewer(QTextBrowser):
|
||||
qDoc.setTheme(self._docTheme)
|
||||
qDoc.initDocument()
|
||||
qDoc.setKeywords(True)
|
||||
qDoc.setCommentType(nwComment.NOTE, CONFIG.viewComments)
|
||||
qDoc.setCommentType(nwComment.STORY, CONFIG.viewComments)
|
||||
qDoc.setCommentType(nwComment.PLAIN, CONFIG.viewComments)
|
||||
qDoc.setCommentType(nwComment.SYNOPSIS, CONFIG.viewSynopsis)
|
||||
qDoc.setCommentType(nwComment.SHORT, CONFIG.viewSynopsis)
|
||||
qDoc.setCommentType(nwComment.STORY, CONFIG.viewNotes)
|
||||
qDoc.setCommentType(nwComment.NOTE, CONFIG.viewNotes)
|
||||
|
||||
# Be extra careful here to prevent crashes when first opening a
|
||||
# project as a crash here leaves no way of recovering.
|
||||
@@ -280,7 +275,6 @@ class GuiDocViewer(QTextBrowser):
|
||||
"""Reload the text in the current document."""
|
||||
if self._docHandle:
|
||||
self.loadText(self._docHandle, updateHistory=False)
|
||||
return
|
||||
|
||||
def docAction(self, action: nwDocAction) -> bool:
|
||||
"""Process document actions on the current document."""
|
||||
@@ -308,7 +302,6 @@ class GuiDocViewer(QTextBrowser):
|
||||
def clearNavHistory(self) -> None:
|
||||
"""Clear the navigation history."""
|
||||
self.docHistory.clear()
|
||||
return
|
||||
|
||||
def updateDocMargins(self) -> None:
|
||||
"""Automatically adjust the margins so the text is centred."""
|
||||
@@ -337,8 +330,6 @@ class GuiDocViewer(QTextBrowser):
|
||||
self.docFooter.setGeometry(tB, fY, tW, fH)
|
||||
self.setViewportMargins(tM, max(cM, tH), tM, max(cM, fH))
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Setters
|
||||
##
|
||||
@@ -347,7 +338,6 @@ class GuiDocViewer(QTextBrowser):
|
||||
"""Set the scrollbar position."""
|
||||
if (vBar := self.verticalScrollBar()) and vBar.isVisible():
|
||||
vBar.setValue(pos)
|
||||
return
|
||||
|
||||
##
|
||||
# Public Slots
|
||||
@@ -359,7 +349,6 @@ class GuiDocViewer(QTextBrowser):
|
||||
if tHandle == self._docHandle and change == nwChange.UPDATE:
|
||||
self.docHeader.setHandle(tHandle)
|
||||
self.updateDocMargins()
|
||||
return
|
||||
|
||||
@pyqtSlot(str)
|
||||
def navigateTo(self, anchor: str) -> None:
|
||||
@@ -367,7 +356,6 @@ class GuiDocViewer(QTextBrowser):
|
||||
if isinstance(anchor, str) and anchor.startswith("#"):
|
||||
logger.debug("Moving to anchor '%s'", anchor)
|
||||
self.setSource(QUrl(anchor))
|
||||
return
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
@@ -377,13 +365,11 @@ class GuiDocViewer(QTextBrowser):
|
||||
def navBackward(self) -> None:
|
||||
"""Navigate backwards in the document view history."""
|
||||
self.docHistory.backward()
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def navForward(self) -> None:
|
||||
"""Navigate forwards in the document view history."""
|
||||
self.docHistory.forward()
|
||||
return
|
||||
|
||||
@pyqtSlot("QUrl")
|
||||
def _linkClicked(self, url: QUrl) -> None:
|
||||
@@ -396,7 +382,6 @@ class GuiDocViewer(QTextBrowser):
|
||||
self.navigateTo(link)
|
||||
elif link.startswith("http"):
|
||||
QDesktopServices.openUrl(QUrl(url))
|
||||
return
|
||||
|
||||
@pyqtSlot("QPoint")
|
||||
def _openContextMenu(self, point: QPoint) -> None:
|
||||
@@ -430,8 +415,6 @@ class GuiDocViewer(QTextBrowser):
|
||||
|
||||
ctxMenu.setParent(None)
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Events
|
||||
##
|
||||
@@ -440,7 +423,6 @@ class GuiDocViewer(QTextBrowser):
|
||||
"""Update document margins when widget is resized."""
|
||||
self.updateDocMargins()
|
||||
super().resizeEvent(event)
|
||||
return
|
||||
|
||||
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
|
||||
"""Capture mouse click events on the document."""
|
||||
@@ -450,7 +432,6 @@ class GuiDocViewer(QTextBrowser):
|
||||
self.navForward()
|
||||
else:
|
||||
super().mouseReleaseEvent(event)
|
||||
return
|
||||
|
||||
def dragEnterEvent(self, event: QDragEnterEvent) -> None:
|
||||
"""Overload drag enter event to handle dragged items."""
|
||||
@@ -458,7 +439,6 @@ class GuiDocViewer(QTextBrowser):
|
||||
event.acceptProposedAction()
|
||||
else:
|
||||
super().dragEnterEvent(event)
|
||||
return
|
||||
|
||||
def dragMoveEvent(self, event: QDragMoveEvent) -> None:
|
||||
"""Overload drag move event to handle dragged items."""
|
||||
@@ -466,7 +446,6 @@ class GuiDocViewer(QTextBrowser):
|
||||
event.acceptProposedAction()
|
||||
else:
|
||||
super().dragMoveEvent(event)
|
||||
return
|
||||
|
||||
def dropEvent(self, event: QDropEvent) -> None:
|
||||
"""Overload drop event to handle dragged items."""
|
||||
@@ -476,7 +455,6 @@ class GuiDocViewer(QTextBrowser):
|
||||
self.openDocumentRequest.emit(handles[0], nwDocMode.VIEW, "", True)
|
||||
else:
|
||||
super().dropEvent(event)
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
@@ -500,16 +478,18 @@ class GuiDocViewer(QTextBrowser):
|
||||
|
||||
self.setTextCursor(cursor)
|
||||
|
||||
return
|
||||
|
||||
def _makePosSelection(self, selType: QTextCursor.SelectionType, pos: QPoint) -> None:
|
||||
"""Handle text selection at a given location."""
|
||||
self.setTextCursor(self.cursorForPosition(pos))
|
||||
self._makeSelection(selType)
|
||||
return
|
||||
|
||||
|
||||
class GuiDocViewHistory:
|
||||
"""GUI: Document Viewer History.
|
||||
|
||||
This class holds the navigation history for the viewer panel, which
|
||||
is used for backward/forward navigation.
|
||||
"""
|
||||
|
||||
def __init__(self, docViewer: GuiDocViewer) -> None:
|
||||
self.docViewer = docViewer
|
||||
@@ -517,7 +497,6 @@ class GuiDocViewHistory:
|
||||
self._posHistory = []
|
||||
self._currPos = -1
|
||||
self._prevPos = -1
|
||||
return
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear the view history."""
|
||||
@@ -526,7 +505,6 @@ class GuiDocViewHistory:
|
||||
self._posHistory = []
|
||||
self._currPos = -1
|
||||
self._prevPos = -1
|
||||
return
|
||||
|
||||
def append(self, tHandle: str) -> bool:
|
||||
"""Append a document handle and its scroll bar position to the
|
||||
@@ -566,7 +544,6 @@ class GuiDocViewHistory:
|
||||
self._currPos = newPos
|
||||
self._updateNavButtons()
|
||||
self._dumpHistory()
|
||||
return
|
||||
|
||||
def backward(self) -> None:
|
||||
"""Navigate to the previous entry in the view history."""
|
||||
@@ -580,7 +557,6 @@ class GuiDocViewHistory:
|
||||
self._currPos = newPos
|
||||
self._updateNavButtons()
|
||||
self._dumpHistory()
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
@@ -590,12 +566,10 @@ class GuiDocViewHistory:
|
||||
"""Update the scrollbar position of the previous entry."""
|
||||
if self._prevPos >= 0 and self._prevPos < len(self._posHistory):
|
||||
self._posHistory[self._prevPos] = self.docViewer.scrollPosition
|
||||
return
|
||||
|
||||
def _updateNavButtons(self) -> None:
|
||||
"""Update the navigation buttons in the document header."""
|
||||
self.docViewer.docHeader.updateNavButtons(0, len(self._navHistory) - 1, self._currPos)
|
||||
return
|
||||
|
||||
def _truncateHistory(self, atPos: int) -> None:
|
||||
"""Truncate the navigation history to the given position. Also
|
||||
@@ -606,7 +580,6 @@ class GuiDocViewHistory:
|
||||
self._posHistory = self._posHistory[nSkip:atPos + 1]
|
||||
self._currPos -= nSkip
|
||||
self._prevPos -= nSkip
|
||||
return
|
||||
|
||||
def _dumpHistory(self) -> None:
|
||||
"""Debug function to dump history to the logger. Since it is a
|
||||
@@ -616,11 +589,10 @@ class GuiDocViewHistory:
|
||||
for i, (h, p) in enumerate(zip(self._navHistory, self._posHistory, strict=False)):
|
||||
a = ">" if i == self._currPos else " "
|
||||
logger.debug(f"History {i + 1:02d}: {a} {h:13s} [x:{p}]")
|
||||
return
|
||||
|
||||
|
||||
class GuiDocViewHeader(QWidget):
|
||||
"""The Embedded Document Header
|
||||
"""The Embedded Document Header.
|
||||
|
||||
Only used by DocViewer, and is at a fixed position in the
|
||||
QTextBrowser's viewport.
|
||||
@@ -711,8 +683,6 @@ class GuiDocViewHeader(QWidget):
|
||||
|
||||
logger.debug("Ready: GuiDocViewHeader")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
@@ -730,7 +700,6 @@ class GuiDocViewHeader(QWidget):
|
||||
self.editButton.setVisible(False)
|
||||
self.refreshButton.setVisible(False)
|
||||
self.closeButton.setVisible(False)
|
||||
return
|
||||
|
||||
def setOutline(self, data: dict[str, tuple[str, int]]) -> None:
|
||||
"""Set the document outline dataset."""
|
||||
@@ -750,13 +719,11 @@ class GuiDocViewHeader(QWidget):
|
||||
lambda _, title=title: self.docViewer.navigateTo(f"#{tHandle}:{title}")
|
||||
)
|
||||
self._docOutline = data
|
||||
return
|
||||
|
||||
def updateFont(self) -> None:
|
||||
"""Update the font settings."""
|
||||
self.setFont(SHARED.theme.guiFont)
|
||||
self.itemTitle.setFont(SHARED.theme.guiFontSmall)
|
||||
return
|
||||
|
||||
def updateTheme(self) -> None:
|
||||
"""Update theme elements."""
|
||||
@@ -777,8 +744,6 @@ class GuiDocViewHeader(QWidget):
|
||||
|
||||
self.matchColors()
|
||||
|
||||
return
|
||||
|
||||
def matchColors(self) -> None:
|
||||
"""Update the colours of the widget to match those of the syntax
|
||||
theme rather than the main GUI.
|
||||
@@ -792,15 +757,13 @@ class GuiDocViewHeader(QWidget):
|
||||
self.itemTitle.setTextColors(
|
||||
color=palette.windowText().color(), faded=SHARED.theme.fadedText
|
||||
)
|
||||
return
|
||||
|
||||
def changeFocusState(self, state: bool) -> None:
|
||||
"""Toggle focus state."""
|
||||
self.itemTitle.setColorState(state)
|
||||
return
|
||||
|
||||
def setHandle(self, tHandle: str) -> None:
|
||||
"""Sets the document title from the handle, or alternatively,
|
||||
"""Set the document title from the handle, or alternatively,
|
||||
set the whole document path.
|
||||
"""
|
||||
self._docHandle = tHandle
|
||||
@@ -819,13 +782,10 @@ class GuiDocViewHeader(QWidget):
|
||||
self.refreshButton.setVisible(True)
|
||||
self.closeButton.setVisible(True)
|
||||
|
||||
return
|
||||
|
||||
def updateNavButtons(self, firstIdx: int, lastIdx: int, currIdx: int) -> None:
|
||||
"""Enable and disable nav buttons based on index in history."""
|
||||
self.backButton.setEnabled(currIdx > firstIdx)
|
||||
self.forwardButton.setEnabled(currIdx < lastIdx)
|
||||
return
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
@@ -836,20 +796,17 @@ class GuiDocViewHeader(QWidget):
|
||||
"""Trigger the close editor/viewer on the main window."""
|
||||
self.clearHeader()
|
||||
self.docViewer.closeDocumentRequest.emit()
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _refreshDocument(self) -> None:
|
||||
"""Reload the content of the document."""
|
||||
self.docViewer.reloadDocumentRequest.emit()
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _editDocument(self) -> None:
|
||||
"""Open the document in the editor."""
|
||||
if tHandle := self._docHandle:
|
||||
self.docViewer.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True)
|
||||
return
|
||||
|
||||
##
|
||||
# Events
|
||||
@@ -861,11 +818,10 @@ class GuiDocViewHeader(QWidget):
|
||||
"""
|
||||
if event.button() == QtMouseLeft:
|
||||
self.docViewer.requestProjectItemSelected.emit(self._docHandle, True)
|
||||
return
|
||||
|
||||
|
||||
class GuiDocViewFooter(QWidget):
|
||||
"""The Embedded Document Footer
|
||||
"""The Embedded Document Footer.
|
||||
|
||||
Only used by DocViewer, and is at a fixed position in the
|
||||
QTextBrowser's viewport.
|
||||
@@ -913,12 +869,23 @@ class GuiDocViewFooter(QWidget):
|
||||
self.showSynopsis.toggled.connect(self._doToggleSynopsis)
|
||||
self.showSynopsis.setToolTip(self.tr("Show Synopsis Comments"))
|
||||
|
||||
# Show Notes
|
||||
self.showNotes = QToolButton(self)
|
||||
self.showNotes.setText(self.tr("Notes"))
|
||||
self.showNotes.setCheckable(True)
|
||||
self.showNotes.setChecked(CONFIG.viewNotes)
|
||||
self.showNotes.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
|
||||
self.showNotes.setIconSize(iSz)
|
||||
self.showNotes.toggled.connect(self._doToggleNotes)
|
||||
self.showNotes.setToolTip(self.tr("Show Notes"))
|
||||
|
||||
# Assemble Layout
|
||||
self.outerBox = QHBoxLayout()
|
||||
self.outerBox.addWidget(self.showHide, 0)
|
||||
self.outerBox.addStretch(1)
|
||||
self.outerBox.addWidget(self.showComments, 0)
|
||||
self.outerBox.addWidget(self.showSynopsis, 0)
|
||||
self.outerBox.addWidget(self.showNotes, 0)
|
||||
self.outerBox.setSpacing(4)
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
@@ -933,8 +900,6 @@ class GuiDocViewFooter(QWidget):
|
||||
|
||||
logger.debug("Ready: GuiDocViewFooter")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
@@ -944,7 +909,7 @@ class GuiDocViewFooter(QWidget):
|
||||
self.setFont(SHARED.theme.guiFont)
|
||||
self.showComments.setFont(SHARED.theme.guiFontSmall)
|
||||
self.showSynopsis.setFont(SHARED.theme.guiFontSmall)
|
||||
return
|
||||
self.showNotes.setFont(SHARED.theme.guiFontSmall)
|
||||
|
||||
def updateTheme(self) -> None:
|
||||
"""Update theme elements."""
|
||||
@@ -955,16 +920,16 @@ class GuiDocViewFooter(QWidget):
|
||||
self.showHide.setThemeIcon("panel")
|
||||
self.showComments.setIcon(bulletIcon)
|
||||
self.showSynopsis.setIcon(bulletIcon)
|
||||
self.showNotes.setIcon(bulletIcon)
|
||||
|
||||
buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
|
||||
self.showHide.setStyleSheet(buttonStyle)
|
||||
self.showComments.setStyleSheet(buttonStyle)
|
||||
self.showSynopsis.setStyleSheet(buttonStyle)
|
||||
self.showNotes.setStyleSheet(buttonStyle)
|
||||
|
||||
self.matchColors()
|
||||
|
||||
return
|
||||
|
||||
def matchColors(self) -> None:
|
||||
"""Update the colours of the widget to match those of the syntax
|
||||
theme rather than the main GUI.
|
||||
@@ -975,7 +940,6 @@ class GuiDocViewFooter(QWidget):
|
||||
palette.setColor(QPalette.ColorRole.WindowText, syntax.text)
|
||||
palette.setColor(QPalette.ColorRole.Text, syntax.text)
|
||||
self.setPalette(palette)
|
||||
return
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
@@ -986,11 +950,15 @@ class GuiDocViewFooter(QWidget):
|
||||
"""Toggle the view comment button and reload the document."""
|
||||
CONFIG.viewComments = state
|
||||
self.docViewer.reloadText()
|
||||
return
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def _doToggleSynopsis(self, state: bool) -> None:
|
||||
"""Toggle the view synopsis button and reload the document."""
|
||||
CONFIG.viewSynopsis = state
|
||||
self.docViewer.reloadText()
|
||||
return
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def _doToggleNotes(self, state: bool) -> None:
|
||||
"""Toggle the view notes button and reload the document."""
|
||||
CONFIG.viewNotes = state
|
||||
self.docViewer.reloadText()
|
||||
|
||||
@@ -20,7 +20,7 @@ General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
""" # noqa
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
@@ -49,6 +49,10 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiDocViewerPanel(QWidget):
|
||||
"""GUI: Document Viewer Panel.
|
||||
|
||||
The panel of project meta data below the viewer.
|
||||
"""
|
||||
|
||||
openDocumentRequest = pyqtSignal(str, Enum, str, bool)
|
||||
loadDocumentTagRequest = pyqtSignal(str, Enum)
|
||||
@@ -96,8 +100,6 @@ class GuiDocViewerPanel(QWidget):
|
||||
|
||||
logger.debug("Ready: GuiDocViewerPanel")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
@@ -113,7 +115,6 @@ class GuiDocViewerPanel(QWidget):
|
||||
for tab in self.kwTabs.values():
|
||||
tab.updateTheme()
|
||||
self._loadAllTags()
|
||||
return
|
||||
|
||||
def openProjectTasks(self) -> None:
|
||||
"""Run open project tasks."""
|
||||
@@ -124,7 +125,6 @@ class GuiDocViewerPanel(QWidget):
|
||||
for key, value in colWidths.items():
|
||||
if key in self.kwTabs and isinstance(value, list):
|
||||
self.kwTabs[key].setColumnWidths(value)
|
||||
return
|
||||
|
||||
def closeProjectTasks(self) -> None:
|
||||
"""Run close project tasks."""
|
||||
@@ -133,7 +133,6 @@ class GuiDocViewerPanel(QWidget):
|
||||
hideInactive = self.aInactive.isChecked()
|
||||
SHARED.project.options.setValue("GuiDocViewerPanel", "colWidths", colWidths)
|
||||
SHARED.project.options.setValue("GuiDocViewerPanel", "hideInactive", hideInactive)
|
||||
return
|
||||
|
||||
##
|
||||
# Public Slots
|
||||
@@ -145,7 +144,6 @@ class GuiDocViewerPanel(QWidget):
|
||||
self.tabBackRefs.clearContent()
|
||||
for cTab in self.kwTabs.values():
|
||||
cTab.clearContent()
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def indexHasAppeared(self) -> None:
|
||||
@@ -153,7 +151,6 @@ class GuiDocViewerPanel(QWidget):
|
||||
self._loadAllTags()
|
||||
self._updateTabVisibility()
|
||||
self.updateHandle(self._lastHandle)
|
||||
return
|
||||
|
||||
@pyqtSlot(str, Enum)
|
||||
def onProjectItemChanged(self, tHandle: str, change: nwChange) -> None:
|
||||
@@ -168,14 +165,12 @@ class GuiDocViewerPanel(QWidget):
|
||||
else:
|
||||
self.kwTabs[tClass].removeEntry(key)
|
||||
self._updateTabVisibility()
|
||||
return
|
||||
|
||||
@pyqtSlot(str)
|
||||
def updateHandle(self, tHandle: str | None) -> None:
|
||||
"""Update the document handle."""
|
||||
self._lastHandle = tHandle
|
||||
self.tabBackRefs.refreshContent(tHandle or None)
|
||||
return
|
||||
|
||||
@pyqtSlot(list, list)
|
||||
def updateChangedTags(self, updated: list[str], deleted: list[str]) -> None:
|
||||
@@ -191,14 +186,12 @@ class GuiDocViewerPanel(QWidget):
|
||||
else:
|
||||
logger.warning("Could not remove tag '%s' from view panel", key)
|
||||
self._updateTabVisibility()
|
||||
return
|
||||
|
||||
@pyqtSlot(str)
|
||||
def updateStatusLabels(self, kind: str) -> None:
|
||||
"""Update the importance labels."""
|
||||
if kind == "i":
|
||||
self._loadAllTags()
|
||||
return
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
@@ -212,7 +205,6 @@ class GuiDocViewerPanel(QWidget):
|
||||
cTab.clearContent()
|
||||
self._loadAllTags()
|
||||
self._updateTabVisibility()
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
@@ -222,7 +214,6 @@ class GuiDocViewerPanel(QWidget):
|
||||
"""Hide class tabs with no content."""
|
||||
for tClass, cTab in self.kwTabs.items():
|
||||
self.mainTabs.setTabVisible(self.idTabs[tClass], cTab.countEntries() > 0)
|
||||
return
|
||||
|
||||
def _loadAllTags(self) -> None:
|
||||
"""Load all tags into the tabs."""
|
||||
@@ -230,7 +221,6 @@ class GuiDocViewerPanel(QWidget):
|
||||
for key, name, tClass, iItem, hItem in data:
|
||||
if tClass in self.kwTabs and iItem and hItem:
|
||||
self.kwTabs[tClass].addUpdateEntry(key, name, iItem, hItem)
|
||||
return
|
||||
|
||||
|
||||
class _ViewPanelBackRefs(QTreeWidget):
|
||||
@@ -278,8 +268,6 @@ class _ViewPanelBackRefs(QTreeWidget):
|
||||
self.clicked.connect(self._treeItemClicked)
|
||||
self.doubleClicked.connect(self._treeItemDoubleClicked)
|
||||
|
||||
return
|
||||
|
||||
def updateTheme(self) -> None:
|
||||
"""Update theme elements."""
|
||||
self._editIcon = SHARED.theme.getIcon("edit", "green")
|
||||
@@ -288,13 +276,11 @@ class _ViewPanelBackRefs(QTreeWidget):
|
||||
if item := self.topLevelItem(i):
|
||||
item.setIcon(self.C_EDIT, self._editIcon)
|
||||
item.setIcon(self.C_VIEW, self._viewIcon)
|
||||
return
|
||||
|
||||
def clearContent(self) -> None:
|
||||
"""Clear the widget."""
|
||||
self.clear()
|
||||
self._treeMap = {}
|
||||
return
|
||||
|
||||
def refreshContent(self, dHandle: str | None) -> None:
|
||||
"""Update the content."""
|
||||
@@ -303,7 +289,6 @@ class _ViewPanelBackRefs(QTreeWidget):
|
||||
refs = SHARED.project.index.getBackReferenceList(dHandle)
|
||||
for tHandle, (sTitle, hItem) in refs.items():
|
||||
self._setTreeItemValues(tHandle, sTitle, hItem)
|
||||
return
|
||||
|
||||
def refreshDocument(self, tHandle: str) -> None:
|
||||
"""Refresh document meta data."""
|
||||
@@ -311,7 +296,6 @@ class _ViewPanelBackRefs(QTreeWidget):
|
||||
for sTitle, hItem in iItem.items():
|
||||
if f"{tHandle}:{sTitle}" in self._treeMap:
|
||||
self._setTreeItemValues(tHandle, sTitle, hItem)
|
||||
return
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
@@ -325,7 +309,6 @@ class _ViewPanelBackRefs(QTreeWidget):
|
||||
self._parent.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True)
|
||||
elif index.column() == self.C_VIEW:
|
||||
self._parent.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", True)
|
||||
return
|
||||
|
||||
@pyqtSlot("QModelIndex")
|
||||
def _treeItemDoubleClicked(self, index: QModelIndex) -> None:
|
||||
@@ -333,7 +316,6 @@ class _ViewPanelBackRefs(QTreeWidget):
|
||||
tHandle = index.siblingAtColumn(self.C_DATA).data(self.D_HANDLE)
|
||||
if index.column() not in (self.C_EDIT, self.C_VIEW):
|
||||
self._parent.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", True)
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
@@ -362,8 +344,6 @@ class _ViewPanelBackRefs(QTreeWidget):
|
||||
self.addTopLevelItem(trItem)
|
||||
self._treeMap[tKey] = trItem
|
||||
|
||||
return
|
||||
|
||||
|
||||
class _ViewPanelKeyWords(QTreeWidget):
|
||||
|
||||
@@ -418,14 +398,11 @@ class _ViewPanelKeyWords(QTreeWidget):
|
||||
self.clicked.connect(self._treeItemClicked)
|
||||
self.doubleClicked.connect(self._treeItemDoubleClicked)
|
||||
|
||||
return
|
||||
|
||||
def updateTheme(self) -> None:
|
||||
"""Update theme elements."""
|
||||
self._classIcon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[self._class], "root")
|
||||
self._editIcon = SHARED.theme.getIcon("edit", "green")
|
||||
self._viewIcon = SHARED.theme.getIcon("view", "blue")
|
||||
return
|
||||
|
||||
def countEntries(self) -> int:
|
||||
"""Return the number of items in the list."""
|
||||
@@ -435,7 +412,6 @@ class _ViewPanelKeyWords(QTreeWidget):
|
||||
"""Clear the list."""
|
||||
self._treeMap = {}
|
||||
self.clear()
|
||||
return
|
||||
|
||||
def addUpdateEntry(self, tag: str, name: str, iItem: IndexNode, hItem: IndexHeading) -> None:
|
||||
"""Add a new entry, or update an existing one."""
|
||||
@@ -470,8 +446,6 @@ class _ViewPanelKeyWords(QTreeWidget):
|
||||
self.addTopLevelItem(trItem)
|
||||
self._treeMap[tag] = trItem
|
||||
|
||||
return
|
||||
|
||||
def removeEntry(self, tag: str) -> bool:
|
||||
"""Remove a tag from the list."""
|
||||
if tag in self._treeMap:
|
||||
@@ -487,7 +461,6 @@ class _ViewPanelKeyWords(QTreeWidget):
|
||||
self.setColumnWidth(self.C_IMPORT, checkInt(widths[1], 100))
|
||||
self.setColumnWidth(self.C_DOC, checkInt(widths[2], 100))
|
||||
self.setColumnWidth(self.C_TITLE, checkInt(widths[3], 100))
|
||||
return
|
||||
|
||||
def getColumnWidths(self) -> list[int]:
|
||||
"""Get the widths of the user-adjustable columns."""
|
||||
@@ -510,7 +483,6 @@ class _ViewPanelKeyWords(QTreeWidget):
|
||||
self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.EDIT)
|
||||
elif index.column() == self.C_VIEW:
|
||||
self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW)
|
||||
return
|
||||
|
||||
@pyqtSlot("QModelIndex")
|
||||
def _treeItemDoubleClicked(self, index: QModelIndex) -> None:
|
||||
@@ -518,4 +490,3 @@ class _ViewPanelKeyWords(QTreeWidget):
|
||||
tag = index.siblingAtColumn(self.C_DATA).data(self.D_TAG)
|
||||
if index.column() not in (self.C_EDIT, self.C_VIEW):
|
||||
self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW)
|
||||
return
|
||||
|
||||
@@ -20,7 +20,7 @@ General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
""" # noqa
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
@@ -42,6 +42,11 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiTextDocument(QTextDocument):
|
||||
"""Custom: Modified QTextDocument.
|
||||
|
||||
A special text document format that incorporates a few additional
|
||||
features including spell checking.
|
||||
"""
|
||||
|
||||
def __init__(self, parent: QObject) -> None:
|
||||
super().__init__(parent=parent)
|
||||
@@ -52,11 +57,8 @@ class GuiTextDocument(QTextDocument):
|
||||
|
||||
logger.debug("Ready: GuiTextDocument")
|
||||
|
||||
return
|
||||
|
||||
def __del__(self) -> None: # pragma: no cover
|
||||
logger.debug("Delete: GuiTextDocument")
|
||||
return
|
||||
|
||||
##
|
||||
# Properties
|
||||
@@ -96,8 +98,6 @@ class GuiTextDocument(QTextDocument):
|
||||
logger.debug("Loaded %d text blocks in %.3f ms", count, 1000*(tMid - tStart))
|
||||
logger.debug("Highlighted document in %.3f ms", 1000*(tEnd - tMid))
|
||||
|
||||
return
|
||||
|
||||
def metaDataAtPos(self, pos: int) -> tuple[str, str]:
|
||||
"""Check if there is meta data available at a given position in
|
||||
the document, and if so, return it.
|
||||
@@ -113,7 +113,7 @@ class GuiTextDocument(QTextDocument):
|
||||
return cData, cType
|
||||
return "", ""
|
||||
|
||||
def spellErrorAtPos(self, pos: int) -> tuple[str, int, int, list[str]]:
|
||||
def spellErrorAtPos(self, pos: int) -> tuple[str, int, list[str]]:
|
||||
"""Check if there is a misspelled word at a given position in
|
||||
the document, and if so, return it.
|
||||
"""
|
||||
@@ -122,15 +122,11 @@ class GuiTextDocument(QTextDocument):
|
||||
block = cursor.block()
|
||||
data = block.userData()
|
||||
if block.isValid() and isinstance(data, TextBlockData):
|
||||
text = block.text()
|
||||
check = pos - block.position()
|
||||
if check >= 0:
|
||||
for cPos, cEnd in data.spellErrors:
|
||||
cLen = cEnd - cPos
|
||||
if cPos <= check <= cEnd:
|
||||
word = text[cPos:cEnd]
|
||||
return word, cPos, cLen, SHARED.spelling.suggestWords(word)
|
||||
return "", -1, -1, []
|
||||
if (check := pos - block.position()) >= 0:
|
||||
for start, end, word in data.spellErrors:
|
||||
if start <= check <= end:
|
||||
return word, start, SHARED.spelling.suggestWords(word)
|
||||
return "", -1, []
|
||||
|
||||
def iterBlockByType(self, cType: int, maxCount: int = 1000) -> Iterable[QTextBlock]:
|
||||
"""Iterate over all text blocks of a given type."""
|
||||
@@ -150,4 +146,3 @@ class GuiTextDocument(QTextDocument):
|
||||
def setSpellCheckState(self, state: bool) -> None:
|
||||
"""Set the spell check state of the syntax highlighter."""
|
||||
self._syntax.setSpellCheck(state)
|
||||
return
|
||||
|
||||
@@ -20,7 +20,7 @@ General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
""" # noqa
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
@@ -43,6 +43,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiItemDetails(QWidget):
|
||||
"""GUI: Project Item Details Panel."""
|
||||
|
||||
def __init__(self, parent: QWidget) -> None:
|
||||
super().__init__(parent=parent)
|
||||
@@ -190,8 +191,6 @@ class GuiItemDetails(QWidget):
|
||||
|
||||
logger.debug("Ready: GuiItemDetails")
|
||||
|
||||
return
|
||||
|
||||
###
|
||||
# Class Methods
|
||||
##
|
||||
@@ -210,7 +209,6 @@ class GuiItemDetails(QWidget):
|
||||
self.cCountData.clear()
|
||||
self.wCountData.clear()
|
||||
self.pCountData.clear()
|
||||
return
|
||||
|
||||
def refreshDetails(self) -> None:
|
||||
"""Reload the content of the details panel."""
|
||||
@@ -219,7 +217,6 @@ class GuiItemDetails(QWidget):
|
||||
def updateTheme(self) -> None:
|
||||
"""Update theme elements."""
|
||||
self.updateViewBox(self._handle)
|
||||
return
|
||||
|
||||
def updateViewBox(self, tHandle: str | None) -> None:
|
||||
"""Populate the details box from a given handle."""
|
||||
@@ -233,14 +230,8 @@ class GuiItemDetails(QWidget):
|
||||
# Label
|
||||
# =====
|
||||
|
||||
if nwItem.isFileType():
|
||||
if nwItem.isActive:
|
||||
self.labelIcon.setPixmap(SHARED.theme.getPixmap("checked", (iPx, iPx), "green"))
|
||||
else:
|
||||
self.labelIcon.setPixmap(SHARED.theme.getPixmap("unchecked", (iPx, iPx), "red"))
|
||||
else:
|
||||
self.labelIcon.setPixmap(SHARED.theme.getPixmap("noncheckable", (iPx, iPx), "faded"))
|
||||
|
||||
_, icon = nwItem.getActiveStatus()
|
||||
self.labelIcon.setPixmap(icon.pixmap(iPx, iPx))
|
||||
self.labelData.setText(elide(nwItem.itemName, 100))
|
||||
|
||||
# Status
|
||||
@@ -289,4 +280,3 @@ class GuiItemDetails(QWidget):
|
||||
self.updateViewBox(tHandle)
|
||||
elif change == nwChange.DELETE:
|
||||
self.updateViewBox(None)
|
||||
return
|
||||
|
||||
+11
-25
@@ -20,7 +20,7 @@ General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
""" # noqa
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
@@ -82,8 +82,6 @@ class GuiMainMenu(QMenuBar):
|
||||
|
||||
logger.debug("Ready: GuiMainMenu")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Public Slots
|
||||
##
|
||||
@@ -92,7 +90,6 @@ class GuiMainMenu(QMenuBar):
|
||||
def setSpellCheckState(self, state: bool) -> None:
|
||||
"""Forward spell check check state to its action."""
|
||||
self.aSpellCheck.setChecked(state)
|
||||
return
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
@@ -105,21 +102,18 @@ class GuiMainMenu(QMenuBar):
|
||||
decision, just pass a None to the function and let it decide.
|
||||
"""
|
||||
self.mainGui.docEditor.toggleSpellCheck(None)
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _openUserManualFile(self) -> None:
|
||||
"""Open the documentation in PDF format."""
|
||||
if isinstance(CONFIG.pdfDocs, Path):
|
||||
openExternalPath(CONFIG.pdfDocs)
|
||||
return
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _changeSpelling(self, language: str) -> None:
|
||||
"""Change the spell check language."""
|
||||
SHARED.project.data.setSpellLang(language)
|
||||
SHARED.updateSpellCheckLanguage()
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
@@ -188,8 +182,6 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aExitNW.triggered.connect(qtLambda(self.mainGui.closeMain))
|
||||
self.mainGui.addAction(self.aExitNW)
|
||||
|
||||
return
|
||||
|
||||
def _buildDocumentMenu(self) -> None:
|
||||
"""Assemble the Document menu."""
|
||||
# Document
|
||||
@@ -236,8 +228,6 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aImportFile = qtAddAction(self.docuMenu, self.tr("Import Text from File"))
|
||||
self.aImportFile.triggered.connect(qtLambda(self.mainGui.importDocument))
|
||||
|
||||
return
|
||||
|
||||
def _buildEditMenu(self) -> None:
|
||||
"""Assemble the Edit menu."""
|
||||
# Edit
|
||||
@@ -305,8 +295,6 @@ class GuiMainMenu(QMenuBar):
|
||||
)
|
||||
self.mainGui.addAction(self.aSelectPar)
|
||||
|
||||
return
|
||||
|
||||
def _buildViewMenu(self) -> None:
|
||||
"""Assemble the View menu."""
|
||||
# View
|
||||
@@ -318,6 +306,7 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aFocusTree.triggered.connect(
|
||||
lambda: self.requestFocusChange.emit(nwFocus.TREE)
|
||||
)
|
||||
self.mainGui.addAction(self.aFocusTree)
|
||||
|
||||
# View > Document Editor
|
||||
self.aFocusDocument = qtAddAction(self.viewMenu, self.tr("Go to Document"))
|
||||
@@ -332,6 +321,7 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aFocusOutline.triggered.connect(
|
||||
lambda: self.requestFocusChange.emit(nwFocus.OUTLINE)
|
||||
)
|
||||
self.mainGui.addAction(self.aFocusOutline)
|
||||
|
||||
# View > Separator
|
||||
self.viewMenu.addSeparator()
|
||||
@@ -365,8 +355,6 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aFullScreen.triggered.connect(self.mainGui.toggleFullScreenMode)
|
||||
self.mainGui.addAction(self.aFullScreen)
|
||||
|
||||
return
|
||||
|
||||
def _buildInsertMenu(self) -> None:
|
||||
"""Assemble the Insert menu."""
|
||||
# Insert
|
||||
@@ -644,8 +632,6 @@ class GuiMainMenu(QMenuBar):
|
||||
lambda: self.requestDocInsert.emit(nwDocInsert.FOOTNOTE)
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
def _buildFormatMenu(self) -> None:
|
||||
"""Assemble the Format menu."""
|
||||
# Format
|
||||
@@ -675,6 +661,14 @@ class GuiMainMenu(QMenuBar):
|
||||
)
|
||||
self.mainGui.addAction(self.aFmtStrike)
|
||||
|
||||
# Format > Highlight
|
||||
self.aFmtMark = qtAddAction(self.fmtMenu, self.tr("Highlight"))
|
||||
self.aFmtMark.setShortcut("Ctrl+M")
|
||||
self.aFmtMark.triggered.connect(
|
||||
lambda: self.requestDocAction.emit(nwDocAction.MD_MARK)
|
||||
)
|
||||
self.mainGui.addAction(self.aFmtStrike)
|
||||
|
||||
# Edit > Separator
|
||||
self.fmtMenu.addSeparator()
|
||||
|
||||
@@ -891,8 +885,6 @@ class GuiMainMenu(QMenuBar):
|
||||
lambda: self.requestDocAction.emit(nwDocAction.RM_BREAKS)
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
def _buildSearchMenu(self) -> None:
|
||||
"""Assemble the Search menu."""
|
||||
# Search
|
||||
@@ -938,8 +930,6 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aFindProj.setShortcut("Ctrl+Shift+F")
|
||||
self.aFindProj.triggered.connect(qtLambda(self.requestViewChange.emit, nwView.SEARCH))
|
||||
|
||||
return
|
||||
|
||||
def _buildToolsMenu(self) -> None:
|
||||
"""Assemble the Tools menu."""
|
||||
# Tools
|
||||
@@ -1009,8 +999,6 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aPreferences.triggered.connect(self.mainGui.showPreferencesDialog)
|
||||
self.mainGui.addAction(self.aPreferences)
|
||||
|
||||
return
|
||||
|
||||
def _buildHelpMenu(self) -> None:
|
||||
"""Assemble the Help menu."""
|
||||
# Help
|
||||
@@ -1056,5 +1044,3 @@ class GuiMainMenu(QMenuBar):
|
||||
# Document > Main Website
|
||||
self.aWebsite = qtAddAction(self.helpMenu, self.tr("The novelWriter Website"))
|
||||
self.aWebsite.triggered.connect(qtLambda(SHARED.openWebsite, nwConst.URL_WEB))
|
||||
|
||||
return
|
||||
|
||||
@@ -24,7 +24,7 @@ General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
""" # noqa
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
@@ -55,6 +55,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiNovelView(QWidget):
|
||||
"""GUI: Novel View Panel."""
|
||||
|
||||
# Signals for user interaction with the novel tree
|
||||
selectedItemChanged = pyqtSignal(str)
|
||||
@@ -82,8 +83,6 @@ class GuiNovelView(QWidget):
|
||||
self.getSelectedHandle = self.novelTree.getSelectedHandle
|
||||
self.refreshCurrentTree = self.novelBar.forceRefreshNovelTree
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
@@ -91,19 +90,16 @@ class GuiNovelView(QWidget):
|
||||
def updateTheme(self) -> None:
|
||||
"""Update theme elements."""
|
||||
self.novelBar.updateTheme()
|
||||
return
|
||||
|
||||
def initSettings(self) -> None:
|
||||
"""Initialise GUI elements that depend on specific settings."""
|
||||
self.novelTree.initSettings()
|
||||
return
|
||||
|
||||
def clearNovelView(self) -> None:
|
||||
"""Clear project-related GUI content."""
|
||||
self.novelBar.clearContent()
|
||||
self.novelBar.setEnabled(False)
|
||||
self.novelTree.clearContent()
|
||||
return
|
||||
|
||||
def openProjectTasks(self) -> None:
|
||||
"""Run open project tasks."""
|
||||
@@ -125,8 +121,6 @@ class GuiNovelView(QWidget):
|
||||
|
||||
self.novelTree.setLastColSize(lastColSize)
|
||||
|
||||
return
|
||||
|
||||
def closeProjectTasks(self) -> None:
|
||||
"""Run closing project tasks."""
|
||||
logger.debug("Saving State: GuiNovelView")
|
||||
@@ -140,12 +134,9 @@ class GuiNovelView(QWidget):
|
||||
|
||||
self.clearNovelView()
|
||||
|
||||
return
|
||||
|
||||
def setTreeFocus(self) -> None:
|
||||
"""Set the focus to the tree widget."""
|
||||
self.novelTree.setFocus()
|
||||
return
|
||||
|
||||
def treeHasFocus(self) -> bool:
|
||||
"""Check if the novel tree has focus."""
|
||||
@@ -159,22 +150,20 @@ class GuiNovelView(QWidget):
|
||||
def setCurrentNovel(self, rootHandle: str | None) -> None:
|
||||
"""Set the current novel to display."""
|
||||
self.novelTree.setNovelModel(rootHandle)
|
||||
return
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setActiveHandle(self, tHandle: str) -> None:
|
||||
"""Highlight the rows associated with a given handle."""
|
||||
self.novelTree.setActiveHandle(tHandle)
|
||||
return
|
||||
|
||||
@pyqtSlot(str, Enum)
|
||||
def updateRootItem(self, tHandle: str, change: nwChange) -> None:
|
||||
"""If any root item changes, rebuild the novel root menu."""
|
||||
self.novelBar.buildNovelRootMenu()
|
||||
return
|
||||
|
||||
|
||||
class GuiNovelToolBar(QWidget):
|
||||
"""GUI: Novel View Panel ToolBar."""
|
||||
|
||||
def __init__(self, novelView: GuiNovelView) -> None:
|
||||
super().__init__(parent=novelView)
|
||||
@@ -249,8 +238,6 @@ class GuiNovelToolBar(QWidget):
|
||||
|
||||
logger.debug("Ready: GuiNovelToolBar")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
@@ -277,20 +264,16 @@ class GuiNovelToolBar(QWidget):
|
||||
|
||||
self.forceRefreshNovelTree()
|
||||
|
||||
return
|
||||
|
||||
def clearContent(self) -> None:
|
||||
"""Run clearing project tasks."""
|
||||
self.novelValue.clear()
|
||||
self.novelValue.setToolTip("")
|
||||
return
|
||||
|
||||
def buildNovelRootMenu(self) -> None:
|
||||
"""Build the novel root menu."""
|
||||
self.novelValue.refreshNovelList()
|
||||
self.novelView.setCurrentNovel(self.novelValue.handle)
|
||||
self.tbNovel.setVisible(self.novelValue.count() > 1)
|
||||
return
|
||||
|
||||
def setCurrentRoot(self, rootHandle: str | None) -> None:
|
||||
"""Set the current active root handle."""
|
||||
@@ -300,7 +283,6 @@ class GuiNovelToolBar(QWidget):
|
||||
SHARED.project.data.setLastHandle(rootHandle, "novel")
|
||||
self.novelView.setCurrentNovel(rootHandle)
|
||||
self.novelView.novelTree.setAccessibleName(self.novelValue.currentText())
|
||||
return
|
||||
|
||||
def setLastColType(self, colType: nwNovelExtra, doRefresh: bool = True) -> None:
|
||||
"""Set the last column type."""
|
||||
@@ -309,7 +291,6 @@ class GuiNovelToolBar(QWidget):
|
||||
if doRefresh:
|
||||
self.forceRefreshNovelTree()
|
||||
self.novelView.novelTree.resizeColumns()
|
||||
return
|
||||
|
||||
def setActive(self, state: bool) -> None:
|
||||
"""Set the widget active state, which enables automatic tree
|
||||
@@ -322,7 +303,6 @@ class GuiNovelToolBar(QWidget):
|
||||
and self._refresh.get(handle, False)
|
||||
):
|
||||
self._refreshNovelTree(self.novelValue.handle)
|
||||
return
|
||||
|
||||
##
|
||||
# Public Slots
|
||||
@@ -335,7 +315,6 @@ class GuiNovelToolBar(QWidget):
|
||||
self.novelView.setCurrentNovel(tHandle)
|
||||
SHARED.project.index.refreshNovelModel(tHandle)
|
||||
self._refresh[tHandle] = False
|
||||
return
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
@@ -349,7 +328,6 @@ class GuiNovelToolBar(QWidget):
|
||||
self._refresh[tHandle] = False
|
||||
else:
|
||||
self._refresh[tHandle] = True
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _selectLastColumnSize(self) -> None:
|
||||
@@ -361,7 +339,6 @@ class GuiNovelToolBar(QWidget):
|
||||
if isOk:
|
||||
self.novelView.novelTree.setLastColSize(newSize)
|
||||
self.novelView.novelTree.resizeColumns()
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
@@ -374,10 +351,10 @@ class GuiNovelToolBar(QWidget):
|
||||
aLast.setActionGroup(self.gLastCol)
|
||||
aLast.triggered.connect(qtLambda(self.setLastColType, colType))
|
||||
self.aLastCol[colType] = aLast
|
||||
return
|
||||
|
||||
|
||||
class GuiNovelTree(NTreeView):
|
||||
"""GUI: Novel View Panel Tree."""
|
||||
|
||||
def __init__(self, novelView: GuiNovelView) -> None:
|
||||
super().__init__(parent=novelView)
|
||||
@@ -414,8 +391,6 @@ class GuiNovelTree(NTreeView):
|
||||
|
||||
logger.debug("Ready: GuiNovelTree")
|
||||
|
||||
return
|
||||
|
||||
def initSettings(self) -> None:
|
||||
"""Set or update tree widget settings."""
|
||||
if CONFIG.hideVScroll:
|
||||
@@ -426,7 +401,6 @@ class GuiNovelTree(NTreeView):
|
||||
self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff)
|
||||
else:
|
||||
self.setHorizontalScrollBarPolicy(QtScrollAsNeeded)
|
||||
return
|
||||
|
||||
##
|
||||
# Properties
|
||||
@@ -466,25 +440,21 @@ class GuiNovelTree(NTreeView):
|
||||
self.resizeColumns()
|
||||
else:
|
||||
self.clearContent()
|
||||
return
|
||||
|
||||
def setActiveHandle(self, tHandle: str | None) -> None:
|
||||
"""Set the handle to be highlighted."""
|
||||
self._actHandle = tHandle
|
||||
if viewport := self.viewport():
|
||||
viewport.repaint()
|
||||
return
|
||||
|
||||
def setLastColType(self, colType: nwNovelExtra) -> None:
|
||||
"""Set the extra column type."""
|
||||
self._lastColType = colType
|
||||
SHARED.project.index.setNovelModelExtraColumn(colType)
|
||||
return
|
||||
|
||||
def setLastColSize(self, colSize: int) -> None:
|
||||
"""Set the extra column size between 15% and 75%."""
|
||||
self._lastColSize = minmax(colSize, 15, 75)/100.0
|
||||
return
|
||||
|
||||
##
|
||||
# Class Methods
|
||||
@@ -493,7 +463,6 @@ class GuiNovelTree(NTreeView):
|
||||
def clearContent(self) -> None:
|
||||
"""Clear the tree view."""
|
||||
self.setModel(None)
|
||||
return
|
||||
|
||||
def resizeColumns(self) -> None:
|
||||
"""Set the correct column sizes."""
|
||||
@@ -506,7 +475,6 @@ class GuiNovelTree(NTreeView):
|
||||
if model.columns == 4:
|
||||
header.setSectionResizeMode(3, QtHeaderToContents)
|
||||
header.setMaximumSectionSize(int(self._lastColSize * vp.width()))
|
||||
return
|
||||
|
||||
##
|
||||
# Overloads
|
||||
@@ -517,7 +485,6 @@ class GuiNovelTree(NTreeView):
|
||||
if (model := self._getModel()) and model.handle(index) == self._actHandle:
|
||||
painter.fillRect(opt.rect, self.palette().alternateBase())
|
||||
super().drawRow(painter, opt, index)
|
||||
return
|
||||
|
||||
##
|
||||
# Events
|
||||
@@ -527,7 +494,6 @@ class GuiNovelTree(NTreeView):
|
||||
"""Process size changed."""
|
||||
super().resizeEvent(event)
|
||||
self.resizeColumns()
|
||||
return
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
@@ -535,36 +501,33 @@ class GuiNovelTree(NTreeView):
|
||||
|
||||
@pyqtSlot(QModelIndex)
|
||||
def _onSingleClick(self, index: QModelIndex) -> None:
|
||||
"""The user single-clicked an index."""
|
||||
"""Process user single-click on an index."""
|
||||
if index.isValid() and (model := self._getModel()):
|
||||
if (tHandle := model.handle(index)) and (sTitle := model.key(index)):
|
||||
self.novelView.selectedItemChanged.emit(tHandle)
|
||||
if index.column() == model.columnCount(index) - 1:
|
||||
pos = self.mapToGlobal(self.visualRect(index).topRight())
|
||||
self._popMetaBox(pos, tHandle, sTitle)
|
||||
return
|
||||
|
||||
@pyqtSlot(QModelIndex)
|
||||
def _onDoubleClick(self, index: QModelIndex) -> None:
|
||||
"""The user double-clicked an index."""
|
||||
"""Process user double-click on an index."""
|
||||
if (
|
||||
(model := self._getModel())
|
||||
and (tHandle := model.handle(index))
|
||||
and (sTitle := model.key(index))
|
||||
):
|
||||
self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle, False)
|
||||
return
|
||||
|
||||
@pyqtSlot(QModelIndex)
|
||||
def _onMiddleClick(self, index: QModelIndex) -> None:
|
||||
"""The user middle-clicked an index."""
|
||||
"""Process user middle-click on an index."""
|
||||
if (
|
||||
(model := self._getModel())
|
||||
and (tHandle := model.handle(index))
|
||||
and (sTitle := model.key(index))
|
||||
):
|
||||
self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, sTitle, False)
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
@@ -582,7 +545,6 @@ class GuiNovelTree(NTreeView):
|
||||
"""Generate a reference list for a given reference key."""
|
||||
if tags := ", ".join(refs.get(key, [])):
|
||||
lines.append(f"<b>{trConst(nwLabels.KEY_NAME[key])}:</b> {tags}")
|
||||
return
|
||||
|
||||
if head := SHARED.project.index.getItemHeading(tHandle, sTitle):
|
||||
logger.debug("Generating meta data tooltip for '%s:%s'", tHandle, sTitle)
|
||||
@@ -610,4 +572,3 @@ class GuiNovelTree(NTreeView):
|
||||
text = f"<p>{refs}</p>"
|
||||
if tooltip := (text + synopsis or self.tr("No meta data")):
|
||||
QToolTip.showText(qPos, tooltip)
|
||||
return
|
||||
|
||||
+10
-57
@@ -24,7 +24,7 @@ General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
""" # noqa
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
@@ -58,6 +58,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiOutlineView(QWidget):
|
||||
"""GUI: Project Outline Panel."""
|
||||
|
||||
loadDocumentTagRequest = pyqtSignal(str, Enum)
|
||||
openDocumentRequest = pyqtSignal(str, Enum, str, bool)
|
||||
@@ -96,8 +97,6 @@ class GuiOutlineView(QWidget):
|
||||
# Function Mappings
|
||||
self.getSelectedHandle = self.outlineTree.getSelectedHandle
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
@@ -109,24 +108,20 @@ class GuiOutlineView(QWidget):
|
||||
self.outlineTree.refreshTree(
|
||||
rootHandle=SHARED.project.data.getLastHandle("outline"), overRide=True
|
||||
)
|
||||
return
|
||||
|
||||
def initSettings(self) -> None:
|
||||
"""Initialise GUI elements that depend on specific settings."""
|
||||
self.outlineTree.initSettings()
|
||||
self.outlineData.initSettings()
|
||||
return
|
||||
|
||||
def refreshTree(self) -> None:
|
||||
"""Refresh the current tree."""
|
||||
self.outlineTree.refreshTree(rootHandle=SHARED.project.data.getLastHandle("outline"))
|
||||
return
|
||||
|
||||
def clearOutline(self) -> None:
|
||||
"""Clear project-related GUI content."""
|
||||
self.outlineData.clearDetails()
|
||||
self.outlineBar.setEnabled(False)
|
||||
return
|
||||
|
||||
def openProjectTasks(self) -> None:
|
||||
"""Run open project tasks."""
|
||||
@@ -142,8 +137,6 @@ class GuiOutlineView(QWidget):
|
||||
self.outlineBar.setEnabled(True)
|
||||
self.outlineData.loadGuiSettings()
|
||||
|
||||
return
|
||||
|
||||
def closeProjectTasks(self) -> None:
|
||||
"""Run closing project tasks."""
|
||||
if self.outlineTree.wasRendered:
|
||||
@@ -152,7 +145,6 @@ class GuiOutlineView(QWidget):
|
||||
self.outlineTree.closeProjectTasks()
|
||||
self.outlineData.updateClasses()
|
||||
self.clearOutline()
|
||||
return
|
||||
|
||||
def splitSizes(self) -> list[int]:
|
||||
"""Get the sizes of the splitter widget."""
|
||||
@@ -175,7 +167,6 @@ class GuiOutlineView(QWidget):
|
||||
"""Handle tasks whenever a root folders changes."""
|
||||
self.outlineBar.populateNovelList()
|
||||
self.outlineData.updateClasses()
|
||||
return
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
@@ -188,23 +179,21 @@ class GuiOutlineView(QWidget):
|
||||
of columns has changed.
|
||||
"""
|
||||
self.outlineBar.setColumnHiddenState(self.outlineTree.hiddenColumns)
|
||||
return
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _tagClicked(self, link: str) -> None:
|
||||
"""Capture the click of a tag in the details panel."""
|
||||
if link:
|
||||
self.loadDocumentTagRequest.emit(link, nwDocMode.VIEW)
|
||||
return
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _rootItemChanged(self, tHandle: str) -> None:
|
||||
"""Handle root novel changed or needs to be refreshed."""
|
||||
self.outlineTree.refreshTree(rootHandle=(tHandle or None), overRide=True)
|
||||
return
|
||||
|
||||
|
||||
class GuiOutlineToolBar(QToolBar):
|
||||
"""GUI: Project Outline Panel ToolBar."""
|
||||
|
||||
loadNovelRootRequest = pyqtSignal(str)
|
||||
outlineExportRequest = pyqtSignal()
|
||||
@@ -263,8 +252,6 @@ class GuiOutlineToolBar(QToolBar):
|
||||
|
||||
logger.debug("Ready: GuiOutlineToolBar")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
@@ -278,22 +265,18 @@ class GuiOutlineToolBar(QToolBar):
|
||||
self.tbColumns.setIcon(SHARED.theme.getIcon("more_vertical"))
|
||||
self.tbColumns.setStyleSheet("QToolButton::menu-indicator {image: none;}")
|
||||
self.novelLabel.setTextColors(color=self.palette().windowText().color())
|
||||
return
|
||||
|
||||
def populateNovelList(self) -> None:
|
||||
"""Reload the content of the novel list."""
|
||||
self.novelValue.refreshNovelList()
|
||||
return
|
||||
|
||||
def setCurrentRoot(self, rootHandle: str | None) -> None:
|
||||
"""Set the current active root handle."""
|
||||
self.novelValue.setHandle(rootHandle)
|
||||
return
|
||||
|
||||
def setColumnHiddenState(self, hiddenState: dict[nwOutline, bool]) -> None:
|
||||
"""Forward the change of column hidden states to the menu."""
|
||||
self.mColumns.setHiddenState(hiddenState)
|
||||
return
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
@@ -303,22 +286,20 @@ class GuiOutlineToolBar(QToolBar):
|
||||
def _novelValueChanged(self, tHandle: str) -> None:
|
||||
"""Emit a signal containing the handle of the selected item."""
|
||||
self.loadNovelRootRequest.emit(tHandle)
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _refreshRequested(self) -> None:
|
||||
"""Emit a signal containing the handle of the selected item."""
|
||||
self.loadNovelRootRequest.emit(self.novelValue.handle)
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _exportRequested(self) -> None:
|
||||
"""Emit a signal that an export of the outline was requested."""
|
||||
self.outlineExportRequest.emit()
|
||||
return
|
||||
|
||||
|
||||
class GuiOutlineTree(QTreeWidget):
|
||||
"""GUI: Project Outline Panel Tree."""
|
||||
|
||||
DEF_WIDTH: Final[dict[nwOutline, int]] = {
|
||||
nwOutline.TITLE: 200,
|
||||
@@ -422,8 +403,6 @@ class GuiOutlineTree(QTreeWidget):
|
||||
|
||||
logger.debug("Ready: GuiOutlineTree")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Properties
|
||||
##
|
||||
@@ -451,7 +430,6 @@ class GuiOutlineTree(QTreeWidget):
|
||||
self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff)
|
||||
else:
|
||||
self.setHorizontalScrollBarPolicy(QtScrollAsNeeded)
|
||||
return
|
||||
|
||||
def clearContent(self) -> None:
|
||||
"""Clear the tree and header and set the default values for the
|
||||
@@ -474,8 +452,6 @@ class GuiOutlineTree(QTreeWidget):
|
||||
|
||||
self._treeNCols = len(self._treeOrder)
|
||||
|
||||
return
|
||||
|
||||
def updateTheme(self) -> None:
|
||||
"""Update theme elements."""
|
||||
iType = nwItemType.FILE
|
||||
@@ -488,15 +464,14 @@ class GuiOutlineTree(QTreeWidget):
|
||||
"H3": SHARED.theme.getItemIcon(iType, iClass, iLayout, "H3"),
|
||||
"H4": SHARED.theme.getItemIcon(iType, iClass, iLayout, "H4"),
|
||||
}
|
||||
return
|
||||
|
||||
def refreshTree(
|
||||
self, rootHandle: str | None = None,
|
||||
overRide: bool = False, novelChanged: bool = False
|
||||
) -> None:
|
||||
"""Called whenever the Outline tab is activated and controls
|
||||
what data to load, and if necessary, force a rebuild of the
|
||||
tree.
|
||||
"""Refresh the outline tree. Called whenever the Outline tab is
|
||||
activated and controls what data to load, and if necessary,
|
||||
force a rebuild of the tree.
|
||||
"""
|
||||
# If it's the first time, we always build
|
||||
if self._firstView or (self._firstView and overRide):
|
||||
@@ -518,11 +493,10 @@ class GuiOutlineTree(QTreeWidget):
|
||||
return
|
||||
|
||||
def closeProjectTasks(self) -> None:
|
||||
"""Called before a project is closed."""
|
||||
"""Call before a project is closed."""
|
||||
self._saveHeaderState()
|
||||
self.clearContent()
|
||||
self._firstView = True
|
||||
return
|
||||
|
||||
def getSelectedHandle(self) -> tuple[str | None, str | None]:
|
||||
"""Get the currently selected handle. If multiple items are
|
||||
@@ -546,7 +520,6 @@ class GuiOutlineTree(QTreeWidget):
|
||||
if hItem in self._colIdx:
|
||||
self.setColumnHidden(self._colIdx[hItem], not isChecked)
|
||||
self._saveHeaderState()
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def exportOutline(self) -> None:
|
||||
@@ -562,7 +535,6 @@ class GuiOutlineTree(QTreeWidget):
|
||||
writer.writerows(
|
||||
self._dumpNovelData(self.outlineView.outlineBar.novelValue.handle)
|
||||
)
|
||||
return
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
@@ -577,7 +549,6 @@ class GuiOutlineTree(QTreeWidget):
|
||||
tHandle, sTitle = self.getSelectedHandle()
|
||||
if tHandle:
|
||||
self.outlineView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "", True)
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _onItemSelectionChanged(self) -> None:
|
||||
@@ -588,7 +559,6 @@ class GuiOutlineTree(QTreeWidget):
|
||||
tHandle = items[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE)
|
||||
sTitle = items[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE)
|
||||
self.activeItemChanged.emit(tHandle, sTitle)
|
||||
return
|
||||
|
||||
@pyqtSlot(int, int, int)
|
||||
def _columnMoved(self, logIdx: int, oldVisualIdx: int, newVisualIdx: int) -> None:
|
||||
@@ -597,7 +567,6 @@ class GuiOutlineTree(QTreeWidget):
|
||||
"""
|
||||
self._treeOrder.insert(newVisualIdx, self._treeOrder.pop(oldVisualIdx))
|
||||
self._saveHeaderState()
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
@@ -637,8 +606,6 @@ class GuiOutlineTree(QTreeWidget):
|
||||
|
||||
self.hiddenStateChanged.emit()
|
||||
|
||||
return
|
||||
|
||||
def _saveHeaderState(self) -> None:
|
||||
"""Save the state of the main tree header, that is, column
|
||||
order, column width and column hidden state. We don't want to
|
||||
@@ -661,7 +628,6 @@ class GuiOutlineTree(QTreeWidget):
|
||||
pOptions = SHARED.project.options
|
||||
pOptions.setValue("GuiOutline", "columnState", colState)
|
||||
pOptions.saveSettings()
|
||||
return
|
||||
|
||||
def _populateTree(self, rootHandle: str | None) -> None:
|
||||
"""Build the tree based on the project index, and the header
|
||||
@@ -746,8 +712,6 @@ class GuiOutlineTree(QTreeWidget):
|
||||
self._lastBuild = time()
|
||||
logger.debug("Project outline built in %.3f ms", 1000.0*(time() - tStart))
|
||||
|
||||
return
|
||||
|
||||
def _dumpNovelData(self, rootHandle: str | None) -> list[list[str | int]]:
|
||||
"""Dump all novel data into a table."""
|
||||
sLabel = SHARED.project.localLookup("Story Structure")
|
||||
@@ -821,6 +785,7 @@ class GuiOutlineTree(QTreeWidget):
|
||||
|
||||
|
||||
class GuiOutlineHeaderMenu(QMenu):
|
||||
"""GUI: Project Outline Panel Header Selection Menu."""
|
||||
|
||||
columnToggled = pyqtSignal(bool, Enum)
|
||||
|
||||
@@ -844,8 +809,6 @@ class GuiOutlineHeaderMenu(QMenu):
|
||||
)
|
||||
self.addAction(self.actionMap[hItem])
|
||||
|
||||
return
|
||||
|
||||
def setHiddenState(self, hiddenState: dict[nwOutline, bool]) -> None:
|
||||
"""Overwrite the checked state of the columns as the inverse of
|
||||
the hidden state. Skip the TITLE column as it cannot be hidden.
|
||||
@@ -859,10 +822,9 @@ class GuiOutlineHeaderMenu(QMenu):
|
||||
|
||||
self.acceptToggle = True
|
||||
|
||||
return
|
||||
|
||||
|
||||
class GuiOutlineDetails(QScrollArea):
|
||||
"""GUI: Project Outline Panel Details View."""
|
||||
|
||||
LVL_MAP: Final[dict[str, str]] = {
|
||||
"H1": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Title"),
|
||||
@@ -1007,8 +969,6 @@ class GuiOutlineDetails(QScrollArea):
|
||||
|
||||
logger.debug("Ready: GuiOutlineDetails")
|
||||
|
||||
return
|
||||
|
||||
def initSettings(self) -> None:
|
||||
"""Set or update outline settings."""
|
||||
if CONFIG.hideVScroll:
|
||||
@@ -1020,7 +980,6 @@ class GuiOutlineDetails(QScrollArea):
|
||||
else:
|
||||
self.setHorizontalScrollBarPolicy(QtScrollAsNeeded)
|
||||
self.updateClasses()
|
||||
return
|
||||
|
||||
def loadGuiSettings(self) -> None:
|
||||
"""Run open project tasks."""
|
||||
@@ -1031,7 +990,6 @@ class GuiOutlineDetails(QScrollArea):
|
||||
pOptions.getInt("GuiOutlineDetails", "detailsWidth", width//3),
|
||||
pOptions.getInt("GuiOutlineDetails", "tagsWidth", 2*width//3),
|
||||
])
|
||||
return
|
||||
|
||||
def saveGuiSettings(self) -> None:
|
||||
"""Run close project tasks."""
|
||||
@@ -1040,7 +998,6 @@ class GuiOutlineDetails(QScrollArea):
|
||||
pOptions = SHARED.project.options
|
||||
pOptions.setValue("GuiOutlineDetails", "detailsWidth", mainSplit[0])
|
||||
pOptions.setValue("GuiOutlineDetails", "tagsWidth", mainSplit[1])
|
||||
return
|
||||
|
||||
def clearDetails(self) -> None:
|
||||
"""Clear all the data labels."""
|
||||
@@ -1057,7 +1014,6 @@ class GuiOutlineDetails(QScrollArea):
|
||||
value.clear()
|
||||
|
||||
self.updateClasses()
|
||||
return
|
||||
|
||||
##
|
||||
# Slots
|
||||
@@ -1090,8 +1046,6 @@ class GuiOutlineDetails(QScrollArea):
|
||||
for key, (_, value) in self.tagValues.items():
|
||||
value.setText(self._formatTags(novRefs, key))
|
||||
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def updateClasses(self) -> None:
|
||||
"""Update the visibility status of class details."""
|
||||
@@ -1102,7 +1056,6 @@ class GuiOutlineDetails(QScrollArea):
|
||||
label, value = self.tagValues[key]
|
||||
label.setVisible(visible)
|
||||
value.setVisible(visible)
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def _formatTags(refs: dict[str, list[str]], key: str) -> str:
|
||||
|
||||
+30
-89
@@ -25,7 +25,7 @@ General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
""" # noqa
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
@@ -61,7 +61,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiProjectView(QWidget):
|
||||
"""This is a wrapper class holding all the elements of the project
|
||||
"""GUI: Project View.
|
||||
|
||||
This is a wrapper class holding all the elements of the project
|
||||
tree. The core object is the project tree itself. Most methods
|
||||
available are mapped through to the project tree class.
|
||||
"""
|
||||
@@ -130,8 +132,6 @@ class GuiProjectView(QWidget):
|
||||
# Function Mappings
|
||||
self.getSelectedHandle = self.projTree.getSelectedHandle
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
@@ -139,19 +139,16 @@ class GuiProjectView(QWidget):
|
||||
def updateTheme(self) -> None:
|
||||
"""Update theme elements."""
|
||||
self.projBar.updateTheme()
|
||||
return
|
||||
|
||||
def initSettings(self) -> None:
|
||||
"""Initialise GUI elements that depend on specific settings."""
|
||||
self.projTree.initSettings()
|
||||
return
|
||||
|
||||
def closeProjectTasks(self) -> None:
|
||||
"""Clear project-related GUI content."""
|
||||
self.projBar.clearContent()
|
||||
self.projBar.setEnabled(False)
|
||||
self.projTree.clearTree()
|
||||
return
|
||||
|
||||
def openProjectTasks(self) -> None:
|
||||
"""Run open project tasks."""
|
||||
@@ -159,26 +156,23 @@ class GuiProjectView(QWidget):
|
||||
self.projBar.buildTemplatesMenu()
|
||||
self.projBar.buildQuickLinksMenu()
|
||||
self.projBar.setEnabled(True)
|
||||
return
|
||||
|
||||
def setTreeFocus(self) -> None:
|
||||
"""Forward the set focus call to the tree widget."""
|
||||
self.projTree.setFocus()
|
||||
return
|
||||
|
||||
def treeHasFocus(self) -> bool:
|
||||
"""Check if the project tree has focus."""
|
||||
return self.projTree.hasFocus()
|
||||
|
||||
def connectMenuActions(self, rename: QAction, delete: QAction, trash: QAction) -> None:
|
||||
"""Main menu actions passed to the project tree."""
|
||||
"""Connect main menu actions passed to the project tree."""
|
||||
self.projTree.addAction(rename)
|
||||
self.projTree.addAction(delete)
|
||||
self.projTree.addAction(trash)
|
||||
rename.triggered.connect(self.renameTreeItem)
|
||||
delete.triggered.connect(self.projTree.processDeleteRequest)
|
||||
trash.triggered.connect(self.projTree.emptyTrash)
|
||||
return
|
||||
|
||||
##
|
||||
# Public Slots
|
||||
@@ -197,41 +191,36 @@ class GuiProjectView(QWidget):
|
||||
if dlgOk:
|
||||
nwItem.setName(newLabel)
|
||||
nwItem.notifyToRefresh()
|
||||
return
|
||||
|
||||
@pyqtSlot(str, bool)
|
||||
def setSelectedHandle(self, tHandle: str, doScroll: bool = False) -> None:
|
||||
"""Select an item and optionally scroll it into view."""
|
||||
self.projTree.setSelectedHandle(tHandle, doScroll=doScroll)
|
||||
return
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setActiveHandle(self, tHandle: str | None) -> None:
|
||||
"""Highlight the active handle."""
|
||||
self.projTree.setActiveHandle(tHandle)
|
||||
return
|
||||
|
||||
@pyqtSlot(str, Enum)
|
||||
def onProjectItemChanged(self, tHandle: str, change: nwChange) -> None:
|
||||
"""Refresh other content when project item changed."""
|
||||
self.projBar.processTemplateDocuments(tHandle)
|
||||
return
|
||||
|
||||
@pyqtSlot(str)
|
||||
def createFileFromTemplate(self, tHandle: str) -> None:
|
||||
"""Create a new document from a template."""
|
||||
logger.debug("Template selected: '%s'", tHandle)
|
||||
self.projTree.newTreeItem(nwItemType.FILE, copyDoc=tHandle)
|
||||
return
|
||||
|
||||
@pyqtSlot(str, Enum)
|
||||
def updateRootItem(self, tHandle: str, change: nwChange) -> None:
|
||||
"""Process root item changes."""
|
||||
self.projBar.buildQuickLinksMenu()
|
||||
return
|
||||
|
||||
|
||||
class GuiProjectToolBar(QWidget):
|
||||
"""GUI> Project View ToolBar."""
|
||||
|
||||
newDocumentFromTemplate = pyqtSignal(str)
|
||||
|
||||
@@ -351,8 +340,6 @@ class GuiProjectToolBar(QWidget):
|
||||
|
||||
logger.debug("Ready: GuiProjectToolBar")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
@@ -383,13 +370,10 @@ class GuiProjectToolBar(QWidget):
|
||||
self.buildQuickLinksMenu()
|
||||
self._buildRootMenu()
|
||||
|
||||
return
|
||||
|
||||
def clearContent(self) -> None:
|
||||
"""Clear dynamic content on the tool bar."""
|
||||
self.mQuick.clear()
|
||||
self.mTemplates.clearMenu()
|
||||
return
|
||||
|
||||
def buildQuickLinksMenu(self) -> None:
|
||||
"""Build the quick link menu."""
|
||||
@@ -402,14 +386,12 @@ class GuiProjectToolBar(QWidget):
|
||||
action.triggered.connect(
|
||||
qtLambda(self.projView.setSelectedHandle, tHandle, doScroll=True)
|
||||
)
|
||||
return
|
||||
|
||||
def buildTemplatesMenu(self) -> None:
|
||||
"""Build the templates menu."""
|
||||
for tHandle, _ in SHARED.project.tree.iterRoots(nwItemClass.TEMPLATE):
|
||||
for dHandle in SHARED.project.tree.subTree(tHandle):
|
||||
self.processTemplateDocuments(dHandle)
|
||||
return
|
||||
|
||||
def processTemplateDocuments(self, tHandle: str) -> None:
|
||||
"""Process change in tree items to update menu content."""
|
||||
@@ -418,7 +400,6 @@ class GuiProjectToolBar(QWidget):
|
||||
self.mTemplates.addUpdate(tHandle, item.itemName, item.getMainIcon())
|
||||
elif tHandle in self.mTemplates:
|
||||
self.mTemplates.remove(tHandle)
|
||||
return
|
||||
|
||||
##
|
||||
# Public Slots
|
||||
@@ -436,7 +417,6 @@ class GuiProjectToolBar(QWidget):
|
||||
self.aAddChap.setVisible(allowDoc)
|
||||
self.aAddPart.setVisible(allowDoc)
|
||||
self.aAddEmpty.setVisible(allowDoc)
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
@@ -451,7 +431,6 @@ class GuiProjectToolBar(QWidget):
|
||||
qtLambda(self.projTree.newTreeItem, nwItemType.ROOT, itemClass)
|
||||
)
|
||||
self.mAddRoot.addAction(aNew)
|
||||
return
|
||||
|
||||
self.mAddRoot.clear()
|
||||
addClass(nwItemClass.NOVEL)
|
||||
@@ -467,10 +446,9 @@ class GuiProjectToolBar(QWidget):
|
||||
addClass(nwItemClass.ARCHIVE)
|
||||
addClass(nwItemClass.TEMPLATE)
|
||||
|
||||
return
|
||||
|
||||
|
||||
class GuiProjectTree(QTreeView):
|
||||
"""GUI: Project View Tree."""
|
||||
|
||||
def __init__(self, projView: GuiProjectView) -> None:
|
||||
super().__init__(parent=projView)
|
||||
@@ -520,8 +498,6 @@ class GuiProjectTree(QTreeView):
|
||||
|
||||
logger.debug("Ready: GuiProjectTree")
|
||||
|
||||
return
|
||||
|
||||
def initSettings(self) -> None:
|
||||
"""Set or update tree widget settings."""
|
||||
if CONFIG.hideVScroll:
|
||||
@@ -532,7 +508,6 @@ class GuiProjectTree(QTreeView):
|
||||
self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff)
|
||||
else:
|
||||
self.setHorizontalScrollBarPolicy(QtScrollAsNeeded)
|
||||
return
|
||||
|
||||
##
|
||||
# External Methods
|
||||
@@ -541,7 +516,6 @@ class GuiProjectTree(QTreeView):
|
||||
def setActiveHandle(self, tHandle: str | None) -> None:
|
||||
"""Set the handle to be highlighted."""
|
||||
self._actHandle = tHandle
|
||||
return
|
||||
|
||||
def getSelectedHandle(self) -> str | None:
|
||||
"""Get the currently selected handle."""
|
||||
@@ -556,7 +530,6 @@ class GuiProjectTree(QTreeView):
|
||||
def clearTree(self) -> None:
|
||||
"""Clear the tree view."""
|
||||
self.setModel(None)
|
||||
return
|
||||
|
||||
def loadModel(self) -> None:
|
||||
"""Load and prepare a new project model."""
|
||||
@@ -583,8 +556,6 @@ class GuiProjectTree(QTreeView):
|
||||
|
||||
self.restoreExpandedState()
|
||||
|
||||
return
|
||||
|
||||
def restoreExpandedState(self) -> None:
|
||||
"""Expand all nodes that were previously expanded."""
|
||||
if model := self._getModel():
|
||||
@@ -592,7 +563,6 @@ class GuiProjectTree(QTreeView):
|
||||
for index in model.allExpanded():
|
||||
self.setExpanded(index, True)
|
||||
self.blockSignals(False)
|
||||
return
|
||||
|
||||
def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> None:
|
||||
"""Set a specific handle as the selected item."""
|
||||
@@ -601,7 +571,6 @@ class GuiProjectTree(QTreeView):
|
||||
if doScroll:
|
||||
self.scrollTo(index, QAbstractItemView.ScrollHint.PositionAtCenter)
|
||||
self.projView.selectedItemChanged.emit(tHandle)
|
||||
return
|
||||
|
||||
def newTreeItem(
|
||||
self, itemType: nwItemType, itemClass: nwItemClass | None = None,
|
||||
@@ -667,7 +636,7 @@ class GuiProjectTree(QTreeView):
|
||||
if itemType == nwItemType.FILE:
|
||||
if tHandle := SHARED.project.newFile(newLabel, sHandle, sPos):
|
||||
if copyDoc:
|
||||
SHARED.project.copyFileContent(tHandle, copyDoc)
|
||||
SHARED.project.copyFileContent(tHandle, copyDoc, newLabel)
|
||||
elif hLevel > 0:
|
||||
SHARED.project.writeNewFile(tHandle, hLevel, not nNote)
|
||||
SHARED.project.index.reIndexHandle(tHandle)
|
||||
@@ -718,9 +687,15 @@ class GuiProjectTree(QTreeView):
|
||||
else:
|
||||
return False
|
||||
|
||||
SHARED.initMainProgress(len(items))
|
||||
self.setEnabled(False)
|
||||
for sHandle in items:
|
||||
SHARED.incMainProgress()
|
||||
docMerger.appendText(sHandle, True, mLabel)
|
||||
|
||||
self.setEnabled(True)
|
||||
SHARED.clearMainProgress()
|
||||
|
||||
if not docMerger.writeTargetDoc():
|
||||
SHARED.error(
|
||||
self.tr("Could not write document content."),
|
||||
@@ -764,7 +739,10 @@ class GuiProjectTree(QTreeView):
|
||||
docSplit.setParentItem(tItem.itemParent)
|
||||
|
||||
docSplit.splitDocument(headerList, text)
|
||||
SHARED.initMainProgress(len(docSplit))
|
||||
self.setEnabled(False)
|
||||
for writeOk in docSplit.writeDocuments(docHierarchy):
|
||||
SHARED.incMainProgress()
|
||||
if not writeOk:
|
||||
SHARED.error(
|
||||
self.tr("Could not write document content."),
|
||||
@@ -774,6 +752,9 @@ class GuiProjectTree(QTreeView):
|
||||
if data.get("moveToTrash", False):
|
||||
self.processDeleteRequest([tHandle], False)
|
||||
|
||||
self.setEnabled(True)
|
||||
SHARED.clearMainProgress()
|
||||
|
||||
return True
|
||||
|
||||
def duplicateFromHandle(self, tHandle: str) -> None:
|
||||
@@ -786,12 +767,13 @@ class GuiProjectTree(QTreeView):
|
||||
else:
|
||||
question = self.tr("Do you want to duplicate this item and all child items?")
|
||||
if SHARED.question(question):
|
||||
self.setEnabled(False)
|
||||
docDup = DocDuplicator(SHARED.project)
|
||||
dHandles = docDup.duplicate(itemTree)
|
||||
if len(dHandles) != len(itemTree):
|
||||
SHARED.warn(self.tr("Could not duplicate all items."))
|
||||
self.setEnabled(True)
|
||||
self.restoreExpandedState()
|
||||
return
|
||||
|
||||
##
|
||||
# Events and Overloads
|
||||
@@ -811,14 +793,12 @@ class GuiProjectTree(QTreeView):
|
||||
self.projView.openDocumentRequest.emit(
|
||||
node.item.itemHandle, nwDocMode.VIEW, "", False
|
||||
)
|
||||
return
|
||||
|
||||
def drawRow(self, painter: QPainter, opt: QStyleOptionViewItem, index: QModelIndex) -> None:
|
||||
"""Draw a box on the active row."""
|
||||
if (node := self._getNode(index)) and node.item.itemHandle == self._actHandle:
|
||||
painter.fillRect(opt.rect, self.palette().alternateBase())
|
||||
super().drawRow(painter, opt, index)
|
||||
return
|
||||
|
||||
##
|
||||
# Public Slots
|
||||
@@ -829,14 +809,12 @@ class GuiProjectTree(QTreeView):
|
||||
"""Move an item up in the tree."""
|
||||
if model := self._getModel():
|
||||
model.internalMove(self.currentIndex(), -1)
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def moveItemDown(self) -> None:
|
||||
"""Move an item down in the tree."""
|
||||
if model := self._getModel():
|
||||
model.internalMove(self.currentIndex(), 1)
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def goToSiblingUp(self) -> None:
|
||||
@@ -844,7 +822,6 @@ class GuiProjectTree(QTreeView):
|
||||
if (node := self._getNode(self.currentIndex())) and (parent := node.parent()):
|
||||
if (move := parent.child(node.row() - 1)) and (model := self._getModel()):
|
||||
self.setCurrentIndex(model.indexFromNode(move))
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def goToSiblingDown(self) -> None:
|
||||
@@ -852,7 +829,6 @@ class GuiProjectTree(QTreeView):
|
||||
if (node := self._getNode(self.currentIndex())) and (parent := node.parent()):
|
||||
if (move := parent.child(node.row() + 1)) and (model := self._getModel()):
|
||||
self.setCurrentIndex(model.indexFromNode(move))
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def goToParent(self) -> None:
|
||||
@@ -863,7 +839,6 @@ class GuiProjectTree(QTreeView):
|
||||
and (parent := node.parent())
|
||||
):
|
||||
self.setCurrentIndex(model.indexFromNode(parent))
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def goToFirstChild(self) -> None:
|
||||
@@ -874,13 +849,11 @@ class GuiProjectTree(QTreeView):
|
||||
and (child := node.child(0))
|
||||
):
|
||||
self.setCurrentIndex(model.indexFromNode(child))
|
||||
return
|
||||
|
||||
@pyqtSlot(QModelIndex)
|
||||
def expandFromIndex(self, index: QModelIndex) -> None:
|
||||
"""Expand all nodes from index."""
|
||||
self.expandRecursively(index)
|
||||
return
|
||||
|
||||
@pyqtSlot(QModelIndex)
|
||||
def collapseFromIndex(self, index: QModelIndex) -> None:
|
||||
@@ -888,7 +861,6 @@ class GuiProjectTree(QTreeView):
|
||||
if (model := self._getModel()) and (node := model.node(index)):
|
||||
for child in node.allChildren():
|
||||
self.setExpanded(model.indexFromNode(child), False)
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def processDeleteRequest(
|
||||
@@ -912,17 +884,23 @@ class GuiProjectTree(QTreeView):
|
||||
if not SHARED.question(self.tr("Permanently delete selected item(s)?")):
|
||||
logger.info("Action cancelled by user")
|
||||
return
|
||||
|
||||
self.setEnabled(False)
|
||||
for index in indices:
|
||||
if node := model.node(index):
|
||||
for child in reversed(node.allChildren()):
|
||||
SHARED.project.removeItem(child.item.itemHandle)
|
||||
SHARED.project.removeItem(node.item.itemHandle)
|
||||
self.setEnabled(True)
|
||||
|
||||
elif trashNode := SHARED.project.tree.trash:
|
||||
if askFirst and not SHARED.question(self.tr("Move selected item(s) to Trash?")):
|
||||
logger.info("Action cancelled by user")
|
||||
return
|
||||
|
||||
self.setEnabled(False)
|
||||
model.multiMove(indices, model.indexFromNode(trashNode))
|
||||
self.setEnabled(True)
|
||||
|
||||
return
|
||||
|
||||
@@ -948,9 +926,7 @@ class GuiProjectTree(QTreeView):
|
||||
@pyqtSlot()
|
||||
@pyqtSlot("QPoint")
|
||||
def openContextMenu(self, point: QPoint | None = None) -> None:
|
||||
"""The user right clicked an element in the project tree, so we
|
||||
open a context menu in-place.
|
||||
"""
|
||||
"""Open a context menu in-place where the user clicked."""
|
||||
if model := self._getModel():
|
||||
if point is None:
|
||||
point = self.visualRect(self.currentIndex()).center()
|
||||
@@ -967,7 +943,6 @@ class GuiProjectTree(QTreeView):
|
||||
if viewport := self.viewport():
|
||||
ctxMenu.exec(viewport.mapToGlobal(point))
|
||||
ctxMenu.setParent(None)
|
||||
return
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
@@ -975,10 +950,9 @@ class GuiProjectTree(QTreeView):
|
||||
|
||||
@pyqtSlot(QModelIndex, QModelIndex)
|
||||
def _onSelectionChange(self, current: QModelIndex, previous: QModelIndex) -> None:
|
||||
"""The user changed which item is selected."""
|
||||
"""Process user changing which item is selected."""
|
||||
if node := self._getNode(current):
|
||||
self.projView.selectedItemChanged.emit(node.item.itemHandle)
|
||||
return
|
||||
|
||||
@pyqtSlot(QModelIndex)
|
||||
def _onDoubleClick(self, index: QModelIndex) -> None:
|
||||
@@ -992,21 +966,18 @@ class GuiProjectTree(QTreeView):
|
||||
)
|
||||
else:
|
||||
self.setExpanded(index, not self.isExpanded(index))
|
||||
return
|
||||
|
||||
@pyqtSlot(QModelIndex)
|
||||
def _onNodeCollapsed(self, index: QModelIndex) -> None:
|
||||
"""Capture a node collapse, and pass it to the model."""
|
||||
if node := self._getNode(index):
|
||||
node.setExpanded(False)
|
||||
return
|
||||
|
||||
@pyqtSlot(QModelIndex)
|
||||
def _onNodeExpanded(self, index: QModelIndex) -> None:
|
||||
"""Capture a node expand, and pass it to the model."""
|
||||
if node := self._getNode(index):
|
||||
node.setExpanded(True)
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
@@ -1018,7 +989,6 @@ class GuiProjectTree(QTreeView):
|
||||
if model := self.selectionModel():
|
||||
# Selection model can be None (#2173)
|
||||
model.clearCurrentIndex()
|
||||
return
|
||||
|
||||
def _selectedRows(self) -> list[QModelIndex]:
|
||||
"""Return all column 0 indexes."""
|
||||
@@ -1046,7 +1016,6 @@ class _UpdatableMenu(QMenu):
|
||||
self._map: dict[str, QAction] = {}
|
||||
self.setTitle(self.tr("From Template"))
|
||||
self.triggered.connect(self._actionTriggered)
|
||||
return
|
||||
|
||||
def __contains__(self, tHandle: str) -> bool:
|
||||
"""Look up a handle in the menu."""
|
||||
@@ -1068,7 +1037,6 @@ class _UpdatableMenu(QMenu):
|
||||
self.addAction(action)
|
||||
self._map[tHandle] = action
|
||||
self.setActionsVisible(True)
|
||||
return
|
||||
|
||||
def remove(self, tHandle: str) -> None:
|
||||
"""Remove a template item."""
|
||||
@@ -1076,19 +1044,16 @@ class _UpdatableMenu(QMenu):
|
||||
self.removeAction(action)
|
||||
if not self._map:
|
||||
self.setActionsVisible(False)
|
||||
return
|
||||
|
||||
def clearMenu(self) -> None:
|
||||
"""Clear all menu content."""
|
||||
self._map.clear()
|
||||
self.clear()
|
||||
return
|
||||
|
||||
def setActionsVisible(self, value: bool) -> None:
|
||||
"""Set the visibility of root action."""
|
||||
if action := self.menuAction():
|
||||
action.setVisible(value)
|
||||
return
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
@@ -1098,7 +1063,6 @@ class _UpdatableMenu(QMenu):
|
||||
def _actionTriggered(self, action: QAction) -> None:
|
||||
"""Translate the menu trigger into an item trigger."""
|
||||
self.menuItemTriggered.emit(str(action.data()))
|
||||
return
|
||||
|
||||
|
||||
class _TreeContextMenu(QMenu):
|
||||
@@ -1119,11 +1083,9 @@ class _TreeContextMenu(QMenu):
|
||||
self._indices = indices
|
||||
self._children = node.childCount() > 0
|
||||
logger.debug("Ready: _TreeContextMenu")
|
||||
return
|
||||
|
||||
def __del__(self) -> None: # pragma: no cover
|
||||
logger.debug("Delete: _TreeContextMenu")
|
||||
return
|
||||
|
||||
##
|
||||
# Methods
|
||||
@@ -1135,7 +1097,6 @@ class _TreeContextMenu(QMenu):
|
||||
action.triggered.connect(self._tree.emptyTrash)
|
||||
if self._children:
|
||||
self._expandCollapse()
|
||||
return
|
||||
|
||||
def buildSingleSelectMenu(self) -> None:
|
||||
"""Build the single-select menu."""
|
||||
@@ -1171,15 +1132,12 @@ class _TreeContextMenu(QMenu):
|
||||
action.triggered.connect(qtLambda(self._tree.duplicateFromHandle, self._handle))
|
||||
self._deleteOrTrash()
|
||||
|
||||
return
|
||||
|
||||
def buildMultiSelectMenu(self) -> None:
|
||||
"""Build the multi-select menu."""
|
||||
self._itemActive()
|
||||
self._itemStatusImport(True)
|
||||
self.addSeparator()
|
||||
self._deleteOrTrash()
|
||||
return
|
||||
|
||||
##
|
||||
# Menu Builders
|
||||
@@ -1197,7 +1155,6 @@ class _TreeContextMenu(QMenu):
|
||||
self._view.openDocumentRequest.emit,
|
||||
self._handle, nwDocMode.VIEW, "", False
|
||||
))
|
||||
return
|
||||
|
||||
def _itemCreation(self) -> None:
|
||||
"""Add create item actions."""
|
||||
@@ -1208,7 +1165,6 @@ class _TreeContextMenu(QMenu):
|
||||
menu.addAction(self._view.projBar.aAddEmpty)
|
||||
menu.addAction(self._view.projBar.aAddNote)
|
||||
menu.addAction(self._view.projBar.aAddFolder)
|
||||
return
|
||||
|
||||
def _itemHeader(self) -> None:
|
||||
"""Check if there is a header that can be used for rename."""
|
||||
@@ -1218,7 +1174,6 @@ class _TreeContextMenu(QMenu):
|
||||
action.triggered.connect(
|
||||
qtLambda(self._view.renameTreeItem, self._handle, hItem.title)
|
||||
)
|
||||
return
|
||||
|
||||
def _itemActive(self) -> None:
|
||||
"""Add Active/Inactive actions."""
|
||||
@@ -1233,7 +1188,6 @@ class _TreeContextMenu(QMenu):
|
||||
else:
|
||||
action = qtAddAction(self, self.tr("Toggle Active"))
|
||||
action.triggered.connect(self._toggleItemActive)
|
||||
return
|
||||
|
||||
def _itemStatusImport(self, multi: bool) -> None:
|
||||
"""Add actions for changing status or importance."""
|
||||
@@ -1275,7 +1229,6 @@ class _TreeContextMenu(QMenu):
|
||||
self._view.projectSettingsRequest.emit,
|
||||
GuiProjectSettings.PAGE_IMPORT
|
||||
))
|
||||
return
|
||||
|
||||
def _itemTransform(self, isFile: bool, isFolder: bool) -> None:
|
||||
"""Add actions for the Transform menu."""
|
||||
@@ -1318,15 +1271,12 @@ class _TreeContextMenu(QMenu):
|
||||
action = qtAddAction(menu, self.tr("Split Document by Headings"))
|
||||
action.triggered.connect(qtLambda(self._tree.splitDocument, self._handle))
|
||||
|
||||
return
|
||||
|
||||
def _expandCollapse(self) -> None:
|
||||
"""Add actions for expand and collapse."""
|
||||
action = qtAddAction(self, self.tr("Expand All"))
|
||||
action.triggered.connect(qtLambda(self._tree.expandFromIndex, self._indices[0]))
|
||||
action = qtAddAction(self, self.tr("Collapse All"))
|
||||
action.triggered.connect(qtLambda(self._tree.collapseFromIndex, self._indices[0]))
|
||||
return
|
||||
|
||||
def _deleteOrTrash(self) -> None:
|
||||
"""Add move to Trash action."""
|
||||
@@ -1339,7 +1289,6 @@ class _TreeContextMenu(QMenu):
|
||||
text = self.tr("Move to Trash")
|
||||
action = qtAddAction(self, text)
|
||||
action.triggered.connect(self._tree.processDeleteRequest)
|
||||
return
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
@@ -1351,7 +1300,6 @@ class _TreeContextMenu(QMenu):
|
||||
if self._item.isFileType():
|
||||
self._item.setActive(not self._item.isActive)
|
||||
self._item.notifyToRefresh()
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
@@ -1365,13 +1313,11 @@ class _TreeContextMenu(QMenu):
|
||||
node.item.setActive(state)
|
||||
refresh.append(node.item.itemHandle)
|
||||
SHARED.project.tree.refreshItems(refresh)
|
||||
return
|
||||
|
||||
def _changeItemStatus(self, key: str) -> None:
|
||||
"""Set a new status value of an item."""
|
||||
self._item.setStatus(key)
|
||||
self._item.notifyToRefresh()
|
||||
return
|
||||
|
||||
def _iterSetItemStatus(self, key: str) -> None:
|
||||
"""Change the status value for multiple items."""
|
||||
@@ -1381,13 +1327,11 @@ class _TreeContextMenu(QMenu):
|
||||
node.item.setStatus(key)
|
||||
refresh.append(node.item.itemHandle)
|
||||
SHARED.project.tree.refreshItems(refresh)
|
||||
return
|
||||
|
||||
def _changeItemImport(self, key: str) -> None:
|
||||
"""Set a new importance value of an item."""
|
||||
self._item.setImport(key)
|
||||
self._item.notifyToRefresh()
|
||||
return
|
||||
|
||||
def _iterSetItemImport(self, key: str) -> None:
|
||||
"""Change the status value for multiple items."""
|
||||
@@ -1397,7 +1341,6 @@ class _TreeContextMenu(QMenu):
|
||||
node.item.setImport(key)
|
||||
refresh.append(node.item.itemHandle)
|
||||
SHARED.project.tree.refreshItems(refresh)
|
||||
return
|
||||
|
||||
def _changeItemLayout(self, itemLayout: nwItemLayout) -> None:
|
||||
"""Set a new item layout value of an item."""
|
||||
@@ -1408,7 +1351,6 @@ class _TreeContextMenu(QMenu):
|
||||
elif itemLayout == nwItemLayout.NOTE:
|
||||
self._item.setLayout(nwItemLayout.NOTE)
|
||||
self._item.notifyToRefresh()
|
||||
return
|
||||
|
||||
def _convertFolderToFile(self, itemLayout: nwItemLayout) -> None:
|
||||
"""Convert a folder to a note or document."""
|
||||
@@ -1428,4 +1370,3 @@ class _TreeContextMenu(QMenu):
|
||||
self._item.notifyToRefresh()
|
||||
else:
|
||||
logger.info("Folder conversion cancelled")
|
||||
return
|
||||
|
||||
@@ -20,7 +20,7 @@ General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
""" # noqa
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
@@ -50,6 +50,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiProjectSearch(QWidget):
|
||||
"""GUI: Project Search Panel."""
|
||||
|
||||
C_NAME = 0
|
||||
C_RESULT = 0
|
||||
@@ -151,8 +152,6 @@ class GuiProjectSearch(QWidget):
|
||||
|
||||
logger.debug("Ready: GuiProjectSearch")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
@@ -174,8 +173,6 @@ class GuiProjectSearch(QWidget):
|
||||
self.toggleWord.setIcon(SHARED.theme.getIcon("search_word"))
|
||||
self.toggleRegEx.setIcon(SHARED.theme.getIcon("search_regex"))
|
||||
|
||||
return
|
||||
|
||||
def processReturn(self) -> None:
|
||||
"""Process a return keypress forwarded from the main GUI."""
|
||||
if self.searchText.hasFocus():
|
||||
@@ -189,7 +186,6 @@ class GuiProjectSearch(QWidget):
|
||||
self.openDocumentSelectRequest.emit(
|
||||
str(data[0]), checkInt(data[1], -1), checkInt(data[2], -1), False
|
||||
)
|
||||
return
|
||||
|
||||
def beginSearch(self, text: str = "") -> None:
|
||||
"""Focus the search box and select its text, if any."""
|
||||
@@ -198,20 +194,17 @@ class GuiProjectSearch(QWidget):
|
||||
if text:
|
||||
self.searchText.setText(text.partition("\n")[0])
|
||||
self.searchText.selectAll()
|
||||
return
|
||||
|
||||
def closeProjectTasks(self) -> None:
|
||||
"""Run close project tasks."""
|
||||
self._map = {}
|
||||
self.searchText.clear()
|
||||
self.searchResult.clear()
|
||||
return
|
||||
|
||||
def refreshCurrentSearch(self) -> None:
|
||||
"""Refresh the search if there is one."""
|
||||
if self.searchResult.topLevelItemCount() > 0:
|
||||
self._processSearch()
|
||||
return
|
||||
|
||||
##
|
||||
# Events
|
||||
@@ -238,7 +231,6 @@ class GuiProjectSearch(QWidget):
|
||||
self.searchText.setFocus()
|
||||
else:
|
||||
super().keyPressEvent(event)
|
||||
return
|
||||
|
||||
##
|
||||
# Public Slots
|
||||
@@ -252,7 +244,6 @@ class GuiProjectSearch(QWidget):
|
||||
results, capped = self._search.searchText(SHARED.mainGui.docEditor.getText())
|
||||
self._displayResultSet(SHARED.project.tree[tHandle], results, capped)
|
||||
logger.debug("Updated search for '%s' in %.3f ms", tHandle, 1000*(time() - start))
|
||||
return
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
@@ -278,7 +269,6 @@ class GuiProjectSearch(QWidget):
|
||||
self._time = time()
|
||||
QApplication.restoreOverrideCursor()
|
||||
self._blocked = False
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _searchResultSelected(self) -> None:
|
||||
@@ -288,7 +278,6 @@ class GuiProjectSearch(QWidget):
|
||||
self.selectedItemChanged.emit(str(data[0]))
|
||||
elif data := items[0].data(0, self.D_HANDLE):
|
||||
self.selectedItemChanged.emit(str(data))
|
||||
return
|
||||
|
||||
@pyqtSlot("QTreeWidgetItem*", int)
|
||||
def _searchResultDoubleClicked(self, item: QTreeWidgetItem, column: int) -> None:
|
||||
@@ -297,28 +286,24 @@ class GuiProjectSearch(QWidget):
|
||||
self.openDocumentSelectRequest.emit(
|
||||
str(data[0]), checkInt(data[1], -1), checkInt(data[2], -1), True
|
||||
)
|
||||
return
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def _toggleCase(self, state: bool) -> None:
|
||||
"""Enable/disable case sensitive mode."""
|
||||
CONFIG.searchProjCase = state
|
||||
self.refreshCurrentSearch()
|
||||
return
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def _toggleWord(self, state: bool) -> None:
|
||||
"""Enable/disable whole word search mode."""
|
||||
CONFIG.searchProjWord = state
|
||||
self.refreshCurrentSearch()
|
||||
return
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def _toggleRegEx(self, state: bool) -> None:
|
||||
"""Enable/disable regular expression search mode."""
|
||||
CONFIG.searchProjRegEx = state
|
||||
self.refreshCurrentSearch()
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
@@ -360,5 +345,3 @@ class GuiProjectSearch(QWidget):
|
||||
self.searchResult.setFirstColumnSpanned(i, parent, True)
|
||||
|
||||
QApplication.processEvents()
|
||||
|
||||
return
|
||||
|
||||
+42
-10
@@ -20,19 +20,20 @@ General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
""" # noqa
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from PyQt6.QtCore import QEvent, QPoint, QSize, pyqtSignal
|
||||
from PyQt6.QtCore import QEvent, QPoint, QSize, pyqtSignal, pyqtSlot
|
||||
from PyQt6.QtWidgets import QMenu, QVBoxLayout, QWidget
|
||||
|
||||
from novelwriter import SHARED
|
||||
from novelwriter import CONFIG, SHARED
|
||||
from novelwriter.common import qtLambda
|
||||
from novelwriter.enum import nwView
|
||||
from novelwriter.constants import nwLabels, trConst
|
||||
from novelwriter.enum import nwTheme, nwView
|
||||
from novelwriter.extensions.eventfilters import StatusTipFilter
|
||||
from novelwriter.extensions.modified import NIconToolButton
|
||||
from novelwriter.gui.theme import STYLES_BIG_TOOLBUTTON
|
||||
@@ -44,6 +45,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiSideBar(QWidget):
|
||||
"""GUI: Main Window SideBar."""
|
||||
|
||||
requestViewChange = pyqtSignal(nwView)
|
||||
|
||||
@@ -77,9 +79,9 @@ class GuiSideBar(QWidget):
|
||||
self.tbOutline.setToolTip("{0} [Ctrl+Shift+T]".format(self.tr("Novel Outline View")))
|
||||
self.tbOutline.clicked.connect(qtLambda(self.requestViewChange.emit, nwView.OUTLINE))
|
||||
|
||||
self.tbBuild = NIconToolButton(self, iSz)
|
||||
self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript")))
|
||||
self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog)
|
||||
self.tbTheme = NIconToolButton(self, iSz)
|
||||
self.tbTheme.setToolTip(self.tr("Switch Colour Theme"))
|
||||
self.tbTheme.clicked.connect(self._cycleColurTheme)
|
||||
|
||||
self.tbDetails = NIconToolButton(self, iSz)
|
||||
self.tbDetails.setToolTip("{0} [Shift+F6]".format(self.tr("Novel Details")))
|
||||
@@ -89,6 +91,10 @@ class GuiSideBar(QWidget):
|
||||
self.tbStats.setToolTip("{0} [F6]".format(self.tr("Writing Statistics")))
|
||||
self.tbStats.clicked.connect(self.mainGui.showWritingStatsDialog)
|
||||
|
||||
self.tbBuild = NIconToolButton(self, iSz)
|
||||
self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript")))
|
||||
self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog)
|
||||
|
||||
# Settings Menu
|
||||
self.tbSettings = NIconToolButton(self, iSz)
|
||||
self.tbSettings.setToolTip(self.tr("Settings"))
|
||||
@@ -111,6 +117,7 @@ class GuiSideBar(QWidget):
|
||||
self.outerBox.addStretch(1)
|
||||
self.outerBox.addWidget(self.tbDetails)
|
||||
self.outerBox.addWidget(self.tbStats)
|
||||
self.outerBox.addWidget(self.tbTheme)
|
||||
self.outerBox.addWidget(self.tbSettings)
|
||||
self.outerBox.setContentsMargins(0, 0, 0, 0)
|
||||
self.outerBox.setSpacing(6)
|
||||
@@ -120,8 +127,6 @@ class GuiSideBar(QWidget):
|
||||
|
||||
logger.debug("Ready: GuiSideBar")
|
||||
|
||||
return
|
||||
|
||||
def updateTheme(self) -> None:
|
||||
"""Initialise GUI elements that depend on specific settings."""
|
||||
buttonStyle = SHARED.theme.getStyleSheet(STYLES_BIG_TOOLBUTTON)
|
||||
@@ -133,6 +138,7 @@ class GuiSideBar(QWidget):
|
||||
self.tbBuild.setStyleSheet(buttonStyle)
|
||||
self.tbDetails.setStyleSheet(buttonStyle)
|
||||
self.tbStats.setStyleSheet(buttonStyle)
|
||||
self.tbTheme.setStyleSheet(buttonStyle)
|
||||
self.tbSettings.setStyleSheet(buttonStyle)
|
||||
|
||||
self.tbProject.setThemeIcon("sb_project")
|
||||
@@ -144,7 +150,33 @@ class GuiSideBar(QWidget):
|
||||
self.tbStats.setThemeIcon("sb_stats")
|
||||
self.tbSettings.setThemeIcon("settings")
|
||||
|
||||
return
|
||||
self._setThemeModeIcon()
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
##
|
||||
|
||||
@pyqtSlot()
|
||||
def _cycleColurTheme(self) -> None:
|
||||
"""Go to nex colour theme."""
|
||||
match CONFIG.themeMode:
|
||||
case nwTheme.AUTO:
|
||||
CONFIG.themeMode = nwTheme.LIGHT
|
||||
case nwTheme.LIGHT:
|
||||
CONFIG.themeMode = nwTheme.DARK
|
||||
case nwTheme.DARK:
|
||||
CONFIG.themeMode = nwTheme.AUTO
|
||||
self.mainGui.checkThemeUpdate()
|
||||
self._setThemeModeIcon()
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _setThemeModeIcon(self) -> None:
|
||||
"""Set the theme button icon."""
|
||||
self.tbTheme.setThemeIcon(nwLabels.THEME_MODE_ICON[CONFIG.themeMode])
|
||||
self.tbTheme.setToolTip(trConst(nwLabels.THEME_MODE_LABEL[CONFIG.themeMode]))
|
||||
|
||||
|
||||
class _PopRightMenu(QMenu):
|
||||
|
||||
@@ -20,7 +20,7 @@ General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
""" # noqa
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
@@ -41,6 +41,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiMainStatus(QStatusBar):
|
||||
"""GUI: Main Window Status Bar."""
|
||||
|
||||
def __init__(self, parent: QWidget) -> None:
|
||||
super().__init__(parent=parent)
|
||||
@@ -112,8 +113,6 @@ class GuiMainStatus(QStatusBar):
|
||||
self.updateTheme()
|
||||
self.clearStatus()
|
||||
|
||||
return
|
||||
|
||||
def initSettings(self) -> None:
|
||||
"""Apply user settings."""
|
||||
if CONFIG.useCharCount:
|
||||
@@ -122,7 +121,6 @@ class GuiMainStatus(QStatusBar):
|
||||
else:
|
||||
self._trStatsCount = trStats(nwLabels.STATS_DISPLAY[nwStats.WORDS])
|
||||
self._trStatsTip = self.tr("Total word count (session change)")
|
||||
return
|
||||
|
||||
def clearStatus(self) -> None:
|
||||
"""Reset all widgets on the status bar to default values."""
|
||||
@@ -132,7 +130,6 @@ class GuiMainStatus(QStatusBar):
|
||||
self.setProjectStatus(None)
|
||||
self.setDocumentStatus(None)
|
||||
self.updateTime()
|
||||
return
|
||||
|
||||
def updateTheme(self) -> None:
|
||||
"""Update theme elements."""
|
||||
@@ -143,14 +140,12 @@ class GuiMainStatus(QStatusBar):
|
||||
self.idlePixmap = SHARED.theme.getPixmap("timer_off", (iPx, iPx))
|
||||
self.timeIcon.setPixmap(self.timePixmap)
|
||||
|
||||
colNone = SHARED.theme.getIconColor("default").darker(150)
|
||||
colSaved = SHARED.theme.getIconColor("green").darker(150)
|
||||
colUnsaved = SHARED.theme.getIconColor("red").darker(150)
|
||||
colNone = SHARED.theme.getBaseColor("default").darker(150)
|
||||
colSaved = SHARED.theme.getBaseColor("green").darker(150)
|
||||
colUnsaved = SHARED.theme.getBaseColor("red").darker(150)
|
||||
self.docIcon.setColors(colNone, colSaved, colUnsaved)
|
||||
self.projIcon.setColors(colNone, colSaved, colUnsaved)
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Setters
|
||||
##
|
||||
@@ -158,17 +153,14 @@ class GuiMainStatus(QStatusBar):
|
||||
def setRefTime(self, refTime: float) -> None:
|
||||
"""Set the reference time for the status bar clock."""
|
||||
self._refTime = refTime
|
||||
return
|
||||
|
||||
def setProjectStatus(self, state: bool | None) -> None:
|
||||
"""Set the project status colour icon."""
|
||||
self.projIcon.setState(state)
|
||||
return
|
||||
|
||||
def setDocumentStatus(self, state: bool | None) -> None:
|
||||
"""Set the document status colour icon."""
|
||||
self.docIcon.setState(state)
|
||||
return
|
||||
|
||||
def setUserIdle(self, idle: bool) -> None:
|
||||
"""Change the idle status icon."""
|
||||
@@ -180,13 +172,11 @@ class GuiMainStatus(QStatusBar):
|
||||
else:
|
||||
self.timeIcon.setPixmap(self.timePixmap)
|
||||
self._userIdle = idle
|
||||
return
|
||||
|
||||
def setProjectStats(self, pWC: int, sWC: int) -> None:
|
||||
"""Update the current project statistics."""
|
||||
self.statsText.setText(self._trStatsCount.format(f"{pWC:n}", f"{sWC:+n}"))
|
||||
self.statsText.setToolTip(self._trStatsTip)
|
||||
return
|
||||
|
||||
def updateTime(self, idleTime: float = 0.0) -> None:
|
||||
"""Update the session clock."""
|
||||
@@ -198,7 +188,6 @@ class GuiMainStatus(QStatusBar):
|
||||
else:
|
||||
sessTime = round(time() - self._refTime)
|
||||
self.timeText.setText(formatTime(sessTime))
|
||||
return
|
||||
|
||||
##
|
||||
# Public Slots
|
||||
@@ -209,7 +198,6 @@ class GuiMainStatus(QStatusBar):
|
||||
"""Set the status bar message to display."""
|
||||
self.showMessage(message, nwConst.STATUS_MSG_TIMEOUT)
|
||||
QApplication.processEvents()
|
||||
return
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def setLanguage(self, language: str, provider: str) -> None:
|
||||
@@ -220,19 +208,16 @@ class GuiMainStatus(QStatusBar):
|
||||
else:
|
||||
self.langText.setText(QLocale(language).nativeLanguageName().title())
|
||||
self.langText.setToolTip(f"{language} ({provider})" if provider else language)
|
||||
return
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def updateProjectStatus(self, status: bool) -> None:
|
||||
"""Update the project status."""
|
||||
self.setProjectStatus(not status)
|
||||
return
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def updateDocumentStatus(self, status: bool) -> None:
|
||||
"""Update the document status."""
|
||||
self.setDocumentStatus(not status)
|
||||
return
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
@@ -244,7 +229,6 @@ class GuiMainStatus(QStatusBar):
|
||||
state = not CONFIG.showSessionTime
|
||||
self.timeText.setVisible(state)
|
||||
CONFIG.showSessionTime = state
|
||||
return
|
||||
|
||||
##
|
||||
# Debug
|
||||
@@ -279,4 +263,3 @@ class GuiMainStatus(QStatusBar):
|
||||
)
|
||||
self.showMessage(f"Debug [{stamp}] {message}", 6000)
|
||||
logger.debug("[MEMINFO] %s", message)
|
||||
return
|
||||
|
||||
+363
-344
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user