diff --git a/manuskript/main.py b/manuskript/main.py index c4d5bb6c..c0e09583 100644 --- a/manuskript/main.py +++ b/manuskript/main.py @@ -20,6 +20,10 @@ import logging LOGGER = logging.getLogger(__name__) def prepare(arguments, tests=False): + # Qt WebEngine demands this attribute be set _before_ we create our QApplication object. + QApplication.setAttribute(Qt.AA_ShareOpenGLContexts, True) + + # Create the foundation that provides our Qt application with its event loop. app = QApplication(sys.argv) app.setOrganizationName("manuskript" + ("_tests" if tests else "")) app.setOrganizationDomain("www.theologeek.ch") @@ -160,7 +164,7 @@ def prepare(arguments, tests=False): QIcon.setThemeSearchPaths(QIcon.themeSearchPaths() + [appPath("icons")]) QIcon.setThemeName("NumixMsk") - # Font siue + # Font size if settings.contains("appFontSize"): f = qApp.font() f.setPointSize(settings.value("appFontSize", type=int)) @@ -175,12 +179,12 @@ def prepare(arguments, tests=False): MW._defaultCursorFlashTime = qApp.cursorFlashTime() # Command line project - #if len(sys.argv) > 1 and sys.argv[1][-4:] == ".msk": if arguments.filename is not None and arguments.filename[-4:] == ".msk": - #TODO: integrate better with argparsing. - if os.path.exists(sys.argv[1]): - path = os.path.abspath(sys.argv[1]) - MW._autoLoadProject = path + # The file is verified to already exist during argument parsing. + # Our ".msk" check has been moved there too for better feedback, + # but leaving it here to err on the side of caution. + path = os.path.abspath(arguments.filename) + MW._autoLoadProject = path return app, MW @@ -255,6 +259,15 @@ def setup_signal_handlers(MW): signal.signal(signal.SIGTERM, sigint_handler("SIGTERM", MW)) +def is_valid_project(parser, arg): + if arg[-4:] != ".msk": + parser.error("only manuskript projects (.msk) are supported!") + if not os.path.isfile(arg): + parser.error("the project %s does not exist!" % arg) + else: + return arg + + def process_commandline(argv): import argparse parser = argparse.ArgumentParser(description="Run the manuskript application.") @@ -262,7 +275,8 @@ def process_commandline(argv): action="store_true") parser.add_argument("-v", "--verbose", action="count", default=1, help="lower the threshold for messages logged to the terminal") parser.add_argument("-L", "--logfile", default=None, help="override the default log file location") - parser.add_argument("filename", nargs="?", metavar="FILENAME", help="the manuskript project (.msk) to open") + parser.add_argument("filename", nargs="?", metavar="FILENAME", help="the manuskript project (.msk) to open", + type=lambda x: is_valid_project(parser, x)) args = parser.parse_args(args=argv) @@ -283,7 +297,7 @@ def run(): 2. So that prepare can be used in tests, without running the whole thing """ # Parse command-line arguments. - arguments = process_commandline(sys.argv) + arguments = process_commandline(sys.argv[1:]) # Initialize logging. (Does not include Qt integration yet.) manuskript.logging.setUp(console_level=arguments.verbose) diff --git a/manuskript/models/characterPOVModel.py b/manuskript/models/characterPOVModel.py index fbbe419e..03309a71 100644 --- a/manuskript/models/characterPOVModel.py +++ b/manuskript/models/characterPOVModel.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # --!-- coding: utf8 --!-- from PyQt5.QtCore import QModelIndex, QSortFilterProxyModel - +from manuskript.enums import Character as C class characterPOVModel(QSortFilterProxyModel): @@ -14,7 +14,11 @@ class characterPOVModel(QSortFilterProxyModel): sourceModel.dataChanged.connect(self.sourceDataChanged) def filterAcceptsRow(self, sourceRow, sourceParent): - return self.sourceModel().pov(sourceRow) + # Although I would prefer to reuse the existing characterModel.pov() method, + # this is simpler to do, actually works and also more ideomatic Qt code. + index = self.sourceModel().index(sourceRow, C.pov.value, sourceParent) + value = self.sourceModel().data(index) + return bool(value) def rowToSource(self, row): index = self.index(row, 0) diff --git a/manuskript/models/outlineItem.py b/manuskript/models/outlineItem.py index 97160aa2..bcb46ae0 100644 --- a/manuskript/models/outlineItem.py +++ b/manuskript/models/outlineItem.py @@ -98,7 +98,7 @@ class outlineItem(abstractItem, searchableItem): ) __repr__ = __str__ - + def charCount(self): return self._data.get(self.enum.charCount, 0) @@ -116,6 +116,9 @@ class outlineItem(abstractItem, searchableItem): return [] else: + # Used to verify nbsp characters not getting clobbered. + #if column == E.text: + # print("GET", str(role), "-->", str([hex(ord(x)) for x in data])) return data elif role == Qt.DecorationRole and column == E.title: @@ -158,6 +161,8 @@ class outlineItem(abstractItem, searchableItem): # Stuff to do before if column == E.text: self.addRevision() + # Used to verify nbsp characters not getting clobbered. + #print("SET", str(role), "-->", str([hex(ord(x)) for x in data])) # Calling base class implementation abstractItem.setData(self, column, data, role) diff --git a/manuskript/ui/views/MDEditView.py b/manuskript/ui/views/MDEditView.py index dcd5e3a9..1efbc807 100644 --- a/manuskript/ui/views/MDEditView.py +++ b/manuskript/ui/views/MDEditView.py @@ -500,7 +500,10 @@ class MDEditView(textEditView): r3.setLeft(self.viewport().geometry().left()) r3.setRight(self.viewport().geometry().right()) refs.append(ClickThing(r3, rx, rx.capturedTexts())) - cursor.movePosition(cursor.Down) + if not cursor.movePosition(cursor.Down): + # Super-rare failure. Leaving log message for future investigation. + LOGGER.debug("Failed to move cursor down while calculating clickables. Aborting.") + break self.clickRects = refs diff --git a/manuskript/ui/views/textEditView.py b/manuskript/ui/views/textEditView.py index b4f07e9d..0c5a7121 100644 --- a/manuskript/ui/views/textEditView.py +++ b/manuskript/ui/views/textEditView.py @@ -20,7 +20,11 @@ from manuskript.models.characterModel import Character, CharacterInfo import logging LOGGER = logging.getLogger(__name__) +# See implementation of QTextDocument::toPlainText() +PLAIN_TRANSLATION_TABLE = {0x2028: "\n", 0x2029: "\n", 0xfdd0: "\n", 0xfdd1: "\n"} + class textEditView(QTextEdit): + def __init__(self, parent=None, index=None, html=None, spellcheck=None, highlighting=False, dict="", autoResize=False): QTextEdit.__init__(self, parent) @@ -278,13 +282,20 @@ class textEditView(QTextEdit): def reconnectDocument(self): self.document().contentsChanged.connect(self.updateTimer.start, F.AUC) + def toIdealText(self): + """QTextDocument::toPlainText() replaces NBSP with spaces, which we don't want. + QTextDocument::toRawText() replaces nothing, but that leaves fancy paragraph and line separators that users would likely complain about. + This reimplements toPlainText(), except without the NBSP destruction.""" + return self.document().toRawText().translate(PLAIN_TRANSLATION_TABLE) + toPlainText = toIdealText + def updateText(self): self._updating.lock() # LOGGER.debug("Updating %s", self.objectName()) if self._index: self.disconnectDocument() - if self.toPlainText() != F.toString(self._index.data()): + if self.toIdealText() != F.toString(self._index.data()): # LOGGER.debug(" Updating plaintext") self.document().setPlainText(F.toString(self._index.data())) self.reconnectDocument() @@ -319,7 +330,7 @@ class textEditView(QTextEdit): self.updateTimer.stop() self._updating.lock() - text = self.toPlainText() + text = self.toIdealText() self._updating.unlock() # LOGGER.debug("Submitting %s", self.objectName())