diff --git a/.travis.yml b/.travis.yml index a39e4233..995f0289 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: generic os: - osx - linux -osx_image: xcode10.1 +osx_image: xcode11 sudo: required install: - if [ "$TRAVIS_OS_NAME" = "osx" ]; then package/prepare_osx.sh; fi diff --git a/i18n/manuskript_fr.ts b/i18n/manuskript_fr.ts index a53aea61..0ee622c1 100644 --- a/i18n/manuskript_fr.ts +++ b/i18n/manuskript_fr.ts @@ -5,7 +5,7 @@ Default exporter, provides basic formats used by other exporters. - Exporteur de base, fournit les formats de base utilisés par les autres exporteurs. + Exportateur de base, fournit les formats de base utilisés par les autres exportateurs. @@ -28,7 +28,7 @@ formats.</p> <p>Website: <a href="http://www.pandoc.org">http://pandoc.org/</a></p> - <p>Un convertisseur universel de document. Peut être utilisé pour convertir du Markdown dans un grand nombre de formats.</p> + <p>Un convertisseur universel de documents. Peut être utilisé pour convertir du Markdown dans un grand nombre de formats.</p> <p>Site internet : <a href="http://www.pandoc.org">http://pandoc.org/</a></p> @@ -36,7 +36,7 @@ Just like plain text, excepts adds markdown titles. Presupposes that texts are formatted in markdown. - Comme en texte brute, mais rajoute les titres markdown. + Comme en texte brut, mais ajoute les titres markdown. @@ -46,7 +46,7 @@ Plain text - Texte brute + Texte brut @@ -99,7 +99,7 @@ Par exemple <a href='www.fountain.io'>Fountain</a>. Export to markdown, using pandoc. Allows more formatting options than the basic manuskript exporter. - Export vers markdown en utilisant pandoc. Permet plus de possibilités que l'exporteur basique intégré à manuskript. + Export vers markdown en utilisant pandoc. Permet plus de possibilités que l'exportateur basique intégré à manuskript. @@ -110,7 +110,7 @@ Par exemple <a href='www.fountain.io'>Fountain</a>. LaTeX is a word processor and document markup language used to create beautiful documents. - LaTeX est une suite d'outil et un format utilisé pour créer des documents magnifiques. + LaTeX est une suite d'outils et un format utilisé pour créer des documents magnifiques. @@ -125,7 +125,7 @@ Par exemple <a href='www.fountain.io'>Fountain</a>. Number of sections level to include in TOC: - Nombre de niveau à inclure dans la table des matières: + Nombre de niveaux à inclure dans la table des matières : @@ -140,7 +140,7 @@ Par exemple <a href='www.fountain.io'>Fountain</a>. Specify the base level for headers: - Spécifier le niveau de base pour les titres: + Spécifier le niveau de base pour les titres : @@ -191,7 +191,7 @@ Par exemple <a href='www.fountain.io'>Fountain</a>. a valid LaTeX installation. Pandoc recommendations can be found on: <a href="https://pandoc.org/installing.html">pandoc.org/installing.html</a>. If you want Unicode support, you need XeLaTeX. - nécessite une installation latex valide. Voir les recommendations de pandoc sur <a href="http://pandoc.org/installing.html">http://pandoc.org/installing.html</a>. Pour le support unicode, il vous faut xelatex. + nécessite une installation LaTeX valide. Voir les recommandations de pandoc sur <a href="http://pandoc.org/installing.html">http://pandoc.org/installing.html</a>. Pour le support unicode, il vous faut XeLaTeX. @@ -222,7 +222,7 @@ Cochez ceci si vous avez des erreurs liées à YAML. Choose output file… - Choisir le fichier de sortie... + Choisir le fichier de sortie… @@ -230,7 +230,7 @@ Cochez ceci si vous avez des erreurs liées à YAML. Manage Exporters - Gérer les Exporteurs + Gérer les Exportateurs @@ -250,27 +250,27 @@ Cochez ceci si vous avez des erreurs liées à YAML. Status - Status + Statut Status: - Status : + Statut : Version: - Version: + Version : Path: - Chemin: + Chemin : ... - ... + @@ -298,7 +298,7 @@ Cochez ceci si vous avez des erreurs liées à YAML. Minimum size: - Taille minimum: + Taille minimum : @@ -318,7 +318,7 @@ Cochez ceci si vous avez des erreurs liées à YAML. Number of words: from - Nombre de mots: de + Nombre de mots : de @@ -355,7 +355,7 @@ Cochez ceci si vous avez des erreurs liées à YAML. are added as scene.</p> <p>Only text files are supported (not images, binary or others).</p> <p><b>Info:</b> Importe la structure d'un dossier. Les dossiers sont crées comme dossiers, et les documents textes sont ajoutés comme des scènes.</p> -<p>Seulement les fichiers textes sont supportés (pas les images, fichiers binaires ou autres).</p> +<p>Seuls les fichiers textes sont pris en charge (pas les images, fichiers binaires ou autres).</p> @@ -375,7 +375,7 @@ Cochez ceci si vous avez des erreurs liées à YAML. Import folder then files - Importer les dossiers en premier + Importer les dossiers puis les fichiers/translation> @@ -385,7 +385,7 @@ Cochez ceci si vous avez des erreurs liées à YAML. File open failed. - Echec de l'ouverture du fichier + Échec de l'ouverture du fichier @@ -410,12 +410,12 @@ Cochez ceci si vous avez des erreurs liées à YAML. Import using: - Importer via: + Importer via : Wrap lines: - Replier les lignes: + Replier les lignes : @@ -425,8 +425,8 @@ Cochez ceci si vous avez des erreurs liées à YAML. <b>none</b>: no line wrap.<br> <b>preserve</b>: tries to preserves line wrap from the original document.</p> - <p>Pandoc doit-il créer des retours à la lignes cosmétiques/non-sémantiques?</p> -<p><b>auto</b>: plier les lignes à 72 charactères.<br> + <p>Pandoc doit-il créer des retours à la ligne cosmétiques/non-sémantiques?</p> +<p><b>auto</b>: retour à la ligne après 72 caractères.<br> <b>none</b>: pas de retours à la lignes.<br> <b>preserve</b>: essaie de préserver les retours à la lignes du document original.</p> @@ -448,7 +448,7 @@ Cochez ceci si vous avez des erreurs liées à YAML. Import tip as: - Importer les pointes comme: + Importer les astuces comme : @@ -494,7 +494,7 @@ Cochez ceci si vous avez des erreurs liées à YAML. License - License + Licence @@ -729,17 +729,17 @@ Cochez ceci si vous avez des erreurs liées à YAML. Situation: - Situation: + Situation : Summary: - Résumé: + Résumé : What if...? - Et si... ? + Et si… ? @@ -839,7 +839,7 @@ Cochez ceci si vous avez des erreurs liées à YAML. Project tree - Arborescence + Arborescence du projet @@ -854,7 +854,7 @@ Cochez ceci si vous avez des erreurs liées à YAML. Create your characters. - Créez ici vos personnage. + Créez ici vos personnages. @@ -874,7 +874,7 @@ Cochez ceci si vous avez des erreurs liées à YAML. Debug info. Sometimes useful. - Des informations pour débugger des fois pendant qu'on code c'est utile. + Des informations pour débugger pendant qu'on code c'est parfois utile. @@ -889,7 +889,7 @@ Cochez ceci si vous avez des erreurs liées à YAML. POV - POV + Pt de vue @@ -919,7 +919,7 @@ Cochez ceci si vous avez des erreurs liées à YAML. Icon - Icone + Icône @@ -1004,7 +1004,7 @@ Cochez ceci si vous avez des erreurs liées à YAML. &Show help texts - &Afficher les bulles d'aides + &Afficher les bulles d'aide @@ -1019,7 +1019,7 @@ Cochez ceci si vous avez des erreurs liées à YAML. &Status... - &Status… + &Statut… @@ -1054,7 +1054,7 @@ Cochez ceci si vous avez des erreurs liées à YAML. Story line - + @@ -1081,7 +1081,7 @@ Cochez ceci si vous avez des erreurs liées à YAML. &About - &A propos + &À propos @@ -1096,12 +1096,12 @@ Cochez ceci si vous avez des erreurs liées à YAML. WARNING: Project {} not saved. - ATTENTION: Le projet {} n'a pas été enregistré. + ATTENTION : Le projet {} n'a pas été enregistré. Build worlds. Create hierarchy of broad categories down to specific details. - Construire des mondes. Crée une hierarchie en partant des catégories les plus larges jusqu'au détails les plus spécifiques. + Construire des mondes. Crée une hiérarchie en partant des catégories les plus larges jusqu'aux détails les plus spécifiques. diff --git a/manuskript/enums.py b/manuskript/enums.py index e5751a7d..0b86392f 100644 --- a/manuskript/enums.py +++ b/manuskript/enums.py @@ -61,6 +61,7 @@ class Outline(IntEnum): textFormat = 15 revisions = 16 customIcon = 17 + charCount = 18 class Abstract(IntEnum): title = 0 diff --git a/manuskript/functions/__init__.py b/manuskript/functions/__init__.py index 8d491921..5f790b58 100644 --- a/manuskript/functions/__init__.py +++ b/manuskript/functions/__init__.py @@ -23,6 +23,14 @@ def wordCount(text): t = [l for l in t if l] return len(t) +def charCount(text, use_spaces = True): + t = text.strip() + + if not use_spaces: + t = t.replace(" ", "") + + return len(t) + validate_ok = lambda *args, **kwargs: True def uiParse(input, default, converter, validator=validate_ok): """ diff --git a/manuskript/functions/spellchecker.py b/manuskript/functions/spellchecker.py index da41f0db..d3b90144 100644 --- a/manuskript/functions/spellchecker.py +++ b/manuskript/functions/spellchecker.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # --!-- coding: utf8 --!-- -import os, gzip, json, glob +import os, gzip, json, glob, re from PyQt5.QtCore import QLocale from collections import OrderedDict from manuskript.functions import writablePath @@ -28,6 +28,11 @@ except ImportError: symspellpy = None +try: + import language_check as languagetool +except: + languagetool = None + class Spellchecker: dictionaries = {} # In order of priority @@ -117,6 +122,17 @@ class Spellchecker: pass return None +class BasicMatch: + def __init__(self, startIndex, endIndex): + self.start = startIndex + self.end = endIndex + self.locqualityissuetype = 'misspelling' + self.replacements = [] + self.msg = '' + + def getWord(self, text): + return text[self.start:self.end] + class BasicDictionary: def __init__(self, name): self._lang = name @@ -162,12 +178,45 @@ class BasicDictionary: def availableDictionaries(): raise NotImplemented + def checkText(self, text): + # Based on http://john.nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check/ + WORDS = r'(?iu)((?:[^_\W]|\')+)[^A-Za-z0-9\']' + # (?iu) means case insensitive and Unicode + # ((?:[^_\W]|\')+) means words exclude underscores but include apostrophes + # [^A-Za-z0-9\'] used with above hack to prevent spellcheck while typing word + # + # See also https://stackoverflow.com/questions/2062169/regex-w-in-utf-8 + + matches = [] + + for word_object in re.finditer(WORDS, text): + word = word_object.group(1) + + if (self.isMisspelled(word) and not self.isCustomWord(word)): + matches.append(BasicMatch( + word_object.start(1), word_object.end(1) + )) + + return matches + def isMisspelled(self, word): raise NotImplemented def getSuggestions(self, word): raise NotImplemented + def findSuggestions(self, text, start, end): + if start < end: + word = text[start:end] + + if (self.isMisspelled(word) and not self.isCustomWord(word)): + match = BasicMatch(start, end) + match.replacements = self.getSuggestions(word) + + return [ match ] + + return [] + def isCustomWord(self, word): return word.lower() in self._customDict @@ -248,6 +297,9 @@ class EnchantDictionary(BasicDictionary): def getSuggestions(self, word): return self._dict.suggest(word) + def findSuggestions(self, text, start, end): + return [] + def isCustomWord(self, word): return self._dict.is_added(word) @@ -422,8 +474,152 @@ class SymSpellDictionary(BasicDictionary): # Since 6.3.8 self._dict.delete_dictionary_entry(word) +class LanguageToolCache: + + def __init__(self, tool, text): + self._length = len(text) + self._matches = self._buildMatches(tool, text) + + def getMatches(self): + return self._matches + + def _buildMatches(self, tool, text): + matches = [] + + for match in tool.check(text): + start = match.offset + end = start + match.errorlength + + basic_match = BasicMatch(start, end) + basic_match.locqualityissuetype = match.locqualityissuetype + basic_match.replacements = match.replacements + basic_match.msg = match.msg + + matches.append(basic_match) + + return matches + + def update(self, tool, text): + if len(text) != self._length: + self._matches = self._buildMatches(tool, text) + +class LanguageToolDictionary(BasicDictionary): + + def __init__(self, name): + BasicDictionary.__init__(self, name) + + if not (self._lang and self._lang in languagetool.get_languages()): + self._lang = self.getDefaultDictionary() + + self._tool = languagetool.LanguageTool(self._lang) + self._cache = {} + + @staticmethod + def getLibraryName(): + return "LanguageCheck" + + @staticmethod + def getLibraryURL(): + return "https://pypi.org/project/language-check/" + + @staticmethod + def isInstalled(): + if languagetool is not None: + + # This check, if Java is installed, is necessary to + # make sure LanguageTool can be run without problems. + # + return (os.system('java -version') == 0) + + return False + + @staticmethod + def availableDictionaries(): + if LanguageToolDictionary.isInstalled(): + languages = list(languagetool.get_languages()) + languages.sort() + return languages + return [] + + @staticmethod + def getDefaultDictionary(): + if not LanguageToolDictionary.isInstalled(): + return None + + default_locale = languagetool.get_locale_language() + if default_locale and not default_locale in languagetool.get_languages(): + default_locale = None + + if default_locale is None: + default_locale = QLocale.system().name() + if default_locale is None: + default_locale = self.availableDictionaries()[0] + + return default_locale + + def checkText(self, text): + matches = [] + + if len(text) == 0: + return matches + + textId = hash(text) + cacheEntry = None + + if not textId in self._cache: + cacheEntry = LanguageToolCache(self._tool, text) + + self._cache[textId] = cacheEntry + else: + cacheEntry = self._cache[textId] + cacheEntry.update(self._tool, text) + + for match in cacheEntry.getMatches(): + word = match.getWord(text) + + if not (match.locqualityissuetype == 'misspelling' and self.isCustomWord(word)): + matches.append(match) + + return matches + + def isMisspelled(self, word): + if self.isCustomWord(word): + return False + + for match in self.checkText(word): + if match.locqualityissuetype == 'misspelling': + return True + + return False + + def getSuggestions(self, word): + suggestions = [] + + for match in self.checkText(word): + suggestions += match.replacements + + return suggestions + + def findSuggestions(self, text, start, end): + matches = [] + checked = self.checkText(text) + + if start == end: + # Check for containing area: + for match in checked: + if (start >= match.start and start <= match.end): + matches.append(match) + else: + # Check for overlapping area: + for match in checked: + if (match.end > start and match.start < end): + matches.append(match) + + return matches + # Register the implementations in order of priority -Spellchecker.implementations.append(EnchantDictionary) +Spellchecker.registerImplementation(EnchantDictionary) Spellchecker.registerImplementation(SymSpellDictionary) Spellchecker.registerImplementation(PySpellcheckerDictionary) +Spellchecker.registerImplementation(LanguageToolDictionary) diff --git a/manuskript/models/outlineItem.py b/manuskript/models/outlineItem.py index 56b64649..b2ab2fa7 100644 --- a/manuskript/models/outlineItem.py +++ b/manuskript/models/outlineItem.py @@ -80,6 +80,9 @@ class outlineItem(abstractItem): def wordCount(self): return self._data.get(self.enum.wordCount, 0) + def charCount(self): + return self._data.get(self.enum.charCount, 0) + def __str__(self): return "{id}: {folder}{title}{children}".format( id=self.ID(), @@ -89,6 +92,9 @@ class outlineItem(abstractItem): ) __repr__ = __str__ + + def charCount(self): + return self._data.get(self.enum.charCount, 0) ####################################################################### # Data @@ -119,7 +125,7 @@ class outlineItem(abstractItem): elif role == Qt.FontRole: f = QFont() - if column == E.wordCount and self.isFolder(): + if (column == E.wordCount or column == E.charCount) and self.isFolder(): f.setItalic(True) elif column == E.goal and self.isFolder() and not self.data(E.setGoal): f.setItalic(True) @@ -140,7 +146,7 @@ class outlineItem(abstractItem): # Checking if we will have to recount words updateWordCount = False - if column in [E.wordCount, E.goal, E.setGoal]: + if column in [E.wordCount, E.charCount, E.goal, E.setGoal]: updateWordCount = not column in self._data or self._data[column] != data # Stuff to do before @@ -153,7 +159,9 @@ class outlineItem(abstractItem): # Stuff to do afterwards if column == E.text: wc = F.wordCount(data) + cc = F.charCount(data, settings.countSpaces) self.setData(E.wordCount, wc) + self.setData(E.charCount, cc) if column == E.compile: # Title changes when compile changes @@ -195,9 +203,12 @@ class outlineItem(abstractItem): else: wc = 0 + cc = 0 for c in self.children(): wc += F.toInt(c.data(self.enum.wordCount)) + cc += F.toInt(c.data(self.enum.charCount)) self._data[self.enum.wordCount] = wc + self._data[self.enum.charCount] = cc setGoal = F.toInt(self.data(self.enum.setGoal)) goal = F.toInt(self.data(self.enum.goal)) @@ -218,7 +229,8 @@ class outlineItem(abstractItem): self.setData(self.enum.goalPercentage, "") self.emitDataChanged([self.enum.goal, self.enum.setGoal, - self.enum.wordCount, self.enum.goalPercentage]) + self.enum.wordCount, self.enum.charCount, + self.enum.goalPercentage]) if self.parent(): self.parent().updateWordCount() @@ -467,6 +479,7 @@ class outlineItem(abstractItem): # We don't want to write some datas (computed) XMLExclude = [enums.Outline.wordCount, + enums.Outline.charCount, enums.Outline.goal, enums.Outline.goalPercentage, enums.Outline.revisions] diff --git a/manuskript/settings.py b/manuskript/settings.py index 96a658e1..8f4884ea 100644 --- a/manuskript/settings.py +++ b/manuskript/settings.py @@ -47,6 +47,8 @@ corkSizeFactor = 100 folderView = "cork" lastTab = 0 openIndexes = [""] +progressChars = False +countSpaces = True autoSave = False autoSaveDelay = 5 autoSaveNoChanges = True @@ -123,7 +125,7 @@ def initDefaultValues(): def save(filename=None, protocol=None): global spellcheck, dict, corkSliderFactor, viewSettings, corkSizeFactor, folderView, lastTab, openIndexes, \ - autoSave, autoSaveDelay, saveOnQuit, autoSaveNoChanges, autoSaveNoChangesDelay, outlineViewColumns, \ + progressChars, autoSave, autoSaveDelay, saveOnQuit, autoSaveNoChanges, autoSaveNoChangesDelay, outlineViewColumns, \ corkBackground, corkStyle, fullScreenTheme, defaultTextType, textEditor, revisions, frequencyAnalyzer, viewMode, \ saveToZip, dontShowDeleteWarning, fullscreenSettings @@ -136,6 +138,8 @@ def save(filename=None, protocol=None): "folderView": folderView, "lastTab": lastTab, "openIndexes": openIndexes, + "progressChars": progressChars, + "countSpaces": countSpaces, "autoSave":autoSave, "autoSaveDelay":autoSaveDelay, # TODO: Settings Cleanup Task -- Rename saveOnQuit to saveOnProjectClose -- see PR #615 @@ -235,6 +239,14 @@ def load(string, fromString=False, protocol=None): global openIndexes openIndexes = allSettings["openIndexes"] + if "progressChars" in allSettings: + global progressChars + progressChars = allSettings["progressChars"] + + if "countSpaces" in allSettings: + global countSpaces + countSpaces = allSettings["countSpaces"] + if "autoSave" in allSettings: global autoSave autoSave = allSettings["autoSave"] diff --git a/manuskript/settingsWindow.py b/manuskript/settingsWindow.py index 4ae34bf6..ac8f8551 100644 --- a/manuskript/settingsWindow.py +++ b/manuskript/settingsWindow.py @@ -111,6 +111,9 @@ class settingsWindow(QWidget, Ui_Settings): self.spnGeneralFontSize.setValue(f.pointSize()) self.spnGeneralFontSize.valueChanged.connect(self.setAppFontSize) + self.chkProgressChars.setChecked(settings.progressChars); + self.chkProgressChars.stateChanged.connect(self.charSettingsChanged) + self.txtAutoSave.setValidator(QIntValidator(0, 999, self)) self.txtAutoSaveNoChanges.setValidator(QIntValidator(0, 999, self)) self.chkAutoSave.setChecked(settings.autoSave) @@ -164,10 +167,12 @@ class settingsWindow(QWidget, Ui_Settings): for item, what, value in [ (self.rdoTreeItemCount, "InfoFolder", "Count"), (self.rdoTreeWC, "InfoFolder", "WC"), + (self.rdoTreeCC, "InfoFolder", "CC"), (self.rdoTreeProgress, "InfoFolder", "Progress"), (self.rdoTreeSummary, "InfoFolder", "Summary"), (self.rdoTreeNothing, "InfoFolder", "Nothing"), (self.rdoTreeTextWC, "InfoText", "WC"), + (self.rdoTreeTextCC, "InfoText", "CC"), (self.rdoTreeTextProgress, "InfoText", "Progress"), (self.rdoTreeTextSummary, "InfoText", "Summary"), (self.rdoTreeTextNothing, "InfoText", "Nothing"), @@ -180,6 +185,9 @@ class settingsWindow(QWidget, Ui_Settings): lambda v: self.lblTreeIconSize.setText("{}x{}".format(v, v))) self.sldTreeIconSize.setValue(settings.viewSettings["Tree"]["iconSize"]) + self.chkCountSpaces.setChecked(settings.countSpaces); + self.chkCountSpaces.stateChanged.connect(self.countSpacesChanged) + self.rdoCorkOldStyle.setChecked(settings.corkStyle == "old") self.rdoCorkNewStyle.setChecked(settings.corkStyle == "new") self.rdoCorkNewStyle.toggled.connect(self.setCorkStyle) @@ -338,6 +346,11 @@ class settingsWindow(QWidget, Ui_Settings): sttgs = QSettings(qApp.organizationName(), qApp.applicationName()) sttgs.setValue("appFontSize", val) + def charSettingsChanged(self): + settings.progressChars = True if self.chkProgressChars.checkState() else False + + self.mw.mainEditor.updateStats() + def saveSettingsChanged(self): if self.txtAutoSave.text() in ["", "0"]: self.txtAutoSave.setText("1") @@ -427,10 +440,12 @@ class settingsWindow(QWidget, Ui_Settings): for item, what, value in [ (self.rdoTreeItemCount, "InfoFolder", "Count"), (self.rdoTreeWC, "InfoFolder", "WC"), + (self.rdoTreeCC, "InfoFolder", "CC"), (self.rdoTreeProgress, "InfoFolder", "Progress"), (self.rdoTreeSummary, "InfoFolder", "Summary"), (self.rdoTreeNothing, "InfoFolder", "Nothing"), (self.rdoTreeTextWC, "InfoText", "WC"), + (self.rdoTreeTextCC, "InfoText", "CC"), (self.rdoTreeTextProgress, "InfoText", "Progress"), (self.rdoTreeTextSummary, "InfoText", "Summary"), (self.rdoTreeTextNothing, "InfoText", "Nothing"), @@ -445,6 +460,11 @@ class settingsWindow(QWidget, Ui_Settings): self.mw.treeRedacOutline.viewport().update() + def countSpacesChanged(self): + settings.countSpaces = True if self.chkCountSpaces.checkState() else False + + self.mw.mainEditor.updateStats() + def setCorkColor(self): color = QColor(settings.corkBackground["color"]) self.colorDialog = QColorDialog(color, self) diff --git a/manuskript/ui/editors/mainEditor.py b/manuskript/ui/editors/mainEditor.py index 967f2832..882292ee 100644 --- a/manuskript/ui/editors/mainEditor.py +++ b/manuskript/ui/editors/mainEditor.py @@ -292,6 +292,7 @@ class mainEditor(QWidget, Ui_mainEditor): return index = self.currentEditor().currentIndex + if index.isValid(): item = index.internalPointer() else: @@ -300,15 +301,21 @@ class mainEditor(QWidget, Ui_mainEditor): if not item: item = self.mw.mdlOutline.rootItem + cc = item.data(Outline.charCount) wc = item.data(Outline.wordCount) goal = item.data(Outline.goal) + chars = item.data(Outline.charCount) # len(item.data(Outline.text)) progress = item.data(Outline.goalPercentage) goal = uiParse(goal, None, int, lambda x: x>=0) progress = uiParse(progress, 0.0, float) + if not cc: + cc = 0 + if not wc: wc = 0 + if goal: self.lblRedacProgress.show() rect = self.lblRedacProgress.geometry() @@ -319,13 +326,31 @@ class mainEditor(QWidget, Ui_mainEditor): drawProgress(p, rect, progress, 2) del p self.lblRedacProgress.setPixmap(self.px) - self.lblRedacWC.setText(self.tr("{} words / {} ").format( - locale.format_string("%d", wc, grouping=True), - locale.format_string("%d", goal, grouping=True))) + + if settings.progressChars: + self.lblRedacWC.setText(self.tr("({} chars) {} words / {} ").format( + locale.format("%d", cc, grouping=True), + locale.format("%d", wc, grouping=True), + locale.format("%d", goal, grouping=True))) + self.lblRedacWC.setToolTip("") + else: + self.lblRedacWC.setText(self.tr("{} words / {} ").format( + locale.format("%d", wc, grouping=True), + locale.format("%d", goal, grouping=True))) + self.lblRedacWC.setToolTip(self.tr("{} chars").format( + locale.format("%d", cc, grouping=True))) else: self.lblRedacProgress.hide() - self.lblRedacWC.setText(self.tr("{} words ").format( - locale.format_string("%d", wc, grouping=True))) + + if settings.progressChars: + self.lblRedacWC.setText(self.tr("{} chars ").format( + locale.format("%d", cc, grouping=True))) + self.lblRedacWC.setToolTip("") + else: + self.lblRedacWC.setText(self.tr("{} words ").format( + locale.format("%d", wc, grouping=True))) + self.lblRedacWC.setToolTip(self.tr("{} chars").format( + locale.format("%d", cc, grouping=True))) ############################################################################### # VIEWS diff --git a/manuskript/ui/highlighters/basicHighlighter.py b/manuskript/ui/highlighters/basicHighlighter.py index 362ee5a2..17ae8bd7 100644 --- a/manuskript/ui/highlighters/basicHighlighter.py +++ b/manuskript/ui/highlighters/basicHighlighter.py @@ -18,7 +18,6 @@ class BasicHighlighter(QSyntaxHighlighter): QSyntaxHighlighter.__init__(self, editor.document()) self.editor = editor - self._misspelledColor = Qt.red self._defaultBlockFormat = QTextBlockFormat() self._defaultCharFormat = QTextCharFormat() self.defaultTextColor = QColor(S.text) @@ -27,6 +26,40 @@ class BasicHighlighter(QSyntaxHighlighter): self.linkColor = QColor(S.link) self.spellingErrorColor = QColor(Qt.red) + # Matches during checking can be separated by their type (all of them listed here): + # https://languagetool.org/development/api/org/languagetool/rules/ITSIssueType.html + # + # These are the colors for actual spell-, grammar- and style-checking: + self._errorColors = { + 'addition' : QColor(255, 215, 0), # gold + 'characters' : QColor(135, 206, 235), # sky blue + 'duplication' : QColor(0, 255, 255), # cyan / aqua + 'formatting' : QColor(0, 128, 128), # teal + 'grammar' : QColor(0, 0, 255), # blue + 'inconsistency' : QColor(128, 128, 0), # olive + 'inconsistententities' : QColor(46, 139, 87), # sea green + 'internationalization' : QColor(255, 165, 0), # orange + 'legal' : QColor(255, 69, 0), # orange red + 'length' : QColor(47, 79, 79), # dark slate gray + 'localespecificcontent' : QColor(188, 143, 143),# rosy brown + 'localeviolation' : QColor(128, 0, 0), # maroon + 'markup' : QColor(128, 0, 128), # purple + 'misspelling' : QColor(255, 0, 0), # red + 'mistranslation' : QColor(255, 0, 255), # magenta / fuchsia + 'nonconformance' : QColor(255, 218, 185), # peach puff + 'numbers' : QColor(65, 105, 225), # royal blue + 'omission' : QColor(255, 20, 147), # deep pink + 'other' : QColor(138, 43, 226), # blue violet + 'patternproblem' : QColor(0, 128, 0), # green + 'register' : QColor(112,128,144), # slate gray + 'style' : QColor(0, 255, 0), # lime + 'terminology' : QColor(0, 0, 128), # navy + 'typographical' : QColor(255, 255, 0), # yellow + 'uncategorized' : QColor(128, 128, 128), # gray + 'untranslated' : QColor(210, 105, 30), # chocolate + 'whitespace' : QColor(192, 192, 192) # silver + } + def setDefaultBlockFormat(self, bf): self._defaultBlockFormat = bf self.rehighlight() @@ -36,7 +69,7 @@ class BasicHighlighter(QSyntaxHighlighter): self.rehighlight() def setMisspelledColor(self, color): - self._misspelledColor = color + self._errorColors['misspelled'] = color def updateColorScheme(self, rehighlight=True): """ @@ -134,32 +167,25 @@ class BasicHighlighter(QSyntaxHighlighter): txt.end() - txt.start(), fmt) - # Spell checking + if hasattr(self.editor, "spellcheck") and self.editor.spellcheck and self.editor._dict: + # Spell checking - # Following algorithm would not check words at the end of line. - # This hacks adds a space to every line where the text cursor is not - # So that it doesn't spellcheck while typing, but still spellchecks at - # end of lines. See github's issue #166. - textedText = text - if self.currentBlock().position() + len(text) != \ - self.editor.textCursor().position(): - textedText = text + " " + # Following algorithm would not check words at the end of line. + # This hacks adds a space to every line where the text cursor is not + # So that it doesn't spellcheck while typing, but still spellchecks at + # end of lines. See github's issue #166. + textedText = text + if self.currentBlock().position() + len(text) != \ + self.editor.textCursor().position(): + textedText = text + " " - # Based on http://john.nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check/ - WORDS = r'(?iu)((?:[^_\W]|\')+)[^A-Za-z0-9\']' - # (?iu) means case insensitive and Unicode - # ((?:[^_\W]|\')+) means words exclude underscores but include apostrophes - # [^A-Za-z0-9\'] used with above hack to prevent spellcheck while typing word - # - # See also https://stackoverflow.com/questions/2062169/regex-w-in-utf-8 - if hasattr(self.editor, "spellcheck") and self.editor.spellcheck: - for word_object in re.finditer(WORDS, textedText): - if (self.editor._dict - and self.editor._dict.isMisspelled(word_object.group(1))): - format = self.format(word_object.start(1)) - format.setUnderlineColor(self._misspelledColor) + # The text should only be checked once as a whole + for match in self.editor._dict.checkText(textedText): + if match.locqualityissuetype in self._errorColors: + highlight_color = self._errorColors[match.locqualityissuetype] + + format = self.format(match.start) + format.setUnderlineColor(highlight_color) # SpellCheckUnderline fails with some fonts format.setUnderlineStyle(QTextCharFormat.WaveUnderline) - self.setFormat(word_object.start(1), - word_object.end(1) - word_object.start(1), - format) + self.setFormat(match.start, match.end - match.start, format) diff --git a/manuskript/ui/settings_ui.py b/manuskript/ui/settings_ui.py index 9053f31a..b34d9823 100644 --- a/manuskript/ui/settings_ui.py +++ b/manuskript/ui/settings_ui.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'manuskript/ui/settings_ui.ui' +# Form implementation generated from reading ui file 'settings_ui.ui' # -# Created by: PyQt5 UI code generator 5.14.1 +# Created by: PyQt5 UI code generator 5.15.0 # -# WARNING! All changes made in this file will be lost! +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. from PyQt5 import QtCore, QtGui, QtWidgets @@ -13,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_Settings(object): def setupUi(self, Settings): Settings.setObjectName("Settings") - Settings.resize(658, 598) + Settings.resize(681, 598) self.horizontalLayout_8 = QtWidgets.QHBoxLayout(Settings) self.horizontalLayout_8.setObjectName("horizontalLayout_8") self.lstMenu = QtWidgets.QListWidget(Settings) @@ -55,50 +56,9 @@ class Ui_Settings(object): self.groupBox_2.setFont(font) self.groupBox_2.setObjectName("groupBox_2") self.formLayout_13 = QtWidgets.QFormLayout(self.groupBox_2) - self.formLayout_13.setFieldGrowthPolicy(QtWidgets.QFormLayout.FieldsStayAtSizeHint) self.formLayout_13.setObjectName("formLayout_13") - self.label_56 = QtWidgets.QLabel(self.groupBox_2) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.label_56.setFont(font) - self.label_56.setObjectName("label_56") - self.formLayout_13.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.label_56) - self.cmbStyle = QtWidgets.QComboBox(self.groupBox_2) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.cmbStyle.setFont(font) - self.cmbStyle.setObjectName("cmbStyle") - self.formLayout_13.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.cmbStyle) - self.label_57 = QtWidgets.QLabel(self.groupBox_2) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.label_57.setFont(font) - self.label_57.setObjectName("label_57") - self.formLayout_13.setWidget(6, QtWidgets.QFormLayout.LabelRole, self.label_57) - self.cmbTranslation = QtWidgets.QComboBox(self.groupBox_2) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.cmbTranslation.setFont(font) - self.cmbTranslation.setObjectName("cmbTranslation") - self.formLayout_13.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.cmbTranslation) - self.label_58 = QtWidgets.QLabel(self.groupBox_2) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.label_58.setFont(font) - self.label_58.setObjectName("label_58") - self.formLayout_13.setWidget(7, QtWidgets.QFormLayout.LabelRole, self.label_58) - self.spnGeneralFontSize = QtWidgets.QSpinBox(self.groupBox_2) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.spnGeneralFontSize.setFont(font) - self.spnGeneralFontSize.setObjectName("spnGeneralFontSize") - self.formLayout_13.setWidget(7, QtWidgets.QFormLayout.FieldRole, self.spnGeneralFontSize) + self.gridLayout_4 = QtWidgets.QGridLayout() + self.gridLayout_4.setObjectName("gridLayout_4") self.label_2 = QtWidgets.QLabel(self.groupBox_2) font = QtGui.QFont() font.setBold(False) @@ -106,7 +66,70 @@ class Ui_Settings(object): self.label_2.setFont(font) self.label_2.setWordWrap(True) self.label_2.setObjectName("label_2") - self.formLayout_13.setWidget(2, QtWidgets.QFormLayout.SpanningRole, self.label_2) + self.gridLayout_4.addWidget(self.label_2, 0, 0, 1, 1) + self.horizontalLayout_12 = QtWidgets.QHBoxLayout() + self.horizontalLayout_12.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) + self.horizontalLayout_12.setObjectName("horizontalLayout_12") + self.formLayout_14 = QtWidgets.QFormLayout() + self.formLayout_14.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow) + self.formLayout_14.setObjectName("formLayout_14") + self.label_56 = QtWidgets.QLabel(self.groupBox_2) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.label_56.setFont(font) + self.label_56.setObjectName("label_56") + self.formLayout_14.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.label_56) + self.cmbStyle = QtWidgets.QComboBox(self.groupBox_2) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.cmbStyle.setFont(font) + self.cmbStyle.setObjectName("cmbStyle") + self.formLayout_14.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.cmbStyle) + self.label_57 = QtWidgets.QLabel(self.groupBox_2) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.label_57.setFont(font) + self.label_57.setObjectName("label_57") + self.formLayout_14.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.label_57) + self.cmbTranslation = QtWidgets.QComboBox(self.groupBox_2) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.cmbTranslation.setFont(font) + self.cmbTranslation.setObjectName("cmbTranslation") + self.formLayout_14.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.cmbTranslation) + self.label_58 = QtWidgets.QLabel(self.groupBox_2) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.label_58.setFont(font) + self.label_58.setObjectName("label_58") + self.formLayout_14.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.label_58) + self.spnGeneralFontSize = QtWidgets.QSpinBox(self.groupBox_2) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.spnGeneralFontSize.setFont(font) + self.spnGeneralFontSize.setObjectName("spnGeneralFontSize") + self.formLayout_14.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.spnGeneralFontSize) + self.horizontalLayout_12.addLayout(self.formLayout_14) + self.formLayout_15 = QtWidgets.QFormLayout() + self.formLayout_15.setObjectName("formLayout_15") + self.chkProgressChars = QtWidgets.QCheckBox(self.groupBox_2) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.chkProgressChars.setFont(font) + self.chkProgressChars.setObjectName("chkProgressChars") + self.formLayout_15.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.chkProgressChars) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.formLayout_15.setItem(0, QtWidgets.QFormLayout.LabelRole, spacerItem) + self.horizontalLayout_12.addLayout(self.formLayout_15) + self.gridLayout_4.addLayout(self.horizontalLayout_12, 1, 0, 1, 1) + self.formLayout_13.setLayout(0, QtWidgets.QFormLayout.SpanningRole, self.gridLayout_4) self.verticalLayout_7.addWidget(self.groupBox_2) self.groupBox_10 = QtWidgets.QGroupBox(self.stackedWidgetPage1) font = QtGui.QFont() @@ -166,8 +189,8 @@ class Ui_Settings(object): self.label.setFont(font) self.label.setObjectName("label") self.horizontalLayout_5.addWidget(self.label) - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_5.addItem(spacerItem) + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_5.addItem(spacerItem1) self.verticalLayout_6.addLayout(self.horizontalLayout_5) self.horizontalLayout_7 = QtWidgets.QHBoxLayout() self.horizontalLayout_7.setObjectName("horizontalLayout_7") @@ -202,8 +225,8 @@ class Ui_Settings(object): self.label_14.setFont(font) self.label_14.setObjectName("label_14") self.horizontalLayout_7.addWidget(self.label_14) - spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_7.addItem(spacerItem1) + spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_7.addItem(spacerItem2) self.verticalLayout_6.addLayout(self.horizontalLayout_7) self.chkSaveOnQuit = QtWidgets.QCheckBox(self.groupBox) font = QtGui.QFont() @@ -223,8 +246,8 @@ class Ui_Settings(object): self.chkSaveToZip.setObjectName("chkSaveToZip") self.verticalLayout_6.addWidget(self.chkSaveToZip) self.verticalLayout_7.addWidget(self.groupBox) - spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout_7.addItem(spacerItem2) + spacerItem3 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout_7.addItem(spacerItem3) self.stack.addWidget(self.stackedWidgetPage1) self.page_3 = QtWidgets.QWidget() self.page_3.setObjectName("page_3") @@ -388,8 +411,8 @@ class Ui_Settings(object): self.label_51.setObjectName("label_51") self.gridLayout_2.addWidget(self.label_51, 6, 1, 1, 1) self.verticalLayout.addWidget(self.chkRevisionRemove) - spacerItem3 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout.addItem(spacerItem3) + spacerItem4 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem4) self.label_revisionDeprecation = QtWidgets.QLabel(self.page_3) self.label_revisionDeprecation.setWordWrap(True) self.label_revisionDeprecation.setOpenExternalLinks(True) @@ -524,6 +547,25 @@ class Ui_Settings(object): self.sldTreeIconSize.setObjectName("sldTreeIconSize") self.horizontalLayout_11.addWidget(self.sldTreeIconSize) self.verticalLayout_17.addWidget(self.groupBox_16) + self.horizontalGroupBox = QtWidgets.QGroupBox(self.tab) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.horizontalGroupBox.setFont(font) + self.horizontalGroupBox.setObjectName("horizontalGroupBox") + self.horizontalLayout_13 = QtWidgets.QHBoxLayout(self.horizontalGroupBox) + self.horizontalLayout_13.setContentsMargins(9, 9, 9, 9) + self.horizontalLayout_13.setObjectName("horizontalLayout_13") + self.chkCountSpaces = QtWidgets.QCheckBox(self.horizontalGroupBox) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.chkCountSpaces.setFont(font) + self.chkCountSpaces.setObjectName("chkCountSpaces") + self.horizontalLayout_13.addWidget(self.chkCountSpaces) + spacerItem5 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_13.addItem(spacerItem5) + self.verticalLayout_17.addWidget(self.horizontalGroupBox) self.horizontalLayout_9 = QtWidgets.QHBoxLayout() self.horizontalLayout_9.setObjectName("horizontalLayout_9") self.groupBox_8 = QtWidgets.QGroupBox(self.tab) @@ -548,6 +590,13 @@ class Ui_Settings(object): self.rdoTreeWC.setFont(font) self.rdoTreeWC.setObjectName("rdoTreeWC") self.verticalLayout_15.addWidget(self.rdoTreeWC) + self.rdoTreeCC = QtWidgets.QRadioButton(self.groupBox_8) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.rdoTreeCC.setFont(font) + self.rdoTreeCC.setObjectName("rdoTreeCC") + self.verticalLayout_15.addWidget(self.rdoTreeCC) self.rdoTreeProgress = QtWidgets.QRadioButton(self.groupBox_8) font = QtGui.QFont() font.setBold(False) @@ -586,6 +635,13 @@ class Ui_Settings(object): self.rdoTreeTextWC.setFont(font) self.rdoTreeTextWC.setObjectName("rdoTreeTextWC") self.verticalLayout_16.addWidget(self.rdoTreeTextWC) + self.rdoTreeTextCC = QtWidgets.QRadioButton(self.groupBox_9) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.rdoTreeTextCC.setFont(font) + self.rdoTreeTextCC.setObjectName("rdoTreeTextCC") + self.verticalLayout_16.addWidget(self.rdoTreeTextCC) self.rdoTreeTextProgress = QtWidgets.QRadioButton(self.groupBox_9) font = QtGui.QFont() font.setBold(False) @@ -607,12 +663,17 @@ class Ui_Settings(object): self.rdoTreeTextNothing.setFont(font) self.rdoTreeTextNothing.setObjectName("rdoTreeTextNothing") self.verticalLayout_16.addWidget(self.rdoTreeTextNothing) - spacerItem4 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout_16.addItem(spacerItem4) + spacerItem6 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout_16.addItem(spacerItem6) + self.rdoTreeTextCC.raise_() + self.rdoTreeTextWC.raise_() + self.rdoTreeTextProgress.raise_() + self.rdoTreeTextSummary.raise_() + self.rdoTreeTextNothing.raise_() self.horizontalLayout_9.addWidget(self.groupBox_9) self.verticalLayout_17.addLayout(self.horizontalLayout_9) - spacerItem5 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout_17.addItem(spacerItem5) + spacerItem7 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout_17.addItem(spacerItem7) icon = QtGui.QIcon.fromTheme("view-list-tree") self.tabViews.addTab(self.tab, icon, "") self.tab_2 = QtWidgets.QWidget() @@ -774,8 +835,8 @@ class Ui_Settings(object): self.chkOutlineTitle.setObjectName("chkOutlineTitle") self.gridLayout.addWidget(self.chkOutlineTitle, 3, 0, 1, 1) self.verticalLayout_11.addWidget(self.groupBox_6) - spacerItem6 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout_11.addItem(spacerItem6) + spacerItem8 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout_11.addItem(spacerItem8) icon = QtGui.QIcon.fromTheme("view-outline") self.tabViews.addTab(self.tab_2, icon, "") self.tab_3 = QtWidgets.QWidget() @@ -821,8 +882,8 @@ class Ui_Settings(object): self.cmbCorkImage.setFont(font) self.cmbCorkImage.setObjectName("cmbCorkImage") self.verticalLayout_8.addWidget(self.cmbCorkImage) - spacerItem7 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout_8.addItem(spacerItem7) + spacerItem9 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout_8.addItem(spacerItem9) self.gridLayout_3.addWidget(self.groupBox_7, 1, 1, 1, 1) self.groupBox_11 = QtWidgets.QGroupBox(self.tab_3) font = QtGui.QFont() @@ -1380,8 +1441,8 @@ class Ui_Settings(object): self.btnLabelColor.setIconSize(QtCore.QSize(64, 64)) self.btnLabelColor.setObjectName("btnLabelColor") self.verticalLayout_2.addWidget(self.btnLabelColor) - spacerItem8 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout_2.addItem(spacerItem8) + spacerItem10 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout_2.addItem(spacerItem10) self.horizontalLayout_2.addLayout(self.verticalLayout_2) self.verticalLayout_3.addLayout(self.horizontalLayout_2) self.horizontalLayout = QtWidgets.QHBoxLayout() @@ -1398,8 +1459,8 @@ class Ui_Settings(object): self.btnLabelRemove.setIcon(icon) self.btnLabelRemove.setObjectName("btnLabelRemove") self.horizontalLayout.addWidget(self.btnLabelRemove) - spacerItem9 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout.addItem(spacerItem9) + spacerItem11 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout.addItem(spacerItem11) self.verticalLayout_3.addLayout(self.horizontalLayout) self.stack.addWidget(self.stackedWidgetPage3) self.stackedWidgetPage4 = QtWidgets.QWidget() @@ -1433,8 +1494,8 @@ class Ui_Settings(object): self.btnStatusRemove.setIcon(icon) self.btnStatusRemove.setObjectName("btnStatusRemove") self.horizontalLayout_3.addWidget(self.btnStatusRemove) - spacerItem10 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_3.addItem(spacerItem10) + spacerItem12 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_3.addItem(spacerItem12) self.verticalLayout_4.addLayout(self.horizontalLayout_3) self.stack.addWidget(self.stackedWidgetPage4) self.page = QtWidgets.QWidget() @@ -1482,8 +1543,8 @@ class Ui_Settings(object): self.btnThemeRemove.setIcon(icon) self.btnThemeRemove.setObjectName("btnThemeRemove") self.horizontalLayout_6.addWidget(self.btnThemeRemove) - spacerItem11 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_6.addItem(spacerItem11) + spacerItem13 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_6.addItem(spacerItem13) self.verticalLayout_12.addLayout(self.horizontalLayout_6) self.themeStack.addWidget(self.stackedWidgetPage1_3) self.stackedWidgetPage2_3 = QtWidgets.QWidget() @@ -1823,9 +1884,9 @@ class Ui_Settings(object): self.horizontalLayout_8.addWidget(self.stack) self.retranslateUi(Settings) - self.stack.setCurrentIndex(2) - self.tabViews.setCurrentIndex(3) - self.themeStack.setCurrentIndex(1) + self.stack.setCurrentIndex(0) + self.tabViews.setCurrentIndex(0) + self.themeStack.setCurrentIndex(0) self.themeEditStack.setCurrentIndex(3) self.lstMenu.currentRowChanged['int'].connect(self.stack.setCurrentIndex) self.chkRevisionsKeep.toggled['bool'].connect(self.chkRevisionRemove.setEnabled) @@ -1851,10 +1912,12 @@ class Ui_Settings(object): self.lstMenu.setSortingEnabled(__sortingEnabled) self.lblTitleGeneral.setText(_translate("Settings", "General settings")) self.groupBox_2.setTitle(_translate("Settings", "Application settings")) + self.label_2.setText(_translate("Settings", "Restarting Manuskript ensures all settings take effect.")) self.label_56.setText(_translate("Settings", "Style:")) self.label_57.setText(_translate("Settings", "Language:")) self.label_58.setText(_translate("Settings", "Font size:")) - self.label_2.setText(_translate("Settings", "Restarting Manuskript ensures all settings take effect.")) + self.chkProgressChars.setText(_translate("Settings", "Show progress in chars next\n" +" to words")) self.groupBox_10.setTitle(_translate("Settings", "Loading")) self.chkAutoLoad.setText(_translate("Settings", "Automatically load last project on startup")) self.groupBox.setTitle(_translate("Settings", "Saving")) @@ -1899,14 +1962,18 @@ class Ui_Settings(object): self.cmbTreeBackground.setItemText(4, _translate("Settings", "Compile")) self.groupBox_16.setTitle(_translate("Settings", "Icon Size")) self.lblTreeIconSize.setText(_translate("Settings", "TextLabel")) + self.horizontalGroupBox.setTitle(_translate("Settings", "Char/Word Counter")) + self.chkCountSpaces.setText(_translate("Settings", "Count spaces as chars")) self.groupBox_8.setTitle(_translate("Settings", "Folders")) self.rdoTreeItemCount.setText(_translate("Settings", "Show ite&m count")) self.rdoTreeWC.setText(_translate("Settings", "Show &word count")) + self.rdoTreeCC.setText(_translate("Settings", "Show char c&ount")) self.rdoTreeProgress.setText(_translate("Settings", "S&how progress")) self.rdoTreeSummary.setText(_translate("Settings", "Show summar&y")) self.rdoTreeNothing.setText(_translate("Settings", "&Nothing")) self.groupBox_9.setTitle(_translate("Settings", "Text")) self.rdoTreeTextWC.setText(_translate("Settings", "&Show word count")) + self.rdoTreeTextCC.setText(_translate("Settings", "Sho&w char count")) self.rdoTreeTextProgress.setText(_translate("Settings", "Show p&rogress")) self.rdoTreeTextSummary.setText(_translate("Settings", "Show summary")) self.rdoTreeTextNothing.setText(_translate("Settings", "Nothing")) diff --git a/manuskript/ui/settings_ui.ui b/manuskript/ui/settings_ui.ui index 6563bcbb..6b1a658d 100644 --- a/manuskript/ui/settings_ui.ui +++ b/manuskript/ui/settings_ui.ui @@ -6,7 +6,7 @@ 0 0 - 658 + 681 598 @@ -54,7 +54,7 @@ - 2 + 0 @@ -98,93 +98,139 @@ Application settings - - QFormLayout::FieldsStayAtSizeHint - - - - - - 50 - false - - - - Style: - - - - - - - - 50 - false - - - - - - - - - 50 - false - - - - Language: - - - - - - - - 50 - false - - - - - - - - - 50 - false - - - - Font size: - - - - - - - - 50 - false - - - - - - - - - 50 - false - - - - Restarting Manuskript ensures all settings take effect. - - - true - - + + + + + + + 50 + false + + + + Restarting Manuskript ensures all settings take effect. + + + true + + + + + + + QLayout::SetDefaultConstraint + + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + + 50 + false + + + + Style: + + + + + + + + 50 + false + + + + + + + + + 50 + false + + + + Language: + + + + + + + + 50 + false + + + + + + + + + 50 + false + + + + Font size: + + + + + + + + 50 + false + + + + + + + + + + + + + 50 + false + + + + Show progress in chars next + to words + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + @@ -817,7 +863,7 @@ - 3 + 0 @@ -1055,6 +1101,59 @@ + + + + + 75 + true + + + + Char/Word Counter + + + + 9 + + + 9 + + + 9 + + + 9 + + + + + + 50 + false + + + + Count spaces as chars + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + @@ -1095,6 +1194,19 @@ + + + + + 50 + false + + + + Show char c&ount + + + @@ -1165,6 +1277,19 @@ + + + + + 50 + false + + + + Sho&w char count + + + @@ -1224,6 +1349,11 @@ + rdoTreeTextCC + rdoTreeTextWC + rdoTreeTextProgress + rdoTreeTextSummary + rdoTreeTextNothing @@ -2974,7 +3104,7 @@ - 1 + 0 diff --git a/manuskript/ui/views/MDEditCompleter.py b/manuskript/ui/views/MDEditCompleter.py index e0db6808..0101238d 100644 --- a/manuskript/ui/views/MDEditCompleter.py +++ b/manuskript/ui/views/MDEditCompleter.py @@ -106,13 +106,18 @@ class MDEditCompleter(MDEditView): self.completer.popup(self.textUnderCursor(select=True)) def mouseMoveEvent(self, event): + """ + When mouse moves, we show tooltip when appropriate. + """ + self.beginTooltipMoveEvent() MDEditView.mouseMoveEvent(self, event) + self.endTooltipMoveEvent() onRef = [r for r in self.refRects if r.contains(event.pos())] if not onRef: qApp.restoreOverrideCursor() - QToolTip.hideText() + self.hideTooltip() return cursor = self.cursorForPosition(event.pos()) @@ -120,7 +125,8 @@ class MDEditCompleter(MDEditView): if ref: if not qApp.overrideCursor(): qApp.setOverrideCursor(Qt.PointingHandCursor) - QToolTip.showText(self.mapToGlobal(event.pos()), Ref.tooltip(ref)) + + self.showTooltip(self.mapToGlobal(event.pos()), Ref.tooltip(ref)) def mouseReleaseEvent(self, event): MDEditView.mouseReleaseEvent(self, event) diff --git a/manuskript/ui/views/MDEditView.py b/manuskript/ui/views/MDEditView.py index e8eb5644..c5f4c338 100644 --- a/manuskript/ui/views/MDEditView.py +++ b/manuskript/ui/views/MDEditView.py @@ -506,13 +506,15 @@ class MDEditView(textEditView): """ When mouse moves, we show tooltip when appropriate. """ + self.beginTooltipMoveEvent() textEditView.mouseMoveEvent(self, event) + self.endTooltipMoveEvent() onRect = [r for r in self.clickRects if r.rect.contains(event.pos())] if not onRect: qApp.restoreOverrideCursor() - QToolTip.hideText() + self.hideTooltip() return ct = onRect[0] @@ -534,7 +536,7 @@ class MDEditView(textEditView): if tooltip: tooltip = self.tr("{} (CTRL+Click to open)").format(tooltip) - QToolTip.showText(self.mapToGlobal(event.pos()), tooltip) + self.showTooltip(self.mapToGlobal(event.pos()), tooltip) def mouseReleaseEvent(self, event): textEditView.mouseReleaseEvent(self, event) diff --git a/manuskript/ui/views/textEditView.py b/manuskript/ui/views/textEditView.py index 41324eb5..ea58ed81 100644 --- a/manuskript/ui/views/textEditView.py +++ b/manuskript/ui/views/textEditView.py @@ -1,11 +1,11 @@ #!/usr/bin/env python # --!-- coding: utf8 --!-- -import re +import re, textwrap from PyQt5.Qt import QApplication from PyQt5.QtCore import QTimer, QModelIndex, Qt, QEvent, pyqtSignal, QRegExp, QLocale, QPersistentModelIndex, QMutex from PyQt5.QtGui import QTextBlockFormat, QTextCharFormat, QFont, QColor, QIcon, QMouseEvent, QTextCursor -from PyQt5.QtWidgets import QWidget, QTextEdit, qApp, QAction, QMenu +from PyQt5.QtWidgets import QWidget, QTextEdit, qApp, QAction, QMenu, QToolTip from manuskript import settings from manuskript.enums import Outline, World, Character, Plot @@ -47,6 +47,8 @@ class textEditView(QTextEdit): self.highlightWord = "" self.highligtCS = False self._dict = None + self._tooltip = { 'depth' : 0, 'active' : 0 } + # self.document().contentsChanged.connect(self.submit, F.AUC) # Submit text changed only after 500ms without modifications @@ -393,6 +395,49 @@ class textEditView(QTextEdit): Qt.LeftButton, Qt.LeftButton, Qt.NoModifier) QTextEdit.mousePressEvent(self, event) + def beginTooltipMoveEvent(self): + self._tooltip['depth'] += 1 + + def endTooltipMoveEvent(self): + self._tooltip['depth'] -= 1 + + def showTooltip(self, pos, text): + QToolTip.showText(pos, text) + self._tooltip['active'] = self._tooltip['depth'] + + def hideTooltip(self): + if self._tooltip['active'] == self._tooltip['depth']: + QToolTip.hideText() + + def mouseMoveEvent(self, event): + """ + When mouse moves, we show tooltip when appropriate. + """ + self.beginTooltipMoveEvent() + QTextEdit.mouseMoveEvent(self, event) + self.endTooltipMoveEvent() + + match = None + + # Check if the selected word has any suggestions for correction + if self.spellcheck and self._dict: + cursor = self.cursorForPosition(event.pos()) + + # Searches for correlating/overlapping matches + suggestions = self._dict.findSuggestions(self.toPlainText(), cursor.selectionStart(), cursor.selectionEnd()) + + if len(suggestions) > 0: + # I think it should focus on one type of error at a time. + match = suggestions[0] + + if match: + # Wrap the message into a fitting width + msg_lines = textwrap.wrap(match.msg, 48) + + self.showTooltip(event.globalPos(), "\n".join(msg_lines)) + else: + self.hideTooltip() + def wheelEvent(self, event): """ We catch wheelEvent if key modifier is CTRL to change font size. @@ -440,42 +485,108 @@ class textEditView(QTextEdit): if not self.spellcheck: return popup_menu - # Select the word under the cursor. - # But only if there is no selection (otherwise it's impossible to select more text to copy/cut) cursor = self.textCursor() - if not cursor.hasSelection(): - cursor.select(QTextCursor.WordUnderCursor) - self.setTextCursor(cursor) + suggestions = [] + selectedWord = None + + # Check for any suggestions for corrections at the cursors position + if self._dict: + text = self.toPlainText() + + suggestions = self._dict.findSuggestions(text, cursor.selectionStart(), cursor.selectionEnd()) + + # Select the word under the cursor if necessary. + # But only if there is no selection (otherwise it's impossible to select more text to copy/cut) + if (not cursor.hasSelection() and len(suggestions) == 0): + cursor.select(QTextCursor.WordUnderCursor) + self.setTextCursor(cursor) + + if cursor.hasSelection(): + selectedWord = cursor.selectedText() + + # Check if the selected word is misspelled and offer spelling + # suggestions if it is. + suggestions = self._dict.findSuggestions(text, cursor.selectionStart(), cursor.selectionEnd()) + + if (len(suggestions) > 0 or selectedWord): + valid = len(suggestions) == 0 - # Check if the selected word is misspelled and offer spelling - # suggestions if it is. - if self._dict and cursor.hasSelection(): - text = str(cursor.selectedText()) - valid = not self._dict.isMisspelled(text) - selectedWord = cursor.selectedText() if not valid: - spell_menu = QMenu(self.tr('Spelling Suggestions'), self) - spell_menu.setIcon(F.themeIcon("spelling")) - for word in self._dict.getSuggestions(text): - action = self.SpellAction(word, spell_menu) - action.correct.connect(self.correctWord) - spell_menu.addAction(action) + # I think it should focus on one type of error at a time. + match = suggestions[0] + popup_menu.insertSeparator(popup_menu.actions()[0]) - # Adds: add to dictionary - addAction = QAction(self.tr("&Add to dictionary"), popup_menu) - addAction.setIcon(QIcon.fromTheme("list-add")) - addAction.triggered.connect(self.addWordToDict) - addAction.setData(selectedWord) - popup_menu.insertAction(popup_menu.actions()[0], addAction) - # Only add the spelling suggests to the menu if there are - # suggestions. - if len(spell_menu.actions()) != 0: - # Adds: suggestions - popup_menu.insertMenu(popup_menu.actions()[0], spell_menu) - # popup_menu.insertSeparator(popup_menu.actions()[0]) + + if match.locqualityissuetype == 'misspelling': + spell_menu = QMenu(self.tr('Spelling Suggestions'), self) + spell_menu.setIcon(F.themeIcon("spelling")) + + if (match.end > match.start and not selectedWord): + # Select the actual area of the match + cursor = self.textCursor() + cursor.setPosition(match.start, QTextCursor.MoveAnchor); + cursor.setPosition(match.end, QTextCursor.KeepAnchor); + self.setTextCursor(cursor) + + selectedWord = cursor.selectedText() + + for word in match.replacements: + action = self.SpellAction(word, spell_menu) + action.correct.connect(self.correctWord) + spell_menu.addAction(action) + + # Adds: add to dictionary + addAction = QAction(self.tr("&Add to dictionary"), popup_menu) + addAction.setIcon(QIcon.fromTheme("list-add")) + addAction.triggered.connect(self.addWordToDict) + addAction.setData(selectedWord) + + popup_menu.insertAction(popup_menu.actions()[0], addAction) + + # Only add the spelling suggests to the menu if there are + # suggestions. + if len(match.replacements) > 0: + # Adds: suggestions + popup_menu.insertMenu(popup_menu.actions()[0], spell_menu) + else: + correct_menu = None + correct_action = None + + if (len(match.replacements) > 0 and match.end > match.start): + # Select the actual area of the match + cursor = self.textCursor() + cursor.setPosition(match.start, QTextCursor.MoveAnchor); + cursor.setPosition(match.end, QTextCursor.KeepAnchor); + self.setTextCursor(cursor) + + if len(match.replacements) > 0: + correct_menu = QMenu(self.tr('&Correction Suggestions'), self) + correct_menu.setIcon(F.themeIcon("spelling")) + + for word in match.replacements: + action = self.SpellAction(word, correct_menu) + action.correct.connect(self.correctWord) + correct_menu.addAction(action) + + if correct_menu == None: + correct_action = QAction(self.tr('&Correction Suggestion'), popup_menu) + correct_action.setIcon(F.themeIcon("spelling")) + correct_action.setEnabled(False) + + # Wrap the message into a fitting width + msg_lines = textwrap.wrap(match.msg, 48) + + # Insert the lines of the message backwards + for i in range(0, len(msg_lines)): + popup_menu.insertSection(popup_menu.actions()[0], msg_lines[len(msg_lines) - (i + 1)]) + + if correct_menu != None: + popup_menu.insertMenu(popup_menu.actions()[0], correct_menu) + else: + popup_menu.insertAction(popup_menu.actions()[0], correct_action) # If word was added to custom dict, give the possibility to remove it - elif valid and self._dict.isCustomWord(selectedWord): + elif self._dict.isCustomWord(selectedWord): popup_menu.insertSeparator(popup_menu.actions()[0]) # Adds: remove from dictionary rmAction = QAction(self.tr("&Remove from custom dictionary"), popup_menu) diff --git a/manuskript/ui/views/treeDelegates.py b/manuskript/ui/views/treeDelegates.py index fa59702c..f95a3c79 100644 --- a/manuskript/ui/views/treeDelegates.py +++ b/manuskript/ui/views/treeDelegates.py @@ -111,6 +111,9 @@ class treeTitleDelegate(QStyledItemDelegate): elif settings.viewSettings["Tree"]["InfoFolder"] == "WC": extraText = item.wordCount() extraText = " ({})".format(extraText) + elif settings.viewSettings["Tree"]["InfoFolder"] == "CC": + extraText = item.charCount() + extraText = " ({})".format(extraText) elif settings.viewSettings["Tree"]["InfoFolder"] == "Progress": extraText = int(toFloat(item.data(Outline.goalPercentage)) * 100) if extraText: @@ -124,6 +127,9 @@ class treeTitleDelegate(QStyledItemDelegate): if settings.viewSettings["Tree"]["InfoText"] == "WC": extraText = item.wordCount() extraText = " ({})".format(extraText) + elif settings.viewSettings["Tree"]["InfoText"] == "CC": + extraText = item.charCount() + extraText = " ({})".format(extraText) elif settings.viewSettings["Tree"]["InfoText"] == "Progress": extraText = int(toFloat(item.data(Outline.goalPercentage)) * 100) if extraText: