Merge branch 'dev' into release_2.2rc1

This commit is contained in:
Veronica Berglyd Olsen
2023-11-26 14:03:05 +01:00
26 changed files with 369 additions and 162 deletions
@@ -1,5 +1,6 @@
{
"Synopsis": "Synopsis",
"Short Description": "Short Description",
"Comment": "Comment",
"Notes": "Notes",
"Tag": "Tag",
+6 -1
View File
@@ -406,6 +406,7 @@ class ProjectBuilder:
chSynop = self.tr("Summary of the chapter.")
scSynop = self.tr("Summary of the scene.")
bfNote = self.tr("A short description.")
# Create chapters
if numChapters > 0:
@@ -446,7 +447,11 @@ class ProjectBuilder:
aHandle = project.newFile(noteTitles[newRoot], rHandle)
ntTag = simplified(noteTitles[newRoot]).replace(" ", "")
aDoc = project.storage.getDocument(aHandle)
aDoc.writeDocument(f"# {noteTitles[newRoot]}\n\n@tag: {ntTag}\n\n")
aDoc.writeDocument(
f"# {noteTitles[newRoot]}\n\n"
f"@tag: {ntTag}\n\n"
f"% Short: {bfNote}\n\n"
)
# Also add the archive and trash folders
project.newRoot(nwItemClass.ARCHIVE)
+22 -10
View File
@@ -36,7 +36,7 @@ from typing import TYPE_CHECKING, ItemsView, Iterable, Iterator
from pathlib import Path
from novelwriter import SHARED
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
from novelwriter.enum import nwComment, nwItemClass, nwItemType, nwItemLayout
from novelwriter.error import logException
from novelwriter.common import checkInt, isHandle, isItemClass, isTitleTag, jsonEncode
from novelwriter.constants import nwFiles, nwKeyWords, nwRegEx, nwUnicode, nwHeaders
@@ -338,14 +338,9 @@ class NWIndex:
elif line.startswith("%"):
if cTitle != TT_NONE:
toCheck = line[1:].lstrip()
synTag = toCheck[:9].lower()
tLen = len(line)
cLen = len(toCheck)
cOff = tLen - cLen
if synTag == "synopsis:":
sText = line[cOff+9:].strip()
self._itemIndex.setHeadingSynopsis(tHandle, cTitle, sText)
cStyle, cText, _ = processComment(line)
if cStyle in (nwComment.SYNOPSIS, nwComment.SHORT):
self._itemIndex.setHeadingSynopsis(tHandle, cTitle, cText)
# Count words for remaining text after last heading
if pTitle != TT_NONE:
@@ -1269,9 +1264,26 @@ class IndexHeading:
# =============================================================================================== #
# Simple Word Counter
# Text Processing Functions
# =============================================================================================== #
CLASSIFIERS = {
"short": nwComment.SHORT,
"synopsis": nwComment.SYNOPSIS,
}
def processComment(text: str) -> tuple[nwComment, str, int]:
"""Extract comment style and text. Should only be called on text
starting with a %.
"""
check = text[1:].lstrip()
classifier, _, content = check.partition(":")
if content and (clean := classifier.strip().lower()) in CLASSIFIERS:
return CLASSIFIERS[clean], content.strip(), text.find(":") + 1
return nwComment.PLAIN, check, 0
def countWords(text: str) -> tuple[int, int, int]:
"""Count words in a piece of text, skipping special syntax and
comments.
+3
View File
@@ -69,6 +69,9 @@ VALID_MAP = {
"GuiManuscriptBuild": {
"winWidth", "winHeight", "fmtWidth", "sumWidth",
},
"GuiDocViewerPanel": {
"colWidths",
}
}
+9 -3
View File
@@ -287,7 +287,10 @@ class ToHtml(Tokenizer):
para.append(stripEscape(tTemp.rstrip()))
elif tType == self.T_SYNOPSIS and self._doSynopsis:
lines.append(self._formatSynopsis(tText))
lines.append(self._formatSynopsis(tText, True))
elif tType == self.T_SHORT and self._doSynopsis:
lines.append(self._formatSynopsis(tText, False))
elif tType == self.T_COMMENT and self._doComments:
lines.append(self._formatComments(tText))
@@ -454,9 +457,12 @@ class ToHtml(Tokenizer):
# Internal Functions
##
def _formatSynopsis(self, text: str) -> str:
def _formatSynopsis(self, text: str, synopsis: bool) -> str:
"""Apply HTML formatting to synopsis."""
sSynop = self._localLookup("Synopsis")
if synopsis:
sSynop = self._localLookup("Synopsis")
else:
sSynop = self._localLookup("Short Description")
if self._genMode == self.M_PREVIEW:
return f"<p class='comment'><span class='synopsis'>{sSynop}:</span> {text}</p>\n"
else:
+24 -17
View File
@@ -34,8 +34,9 @@ from pathlib import Path
from functools import partial
from PyQt5.QtCore import QCoreApplication, QRegularExpression
from novelwriter.core.index import processComment
from novelwriter.enum import nwItemLayout
from novelwriter.enum import nwComment, nwItemLayout
from novelwriter.common import formatTimeStamp, numberToRoman, checkInt
from novelwriter.constants import nwHeadFmt, nwRegEx, nwShortcode, nwUnicode
from novelwriter.core.project import NWProject
@@ -79,17 +80,18 @@ class Tokenizer(ABC):
# Block Type
T_EMPTY = 1 # Empty line (new paragraph)
T_SYNOPSIS = 2 # Synopsis comment
T_COMMENT = 3 # Comment line
T_KEYWORD = 4 # Command line
T_TITLE = 5 # Title
T_UNNUM = 6 # Unnumbered
T_HEAD1 = 7 # Header 1
T_HEAD2 = 8 # Header 2
T_HEAD3 = 9 # Header 3
T_HEAD4 = 10 # Header 4
T_TEXT = 11 # Text line
T_SEP = 12 # Scene separator
T_SKIP = 13 # Paragraph break
T_SHORT = 3 # Short description comment
T_COMMENT = 4 # Comment line
T_KEYWORD = 5 # Command line
T_TITLE = 6 # Title
T_UNNUM = 7 # Unnumbered
T_HEAD1 = 8 # Header 1
T_HEAD2 = 9 # Header 2
T_HEAD3 = 10 # Header 3
T_HEAD4 = 11 # Header 4
T_TEXT = 12 # Text line
T_SEP = 13 # Scene separator
T_SKIP = 14 # Paragraph break
# Block Style
A_NONE = 0x0000 # No special style
@@ -461,17 +463,22 @@ class Tokenizer(ABC):
continue
if aLine[0] == "%":
cLine = aLine[1:].lstrip()
synTag = cLine[:9].lower()
if synTag == "synopsis:":
cStyle, cText, _ = processComment(aLine)
if cStyle == nwComment.SYNOPSIS:
self._tokens.append((
self.T_SYNOPSIS, nHead, cLine[9:].strip(), None, sAlign
self.T_SYNOPSIS, nHead, cText, None, sAlign
))
if self._doSynopsis and self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif cStyle == nwComment.SHORT:
self._tokens.append((
self.T_SHORT, nHead, cText, None, sAlign
))
if self._doSynopsis and self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
else:
self._tokens.append((
self.T_COMMENT, nHead, aLine[1:].strip(), None, sAlign
self.T_COMMENT, nHead, cText, None, sAlign
))
if self._doComments and self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
+4
View File
@@ -170,6 +170,10 @@ class ToMarkdown(Tokenizer):
label = self._localLookup("Synopsis")
lines.append(f"**{label}:** {tText}\n\n")
elif tType == self.T_SHORT and self._doSynopsis:
label = self._localLookup("Short Description")
lines.append(f"**{label}:** {tText}\n\n")
elif tType == self.T_COMMENT and self._doComments:
label = self._localLookup("Comment")
lines.append(f"**{label}:** {tText}\n\n")
+10 -3
View File
@@ -481,7 +481,11 @@ class ToOdt(Tokenizer):
pFmt.append(tFormat)
elif tType == self.T_SYNOPSIS and self._doSynopsis:
tTemp, fTemp = self._formatSynopsis(tText)
tTemp, fTemp = self._formatSynopsis(tText, True)
self._addTextPar("Text_20_Meta", oStyle, tTemp, tFmt=fTemp)
elif tType == self.T_SHORT and self._doSynopsis:
tTemp, fTemp = self._formatSynopsis(tText, False)
self._addTextPar("Text_20_Meta", oStyle, tTemp, tFmt=fTemp)
elif tType == self.T_COMMENT and self._doComments:
@@ -552,9 +556,12 @@ class ToOdt(Tokenizer):
# Internal Functions
##
def _formatSynopsis(self, text: str) -> tuple[str, list[tuple[int, int]]]:
def _formatSynopsis(self, text: str, synopsis: bool) -> tuple[str, list[tuple[int, int]]]:
"""Apply formatting to synopsis lines."""
name = self._localLookup("Synopsis")
if synopsis:
name = self._localLookup("Synopsis")
else:
name = self._localLookup("Short Description")
rTxt = f"{name}: {text}"
rFmt = [(0, self.FMT_B_B), (len(name) + 1, self.FMT_B_E)]
return rTxt, rFmt
+13 -3
View File
@@ -61,6 +61,15 @@ class nwItemLayout(Enum):
# END Enum nwItemLayout
class nwComment(Enum):
PLAIN = 0
SYNOPSIS = 1
SHORT = 2
# END Enum nwComment
class nwTrinary(Enum):
NEGATIVE = -1
@@ -127,9 +136,10 @@ class nwDocInsert(Enum):
QUOTE_LD = 3
QUOTE_RD = 4
SYNOPSIS = 5
NEW_PAGE = 6
VSPACE_S = 7
VSPACE_M = 8
SHORT = 6
NEW_PAGE = 7
VSPACE_S = 8
VSPACE_M = 9
# END Enum nwDocInsert
+4
View File
@@ -834,6 +834,10 @@ class GuiDocEditor(QPlainTextEdit):
text = "% Synopsis: "
newBlock = True
goAfter = True
elif insert == nwDocInsert.SHORT:
text = "% Short: "
newBlock = True
goAfter = True
elif insert == nwDocInsert.NEW_PAGE:
text = "[newpage]"
newBlock = True
+7 -9
View File
@@ -36,6 +36,8 @@ from PyQt5.QtGui import (
from novelwriter import CONFIG, SHARED
from novelwriter.common import checkInt
from novelwriter.constants import nwRegEx, nwUnicode
from novelwriter.core.index import processComment
from novelwriter.enum import nwComment
logger = logging.getLogger(__name__)
@@ -352,16 +354,12 @@ class GuiDocHighlighter(QSyntaxHighlighter):
elif text.startswith("%"): # Comments
self.setCurrentBlockState(self.BLOCK_TEXT)
toCheck = text[1:].lstrip()
synTag = toCheck[:9].lower()
tLen = len(text)
cLen = len(toCheck)
cOff = tLen - cLen
if synTag == "synopsis:":
self.setFormat(0, cOff+9, self._hStyles["modifier"])
self.setFormat(cOff+9, tLen, self._hStyles["hidden"])
cStyle, _, cPos = processComment(text)
if cStyle == nwComment.PLAIN:
self.setFormat(0, len(text), self._hStyles["hidden"])
else:
self.setFormat(0, tLen, self._hStyles["hidden"])
self.setFormat(0, cPos, self._hStyles["modifier"])
self.setFormat(cPos, len(text), self._hStyles["hidden"])
else: # Text Paragraph
+61 -4
View File
@@ -34,9 +34,10 @@ from PyQt5.QtWidgets import (
)
from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwDocMode, nwItemClass
from novelwriter.common import checkInt
from novelwriter.constants import nwHeaders, nwLabels, nwLists, trConst
from novelwriter.core.index import IndexHeading, IndexItem
from novelwriter.enum import nwDocMode, nwItemClass
logger = logging.getLogger(__name__)
@@ -103,6 +104,23 @@ class GuiDocViewerPanel(QWidget):
return
def openProjectTasks(self) -> None:
"""Run open project tasks."""
widths = SHARED.project.options.getValue("GuiDocViewerPanel", "colWidths", {})
if isinstance(widths, dict):
for key, value in widths.items():
if key in self.kwTabs and isinstance(value, list):
self.kwTabs[key].setColumnWidths(value)
return
def closeProjectTasks(self) -> None:
"""Run close project tasks."""
widths = {}
for key, tab in self.kwTabs.items():
widths[key] = tab.getColumnWidths()
SHARED.project.options.setValue("GuiDocViewerPanel", "colWidths", widths)
return
##
# Public Slots
##
@@ -213,6 +231,7 @@ class _ViewPanelBackRefs(QTreeWidget):
# Signals
self.clicked.connect(self._treeItemClicked)
self.doubleClicked.connect(self._treeItemDoubleClicked)
return
@@ -253,6 +272,14 @@ class _ViewPanelBackRefs(QTreeWidget):
self._parent.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", True)
return
@pyqtSlot("QModelIndex")
def _treeItemDoubleClicked(self, index: QModelIndex) -> None:
"""Emit follow tag signal on user double click."""
tHandle = index.siblingAtColumn(self.C_DATA).data(self.D_HANDLE)
if index.column() == self.C_DOC:
self._parent.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", True)
return
##
# Internal Functions
##
@@ -295,6 +322,7 @@ class _ViewPanelKeyWords(QTreeWidget):
C_VIEW = 2
C_DOC = 3
C_TITLE = 4
C_SHORT = 5
D_TAG = Qt.ItemDataRole.UserRole
@@ -307,11 +335,16 @@ class _ViewPanelKeyWords(QTreeWidget):
iPx = SHARED.theme.baseIconSize
cMg = CONFIG.pxInt(6)
self.setHeaderLabels([self.tr("Tag"), "", "", self.tr("Document"), self.tr("Heading")])
self.setHeaderLabels([
self.tr("Tag"), "", "", self.tr("Document"),
self.tr("Heading"), self.tr("Short Description")
])
self.setIndentation(0)
self.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.setIconSize(QSize(iPx, iPx))
self.setFrameStyle(QFrame.Shape.NoFrame)
self.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.setExpandsOnDoubleClick(False)
self.setDragEnabled(False)
self.setSortingEnabled(True)
self.sortByColumn(self.C_NAME, Qt.SortOrder.AscendingOrder)
@@ -321,9 +354,9 @@ class _ViewPanelKeyWords(QTreeWidget):
treeHeader.setSectionResizeMode(self.C_NAME, QHeaderView.ResizeMode.ResizeToContents)
treeHeader.setSectionResizeMode(self.C_EDIT, QHeaderView.ResizeMode.Fixed)
treeHeader.setSectionResizeMode(self.C_VIEW, QHeaderView.ResizeMode.Fixed)
treeHeader.setSectionResizeMode(self.C_DOC, QHeaderView.ResizeMode.ResizeToContents)
treeHeader.resizeSection(self.C_EDIT, iPx + cMg)
treeHeader.resizeSection(self.C_VIEW, iPx + cMg)
treeHeader.setSectionsMovable(False)
# Cache Icons Locally
self._classIcon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[itemClass])
@@ -332,6 +365,7 @@ class _ViewPanelKeyWords(QTreeWidget):
# Signals
self.clicked.connect(self._treeItemClicked)
self.doubleClicked.connect(self._treeItemDoubleClicked)
return
@@ -367,6 +401,7 @@ class _ViewPanelKeyWords(QTreeWidget):
trItem.setText(self.C_DOC, nwItem.itemName)
trItem.setText(self.C_TITLE, hItem.title)
trItem.setData(self.C_TITLE, Qt.ItemDataRole.DecorationRole, hDec)
trItem.setText(self.C_SHORT, hItem.synopsis)
trItem.setData(self.C_DATA, self.D_TAG, tag)
if tag not in self._treeMap:
@@ -383,6 +418,20 @@ class _ViewPanelKeyWords(QTreeWidget):
return True
return False
def setColumnWidths(self, widths: list[int]) -> None:
"""Set the column widths."""
if isinstance(widths, list) and len(widths) >= 2:
self.setColumnWidth(self.C_DOC, CONFIG.pxInt(checkInt(widths[0], 100)))
self.setColumnWidth(self.C_TITLE, CONFIG.pxInt(checkInt(widths[1], 100)))
return
def getColumnWidths(self) -> list[int]:
"""Get the widths of the user-adjustable columns."""
return [
CONFIG.rpxInt(self.columnWidth(self.C_DOC)),
CONFIG.rpxInt(self.columnWidth(self.C_TITLE)),
]
##
# Private Slots
##
@@ -397,4 +446,12 @@ class _ViewPanelKeyWords(QTreeWidget):
self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW)
return
@pyqtSlot("QModelIndex")
def _treeItemDoubleClicked(self, index: QModelIndex) -> None:
"""Emit follow tag signal on user double click."""
tag = index.siblingAtColumn(self.C_DATA).data(self.D_TAG)
if index.column() == self.C_NAME:
self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW)
return
# END Class _ViewPanelKeyWords
+8 -1
View File
@@ -171,7 +171,7 @@ class GuiMainMenu(QMenuBar):
# Project > Delete
self.aDeleteItem = self.projMenu.addAction(self.tr("Delete Item"))
self.aDeleteItem.setShortcuts(["Ctrl+Del", "Ctrl+Shift+Del"]) # Latter is deprecated
self.aDeleteItem.setShortcut("Ctrl+Shift+Del") # Cannot be Ctrl+Del, see #629
self.aDeleteItem.triggered.connect(lambda: self.mainGui.projView.requestDeleteItem(None))
# Project > Empty Trash
@@ -564,6 +564,13 @@ class GuiMainMenu(QMenuBar):
lambda: self.requestDocInsert.emit(nwDocInsert.SYNOPSIS)
)
# Insert > Short Description Comment
self.aInsShort = self.mInsComments.addAction(self.tr("Short Description Comment"))
self.aInsShort.setShortcut("Ctrl+K, U")
self.aInsShort.triggered.connect(
lambda: self.requestDocInsert.emit(nwDocInsert.SHORT)
)
# Insert > Symbols
self.mInsBreaks = self.insMenu.addMenu(self.tr("Page Break and Space"))
+7 -7
View File
@@ -345,11 +345,11 @@ class GuiMain(QMainWindow):
logger.debug("Ready: GUI")
if __hexversion__[-2] == "a" and not CONFIG.isDebug:
SHARED.warn(self.tr(
SHARED.warn(
"You are running an untested development version of novelWriter. "
"Please be careful when working on a live project "
"Please be careful when you are working on live projects "
"and make sure you take regular backups."
))
)
logger.info("novelWriter is ready ...")
self.mainStatus.setStatusMessage(self.tr("novelWriter is ready ..."))
@@ -454,6 +454,7 @@ class GuiMain(QMainWindow):
self.docViewer.clearNavHistory()
self.closeDocViewer(byUser=False)
self.docViewerPanel.closeProjectTasks()
self.outlineView.closeProjectTasks()
self.novelView.closeProjectTasks()
self.projView.clearProjectView()
@@ -527,6 +528,7 @@ class GuiMain(QMainWindow):
self.projView.openProjectTasks()
self.novelView.openProjectTasks()
self.outlineView.openProjectTasks()
self.docViewerPanel.openProjectTasks()
self._updateStatusWordCount()
# Restore previously open documents, if any
@@ -1164,10 +1166,7 @@ class GuiMain(QMainWindow):
"""Capture the closing event of the GUI and call the close
function to handle all the close process steps.
"""
if self.closeMain():
event.accept()
else:
event.ignore()
event.accept() if self.closeMain() else event.ignore()
return
##
@@ -1462,6 +1461,7 @@ class GuiMain(QMainWindow):
self.addAction(self.mainMenu.aInsTimes)
self.addAction(self.mainMenu.aInsDivide)
self.addAction(self.mainMenu.aInsSynopsis)
self.addAction(self.mainMenu.aInsShort)
for mAction, _ in self.mainMenu.mInsKWItems.values():
self.addAction(mAction)