def _doSave(self): """Save the new word list and close. """ self._saveGuiSettings() dctFile = os.path.join(self.theProject.projMeta, nwFiles.PROJ_DICT) tmpFile = dctFile + "~" 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") except Exception: logger.error("Could not save new word list") logException() self.reject() return False if os.path.isfile(dctFile): os.unlink(dctFile) os.rename(tmpFile, dctFile) self.accept() return True
def loadIndex(self): """Load index from last session from the project meta folder. """ theData = {} indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE) tStart = time() if os.path.isfile(indexFile): logger.debug("Loading index file") try: with open(indexFile, mode="r", encoding="utf-8") as inFile: theData = json.load(inFile) except Exception: logger.error("Failed to load index file") logException() self._indexBroken = True return False self._tagIndex = theData.get("tagIndex", {}) self._refIndex = theData.get("refIndex", {}) self._fileIndex = theData.get("fileIndex", {}) self._fileMeta = theData.get("fileMeta", {}) nowTime = round(time()) self._timeNovel = nowTime self._timeNotes = nowTime self._timeIndex = nowTime logger.verbose("Index loaded in %.3f ms", (time() - tStart)*1000) self._checkIndex() return True
def saveIndex(self): """Save the current index as a json file in the project meta data folder. """ logger.debug("Saving index file") indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE) tStart = time() try: with open(indexFile, mode="w+", encoding="utf-8") as outFile: outFile.write("{\n") outFile.write(f' "tagIndex": {jsonEncode(self._tagIndex, n=1, nmax=2)},\n') outFile.write(f' "refIndex": {jsonEncode(self._refIndex, n=1, nmax=3)},\n') outFile.write(f' "fileIndex": {jsonEncode(self._fileIndex, n=1, nmax=3)},\n') outFile.write(f' "fileMeta": {jsonEncode(self._fileMeta, n=1, nmax=2)}\n') outFile.write("}\n") except Exception: logger.error("Failed to save index file") logException() return False logger.verbose("Index saved in %.3f ms", (time() - tStart)*1000) return True
def _checkIndex(self): """Check that the entries in the index are valid and contain the elements it should. Also check that each file present in the contents folder when the project was loaded are also present in the fileMeta index. """ logger.debug("Checking index") tStart = time() try: self._checkTagIndex() self._checkRefIndex() self._checkFileIndex() self._checkFileMeta() self._indexBroken = False except Exception: logger.error("Error while checking index") logException() self._indexBroken = True # Check that project files are indexed for fHandle in self.theProject.projFiles: if fHandle not in self._fileMeta: self._indexBroken = True break logger.verbose("Index check completed in %.3f ms", (time() - tStart)*1000) if self._indexBroken: self.clearIndex() return
def _loadCache(self): """Save the current data to cache. """ buildCache = os.path.join(self.theProject.projCache, nwFiles.BUILD_CACHE) dataCount = 0 if os.path.isfile(buildCache): logger.debug("Loading build cache") try: with open(buildCache, mode="r", encoding="utf-8") as inFile: theJson = inFile.read() theData = json.loads(theJson) except Exception: logger.error("Failed to load build cache") logException() return False if "buildTime" in theData.keys(): self.buildTime = theData["buildTime"] if "htmlStyle" in theData.keys(): self.htmlStyle = theData["htmlStyle"] dataCount += 1 if "htmlText" in theData.keys(): self.htmlText = theData["htmlText"] dataCount += 1 return dataCount == 2
def loadSettings(self): """Load the options dictionary from the project settings file. """ if self.theProject.projMeta is None: return False stateFile = os.path.join(self.theProject.projMeta, nwFiles.OPTS_FILE) theState = {} if os.path.isfile(stateFile): logger.debug("Loading GUI options file") try: with open(stateFile, mode="r", encoding="utf-8") as inFile: theState = json.load(inFile) except Exception: logger.error("Failed to load GUI options file") logException() return False # Filter out unused variables for aGroup in theState: if aGroup in VALID_MAP: self._theState[aGroup] = {} for anOpt in theState[aGroup]: if anOpt in VALID_MAP[aGroup]: self._theState[aGroup][anOpt] = theState[aGroup][anOpt] return True
def _readProjectDictionary(self, projectDict): """Read the content of the project dictionary, and add it to the lookup lists. """ self._projDict = set() self._projectDict = projectDict if projectDict is None: return False if not os.path.isfile(projectDict): return False try: logger.debug("Loading project word list") with open(projectDict, mode="r", encoding="utf-8") as wordsFile: for theLine in wordsFile: theLine = theLine.strip() if len(theLine) > 0 and theLine not in self._projDict: self._projDict.add(theLine) logger.debug("Project word list contains %d words", len(self._projDict)) except Exception: logger.error("Failed to load project word list") logException() return False return True
def _loadInternalName(confParser, confFile): """Open a conf file and read the 'name' setting. """ try: with open(confFile, mode="r", encoding="utf-8") as inFile: confParser.read_file(inFile) except Exception: logger.error("Could not load file: %s", confFile) logException() return "" return confParser.rdStr("Main", "name", "")
def loadSyntax(self): """Load the currently specified syntax highlighter theme. """ logger.info("Loading syntax theme '%s'", self.guiSyntax) confParser = NWConfigParser() try: with open(self.syntaxFile, mode="r", encoding="utf-8") as inFile: confParser.read_file(inFile) except Exception: logger.error("Could not load syntax colours from: %s", self.syntaxFile) logException() return False # Main cnfSec = "Main" if confParser.has_section(cnfSec): self.syntaxName = confParser.rdStr(cnfSec, "name", "") self.syntaxDescription = confParser.rdStr(cnfSec, "description", "") self.syntaxAuthor = confParser.rdStr(cnfSec, "author", "") self.syntaxCredit = confParser.rdStr(cnfSec, "credit", "") self.syntaxUrl = confParser.rdStr(cnfSec, "url", "") self.syntaxLicense = confParser.rdStr(cnfSec, "license", "") self.syntaxLicenseUrl = confParser.rdStr(cnfSec, "licenseurl", "") # Syntax cnfSec = "Syntax" if confParser.has_section(cnfSec): self.colBack = self._loadColour(confParser, cnfSec, "background") self.colText = self._loadColour(confParser, cnfSec, "text") self.colLink = self._loadColour(confParser, cnfSec, "link") self.colHead = self._loadColour(confParser, cnfSec, "headertext") self.colHeadH = self._loadColour(confParser, cnfSec, "headertag") self.colEmph = self._loadColour(confParser, cnfSec, "emphasis") self.colDialN = self._loadColour(confParser, cnfSec, "straightquotes") self.colDialD = self._loadColour(confParser, cnfSec, "doublequotes") self.colDialS = self._loadColour(confParser, cnfSec, "singlequotes") self.colHidden = self._loadColour(confParser, cnfSec, "hidden") self.colKey = self._loadColour(confParser, cnfSec, "keyword") self.colVal = self._loadColour(confParser, cnfSec, "value") self.colSpell = self._loadColour(confParser, cnfSec, "spellcheckline") self.colError = self._loadColour(confParser, cnfSec, "errorline") self.colRepTag = self._loadColour(confParser, cnfSec, "replacetag") self.colMod = self._loadColour(confParser, cnfSec, "modifier") return True
def describeDict(self): """Return the tag and provider of the currently loaded dictionary. """ try: spTag = self._theDict.tag spName = self._theDict.provider.name except Exception: logger.error("Failed to extract information about the dictionary") logException() spTag = "" spName = "" return spTag, spName
def readTextFile(filePath): """Read the content of a text file in a robust manner. """ if not os.path.isfile(filePath): return "" fileText = "" try: with open(filePath, mode="r", encoding="utf-8") as inFile: fileText = inFile.read() except Exception: logger.error("Could not read file: %s", filePath) logException() return "" return fileText
def sha256sum(filePath): """Make a shasum of a file using a buffer. Based on: https://stackoverflow.com/a/44873382/5825851 """ hDigest = hashlib.sha256() bData = bytearray(65536) mData = memoryview(bData) try: with open(filePath, mode="rb", buffering=0) as inFile: for n in iter(lambda: inFile.readinto(mData), 0): hDigest.update(mData[:n]) except Exception: logger.error("Could not create sha256sum of: %s", filePath) logException() return None return hDigest.hexdigest()
def _saveCache(self): """Save the current data to cache. """ buildCache = os.path.join(self.theProject.projCache, nwFiles.BUILD_CACHE) logger.debug("Saving build cache") try: with open(buildCache, mode="w+", encoding="utf-8") as outFile: outFile.write(json.dumps({ "buildTime": self.buildTime, "htmlStyle": self.htmlStyle, "htmlText": self.htmlText, }, indent=2)) except Exception: logger.error("Failed to save build cache") logException() return False return True
def saveSettings(self): """Save the options dictionary to the project settings file. """ if self.theProject.projMeta is None: return False stateFile = os.path.join(self.theProject.projMeta, nwFiles.OPTS_FILE) logger.debug("Saving GUI options file") try: with open(stateFile, mode="w+", encoding="utf-8") as outFile: json.dump(self._theState, outFile, indent=2) except Exception: logger.error("Failed to save GUI options file") logException() return False return True
def writeToCFile(self): """Write the convenience table of contents file in the root of the project directory. """ tocList = [] tocLen = 0 for tHandle in self._treeOrder: tItem = self.__getitem__(tHandle) if tItem is None: continue tFile = tHandle + ".nwd" if os.path.isfile(os.path.join(self.theProject.projContent, tFile)): tocLine = "{0:<25s} {1:<9s} {2:<8s} {3:s}".format( os.path.join("content", tFile), tItem.itemClass.name, tItem.itemLayout.name, tItem.itemName, ) tocList.append(tocLine) tocLen = max(tocLen, len(tocLine)) try: # Dump the text tocText = os.path.join(self.theProject.projPath, nwFiles.TOC_TXT) with open(tocText, mode="w", encoding="utf-8") as outFile: outFile.write("\n") outFile.write("Table of Contents\n") outFile.write("=================\n") outFile.write("\n") outFile.write("{0:<25s} {1:<9s} {2:<8s} {3:s}\n".format( "File Name", "Class", "Layout", "Document Label")) outFile.write("-" * max(tocLen, 62) + "\n") outFile.write("\n".join(tocList)) outFile.write("\n") except Exception: logger.error("Could not write ToC file") logException() return False return True
def loadIndex(self): """Load index from last session from the project meta folder. """ theData = {} indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE) tStart = time() self._indexBroken = False if os.path.isfile(indexFile): logger.debug("Loading index file") try: with open(indexFile, mode="r", encoding="utf-8") as inFile: theData = json.load(inFile) except Exception: logger.error("Failed to load index file") logException() self._indexBroken = True return False try: self._tagsIndex.unpackData(theData["tagsIndex"]) self._itemIndex.unpackData(theData["itemIndex"]) except Exception: logger.error("The index content is invalid") logException() self._indexBroken = True return False logger.debug("Checking index") # Check that all files are indexed for fHandle in self.theProject.projFiles: if fHandle not in self._itemIndex: logger.warning("Item '%s' is not in the index", fHandle) self.reIndexHandle(fHandle) self._indexChange = round(time()) logger.verbose("Index loaded in %.3f ms", (time() - tStart) * 1000) return True
def addWord(self, newWord): """Add a word to the project dictionary. """ try: self._theDict.add_to_session(newWord) except Exception: return False if self._projectDict is not None and newWord not in self._projDict: newWord = newWord.strip() try: with open(self._projectDict, mode="a+", encoding="utf-8") as outFile: outFile.write("%s\n" % newWord) self._projDict.add(newWord) except Exception: logger.error("Failed to add word to project word list %s", str(self._projectDict)) logException() return False return True return False
def _parseLine(self, section, option, default, type): """Parse a line and return the correct datatype. """ 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
def initConfig(self, confPath=None, dataPath=None): """Initialise the config class. The manual setting of confPath and dataPath is mainly intended for the test suite. """ logger.debug("Initialising Config ...") if confPath is None: confRoot = QStandardPaths.writableLocation( QStandardPaths.ConfigLocation) self.confPath = os.path.join(os.path.abspath(confRoot), self.appHandle) else: logger.info("Setting config from alternative path: %s", confPath) self.confPath = confPath if dataPath is None: if self.verQtValue >= 50400: dataRoot = QStandardPaths.writableLocation( QStandardPaths.AppDataLocation) else: dataRoot = QStandardPaths.writableLocation( QStandardPaths.DataLocation) self.dataPath = os.path.join(os.path.abspath(dataRoot), self.appHandle) else: logger.info("Setting data path from alternative path: %s", dataPath) self.dataPath = dataPath logger.verbose("Config path: %s", self.confPath) logger.verbose("Data path: %s", self.dataPath) # Check Data Path Subdirs dataDirs = ["syntax", "themes"] for dataDir in dataDirs: dirPath = os.path.join(self.dataPath, dataDir) if not os.path.isdir(dirPath): try: os.mkdir(dirPath) logger.info("Created folder: %s", dirPath) except Exception: logger.error("Could not create folder: %s", dirPath) logException() 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)) if os.path.isfile(self.appRoot): # novelWriter is packaged as a single file, so the app and # root paths are the same, and equal to the folder that # contains the single executable. self.appRoot = os.path.dirname(self.appRoot) self.appPath = self.appRoot # Assets self.assetPath = os.path.join(self.appPath, "assets") self.appIcon = os.path.join(self.assetPath, "icons", "novelwriter.svg") # Internationalisation self.nwLangPath = os.path.join(self.assetPath, "i18n") logger.debug("Assets: %s", self.assetPath) logger.verbose("App path: %s", self.appPath) logger.verbose("Last path: %s", self.lastPath) # If the config folder does not exist, create it. # This assumes that the os config folder itself exists. if not os.path.isdir(self.confPath): try: os.mkdir(self.confPath) except Exception as exc: logger.error("Could not create folder: %s", self.confPath) logException() self.hasError = True self.errData.append("Could not create folder: %s" % self.confPath) self.errData.append(formatException(exc)) self.confPath = None # Check if config file exists if self.confPath is not None: if os.path.isfile(os.path.join(self.confPath, self.confFile)): # If it exists, load it self.loadConfig() else: # If it does not exist, save a copy of the default values self.saveConfig() # If the data folder does not exist, create it. # This assumes that the os data folder itself exists. if self.dataPath is not None: if not os.path.isdir(self.dataPath): try: os.mkdir(self.dataPath) except Exception as exc: logger.error("Could not create folder: %s", self.dataPath) logException() self.hasError = True self.errData.append("Could not create folder: %s" % self.dataPath) self.errData.append(formatException(exc)) self.dataPath = None # Host and Kernel if self.verQtValue >= 50600: self.hostName = QSysInfo.machineHostName() self.kernelVer = QSysInfo.kernelVersion() # Load recent projects cache self.loadRecentCache() # Check the availability of optional packages self._checkOptionalPackages() if self.spellLanguage is None: self.spellLanguage = "en" # Look for a PDF version of the manual pdfDocs = os.path.join(self.assetPath, "manual.pdf") if os.path.isfile(pdfDocs): logger.debug("Found manual: %s", pdfDocs) self.pdfDocs = pdfDocs logger.debug("Config initialisation complete") return True
def main(sysArgs=None): """Parse command line, set up logging, and launch main GUI. """ if sysArgs is None: sysArgs = sys.argv[1:] # Valid Input Options shortOpt = "hv" longOpt = [ "help", "version", "info", "debug", "verbose", "style=", "config=", "data=", "testmode", ] helpMsg = ( f"novelWriter {__version__} ({__date__})\n" f"{__copyright__}\n" "\n" "This program is distributed in the hope that it will be useful,\n" "but WITHOUT ANY WARRANTY; without even the implied warranty of\n" "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" "GNU General Public Licence for more details.\n" "\n" "Usage:\n" " -h, --help Print this message.\n" " -v, --version Print program version and exit.\n" " --info Print additional runtime information.\n" " --debug Print debug output. Includes --info.\n" " --verbose Increase verbosity of debug output. Includes --debug.\n" " --style= Sets Qt5 style flag. Defaults to 'Fusion'.\n" " --config= Alternative config file.\n" " --data= Alternative user data path.\n") # Defaults logLevel = logging.WARN logFormat = "{levelname:8} {message:}" confPath = None dataPath = None testMode = False qtStyle = "Fusion" cmdOpen = None # Parse Options try: inOpts, inRemain = getopt.getopt(sysArgs, shortOpt, longOpt) except getopt.GetoptError as exc: print(helpMsg) print(f"ERROR: {str(exc)}") sys.exit(2) if len(inRemain) > 0: cmdOpen = inRemain[0] for inOpt, inArg in inOpts: if inOpt in ("-h", "--help"): print(helpMsg) sys.exit(0) elif inOpt in ("-v", "--version"): print("novelWriter Version %s [%s]" % (__version__, __date__)) sys.exit(0) elif inOpt == "--info": logLevel = logging.INFO elif inOpt == "--debug": logLevel = logging.DEBUG logFormat = "[{asctime:}] {filename:>17}:{lineno:<4d} {levelname:8} {message:}" elif inOpt == "--verbose": logLevel = VERBOSE logFormat = "[{asctime:}] {filename:>17}:{lineno:<4d} {levelname:8} {message:}" elif inOpt == "--style": qtStyle = inArg elif inOpt == "--config": confPath = inArg elif inOpt == "--data": dataPath = inArg elif inOpt == "--testmode": testMode = True # Set Config Options CONFIG.cmdOpen = cmdOpen # Set Logging cHandle = logging.StreamHandler() cHandle.setFormatter(logging.Formatter(fmt=logFormat, style="{")) pkgLogger = logging.getLogger(__package__) pkgLogger.addHandler(cHandle) pkgLogger.setLevel(logLevel) logger.info("Starting novelWriter %s (%s) %s", __version__, __hexversion__, __date__) # Check Packages and Versions errorData = [] errorCode = 0 if sys.hexversion < 0x030700f0: errorData.append("At least Python 3.7 is required, found %s" % CONFIG.verPyString) errorCode |= 0x04 if CONFIG.verQtValue < 50300: errorData.append("At least Qt5 version 5.3 is required, found %s" % CONFIG.verQtString) errorCode |= 0x08 if CONFIG.verPyQtValue < 50300: errorData.append("At least PyQt5 version 5.3 is required, found %s" % CONFIG.verPyQtString) errorCode |= 0x10 try: import lxml # noqa: F401 except ImportError: errorData.append("Python module 'lxml' is missing") errorCode |= 0x20 if errorData: errApp = QApplication([]) errDlg = QErrorMessage() errDlg.resize(500, 300) errDlg.showMessage( ("<h3>A critical error has been encountered</h3>" "<p>novelWriter cannot start due to the following issues:<p>" "<p> - %s</p>" "<p>Shutting down ...</p>") % ("<br> - ".join(errorData))) for errLine in errorData: logger.critical(errLine) errApp.exec_() sys.exit(errorCode) # Finish initialising config CONFIG.initConfig(confPath, dataPath) if CONFIG.osDarwin: try: from Foundation import NSBundle bundle = NSBundle.mainBundle() info = bundle.localizedInfoDictionary() or bundle.infoDictionary() info["CFBundleName"] = "novelWriter" except Exception: logger.error("Failed to set application name") logException() elif CONFIG.osWindows: try: import ctypes appID = f"io.novelwriter.{__version__}" ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( appID) except Exception: logger.error("Failed to set application name") logException() # Import GUI (after dependency checks), and launch from novelwriter.guimain import GuiMain if testMode: nwGUI = GuiMain() return nwGUI else: nwApp = QApplication([CONFIG.appName, (f"-style={qtStyle}")]) nwApp.setApplicationName(CONFIG.appName) nwApp.setApplicationVersion(__version__) nwApp.setWindowIcon(QIcon(CONFIG.appIcon)) nwApp.setOrganizationDomain(__domain__) # Connect the exception handler before making the main GUI sys.excepthook = exceptionHandler # Launch main GUI CONFIG.initLocalisation(nwApp) nwGUI = GuiMain() if not nwGUI.hasProject: nwGUI.showProjectLoadDialog() nwGUI.releaseNotes() sys.exit(nwApp.exec_())
def _doBuild(self, bldObj, isPreview=False, doConvert=True): """Rund the build with a specific build object. """ tStart = int(time()) # Get Settings fmtTitle = self.fmtTitle.text() fmtChapter = self.fmtChapter.text() fmtUnnumbered = self.fmtUnnumbered.text() fmtScene = self.fmtScene.text() fmtSection = self.fmtSection.text() buildLang = self.buildLang.currentData() hideScene = self.hideScene.isChecked() hideSection = self.hideSection.isChecked() textFont = self.textFont.text() textSize = self.textSize.value() lineHeight = self.lineHeight.value() justifyText = self.justifyText.isChecked() noStyling = self.noStyling.isChecked() incSynopsis = self.includeSynopsis.isChecked() incComments = self.includeComments.isChecked() incKeywords = self.includeKeywords.isChecked() novelFiles = self.novelFiles.isChecked() noteFiles = self.noteFiles.isChecked() ignoreFlag = self.ignoreFlag.isChecked() includeBody = self.includeBody.isChecked() replaceUCode = self.replaceUCode.isChecked() # The language lookup dict is reloaded if needed self.theProject.setProjectLang(buildLang) # Get font information fontInfo = QFontInfo(QFont(textFont, textSize)) textFixed = fontInfo.fixedPitch() isHtml = isinstance(bldObj, ToHtml) isOdt = isinstance(bldObj, ToOdt) bldObj.setTitleFormat(fmtTitle) bldObj.setChapterFormat(fmtChapter) bldObj.setUnNumberedFormat(fmtUnnumbered) bldObj.setSceneFormat(fmtScene, hideScene) bldObj.setSectionFormat(fmtSection, hideSection) bldObj.setFont(textFont, textSize, textFixed) bldObj.setJustify(justifyText) bldObj.setLineHeight(lineHeight) bldObj.setSynopsis(incSynopsis) bldObj.setComments(incComments) bldObj.setKeywords(incKeywords) bldObj.setBodyText(includeBody) if isHtml: bldObj.setStyles(not noStyling) bldObj.setReplaceUnicode(replaceUCode) if isOdt: bldObj.setColourHeaders(not noStyling) bldObj.setLanguage(buildLang) bldObj.initDocument() # Make sure the project and document is up to date self.mainGui.saveDocument() self.buildProgress.setMaximum(len(self.theProject.tree)) self.buildProgress.setValue(0) for nItt, tItem in enumerate(self.theProject.tree): noteRoot = noteFiles noteRoot &= tItem.itemType == nwItemType.ROOT noteRoot &= tItem.itemClass != nwItemClass.NOVEL noteRoot &= tItem.itemClass != nwItemClass.ARCHIVE try: if noteRoot: # Add headers for root folders of notes bldObj.addRootHeading(tItem.itemHandle) if doConvert: bldObj.doConvert() elif self._checkInclude(tItem, noteFiles, novelFiles, ignoreFlag): bldObj.setText(tItem.itemHandle) bldObj.doPreProcessing() bldObj.tokenizeText() bldObj.doHeaders() if doConvert: bldObj.doConvert() bldObj.doPostProcessing() except Exception: logger.error("Failed to build document '%s'", tItem.itemHandle) logException() if isPreview: self.docView.setText(( "Failed to generate preview. " "Document with title '%s' could not be parsed." ) % tItem.itemName) return False # Update progress bar, also for skipped items self.buildProgress.setValue(nItt+1) if isOdt: bldObj.closeDocument() tEnd = int(time()) logger.debug("Built project in %.3f ms", 1000*(tEnd - tStart)) if bldObj.errData: self.mainGui.makeAlert([ self.tr("There were problems when building the project:") ] + bldObj.errData, nwAlert.ERROR) return
def loadText(self, tHandle, updateHistory=True): """Load text into the viewer from an item handle. """ if not self.theProject.tree.checkType(tHandle, nwItemType.FILE): logger.warning("Item not found") return False logger.debug("Generating preview for item '%s'", tHandle) qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) sPos = self.verticalScrollBar().value() aDoc = ToHtml(self.theProject) aDoc.setPreview(self.mainConf.viewComments, self.mainConf.viewSynopsis) aDoc.setLinkHeaders(True) # Be extra careful here to prevent crashes when first opening a # project as a crash here leaves no way of recovering. # See issue #298 try: aDoc.setText(tHandle) aDoc.doPreProcessing() aDoc.tokenizeText() aDoc.doConvert() aDoc.doPostProcessing() except Exception: logger.error("Failed to generate preview for document with handle '%s'", tHandle) logException() self.setText(self.tr("An error occurred while generating the preview.")) qApp.restoreOverrideCursor() return False # Refresh the tab stops if self.mainConf.verQtValue >= 51000: self.setTabStopDistance(self.mainConf.getTabWidth()) else: self.setTabStopWidth(self.mainConf.getTabWidth()) # Must be before setHtml if updateHistory: self.docHistory.append(tHandle) self.setHtml(aDoc.theResult.replace("\t", "!!tab!!")) self.setDocumentTitle(tHandle) # Loop through the text and put back in the tabs. Tabs are removed by # the setHtml function, so the ToHtml class puts in a placeholder. while self.find("!!tab!!"): theCursor = self.textCursor() theCursor.insertText("\t") if self._docHandle == tHandle: # This is a refresh, so we set the scrollbar back to where it was self.verticalScrollBar().setValue(sPos) self._docHandle = tHandle self.theProject.setLastViewed(tHandle) self.docHeader.setTitleFromHandle(self._docHandle) self.updateDocMargins() # Make sure the main GUI knows we changed the content self.mainGui.viewMeta.refreshReferences(tHandle) # Since we change the content while it may still be rendering, we mark # the document dirty again to make sure it's re-rendered properly. self.redrawText() qApp.restoreOverrideCursor() return True
def loadTheme(self): """Load the currently specified GUI theme. """ logger.info("Loading GUI theme '%s'", self.guiTheme) # Config File confParser = NWConfigParser() try: with open(self.themeFile, mode="r", encoding="utf-8") as inFile: confParser.read_file(inFile) except Exception: logger.error("Could not load theme settings from: %s", self.themeFile) logException() return False # Main cnfSec = "Main" if confParser.has_section(cnfSec): self.themeName = confParser.rdStr(cnfSec, "name", "") self.themeDescription = confParser.rdStr(cnfSec, "description", "N/A") self.themeAuthor = confParser.rdStr(cnfSec, "author", "N/A") self.themeCredit = confParser.rdStr(cnfSec, "credit", "N/A") self.themeUrl = confParser.rdStr(cnfSec, "url", "") self.themeLicense = confParser.rdStr(cnfSec, "license", "N/A") self.themeLicenseUrl = confParser.rdStr(cnfSec, "licenseurl", "") # Palette cnfSec = "Palette" if confParser.has_section(cnfSec): self._setPalette(confParser, cnfSec, "window", QPalette.Window) self._setPalette(confParser, cnfSec, "windowtext", QPalette.WindowText) self._setPalette(confParser, cnfSec, "base", QPalette.Base) self._setPalette(confParser, cnfSec, "alternatebase", QPalette.AlternateBase) self._setPalette(confParser, cnfSec, "text", QPalette.Text) self._setPalette(confParser, cnfSec, "tooltipbase", QPalette.ToolTipBase) self._setPalette(confParser, cnfSec, "tooltiptext", QPalette.ToolTipText) self._setPalette(confParser, cnfSec, "button", QPalette.Button) self._setPalette(confParser, cnfSec, "buttontext", QPalette.ButtonText) self._setPalette(confParser, cnfSec, "brighttext", QPalette.BrightText) self._setPalette(confParser, cnfSec, "highlight", QPalette.Highlight) self._setPalette(confParser, cnfSec, "highlightedtext", QPalette.HighlightedText) self._setPalette(confParser, cnfSec, "link", QPalette.Link) self._setPalette(confParser, cnfSec, "linkvisited", QPalette.LinkVisited) # GUI cnfSec = "GUI" if confParser.has_section(cnfSec): self.statNone = self._loadColour(confParser, cnfSec, "statusnone") self.statUnsaved = self._loadColour(confParser, cnfSec, "statusunsaved") self.statSaved = self._loadColour(confParser, cnfSec, "statussaved") # CSS File cssData = readTextFile(self.cssFile) if cssData: qApp.setStyleSheet(cssData) # Apply Styles qApp.setPalette(self._guiPalette) return True
def updateTheme(self): """Update the theme map. This is more of an init, since many of the GUI icons cannot really be replaced without writing specific update functions for the classes where they're used. """ self._themeMap = {} themePath = self._getThemePath() if themePath is None: logger.warning("No icons loaded") return False self._themePath = themePath themeConf = os.path.join(themePath, self._confName) logger.info("Loading icon theme '%s'", self.mainConf.guiIcons) # Config File confParser = NWConfigParser() try: with open(themeConf, mode="r", encoding="utf-8") as inFile: confParser.read_file(inFile) except Exception: logger.error("Could not load icon theme settings from: %s", themeConf) logException() return False # Main cnfSec = "Main" if confParser.has_section(cnfSec): self.themeName = confParser.rdStr(cnfSec, "name", "") self.themeDescription = confParser.rdStr(cnfSec, "description", "") self.themeAuthor = confParser.rdStr(cnfSec, "author", "N/A") self.themeCredit = confParser.rdStr(cnfSec, "credit", "N/A") self.themeUrl = confParser.rdStr(cnfSec, "url", "") self.themeLicense = confParser.rdStr(cnfSec, "license", "N/A") self.themeLicenseUrl = confParser.rdStr(cnfSec, "licenseurl", "") # Populate Icon Map cnfSec = "Map" if confParser.has_section(cnfSec): for iconName, iconFile in confParser.items(cnfSec): if iconName not in self.ICON_KEYS: logger.error("Unknown icon name '%s' in config file", iconName) else: iconPath = os.path.join(self._themePath, iconFile) if os.path.isfile(iconPath): self._themeMap[iconName] = iconPath logger.verbose("Icon slot '%s' using file '%s'", iconName, iconFile) else: logger.error("Icon file '%s' not in theme folder", iconFile) # Check that icons have been defined logger.debug("Scanning theme icons") for iconKey in self.ICON_KEYS: if iconKey in ("novelwriter", "proj_nwx"): # These are not part of the theme itself continue if iconKey not in self._themeMap: logger.error("No icon file specified for '%s'", iconKey) return True
def loadConfig(self): """Load preferences from file and replace default settings. """ logger.debug("Loading config file") if self.confPath is None: return False theConf = NWConfigParser() cnfPath = os.path.join(self.confPath, self.confFile) try: with open(cnfPath, mode="r", encoding="utf-8") as inFile: theConf.read_file(inFile) except Exception as exc: logger.error("Could not load config file") logException() self.hasError = True self.errData.append("Could not load config file") self.errData.append(formatException(exc)) return False # Main cnfSec = "Main" self.guiTheme = theConf.rdStr(cnfSec, "theme", self.guiTheme) self.guiSyntax = theConf.rdStr(cnfSec, "syntax", self.guiSyntax) self.guiIcons = theConf.rdStr(cnfSec, "icons", self.guiIcons) self.guiFont = theConf.rdStr(cnfSec, "guifont", self.guiFont) self.guiFontSize = theConf.rdInt(cnfSec, "guifontsize", self.guiFontSize) self.lastNotes = theConf.rdStr(cnfSec, "lastnotes", self.lastNotes) self.guiLang = theConf.rdStr(cnfSec, "guilang", self.guiLang) self.hideVScroll = theConf.rdBool(cnfSec, "hidevscroll", self.hideVScroll) self.hideHScroll = theConf.rdBool(cnfSec, "hidehscroll", self.hideHScroll) # Sizes cnfSec = "Sizes" self.winGeometry = theConf.rdIntList(cnfSec, "geometry", self.winGeometry) self.prefGeometry = theConf.rdIntList(cnfSec, "preferences", self.prefGeometry) self.projColWidth = theConf.rdIntList(cnfSec, "projcols", self.projColWidth) self.mainPanePos = theConf.rdIntList(cnfSec, "mainpane", self.mainPanePos) self.docPanePos = theConf.rdIntList(cnfSec, "docpane", self.docPanePos) self.viewPanePos = theConf.rdIntList(cnfSec, "viewpane", self.viewPanePos) self.outlnPanePos = theConf.rdIntList(cnfSec, "outlinepane", self.outlnPanePos) self.isFullScreen = theConf.rdBool(cnfSec, "fullscreen", self.isFullScreen) # Project cnfSec = "Project" self.autoSaveProj = theConf.rdInt(cnfSec, "autosaveproject", self.autoSaveProj) self.autoSaveDoc = theConf.rdInt(cnfSec, "autosavedoc", self.autoSaveDoc) self.emphLabels = theConf.rdBool(cnfSec, "emphlabels", self.emphLabels) # Editor cnfSec = "Editor" self.textFont = theConf.rdStr(cnfSec, "textfont", self.textFont) self.textSize = theConf.rdInt(cnfSec, "textsize", self.textSize) self.textWidth = theConf.rdInt(cnfSec, "width", self.textWidth) self.textMargin = theConf.rdInt(cnfSec, "margin", self.textMargin) self.tabWidth = theConf.rdInt(cnfSec, "tabwidth", self.tabWidth) self.focusWidth = theConf.rdInt(cnfSec, "focuswidth", self.focusWidth) self.hideFocusFooter = theConf.rdBool(cnfSec, "hidefocusfooter", self.hideFocusFooter) self.doJustify = theConf.rdBool(cnfSec, "justify", self.doJustify) self.autoSelect = theConf.rdBool(cnfSec, "autoselect", self.autoSelect) self.doReplace = theConf.rdBool(cnfSec, "autoreplace", self.doReplace) self.doReplaceSQuote = theConf.rdBool(cnfSec, "repsquotes", self.doReplaceSQuote) self.doReplaceDQuote = theConf.rdBool(cnfSec, "repdquotes", self.doReplaceDQuote) self.doReplaceDash = theConf.rdBool(cnfSec, "repdash", self.doReplaceDash) self.doReplaceDots = theConf.rdBool(cnfSec, "repdots", self.doReplaceDots) self.scrollPastEnd = theConf.rdInt(cnfSec, "scrollpastend", self.scrollPastEnd) self.autoScroll = theConf.rdBool(cnfSec, "autoscroll", self.autoScroll) self.autoScrollPos = theConf.rdInt(cnfSec, "autoscrollpos", self.autoScrollPos) self.fmtSingleQuotes = theConf.rdStrList(cnfSec, "fmtsinglequote", self.fmtSingleQuotes) self.fmtDoubleQuotes = theConf.rdStrList(cnfSec, "fmtdoublequote", self.fmtDoubleQuotes) self.fmtPadBefore = theConf.rdStr(cnfSec, "fmtpadbefore", self.fmtPadBefore) self.fmtPadAfter = theConf.rdStr(cnfSec, "fmtpadafter", self.fmtPadAfter) self.fmtPadThin = theConf.rdBool(cnfSec, "fmtpadthin", self.fmtPadThin) self.spellLanguage = theConf.rdStr(cnfSec, "spellcheck", self.spellLanguage) self.showTabsNSpaces = theConf.rdBool(cnfSec, "showtabsnspaces", self.showTabsNSpaces) self.showLineEndings = theConf.rdBool(cnfSec, "showlineendings", self.showLineEndings) self.showMultiSpaces = theConf.rdBool(cnfSec, "showmultispaces", self.showMultiSpaces) self.wordCountTimer = theConf.rdFlt(cnfSec, "wordcounttimer", self.wordCountTimer) self.bigDocLimit = theConf.rdInt(cnfSec, "bigdoclimit", self.bigDocLimit) self.incNotesWCount = theConf.rdBool(cnfSec, "incnoteswcount", self.incNotesWCount) self.showFullPath = theConf.rdBool(cnfSec, "showfullpath", self.showFullPath) self.highlightQuotes = theConf.rdBool(cnfSec, "highlightquotes", self.highlightQuotes) self.allowOpenSQuote = theConf.rdBool(cnfSec, "allowopensquote", self.allowOpenSQuote) self.allowOpenDQuote = theConf.rdBool(cnfSec, "allowopendquote", self.allowOpenDQuote) self.highlightEmph = theConf.rdBool(cnfSec, "highlightemph", self.highlightEmph) self.stopWhenIdle = theConf.rdBool(cnfSec, "stopwhenidle", self.stopWhenIdle) self.userIdleTime = theConf.rdInt(cnfSec, "useridletime", self.userIdleTime) # Backup cnfSec = "Backup" self.backupPath = theConf.rdStr(cnfSec, "backuppath", self.backupPath) self.backupOnClose = theConf.rdBool(cnfSec, "backuponclose", self.backupOnClose) self.askBeforeBackup = theConf.rdBool(cnfSec, "askbeforebackup", self.askBeforeBackup) # State cnfSec = "State" self.showRefPanel = theConf.rdBool(cnfSec, "showrefpanel", self.showRefPanel) self.viewComments = theConf.rdBool(cnfSec, "viewcomments", self.viewComments) self.viewSynopsis = theConf.rdBool(cnfSec, "viewsynopsis", self.viewSynopsis) self.searchCase = theConf.rdBool(cnfSec, "searchcase", self.searchCase) self.searchWord = theConf.rdBool(cnfSec, "searchword", self.searchWord) self.searchRegEx = theConf.rdBool(cnfSec, "searchregex", self.searchRegEx) self.searchLoop = theConf.rdBool(cnfSec, "searchloop", self.searchLoop) self.searchNextFile = theConf.rdBool(cnfSec, "searchnextfile", self.searchNextFile) self.searchMatchCap = theConf.rdBool(cnfSec, "searchmatchcap", self.searchMatchCap) # Path cnfSec = "Path" self.lastPath = theConf.rdStr(cnfSec, "lastpath", self.lastPath) # Check Certain Values for None self.spellLanguage = self._checkNone(self.spellLanguage) # If we're using straight quotes, disable auto-replace if self.fmtSingleQuotes == ["'", "'"] and self.doReplaceSQuote: logger.info( "Using straight single quotes, so disabling auto-replace") self.doReplaceSQuote = False if self.fmtDoubleQuotes == ['"', '"'] and self.doReplaceDQuote: logger.info( "Using straight double quotes, so disabling auto-replace") self.doReplaceDQuote = False # Check deprecated settings if self.guiIcons in ("typicons_colour_dark", "typicons_grey_dark"): self.guiIcons = "typicons_dark" elif self.guiIcons in ("typicons_colour_light", "typicons_grey_light"): self.guiIcons = "typicons_light" return True
def saveConfig(self): """Save the current preferences to file. """ logger.debug("Saving config file") if self.confPath is None: return False theConf = NWConfigParser() theConf["Main"] = { "timestamp": formatTimeStamp(time()), "theme": str(self.guiTheme), "syntax": str(self.guiSyntax), "icons": str(self.guiIcons), "guifont": str(self.guiFont), "guifontsize": str(self.guiFontSize), "lastnotes": str(self.lastNotes), "guilang": str(self.guiLang), "hidevscroll": str(self.hideVScroll), "hidehscroll": str(self.hideHScroll), } theConf["Sizes"] = { "geometry": self._packList(self.winGeometry), "preferences": self._packList(self.prefGeometry), "projcols": self._packList(self.projColWidth), "mainpane": self._packList(self.mainPanePos), "docpane": self._packList(self.docPanePos), "viewpane": self._packList(self.viewPanePos), "outlinepane": self._packList(self.outlnPanePos), "fullscreen": str(self.isFullScreen), } theConf["Project"] = { "autosaveproject": str(self.autoSaveProj), "autosavedoc": str(self.autoSaveDoc), "emphlabels": str(self.emphLabels), } theConf["Editor"] = { "textfont": str(self.textFont), "textsize": str(self.textSize), "width": str(self.textWidth), "margin": str(self.textMargin), "tabwidth": str(self.tabWidth), "focuswidth": str(self.focusWidth), "hidefocusfooter": str(self.hideFocusFooter), "justify": str(self.doJustify), "autoselect": str(self.autoSelect), "autoreplace": str(self.doReplace), "repsquotes": str(self.doReplaceSQuote), "repdquotes": str(self.doReplaceDQuote), "repdash": str(self.doReplaceDash), "repdots": str(self.doReplaceDots), "scrollpastend": str(self.scrollPastEnd), "autoscroll": str(self.autoScroll), "autoscrollpos": str(self.autoScrollPos), "fmtsinglequote": self._packList(self.fmtSingleQuotes), "fmtdoublequote": self._packList(self.fmtDoubleQuotes), "fmtpadbefore": str(self.fmtPadBefore), "fmtpadafter": str(self.fmtPadAfter), "fmtpadthin": str(self.fmtPadThin), "spellcheck": str(self.spellLanguage), "showtabsnspaces": str(self.showTabsNSpaces), "showlineendings": str(self.showLineEndings), "showmultispaces": str(self.showMultiSpaces), "wordcounttimer": str(self.wordCountTimer), "bigdoclimit": str(self.bigDocLimit), "incnoteswcount": str(self.incNotesWCount), "showfullpath": str(self.showFullPath), "highlightquotes": str(self.highlightQuotes), "allowopensquote": str(self.allowOpenSQuote), "allowopendquote": str(self.allowOpenDQuote), "highlightemph": str(self.highlightEmph), "stopwhenidle": str(self.stopWhenIdle), "useridletime": str(self.userIdleTime), } theConf["Backup"] = { "backuppath": str(self.backupPath), "backuponclose": str(self.backupOnClose), "askbeforebackup": str(self.askBeforeBackup), } theConf["State"] = { "showrefpanel": str(self.showRefPanel), "viewcomments": str(self.viewComments), "viewsynopsis": str(self.viewSynopsis), "searchcase": str(self.searchCase), "searchword": str(self.searchWord), "searchregex": str(self.searchRegEx), "searchloop": str(self.searchLoop), "searchnextfile": str(self.searchNextFile), "searchmatchcap": str(self.searchMatchCap), } theConf["Path"] = { "lastpath": str(self.lastPath), } # Write config file cnfPath = os.path.join(self.confPath, self.confFile) try: with open(cnfPath, mode="w", encoding="utf-8") as outFile: theConf.write(outFile) self.confChanged = False except Exception as exc: logger.error("Could not save config file") logException() self.hasError = True self.errData.append("Could not save config file") self.errData.append(formatException(exc)) return False return True