diff --git a/novelwriter/common.py b/novelwriter/common.py index e0c2dc67..60d1f7fa 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -216,25 +216,25 @@ def formatInt(value): return str(value) -def formatTimeStamp(theTime, fileSafe=False): +def formatTimeStamp(value, fileSafe=False): """Take a number (on the format returned by time.time()) and convert it to a timestamp string. """ if fileSafe: - return datetime.fromtimestamp(theTime).strftime(nwConst.FMT_FSTAMP) + return datetime.fromtimestamp(value).strftime(nwConst.FMT_FSTAMP) else: - return datetime.fromtimestamp(theTime).strftime(nwConst.FMT_TSTAMP) + return datetime.fromtimestamp(value).strftime(nwConst.FMT_TSTAMP) -def formatTime(tS): +def formatTime(t): """Format a time in seconds in HH:MM:SS format or d-HH:MM:SS format if a full day or longer. """ - if isinstance(tS, int): - if tS >= 86400: - return f"{tS//86400:d}-{tS%86400//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}" + if isinstance(t, int): + if t >= 86400: + return f"{t//86400:d}-{t%86400//3600:02d}:{t%3600//60:02d}:{t%60:02d}" else: - return f"{tS//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}" + return f"{t//3600:02d}:{t%3600//60:02d}:{t%60:02d}" return "ERROR" @@ -276,111 +276,111 @@ def splitVersionNumber(value): return vMajor, vMinor, vPatch, vInt -def transferCase(theSource, theTarget): +def transferCase(source, target): """Transfers the case of the source word to the target word. This will consider all upper or lower, and first char capitalisation. """ - theResult = theTarget + theResult = target - if not isinstance(theSource, str) or not isinstance(theTarget, str): + if not isinstance(source, str) or not isinstance(target, str): return theResult - if len(theTarget) < 1 or len(theSource) < 1: + if len(target) < 1 or len(source) < 1: return theResult - if theSource.istitle(): - theResult = theTarget.title() + if source.istitle(): + theResult = target.title() - if theSource.isupper(): - theResult = theTarget.upper() - elif theSource.islower(): - theResult = theTarget.lower() + if source.isupper(): + theResult = target.upper() + elif source.islower(): + theResult = target.lower() return theResult -def fuzzyTime(secDiff): +def fuzzyTime(seconds): """Converts a time difference in seconds into a fuzzy time string. """ - if secDiff < 0: + if seconds < 0: return QCoreApplication.translate( "Common", "in the future" ) - elif secDiff < 30: + elif seconds < 30: return QCoreApplication.translate( "Common", "just now" ) - elif secDiff < 90: + elif seconds < 90: return QCoreApplication.translate( "Common", "a minute ago" ) - elif secDiff < 3300: # 55 minutes + elif seconds < 3300: # 55 minutes return QCoreApplication.translate( "Common", "{0} minutes ago" - ).format(int(round(secDiff/60))) - elif secDiff < 5400: # 90 minutes + ).format(int(round(seconds/60))) + elif seconds < 5400: # 90 minutes return QCoreApplication.translate( "Common", "an hour ago" ) - elif secDiff < 84600: # 23.5 hours + elif seconds < 84600: # 23.5 hours return QCoreApplication.translate( "Common", "{0} hours ago" - ).format(int(round(secDiff/3600))) - elif secDiff < 129600: # 1.5 days + ).format(int(round(seconds/3600))) + elif seconds < 129600: # 1.5 days return QCoreApplication.translate( "Common", "a day ago" ) - elif secDiff < 561600: # 6.5 days + elif seconds < 561600: # 6.5 days return QCoreApplication.translate( "Common", "{0} days ago" - ).format(int(round(secDiff/86400))) - elif secDiff < 907200: # 10.5 days + ).format(int(round(seconds/86400))) + elif seconds < 907200: # 10.5 days return QCoreApplication.translate( "Common", "a week ago" ) - elif secDiff < 2419200: # 28 days + elif seconds < 2419200: # 28 days return QCoreApplication.translate( "Common", "{0} weeks ago" - ).format(int(round(secDiff/604800))) - elif secDiff < 3888000: # 45 days + ).format(int(round(seconds/604800))) + elif seconds < 3888000: # 45 days return QCoreApplication.translate( "Common", "a month ago" ) - elif secDiff < 29808000: # 345 days + elif seconds < 29808000: # 345 days return QCoreApplication.translate( "Common", "{0} months ago" - ).format(int(round(secDiff/2592000))) - elif secDiff < 47336400: # 1.5 years + ).format(int(round(seconds/2592000))) + elif seconds < 47336400: # 1.5 years return QCoreApplication.translate( "Common", "a year ago" ) else: return QCoreApplication.translate( "Common", "{0} years ago" - ).format(int(round(secDiff/31557600))) + ).format(int(round(seconds/31557600))) -def numberToRoman(numVal, toLower=False): +def numberToRoman(value, toLower=False): """Convert an integer to a Roman number. """ - if not isinstance(numVal, int): + if not isinstance(value, int): return "NAN" - if numVal < 1 or numVal > 4999: + if value < 1 or value > 4999: return "OOR" - theValues = [ + lookup = [ (1000, "M"), (900, "CM"), (500, "D"), (400, "CD"), (100, "C"), (90, "XC"), (50, "L"), (40, "XL"), (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"), ] - romNum = "" - for theDiv, theSym in theValues: - n = numVal//theDiv - romNum += n*theSym - numVal -= n*theDiv - if numVal <= 0: + roman = "" + for divisor, symbol in lookup: + n = value//divisor + roman += n*symbol + value -= n*divisor + if value <= 0: break - return romNum.lower() if toLower else romNum + return roman.lower() if toLower else roman # =============================================================================================== # @@ -438,70 +438,70 @@ def jsonEncode(data, n=0, nmax=0): # File and File System Functions # =============================================================================================== # -def readTextFile(filePath): +def readTextFile(path): """Read the content of a text file in a robust manner. """ - if not os.path.isfile(filePath): + if not os.path.isfile(path): return "" - fileText = "" + text = "" try: - with open(filePath, mode="r", encoding="utf-8") as inFile: - fileText = inFile.read() + with open(path, mode="r", encoding="utf-8") as inFile: + text = inFile.read() except Exception: - logger.error("Could not read file: %s", filePath) + logger.error("Could not read file: %s", path) logException() return "" - return fileText + return text def makeFileNameSafe(value): """Returns a filename safe string of the value. """ - cleanName = "" + clean = "" for c in str(value).strip(): if c.isalpha() or c.isdigit() or c == " ": - cleanName += c - return cleanName + clean += c + return clean -def ensureFolder(dirPath, parentPath=None, errLog=None): +def ensureFolder(path, parent=None, errLog=None): """Make sure a folder exists, and if it doesn't, create it. """ try: - if parentPath: - dirPath = os.path.join(parentPath, dirPath) - if not os.path.isdir(dirPath): - os.mkdir(dirPath) + if parent: + path = os.path.join(parent, path) + if not os.path.isdir(path): + os.mkdir(path) except Exception as exc: - logger.error("Could not create folder: %s", dirPath) + logger.error("Could not create folder: %s", path) logException() if isinstance(errLog, list): - errLog.append(f"Could not create folder: {dirPath}") + errLog.append(f"Could not create folder: {path}") errLog.append(formatException(exc)) return False return True -def sha256sum(filePath): +def sha256sum(path): """Make a shasum of a file using a buffer. Based on: https://stackoverflow.com/a/44873382/5825851 """ - hDigest = hashlib.sha256() + digest = hashlib.sha256() bData = bytearray(65536) mData = memoryview(bData) try: - with open(filePath, mode="rb", buffering=0) as inFile: + with open(path, mode="rb", buffering=0) as inFile: for n in iter(lambda: inFile.readinto(mData), 0): - hDigest.update(mData[:n]) + digest.update(mData[:n]) except Exception: - logger.error("Could not create sha256sum of: %s", filePath) + logger.error("Could not create sha256sum of: %s", path) logException() return None - return hDigest.hexdigest() + return digest.hexdigest() # =============================================================================================== # @@ -523,87 +523,59 @@ def getGuiItem(objName): class NWConfigParser(ConfigParser): - CNF_STR = 0 - CNF_INT = 1 - CNF_FLOAT = 2 - CNF_BOOL = 3 - CNF_S_LST = 4 - CNF_I_LST = 5 - def __init__(self): super().__init__() def rdStr(self, section, option, default): """Read string value. """ - return self._parseLine(section, option, default, self.CNF_STR) + return self.get(section, option, fallback=default) def rdInt(self, section, option, default): """Read integer value. """ - return self._parseLine(section, option, default, self.CNF_INT) + try: + return self.getint(section, option, fallback=default) + except ValueError: + logger.error("Could not read '%s':'%s' from config", section, option) + return default def rdFlt(self, section, option, default): """Read float value. """ - return self._parseLine(section, option, default, self.CNF_FLOAT) + try: + return self.getfloat(section, option, fallback=default) + except ValueError: + logger.error("Could not read '%s':'%s' from config", section, option) + return default def rdBool(self, section, option, default): """Read boolean value. """ - return self._parseLine(section, option, default, self.CNF_BOOL) + try: + return self.getboolean(section, option, fallback=default) + except ValueError: + logger.error("Could not read '%s':'%s' from config", section, option) + return default def rdStrList(self, section, option, default): """Read string list. """ - return self._parseLine(section, option, default, self.CNF_S_LST) + result = default.copy() if isinstance(default, list) else [] + if self.has_option(section, option): + data = self.get(section, option, fallback="").split(",") + for i in range(min(len(data), len(result))): + result[i] = data[i].strip() + return result def rdIntList(self, section, option, default): """Read integer list. """ - return self._parseLine(section, option, default, self.CNF_I_LST) - - ## - # Internal Functions - ## - - def _unpackList(self, value, default, type): - """Unpack a comma-separated string of items into a list. - """ - inList = value.split(",") - outList = [] - if isinstance(default, list): - outList = default.copy() - for i in range(min(len(inList), len(outList))): - try: - if type == self.CNF_S_LST: - outList[i] = inList[i].strip() - elif type == self.CNF_I_LST: - outList[i] = int(inList[i].strip()) - except Exception: - continue - return outList - - def _parseLine(self, section, option, default, type): - """Parse a line and return the correct datatype. - """ + result = default.copy() if isinstance(default, list) else [] if self.has_option(section, option): - try: - if type == self.CNF_STR: - return self.get(section, option) - elif type == self.CNF_INT: - return self.getint(section, option) - elif type == self.CNF_FLOAT: - return self.getfloat(section, option) - elif type == self.CNF_BOOL: - return self.getboolean(section, option) - elif type in (self.CNF_I_LST, self.CNF_S_LST): - return self._unpackList(self.get(section, option), default, type) - except ValueError: - logger.error("Could not read '%s':'%s' from config", str(section), str(option)) - logException() - return default - - return default + data = self.get(section, option, fallback="").split(",") + for i in range(min(len(data), len(result))): + result[i] = checkInt(data[i].strip(), result[i]) + return result # END Class NWConfigParser diff --git a/novelwriter/config.py b/novelwriter/config.py index e0a24e65..73a8c3c6 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -52,12 +52,11 @@ class Config: # Set Application Variables self.appName = "novelWriter" - self.appHandle = self.appName.lower() + self.appHandle = "novelwriter" # Set Paths self.cmdOpen = None # Path from command line for project to be opened on launch self.confPath = None # Folder where the config is saved - self.confFile = None # The config file name self.dataPath = None # Folder where app data is stored self.lastPath = None # The last user-selected folder (browse dialogs) self.appPath = None # The full path to the novelwriter package folder @@ -271,7 +270,6 @@ class Config: logger.debug("Config path: %s", self.confPath) logger.debug("Data path: %s", self.dataPath) - self.confFile = self.appHandle+".conf" self.lastPath = os.path.expanduser("~") self.appPath = getattr(sys, "_MEIPASS", os.path.abspath(os.path.dirname(__file__))) self.appRoot = os.path.abspath(os.path.join(self.appPath, os.path.pardir)) @@ -306,12 +304,12 @@ class Config: # We don't error on these failing since they are not essential if self.dataPath is not None: - ensureFolder("syntax", parentPath=self.dataPath) - ensureFolder("themes", parentPath=self.dataPath) + ensureFolder("syntax", parent=self.dataPath) + ensureFolder("themes", parent=self.dataPath) # Check if config file exists if self.confPath is not None: - if os.path.isfile(os.path.join(self.confPath, self.confFile)): + if os.path.isfile(os.path.join(self.confPath, nwFiles.CONF_FILE)): # If it exists, load it self.loadConfig() else: @@ -396,7 +394,7 @@ class Config: return False theConf = NWConfigParser() - cnfPath = os.path.join(self.confPath, self.confFile) + cnfPath = os.path.join(self.confPath, nwFiles.CONF_FILE) try: with open(cnfPath, mode="r", encoding="utf-8") as inFile: theConf.read_file(inFile) @@ -619,7 +617,7 @@ class Config: } # Write config file - cnfPath = os.path.join(self.confPath, self.confFile) + cnfPath = os.path.join(self.confPath, nwFiles.CONF_FILE) try: with open(cnfPath, mode="w", encoding="utf-8") as outFile: theConf.write(outFile) diff --git a/novelwriter/constants.py b/novelwriter/constants.py index 2cd5b15c..87293a52 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -68,6 +68,7 @@ class nwHeaders: class nwFiles: + CONF_FILE = "novelwriter.conf" PROJ_FILE = "nwProject.nwx" PROJ_DICT = "wordlist.txt" PROJ_LOCK = "nwProject.lock" diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index b7f81370..66f9617b 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -85,7 +85,6 @@ class NWProject: self.projDict = None # The spell check dictionary self.projSpell = None # The spell check language, if different than default self.projLang = None # The project language, used for builds - self.projFile = None # The file name of the project main XML file self.projFiles = [] # A list of all files in the content folder on load # Project Meta @@ -260,7 +259,6 @@ class NWProject: self.projDict = None self.projSpell = None self.projLang = None - self.projFile = nwFiles.PROJ_FILE self.projFiles = [] self.projName = "" self.bookTitle = "" @@ -774,9 +772,9 @@ class NWProject: self._projTree.packXML(nwXML) # Write the xml tree to file - tempFile = os.path.join(self.projPath, self.projFile+"~") - saveFile = os.path.join(self.projPath, self.projFile) - backFile = os.path.join(self.projPath, self.projFile[:-3]+"bak") + tempFile = os.path.join(self.projPath, nwFiles.PROJ_FILE+"~") + saveFile = os.path.join(self.projPath, nwFiles.PROJ_FILE) + backFile = os.path.join(self.projPath, nwFiles.PROJ_FILE[:-3]+"bak") try: with open(tempFile, mode="wb") as outFile: outFile.write(etree.tostring( @@ -816,7 +814,7 @@ class NWProject: return True - def closeProject(self, idleTime=0): + def closeProject(self, idleTime=0.0): """Close the current project and clear all meta data. """ logger.info("Closing project: %s", self.projPath) @@ -1185,7 +1183,7 @@ class NWProject: information to the GUI statusbar. """ self.projChanged = bValue - self.mainGui.statusBar.doUpdateProjectStatus(bValue) + self.mainGui.mainStatus.doUpdateProjectStatus(bValue) if bValue: # If we've changed the project at all, this should be True self.projAltered = True diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py index 9bc822d4..b6fd8381 100644 --- a/novelwriter/dialogs/docmerge.py +++ b/novelwriter/dialogs/docmerge.py @@ -117,7 +117,7 @@ class GuiDocMerge(QDialog): finalItems = [] for i in range(self.listBox.count()): item = self.listBox.item(i) - if item.checkState() == Qt.Checked: + if item is not None and item.checkState() == Qt.Checked: finalItems.append(item.data(Qt.UserRole)) self._data["moveToTrash"] = self.trashSwitch.isChecked() diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py index 74bdda40..1b6a5f70 100644 --- a/novelwriter/dialogs/docsplit.py +++ b/novelwriter/dialogs/docsplit.py @@ -145,9 +145,12 @@ class GuiDocSplit(QDialog): headerList = [] for i in range(self.listBox.count()): item = self.listBox.item(i) - headerList.append( - (item.data(self.LINE_ROLE), item.data(self.LEVEL_ROLE), item.data(self.LABEL_ROLE)) - ) + if item is not None: + headerList.append(( + item.data(self.LINE_ROLE), + item.data(self.LEVEL_ROLE), + item.data(self.LABEL_ROLE), + )) spLevel = self.splitLevel.currentData() intoFolder = self.folderSwitch.isChecked() diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index 2d6a35a6..86c268d6 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -365,11 +365,12 @@ class GuiProjectEditStatus(QWidget): newList = [] for n in range(self.listBox.topLevelItemCount()): item = self.listBox.topLevelItem(n) - newList.append({ - "key": item.data(self.COL_LABEL, self.KEY_ROLE), - "name": item.text(self.COL_LABEL), - "cols": item.data(self.COL_LABEL, self.COL_ROLE), - }) + if item is not None: + newList.append({ + "key": item.data(self.COL_LABEL, self.KEY_ROLE), + "name": item.text(self.COL_LABEL), + "cols": item.data(self.COL_LABEL, self.COL_ROLE), + }) return newList, self.colDeleted return [], [] @@ -468,7 +469,8 @@ class GuiProjectEditStatus(QWidget): self.listBox.insertTopLevelItem(nIndex, cItem) self.listBox.clearSelection() - cItem.setSelected(True) + if cItem is not None: + cItem.setSelected(True) self.colChanged = True return @@ -617,10 +619,11 @@ class GuiProjectEditReplace(QWidget): newList = {} for n in range(self.listBox.topLevelItemCount()): tItem = self.listBox.topLevelItem(n) - aKey = self._stripNotAllowed(tItem.text(0)) - aVal = tItem.text(1) - if len(aKey) > 0: - newList[aKey] = aVal + if tItem is not None: + aKey = self._stripNotAllowed(tItem.text(0)) + aVal = tItem.text(1) + if len(aKey) > 0: + newList[aKey] = aVal return newList diff --git a/novelwriter/dialogs/updates.py b/novelwriter/dialogs/updates.py index 8b8f1d27..1e79dc26 100644 --- a/novelwriter/dialogs/updates.py +++ b/novelwriter/dialogs/updates.py @@ -135,7 +135,7 @@ class GuiUpdates(QDialog): logException() relVersion = rawData.get("tag_name", "Unknown") - relDate = rawData.get("created_at", None) + relDate = rawData.get("created_at", "") try: relDate = datetime.strptime(relDate[:10], "%Y-%m-%d").strftime("%x") diff --git a/novelwriter/dialogs/wordlist.py b/novelwriter/dialogs/wordlist.py index f155c25e..01a8afff 100644 --- a/novelwriter/dialogs/wordlist.py +++ b/novelwriter/dialogs/wordlist.py @@ -156,7 +156,9 @@ class GuiWordList(QDialog): try: with open(tmpFile, mode="w", encoding="utf-8") as outFile: for i in range(self.listBox.count()): - outFile.write(self.listBox.item(i).text() + "\n") + item = self.listBox.item(i) + if item is not None: + outFile.write(item.text() + "\n") except Exception: logger.error("Could not save new word list") diff --git a/novelwriter/error.py b/novelwriter/error.py index dd0156c8..855f3723 100644 --- a/novelwriter/error.py +++ b/novelwriter/error.py @@ -199,8 +199,8 @@ def exceptionHandler(exType, exValue, exTrace): try: # Try a controlled shutdown - nwGUI.closeProject(isYes=True) - nwGUI.closeMain() + nwGUI.closeProject(isYes=True) # type: ignore + nwGUI.closeMain() # type: ignore logger.info("Emergency shutdown successful") except Exception as exc: diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index a37cd80d..8a1b60bb 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -513,7 +513,7 @@ class GuiDocEditor(QTextEdit): newHeader = self._nwItem.mainHeading # ToDo: This should be a signal - if self._updateHeaders(checkLevel=True): + if self._updateHeaders(): self.mainGui.requestNovelTreeRefresh() else: self.mainGui.novelView.updateWordCounts(tHandle) @@ -740,7 +740,7 @@ class GuiDocEditor(QTextEdit): qApp.restoreOverrideCursor() afTime = time() logger.debug("Document highlighted in %.3f ms", 1000*(afTime-bfTime)) - self.mainGui.statusBar.setStatus(self.tr("Spell check complete")) + self.mainGui.mainStatus.setStatus(self.tr("Spell check complete")) return True @@ -2025,7 +2025,7 @@ class GuiDocEditor(QTextEdit): return False return True - def _updateHeaders(self, checkPos=False, checkLevel=False): + def _updateHeaders(self): """Update the headers record and return True if anything changed, if a check flag was provided. """ @@ -2033,21 +2033,12 @@ class GuiDocEditor(QTextEdit): return False newHeaders = self.theProject.index.getHandleHeaders(self._docHandle) - if checkPos: - newPos = [x[0] for x in newHeaders] - oldPos = [x[0] for x in self._docHeaders] - if checkLevel: - newLev = [x[1] for x in newHeaders] - oldLev = [x[1] for x in self._docHeaders] + newLev = [x[1] for x in newHeaders] + oldLev = [x[1] for x in self._docHeaders] self._docHeaders = newHeaders - if checkPos: - return newPos != oldPos - if checkLevel: - return newLev != oldLev - - return False + return newLev != oldLev def _checkDocSize(self, theSize): """Check if document size crosses the big document limit set in diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 47cc1049..acadd29f 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -508,16 +508,17 @@ class GuiNovelTree(QTreeWidget): self._actHandle = tHandle for i in range(self.topLevelItemCount()): tItem = self.topLevelItem(i) - if tItem.data(self.C_TITLE, self.D_HANDLE) == tHandle: - tItem.setBackground(self.C_TITLE, self.palette().alternateBase()) - tItem.setBackground(self.C_WORDS, self.palette().alternateBase()) - tItem.setBackground(self.C_EXTRA, self.palette().alternateBase()) - tItem.setBackground(self.C_MORE, self.palette().alternateBase()) - else: - tItem.setBackground(self.C_TITLE, self.palette().base()) - tItem.setBackground(self.C_WORDS, self.palette().base()) - tItem.setBackground(self.C_EXTRA, self.palette().base()) - tItem.setBackground(self.C_MORE, self.palette().base()) + if tItem is not None: + if tItem.data(self.C_TITLE, self.D_HANDLE) == tHandle: + tItem.setBackground(self.C_TITLE, self.palette().alternateBase()) + tItem.setBackground(self.C_WORDS, self.palette().alternateBase()) + tItem.setBackground(self.C_EXTRA, self.palette().alternateBase()) + tItem.setBackground(self.C_MORE, self.palette().alternateBase()) + else: + tItem.setBackground(self.C_TITLE, self.palette().base()) + tItem.setBackground(self.C_WORDS, self.palette().base()) + tItem.setBackground(self.C_EXTRA, self.palette().base()) + tItem.setBackground(self.C_MORE, self.palette().base()) logger.debug("Highlighted Novel Tree in %.3f ms", (time() - tStart)*1000) diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index a4e133f9..bb900d9f 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -675,9 +675,10 @@ class GuiOutlineTree(QTreeWidget): self.setColumnHidden(self._colIdx[nwOutline.TITLE], False) headItem = self.headerItem() - headItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight) - headItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight) - headItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight) + if headItem is not None: + headItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight) + headItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight) + headItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight) novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True) for _, tHandle, sTitle, novIdx in novStruct: diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 5cadd78e..5480cafe 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -1211,13 +1211,13 @@ class GuiProjectTree(QTreeWidget): if hasChild and isFolder: mTrans.addAction( - self.tr("Combine Documents in Folder"), + self.tr("Merge Documents in Folder"), lambda: self._mergeDocuments(tHandle, True) ) if isFile: mTrans.addAction( - self.tr("Split Document by Header"), + self.tr("Split Document by Headers"), lambda: self._splitDocument(tHandle) ) @@ -1309,7 +1309,8 @@ class GuiProjectTree(QTreeWidget): self._postItemMove(sHandle, wCount) self._recordLastMove(sItem, pItem, pIndex) self._alertTreeChange(sHandle, flush=True) - sItem.setExpanded(isExpanded) + if sItem is not None: + sItem.setExpanded(isExpanded) return diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index a03eab12..fee7c7d6 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -55,6 +55,7 @@ from novelwriter.enum import ( nwDocMode, nwItemType, nwItemClass, nwAlert, nwWidget, nwState, nwView ) from novelwriter.common import getGuiItem, hexToInt +from novelwriter.constants import nwFiles logger = logging.getLogger(__name__) @@ -104,7 +105,7 @@ class GuiMain(QMainWindow): hWd = self.mainConf.pxInt(4) # Main GUI Elements - self.statusBar = GuiMainStatus(self) + self.mainStatus = GuiMainStatus(self) self.projView = GuiProjectView(self) self.novelView = GuiNovelView(self) self.docEditor = GuiDocEditor(self) @@ -189,7 +190,7 @@ class GuiMain(QMainWindow): # Set Main Window Elements self.setMenuBar(self.mainMenu) self.setCentralWidget(self.mainStack) - self.setStatusBar(self.statusBar) + self.setStatusBar(self.mainStatus) self.addToolBar(Qt.LeftToolBarArea, self.viewsBar) self.setContextMenuPolicy(Qt.NoContextMenu) # Issue #1147 @@ -211,8 +212,8 @@ class GuiMain(QMainWindow): self.novelView.selectedItemChanged.connect(self.itemDetails.updateViewBox) self.novelView.openDocumentRequest.connect(self._openDocument) - self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage) - self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus) + self.docEditor.spellDictionaryChanged.connect(self.mainStatus.setLanguage) + self.docEditor.docEditedStatusChanged.connect(self.mainStatus.doUpdateDocumentStatus) self.docEditor.docCountsChanged.connect(self.itemDetails.updateCounts) self.docEditor.docCountsChanged.connect(self.projView.updateCounts) self.docEditor.loadDocumentTagRequest.connect(self._followTag) @@ -254,7 +255,7 @@ class GuiMain(QMainWindow): keyEscape.activated.connect(self._keyPressEscape) # Forward Functions - self.setStatus = self.statusBar.setStatus + self.setStatus = self.mainStatus.setStatus # Force a show of the GUI self.show() @@ -266,7 +267,7 @@ class GuiMain(QMainWindow): self.initMain() self.asProjTimer.start() self.asDocTimer.start() - self.statusBar.clearStatus() + self.mainStatus.clearStatus() # Handle Windows Mode self.showNormal() @@ -307,7 +308,7 @@ class GuiMain(QMainWindow): self.outlineView.clearProject() # General - self.statusBar.clearStatus() + self.mainStatus.clearStatus() self._updateWindowTitle() return True @@ -353,7 +354,7 @@ class GuiMain(QMainWindow): logger.error("No projData or projPath set") return False - if os.path.isfile(os.path.join(projPath, self.theProject.projFile)): + if os.path.isfile(os.path.join(projPath, nwFiles.PROJ_FILE)): self.makeAlert(self.tr( "A project already exists in that location. " "Please choose another folder." @@ -376,10 +377,10 @@ class GuiMain(QMainWindow): self.outlineView.openProjectTasks() self.rebuildIndex(beQuiet=True) - self.statusBar.setRefTime(self.theProject.projOpened) - self.statusBar.setProjectStatus(nwState.GOOD) - self.statusBar.setDocumentStatus(nwState.NONE) - self.statusBar.setStatus(self.tr("New project created ...")) + self.mainStatus.setRefTime(self.theProject.projOpened) + self.mainStatus.setProjectStatus(nwState.GOOD) + self.mainStatus.setDocumentStatus(nwState.NONE) + self.mainStatus.setStatus(self.tr("New project created ...")) self._updateWindowTitle(self.theProject.projName) @@ -523,7 +524,7 @@ class GuiMain(QMainWindow): self.rebuildTrees() self.docEditor.setDictionaries() self.docEditor.toggleSpellCheck(self.theProject.spellCheck) - self.statusBar.setRefTime(self.theProject.projOpened) + self.mainStatus.setRefTime(self.theProject.projOpened) self.projView.openProjectTasks() self.novelView.openProjectTasks() self.outlineView.openProjectTasks() @@ -625,7 +626,7 @@ class GuiMain(QMainWindow): fHandle = None # The first file handle we encounter foundIt = False # We've found tHandle, pick the next we see for tItem in self.theProject.tree: - if not self.theProject.tree.checkType(tItem.itemHandle, nwItemType.FILE): + if tItem is None or not tItem.isFileType(): continue if fHandle is None: fHandle = tItem.itemHandle @@ -949,6 +950,7 @@ class GuiMain(QMainWindow): dlgDetails = getGuiItem("GuiProjectDetails") if dlgDetails is None: dlgDetails = GuiProjectDetails(self) + assert isinstance(dlgDetails, GuiProjectDetails) dlgDetails.setModal(False) dlgDetails.show() @@ -967,6 +969,7 @@ class GuiMain(QMainWindow): dlgBuild = getGuiItem("GuiBuildNovel") if dlgBuild is None: dlgBuild = GuiBuildNovel(self) + assert isinstance(dlgBuild, GuiBuildNovel) dlgBuild.setModal(False) dlgBuild.show() @@ -986,6 +989,7 @@ class GuiMain(QMainWindow): dlgLipsum = getGuiItem("GuiLipsum") if dlgLipsum is None: dlgLipsum = GuiLipsum(self) + assert isinstance(dlgLipsum, GuiLipsum) dlgLipsum.setModal(False) dlgLipsum.show() @@ -1020,6 +1024,7 @@ class GuiMain(QMainWindow): dlgStats = getGuiItem("GuiWritingStats") if dlgStats is None: dlgStats = GuiWritingStats(self) + assert isinstance(dlgStats, GuiWritingStats) dlgStats.setModal(False) dlgStats.show() @@ -1035,6 +1040,7 @@ class GuiMain(QMainWindow): dlgAbout = getGuiItem("GuiAbout") if dlgAbout is None: dlgAbout = GuiAbout(self) + assert isinstance(dlgAbout, GuiAbout) dlgAbout.setModal(True) dlgAbout.show() @@ -1060,6 +1066,7 @@ class GuiMain(QMainWindow): dlgUpdate = getGuiItem("GuiUpdates") if dlgUpdate is None: dlgUpdate = GuiUpdates(self) + assert isinstance(dlgUpdate, GuiUpdates) dlgUpdate.setModal(True) dlgUpdate.show() @@ -1221,7 +1228,7 @@ class GuiMain(QMainWindow): isVisible = not self.isFocusMode self.treePane.setVisible(isVisible) - self.statusBar.setVisible(isVisible) + self.mainStatus.setVisible(isVisible) self.mainMenu.setVisible(isVisible) self.viewsBar.setVisible(isVisible) @@ -1505,12 +1512,12 @@ class GuiMain(QMainWindow): if editIdle or userIdle: self.idleTime += currTime - self.idleRefTime - self.statusBar.setUserIdle(True) + self.mainStatus.setUserIdle(True) else: - self.statusBar.setUserIdle(False) + self.mainStatus.setUserIdle(False) self.idleRefTime = currTime - self.statusBar.updateTime(idleTime=self.idleTime) + self.mainStatus.updateTime(idleTime=self.idleTime) return @@ -1519,7 +1526,7 @@ class GuiMain(QMainWindow): """Update the word count on the status bar. """ if not self.hasProject: - self.statusBar.setProjectStats(0, 0) + self.mainStatus.setProjectStats(0, 0) self.theProject.updateWordCounts() if self.mainConf.incNotesWCount: @@ -1529,7 +1536,7 @@ class GuiMain(QMainWindow): currWords = self.theProject.currNovelWC diffWords = currWords - self.theProject.lastNovelWC - self.statusBar.setProjectStats(currWords, diffWords) + self.mainStatus.setProjectStats(currWords, diffWords) return diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py index eb2f8199..2a579837 100644 --- a/novelwriter/tools/writingstats.py +++ b/novelwriter/tools/writingstats.py @@ -112,9 +112,10 @@ class GuiWritingStats(QDialog): self.listBox.setColumnWidth(self.C_COUNT, wCol3) hHeader = self.listBox.headerItem() - hHeader.setTextAlignment(self.C_LENGTH, Qt.AlignRight) - hHeader.setTextAlignment(self.C_IDLE, Qt.AlignRight) - hHeader.setTextAlignment(self.C_COUNT, Qt.AlignRight) + if hHeader is not None: + hHeader.setTextAlignment(self.C_LENGTH, Qt.AlignRight) + hHeader.setTextAlignment(self.C_IDLE, Qt.AlignRight) + hHeader.setTextAlignment(self.C_COUNT, Qt.AlignRight) sortCol = minmax(pOptions.getInt("GuiWritingStats", "sortCol", 0), 0, 2) sortOrder = checkIntTuple( diff --git a/tests/mock.py b/tests/mock.py index 6174a3cb..ff67f4bf 100644 --- a/tests/mock.py +++ b/tests/mock.py @@ -30,7 +30,7 @@ class MockGuiMain: self.mainConf = None self.hasProject = True self.theProject = None - self.statusBar = MockStatusBar() + self.mainStatus = MockStatusBar() # Test Variables self.askResponse = True diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py index aa2d44e1..23b487e7 100644 --- a/tests/test_base/test_base_common.py +++ b/tests/test_base/test_base_common.py @@ -556,13 +556,13 @@ def testBaseCommon_EnsureFolder(monkeypatch, fncDir): assert ensureFolder(newDir1) is True assert os.path.isdir(newDir1) - assert ensureFolder("newDir2", parentPath=fncDir) is True + assert ensureFolder("newDir2", parent=fncDir) is True assert os.path.isdir(newDir2) with monkeypatch.context() as mp: mp.setattr("os.mkdir", causeOSError) errLog = [] - assert ensureFolder("newDir3", parentPath=fncDir, errLog=errLog) is False + assert ensureFolder("newDir3", parent=fncDir, errLog=errLog) is False assert errLog[0] == f"Could not create folder: {newDir3}" assert not os.path.isdir(newDir3) @@ -673,10 +673,10 @@ def testBaseCommon_NWConfigParser(fncDir): # Read Float assert cfgParser.rdFlt("main", "intopt1", 13.0) == 42.0 assert cfgParser.rdFlt("main", "float1", 13.0) == 4.2 - assert cfgParser.rdInt("main", "stropt", 13.0) == 13.0 + assert cfgParser.rdFlt("main", "stropt", 13.0) == 13.0 - assert cfgParser.rdInt("nope", "intopt1", 13.0) == 13.0 - assert cfgParser.rdInt("main", "blabla", 13.0) == 13.0 + assert cfgParser.rdFlt("nope", "intopt1", 13.0) == 13.0 + assert cfgParser.rdFlt("main", "blabla", 13.0) == 13.0 # Read String List assert cfgParser.rdStrList("main", "list1", []) == [] @@ -704,9 +704,4 @@ def testBaseCommon_NWConfigParser(fncDir): assert cfgParser.rdIntList("nope", "list2", [1]) == [1] assert cfgParser.rdIntList("main", "blabla", [1]) == [1] - # Internal - # ======== - - assert cfgParser._parseLine("main", "stropt", None, 999) is None - # END Test testBaseCommon_NWConfigParser diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index e9a03371..5d434601 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -31,6 +31,7 @@ from PyQt5.QtWidgets import QMessageBox, QInputDialog from novelwriter.enum import nwItemType, nwView, nwWidget from novelwriter.tools import GuiProjectWizard from novelwriter.dialogs import GuiEditLabel +from novelwriter.constants import nwFiles from novelwriter.gui.outline import GuiOutlineView from novelwriter.gui.projtree import GuiProjectTree from novelwriter.gui.doceditor import GuiDocEditor @@ -91,7 +92,7 @@ def testGuiMain_NewProject(monkeypatch, nwGUI, fncProj): assert nwGUI.newProject(projData={}) is False # Project file already exists - projFile = os.path.join(fncProj, nwGUI.theProject.projFile) + projFile = os.path.join(fncProj, nwFiles.PROJ_FILE) writeFile(projFile, "Stuff") assert nwGUI.newProject(projData={"projPath": fncProj}) is False os.unlink(projFile) @@ -102,7 +103,7 @@ def testGuiMain_NewProject(monkeypatch, nwGUI, fncProj): # This one should work just fine assert nwGUI.newProject(projData={"projPath": fncProj}) is True - assert os.path.isfile(os.path.join(fncProj, nwGUI.theProject.projFile)) + assert os.path.isfile(os.path.join(fncProj, nwFiles.PROJ_FILE)) assert os.path.isdir(os.path.join(fncProj, "content")) # END Test testGuiMain_NewProject @@ -183,7 +184,6 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock assert nwGUI.theProject.tree.trashRoot() is None assert nwGUI.theProject.projPath is None assert nwGUI.theProject.projMeta is None - assert nwGUI.theProject.projFile == "nwProject.nwx" assert nwGUI.theProject.projName == "" assert nwGUI.theProject.bookTitle == "" assert len(nwGUI.theProject.bookAuthors) == 0 @@ -208,7 +208,6 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock assert nwGUI.theProject.tree.trashRoot() is None assert nwGUI.theProject.projPath == fncProj assert nwGUI.theProject.projMeta == os.path.join(fncProj, "meta") - assert nwGUI.theProject.projFile == "nwProject.nwx" assert nwGUI.theProject.projName == "New Project" assert nwGUI.theProject.bookTitle == "New Novel" assert len(nwGUI.theProject.bookAuthors) == 1 @@ -575,7 +574,7 @@ def testGuiMain_FocusFullMode(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Enable focus mode assert nwGUI.toggleFocusMode() is True assert nwGUI.treePane.isVisible() is False - assert nwGUI.statusBar.isVisible() is False + assert nwGUI.mainStatus.isVisible() is False assert nwGUI.mainMenu.isVisible() is False assert nwGUI.viewsBar.isVisible() is False assert nwGUI.splitView.isVisible() is False @@ -583,7 +582,7 @@ def testGuiMain_FocusFullMode(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Disable focus mode assert nwGUI.toggleFocusMode() is True assert nwGUI.treePane.isVisible() is True - assert nwGUI.statusBar.isVisible() is True + assert nwGUI.mainStatus.isVisible() is True assert nwGUI.mainMenu.isVisible() is True assert nwGUI.viewsBar.isVisible() is True assert nwGUI.splitView.isVisible() is True diff --git a/tests/test_gui/test_gui_statusbar.py b/tests/test_gui/test_gui_statusbar.py index e449a3e3..6c99642f 100644 --- a/tests/test_gui/test_gui_statusbar.py +++ b/tests/test_gui/test_gui_statusbar.py @@ -45,57 +45,57 @@ def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Reference Time refTime = time.time() - nwGUI.statusBar.setRefTime(refTime) - assert nwGUI.statusBar.refTime == refTime + nwGUI.mainStatus.setRefTime(refTime) + assert nwGUI.mainStatus.refTime == refTime # Project Status - nwGUI.statusBar.setProjectStatus(nwState.NONE) - assert nwGUI.statusBar.projIcon._theCol == nwGUI.statusBar.projIcon._colNone - nwGUI.statusBar.setProjectStatus(nwState.BAD) - assert nwGUI.statusBar.projIcon._theCol == nwGUI.statusBar.projIcon._colBad - nwGUI.statusBar.setProjectStatus(nwState.GOOD) - assert nwGUI.statusBar.projIcon._theCol == nwGUI.statusBar.projIcon._colGood + nwGUI.mainStatus.setProjectStatus(nwState.NONE) + assert nwGUI.mainStatus.projIcon._theCol == nwGUI.mainStatus.projIcon._colNone + nwGUI.mainStatus.setProjectStatus(nwState.BAD) + assert nwGUI.mainStatus.projIcon._theCol == nwGUI.mainStatus.projIcon._colBad + nwGUI.mainStatus.setProjectStatus(nwState.GOOD) + assert nwGUI.mainStatus.projIcon._theCol == nwGUI.mainStatus.projIcon._colGood # Document Status - nwGUI.statusBar.setDocumentStatus(nwState.NONE) - assert nwGUI.statusBar.docIcon._theCol == nwGUI.statusBar.docIcon._colNone - nwGUI.statusBar.setDocumentStatus(nwState.BAD) - assert nwGUI.statusBar.docIcon._theCol == nwGUI.statusBar.docIcon._colBad - nwGUI.statusBar.setDocumentStatus(nwState.GOOD) - assert nwGUI.statusBar.docIcon._theCol == nwGUI.statusBar.docIcon._colGood + nwGUI.mainStatus.setDocumentStatus(nwState.NONE) + assert nwGUI.mainStatus.docIcon._theCol == nwGUI.mainStatus.docIcon._colNone + nwGUI.mainStatus.setDocumentStatus(nwState.BAD) + assert nwGUI.mainStatus.docIcon._theCol == nwGUI.mainStatus.docIcon._colBad + nwGUI.mainStatus.setDocumentStatus(nwState.GOOD) + assert nwGUI.mainStatus.docIcon._theCol == nwGUI.mainStatus.docIcon._colGood # Idle Status - nwGUI.statusBar.mainConf.stopWhenIdle = False - nwGUI.statusBar.setUserIdle(True) - nwGUI.statusBar.updateTime() - assert nwGUI.statusBar.userIdle is False - assert nwGUI.statusBar.timeText.text() == "00:00:00" + nwGUI.mainStatus.mainConf.stopWhenIdle = False + nwGUI.mainStatus.setUserIdle(True) + nwGUI.mainStatus.updateTime() + assert nwGUI.mainStatus.userIdle is False + assert nwGUI.mainStatus.timeText.text() == "00:00:00" - nwGUI.statusBar.mainConf.stopWhenIdle = True - nwGUI.statusBar.setUserIdle(True) - nwGUI.statusBar.updateTime(5) - assert nwGUI.statusBar.userIdle is True - assert nwGUI.statusBar.timeText.text() != "00:00:00" + nwGUI.mainStatus.mainConf.stopWhenIdle = True + nwGUI.mainStatus.setUserIdle(True) + nwGUI.mainStatus.updateTime(5) + assert nwGUI.mainStatus.userIdle is True + assert nwGUI.mainStatus.timeText.text() != "00:00:00" - nwGUI.statusBar.setUserIdle(False) - nwGUI.statusBar.updateTime(5) - assert nwGUI.statusBar.userIdle is False - assert nwGUI.statusBar.timeText.text() != "00:00:00" + nwGUI.mainStatus.setUserIdle(False) + nwGUI.mainStatus.updateTime(5) + assert nwGUI.mainStatus.userIdle is False + assert nwGUI.mainStatus.timeText.text() != "00:00:00" # Language - nwGUI.statusBar.setLanguage("None", "None") - assert nwGUI.statusBar.langText.text() == "None" - nwGUI.statusBar.setLanguage("en", "None") - assert nwGUI.statusBar.langText.text() == "American English" + nwGUI.mainStatus.setLanguage("None", "None") + assert nwGUI.mainStatus.langText.text() == "None" + nwGUI.mainStatus.setLanguage("en", "None") + assert nwGUI.mainStatus.langText.text() == "American English" # Project Stats - nwGUI.statusBar.mainConf.incNotesWCount = False + nwGUI.mainStatus.mainConf.incNotesWCount = False nwGUI._updateStatusWordCount() - assert nwGUI.statusBar.statsText.text() == "Words: 9 (+9)" - nwGUI.statusBar.mainConf.incNotesWCount = True + assert nwGUI.mainStatus.statsText.text() == "Words: 9 (+9)" + nwGUI.mainStatus.mainConf.incNotesWCount = True nwGUI._updateStatusWordCount() - assert nwGUI.statusBar.statsText.text() == "Words: 11 (+11)" + assert nwGUI.mainStatus.statsText.text() == "Words: 11 (+11)" - # qtbot.stopForInteraction() + # qtbot.stop() # END Test testGuiStatusBar_Init