Merge branch 'main' into multi_root
This commit is contained in:
@@ -60,9 +60,9 @@ __license__ = "GPLv3"
|
||||
__author__ = "Veronica Berglyd Olsen"
|
||||
__maintainer__ = "Veronica Berglyd Olsen"
|
||||
__email__ = "code@vkbo.net"
|
||||
__version__ = "1.6-rc1"
|
||||
__hexversion__ = "0x010600c1"
|
||||
__date__ = "2022-02-06"
|
||||
__version__ = "1.7-alpha0"
|
||||
__hexversion__ = "0x010700a0"
|
||||
__date__ = "2022-02-20"
|
||||
__status__ = "Stable"
|
||||
__domain__ = "novelwriter.io"
|
||||
__url__ = "https://novelwriter.io"
|
||||
@@ -209,9 +209,9 @@ def main(sysArgs=None):
|
||||
# Check Packages and Versions
|
||||
errorData = []
|
||||
errorCode = 0
|
||||
if sys.hexversion < 0x030600f0:
|
||||
if sys.hexversion < 0x030700f0:
|
||||
errorData.append(
|
||||
"At least Python 3.6 is required, found %s" % CONFIG.verPyString
|
||||
"At least Python 3.7 is required, found %s" % CONFIG.verPyString
|
||||
)
|
||||
errorCode |= 0x04
|
||||
if CONFIG.verQtValue < 50300:
|
||||
|
||||
@@ -2,16 +2,56 @@
|
||||
<html>
|
||||
<body>
|
||||
|
||||
<h2>Release Notes for 1.6 RC 1</h2>
|
||||
<p><i>Released on 6 February 2022</i></p>
|
||||
<h2>Release Notes for 1.6</h2>
|
||||
<p><i>Released on 20 February 2022</i></p>
|
||||
|
||||
<p>This is a release candidate for the next release version, and is intended for testing purposes.
|
||||
Please be careful when using this version on live writing projects, and make sure you take frequent
|
||||
backups.</p>
|
||||
<p>Please check the changelog for an overview of changes. The full release notes will be added to
|
||||
the final release.</p>
|
||||
<p>This release does not introduce any major new features, but is instead a collection of minor
|
||||
improvements and tweaks based on user requests. There are also a number of changes under the hood
|
||||
to improve the structure and performance of novelWriter.</p>
|
||||
<p><u>Some key improvements to the user interface are:</u></p>
|
||||
<p>✓ The max text width setting in Preferences now also applies to the document viewer, and
|
||||
the setting itself on the Preference dialog has been simplified a bit.</p>
|
||||
<p>✓ When text is selected in the document editor, the number of words selected is displayed
|
||||
in the editor's footer area.</p>
|
||||
<p>✓ The search tool in the document editor now shows the number of results in the
|
||||
document.</p>
|
||||
<p>✓ The Enter and Ctrl+O keyboard shortcuts should now work the same way in all tree
|
||||
views.</p>
|
||||
<p>✓ It is now possible to set a blank section title format on the Build Novel Project tool
|
||||
and get empty paragraphs in the output. Previously, a blank format would just remove the section
|
||||
break entirely. This change allows the user to define hard and soft scene breaks using level three
|
||||
and four headings. The scene and section titles can be hidden completely with two new switches
|
||||
added to the user interface.</p>
|
||||
<p><u>Other feature changes include:</u></p>
|
||||
<p>✓ The project index is now automatically rebuilt in the event it is empty or incomplete
|
||||
when the project is opened.</p>
|
||||
<p>✓ The user can now add their own syntax and GUI theme files in the app folder in their
|
||||
user area on the host operating system. Where the custom files must be added is described in the
|
||||
documentation.</p>
|
||||
<p>✓ A Windows installer is yet again provided for novelWriter. If you have novelWriter
|
||||
installed using another method, make sure you uninstall it properly first as the two methods are
|
||||
not compatible.</p>
|
||||
<p>✓ Release versions for Ubuntu 21.04 have been dropped, and added for the upcoming Ubuntu
|
||||
22.04.</p>
|
||||
<p>✓ Most translations have been updated. A Dutch translation is in the works.</p>
|
||||
|
||||
<p><i>See also the <a href="https://github.com/vkbo/novelWriter/releases">Releases</a> page.</i></p>
|
||||
|
||||
<h2>Patch Notes</h2>
|
||||
|
||||
<h3>Patch 1.6.1 – 16 March 2022</h3>
|
||||
|
||||
<p>This is a bugfix and patch release that fixes two recursion/loop issues. One would potentially
|
||||
cause a crash if the window was resized rapidly, and one would cause a hang with certain search
|
||||
parameters in the editor's search box. The Latin American Spanish translation has also been
|
||||
updated.</p>
|
||||
|
||||
<h3>Patch 1.6.2 – 20 March 2022</h3>
|
||||
|
||||
<p>This is a bugfix release that fixes a couple of minor issues. Projects containing one or more
|
||||
empty documents would trigger a rebuild of the index each time the project was opened. This has now
|
||||
been fixed. Another fix resolves an error message being written to the console logging output when
|
||||
a new document was created. Both errors were harmless.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -241,19 +241,6 @@ def formatTime(tS):
|
||||
return "ERROR"
|
||||
|
||||
|
||||
def parseTimeStamp(theStamp, default, allowNone=False):
|
||||
"""Parses a text representation of a timestamp and converts it into
|
||||
a float. Note that negative timestamps cause an OSError on Windows.
|
||||
See https://bugs.python.org/issue29097
|
||||
"""
|
||||
if str(theStamp).lower() == "none" and allowNone:
|
||||
return None
|
||||
try:
|
||||
return datetime.strptime(theStamp, nwConst.FMT_TSTAMP).timestamp()
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
# String Functions
|
||||
# =============================================================================================== #
|
||||
|
||||
@@ -89,9 +89,10 @@ class NWDoc():
|
||||
|
||||
theText = ""
|
||||
self._docMeta = {}
|
||||
self._prevHash = sha256sum(docPath)
|
||||
self._prevHash = None
|
||||
|
||||
if os.path.isfile(docPath):
|
||||
self._prevHash = sha256sum(docPath)
|
||||
try:
|
||||
with open(docPath, mode="r", encoding="utf-8") as inFile:
|
||||
# Check the first <= 10 lines for metadata
|
||||
|
||||
@@ -112,8 +112,7 @@ class NWIndex():
|
||||
|
||||
theDoc = NWDoc(self.theProject, tHandle)
|
||||
theText = theDoc.readDocument()
|
||||
if theText:
|
||||
self.scanText(tHandle, theText)
|
||||
self.scanText(tHandle, theText if theText is not None else "")
|
||||
|
||||
return True
|
||||
|
||||
@@ -217,18 +216,19 @@ class NWIndex():
|
||||
if theItem.itemType != nwItemType.FILE:
|
||||
logger.info("Not indexing non-file item '%s'", tHandle)
|
||||
return False
|
||||
|
||||
# Run word counter for the whole text
|
||||
cC, wC, pC = countWords(theText)
|
||||
self._fileMeta[tHandle] = ["H0", cC, wC, pC]
|
||||
|
||||
# If the file's meta data is missing, or the file is out of the
|
||||
# main project, we don't index the content
|
||||
if theItem.itemLayout == nwItemLayout.NO_LAYOUT:
|
||||
logger.info("Not indexing no-layout item '%s'", tHandle)
|
||||
return False
|
||||
if theItem.itemParent is None:
|
||||
logger.info("Not indexing orphaned item '%s'", tHandle)
|
||||
return False
|
||||
|
||||
# Run word counter for the whole text
|
||||
cC, wC, pC = countWords(theText)
|
||||
self._fileMeta[tHandle] = ["H0", cC, wC, pC]
|
||||
|
||||
# If the file is archived or in trash, we don't index the content
|
||||
if self.theProject.projTree.isTrashRoot(theItem.itemParent):
|
||||
logger.debug("Not indexing trash item '%s'", tHandle)
|
||||
return False
|
||||
|
||||
+40
-23
@@ -139,24 +139,32 @@ class NWItem():
|
||||
def packXML(self, xParent):
|
||||
"""Pack all the data in the class instance into an XML object.
|
||||
"""
|
||||
xPack = etree.SubElement(xParent, "item", attrib={
|
||||
"handle": str(self._handle),
|
||||
"order": str(self._order),
|
||||
"parent": str(self._parent),
|
||||
})
|
||||
self._subPack(xPack, "name", text=str(self._name))
|
||||
self._subPack(xPack, "type", text=str(self._type.name))
|
||||
self._subPack(xPack, "class", text=str(self._class.name))
|
||||
self._subPack(xPack, "status", text=str(self._status))
|
||||
itemAttrib = {}
|
||||
itemAttrib["handle"] = str(self._handle)
|
||||
itemAttrib["parent"] = str(self._parent)
|
||||
itemAttrib["order"] = str(self._order)
|
||||
itemAttrib["type"] = str(self._type.name)
|
||||
itemAttrib["class"] = str(self._class.name)
|
||||
if self._type == nwItemType.FILE:
|
||||
self._subPack(xPack, "exported", text=str(self._exported))
|
||||
self._subPack(xPack, "layout", text=str(self._layout.name))
|
||||
self._subPack(xPack, "charCount", text=str(self._charCount), none=False)
|
||||
self._subPack(xPack, "wordCount", text=str(self._wordCount), none=False)
|
||||
self._subPack(xPack, "paraCount", text=str(self._paraCount), none=False)
|
||||
self._subPack(xPack, "cursorPos", text=str(self._cursorPos), none=False)
|
||||
itemAttrib["layout"] = str(self._layout.name)
|
||||
|
||||
metaAttrib = {}
|
||||
if self._type == nwItemType.FILE:
|
||||
metaAttrib["charCount"] = str(self._charCount)
|
||||
metaAttrib["wordCount"] = str(self._wordCount)
|
||||
metaAttrib["paraCount"] = str(self._paraCount)
|
||||
metaAttrib["cursorPos"] = str(self._cursorPos)
|
||||
else:
|
||||
self._subPack(xPack, "expanded", text=str(self._expanded))
|
||||
metaAttrib["expanded"] = str(self._expanded)
|
||||
|
||||
nameAttrib = {}
|
||||
nameAttrib["status"] = str(self._status)
|
||||
if self._type == nwItemType.FILE:
|
||||
nameAttrib["exported"] = str(self._exported)
|
||||
|
||||
xPack = etree.SubElement(xParent, "item", attrib=itemAttrib)
|
||||
self._subPack(xPack, "meta", attrib=metaAttrib)
|
||||
self._subPack(xPack, "name", text=str(self._name), attrib=nameAttrib)
|
||||
|
||||
return
|
||||
|
||||
@@ -175,19 +183,31 @@ class NWItem():
|
||||
|
||||
self.setParent(xItem.attrib.get("parent", None))
|
||||
self.setOrder(xItem.attrib.get("order", 0))
|
||||
self.setType(xItem.attrib.get("type", nwItemType.NO_TYPE))
|
||||
self.setClass(xItem.attrib.get("class", nwItemClass.NO_CLASS))
|
||||
self.setLayout(xItem.attrib.get("layout", nwItemLayout.NO_LAYOUT))
|
||||
|
||||
tmpStatus = ""
|
||||
for xValue in xItem:
|
||||
if xValue.tag == "name":
|
||||
if xValue.tag == "meta":
|
||||
self.setExpanded(xValue.attrib.get("expanded", False))
|
||||
self.setCharCount(xValue.attrib.get("charCount", 0))
|
||||
self.setWordCount(xValue.attrib.get("wordCount", 0))
|
||||
self.setParaCount(xValue.attrib.get("paraCount", 0))
|
||||
self.setCursorPos(xValue.attrib.get("cursorPos", 0))
|
||||
elif xValue.tag == "name":
|
||||
self.setName(xValue.text)
|
||||
self.setStatus(xValue.attrib.get("status", None))
|
||||
self.setExported(xValue.attrib.get("exported", True))
|
||||
|
||||
# Legacy Format (1.3 and earlier)
|
||||
elif xValue.tag == "status":
|
||||
self.setStatus(xValue.text)
|
||||
elif xValue.tag == "type":
|
||||
self.setType(xValue.text)
|
||||
elif xValue.tag == "class":
|
||||
self.setClass(xValue.text)
|
||||
elif xValue.tag == "layout":
|
||||
self.setLayout(xValue.text)
|
||||
elif xValue.tag == "status":
|
||||
tmpStatus = xValue.text
|
||||
elif xValue.tag == "expanded":
|
||||
self.setExpanded(xValue.text)
|
||||
elif xValue.tag == "exported":
|
||||
@@ -206,9 +226,6 @@ class NWItem():
|
||||
# version of novelWriter that doesn't know the tag
|
||||
logger.error("Unknown tag '%s'", xValue.tag)
|
||||
|
||||
# Guarantees that <status> is parsed after <class>
|
||||
self.setStatus(tmpStatus)
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -53,7 +53,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class NWProject():
|
||||
|
||||
FILE_VERSION = "1.3"
|
||||
FILE_VERSION = "1.4"
|
||||
|
||||
def __init__(self, theParent):
|
||||
|
||||
@@ -474,8 +474,11 @@ class NWProject():
|
||||
# 1.3 : Reduces the number of layouts to only two. One for novel
|
||||
# documents and one for project notes. Introduced in
|
||||
# version 1.5.
|
||||
# 1.4 : Introduces a more compact format for storing items. All
|
||||
# settings aside from name are now attributes. Introduced
|
||||
# in version 1.7.
|
||||
|
||||
if fileVersion not in ("1.0", "1.1", "1.2", "1.3"):
|
||||
if fileVersion not in ("1.0", "1.1", "1.2", "1.3", "1.4"):
|
||||
self.theParent.makeAlert(self.tr(
|
||||
"Unknown or unsupported novelWriter project file format. "
|
||||
"The project cannot be opened by this version of novelWriter. "
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
novelWriter – Text Tokenizer
|
||||
============================
|
||||
Splits a piece of novelWriter markdown text into its elements
|
||||
Split novelWriter plain text into its elements
|
||||
|
||||
File History:
|
||||
Created: 2019-05-05 [0.0.1]
|
||||
@@ -27,6 +27,7 @@ import re
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from operator import itemgetter
|
||||
from functools import partial
|
||||
|
||||
@@ -40,7 +41,7 @@ from novelwriter.core.document import NWDoc
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Tokenizer():
|
||||
class Tokenizer(ABC):
|
||||
|
||||
# In-Text Format
|
||||
FMT_B_B = 1 # Begin bold
|
||||
@@ -267,6 +268,10 @@ class Tokenizer():
|
||||
# Class Methods
|
||||
##
|
||||
|
||||
@abstractmethod
|
||||
def doConvert(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def addRootHeading(self, theHandle):
|
||||
"""Add a heading at the start of a new root folder.
|
||||
"""
|
||||
|
||||
@@ -330,7 +330,7 @@ class ToOdt(Tokenizer):
|
||||
|
||||
# Meta Data
|
||||
xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "creation-date"))
|
||||
xMeta.text = datetime.now().strftime(r"%Y-%m-%dT%H:%M:%S")
|
||||
xMeta.text = datetime.now().isoformat(sep="T", timespec="seconds")
|
||||
|
||||
xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "generator"))
|
||||
xMeta.text = f"novelWriter/{novelwriter.__version__}"
|
||||
|
||||
@@ -187,6 +187,7 @@ class GuiAbout(QDialog):
|
||||
("Português", "Bruno Meneguello"),
|
||||
("简体中文", "Qianzhi Long"),
|
||||
("Español Latinoamericano", "Tommy Marplatt"),
|
||||
("Nederlands", "Martijn van der Kleijn"),
|
||||
])
|
||||
)
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import random
|
||||
import logging
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
@@ -86,6 +87,13 @@ class NWErrorMessage(QDialog):
|
||||
self.mainBox.addWidget(self.btnBox, 2, 0, 1, 2)
|
||||
self.mainBox.setSpacing(16)
|
||||
|
||||
self.setWindowTitle([
|
||||
"+++ Out of Cheese Error +++",
|
||||
"+++ Divide by Cucumber Error +++",
|
||||
"+++ Whoops! Here Comes The Cheese! +++",
|
||||
"+++ Please Reinstall Universe and Reboot +++",
|
||||
][random.randint(0, 3)])
|
||||
|
||||
self.setLayout(self.mainBox)
|
||||
|
||||
self.setMinimumWidth(500)
|
||||
|
||||
@@ -1061,8 +1061,8 @@ class GuiDocEditor(QTextEdit):
|
||||
"""If the text editor is resized, we must make sure the document
|
||||
has its margins adjusted according to user preferences.
|
||||
"""
|
||||
QTextEdit.resizeEvent(self, theEvent)
|
||||
self.updateDocMargins()
|
||||
QTextEdit.resizeEvent(self, theEvent)
|
||||
return
|
||||
|
||||
##
|
||||
@@ -1436,10 +1436,16 @@ class GuiDocEditor(QTextEdit):
|
||||
theCursor.setPosition(0)
|
||||
self.setTextCursor(theCursor)
|
||||
|
||||
while self.find(searchFor, findOpt):
|
||||
# Search up to a maximum of 1000, and make sure certain special
|
||||
# searches like a regex search for .* turns into an infinite loop
|
||||
while self.find(searchFor, findOpt) and len(resE) <= 1000:
|
||||
theCursor = self.textCursor()
|
||||
resS.append(theCursor.selectionStart())
|
||||
resE.append(theCursor.selectionEnd())
|
||||
if theCursor.hasSelection():
|
||||
resS.append(theCursor.selectionStart())
|
||||
resE.append(theCursor.selectionEnd())
|
||||
else:
|
||||
logger.warning("The search returned an empty result")
|
||||
break
|
||||
|
||||
if hasSelection:
|
||||
theCursor.setPosition(origA, QTextCursor.MoveAnchor)
|
||||
@@ -2410,7 +2416,7 @@ class GuiDocEditSearch(QFrame):
|
||||
"""Set the count values for the current search.
|
||||
"""
|
||||
currRes = "?" if currRes is None else currRes
|
||||
resCount = "?" if resCount is None else resCount
|
||||
resCount = "?" if resCount is None else "1000+" if resCount > 1000 else resCount
|
||||
minWidth = self.theTheme.getTextWidth(f"{resCount}//{resCount}", self.boxFont)
|
||||
self.resultLabel.setText(f"{currRes}/{resCount}")
|
||||
self.resultLabel.setMinimumWidth(minWidth)
|
||||
|
||||
@@ -471,10 +471,11 @@ class GuiDocViewer(QTextBrowser):
|
||||
##
|
||||
|
||||
def resizeEvent(self, theEvent):
|
||||
"""Make sure the document title is the same width as the window.
|
||||
"""If the text editor is resized, we must make sure the document
|
||||
has its margins adjusted according to user preferences.
|
||||
"""
|
||||
QTextBrowser.resizeEvent(self, theEvent)
|
||||
self.updateDocMargins()
|
||||
QTextBrowser.resizeEvent(self, theEvent)
|
||||
return
|
||||
|
||||
def mouseReleaseEvent(self, theEvent):
|
||||
|
||||
@@ -457,12 +457,8 @@ class GuiWritingStats(QDialog):
|
||||
if len(inData) < 6:
|
||||
continue
|
||||
|
||||
dStart = datetime.strptime(
|
||||
"%s %s" % (inData[0], inData[1]), nwConst.FMT_TSTAMP
|
||||
)
|
||||
dEnd = datetime.strptime(
|
||||
"%s %s" % (inData[2], inData[3]), nwConst.FMT_TSTAMP
|
||||
)
|
||||
dStart = datetime.fromisoformat(" ".join(inData[0:2]))
|
||||
dEnd = datetime.fromisoformat(" ".join(inData[2:4]))
|
||||
|
||||
sIdle = 0
|
||||
if len(inData) > 6:
|
||||
|
||||
Reference in New Issue
Block a user