Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically add character names to dictionary #839

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
.project
.pydevproject
.settings/org.eclipse.core.resources.prefs
.vscode
ExportTest
Notes.t2t
dist
Expand Down
1 change: 1 addition & 0 deletions manuskript/functions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,3 +452,4 @@ def inspect():

# Spellchecker loads writablePath from this file, so we need to load it after they get defined
from manuskript.functions.spellchecker import Spellchecker
from manuskript.functions.spellcheckNames import SpellcheckNames
82 changes: 82 additions & 0 deletions manuskript/functions/spellcheckNames.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from ..enums import Character as C


class SpellcheckNames:
"""
An interface responsible for watching for changes in
the character model, and updating the spellchecking
dictionary accordingly.

This could probably be expanded in the future to also
track other proper names, such as place names.
"""

def __init__(self, onChangedCallback):
self.mdlCharacter = None
self.dictionary = None
self.characterNames = set()
self.onChangedCallback = onChangedCallback


def onDictionaryChanged(self, newDictionary):
"""
Adds the names of all characters to the new dictionary

Call this once when spellcheking is first initialized,
and afterward any time the spellchecking dictionary is
changed.
"""
self.dictionary = newDictionary
if self.dictionary is not None:
self.dictionary.addWords(self.characterNames)


def onCharacterModelChanged(self, newModel):
"""
Updates the spellchecking dictionary with the changes
to names in the character model.

Call this to pass an entirely new
character should that need ever arise.
"""
self.mdlCharacter = newModel
self._updateAll()


def _updateAll(self):
if self.mdlCharacter is None:
# No character model has been initialized yet
return
# Get the differences between the current and previous names
currentNames = set(name
for character in self.mdlCharacter.characters
for name in character.name().split()) # Add given names and surname seperately
addedNames = currentNames - self.characterNames
removedNames = self.characterNames - currentNames
self.characterNames = currentNames
# Actually update the dictionary
if self.dictionary is not None and (addedNames or removedNames):
self.dictionary.removeWords(removedNames)
self.dictionary.addWords(addedNames)
self.onChangedCallback()


def onCharacterModelUpdated(self, index):
"""
Updates the spellchecking dictionary with the changes
to names in the character model.

Call this when any changes have been made to character
names.
"""
if index.column() != C.name:
# Only update the dictionary if the name has changed, not anything
# else about the character
return
# There's not really a good way to get the original value of the name,
# so we still just call updateAll.
self._updateAll()




98 changes: 96 additions & 2 deletions manuskript/functions/spellchecker.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,7 @@ def __init__(self, name):
try:
with gzip.open(customPath, "rt", encoding='utf-8') as f:
self._customDict = set(json.loads(f.read()))
for word in self._customDict:
self._dict.create_dictionary_entry(word, self.CUSTOM_COUNT)
self.addCustomEntries(self._customDict)
except:
# If error loading the file, overwrite with empty dictionary
self._saveCustomDict()
Expand Down Expand Up @@ -221,17 +220,70 @@ def isCustomWord(self, word):
return word.lower() in self._customDict

def addWord(self, word):
"""
Add a word to the custom dictionary.

It is safe to call this method multiple times with the same word,
it will not be added twice.
"""
word = word.lower()
if not word in self._customDict:
self._customDict.add(word)
self._saveCustomDict()

def removeWord(self, word):
"""
Remove a word from the custom dictionary.

It is safe to call this method multiple times with the same word,
or with words that are not found in the custom dictionary. In
this situation the call does nothing.
"""
word = word.lower()
if word in self._customDict:
self._customDict.remove(word)
self._saveCustomDict()

def addWords(self, words):
"""
Add a list of words to the custom dictionary.

It is safe to call this method multiple times with the same word,
it will not be added twice.

This is more efficient than calling `addWord` in a loop, as
here changes are not committed to disk until the end.
"""
changesMade = False
for word in words:
word = word.lower()
if not word in self._customDict:
self._customDict.add(word)
changesMade = True
if changesMade:
self._saveCustomDict()

def removeWords(self, words):
"""
Remove a list of words from the custom dictionary.

It is safe to call this method multiple times with the same word,
or with words that are not found in the custom dictionary. In
this situation the call does nothing.

This is more efficient than calling `removeWord` in a loop, as
here changes are not committed to disk until the end.
"""
changesMade = False
for word in words:
word = word.lower()
if word in self._customDict:
self._customDict.remove(word)
changesMade = True
if changesMade:
self._saveCustomDict()


@classmethod
def getResourcesPath(cls):
path = os.path.join(writablePath(), "resources", "dictionaries", cls.getLibraryName())
Expand All @@ -246,6 +298,19 @@ def _saveCustomDict(self):
customPath = self.getCustomDictionaryPath()
with gzip.open(customPath, "wt") as f:
f.write(json.dumps(list(self._customDict)))

def addCustomEntries(self, words):
"""
Adds words from the custom dictionary, or another custom set of words
(such as character names), to the spellcheching engine. This method
does not permanently add the given words to the custom dictionary,
it only loads the words into the spell-checking engine.

Takes any iterable of string entries.
"""
for word in words:
self._dict.create_dictionary_entry(word, self.CUSTOM_COUNT)



class EnchantDictionary(BasicDictionary):
Expand Down Expand Up @@ -305,6 +370,14 @@ def addWord(self, word):

def removeWord(self, word):
self._dict.remove(word)

def addWords(self, words):
for word in words:
self._dict.add(word)

def removeWords(self, words):
for word in words:
self._dict.remove(word)

def getCustomDictionaryPath(self):
return os.path.join(self.getResourcesPath(), "{}.txt".format(self.name))
Expand Down Expand Up @@ -369,6 +442,16 @@ def removeWord(self, word):
BasicDictionary.removeWord(self, word)
self._dict.word_frequency.remove(word.lower())

def addWords(self, words):
BasicDictionary.addWords(self, words)
for word in words:
self._dict.word_frequency.add(word.lower())

def removeWords(self, words):
BasicDictionary.removeWords(self, words)
for word in words:
self._dict.word_frequency.remove(word.lower())

class SymSpellDictionary(BasicDictionary):
CUSTOM_COUNT = 1
DISTANCE = 2
Expand Down Expand Up @@ -470,6 +553,17 @@ def removeWord(self, word):
BasicDictionary.removeWord(self, word)
# Since 6.3.8
self._dict.delete_dictionary_entry(word)

def addWords(self, words):
BasicDictionary.addWords(self, words)
for word in words:
self._dict.create_dictionary_entry(word.lower(), self.CUSTOM_COUNT)

def removeWords(self, words):
BasicDictionary.removeWords(self, words)
# Since 6.3.8
for word in words:
self._dict.delete_dictionary_entry(word)

class LanguageToolCache:

Expand Down
23 changes: 22 additions & 1 deletion manuskript/mainWindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

# Spellcheck support
from manuskript.ui.views.textEditView import textEditView
from manuskript.functions import Spellchecker
from manuskript.functions import Spellchecker, SpellcheckNames

class MainWindow(QMainWindow, Ui_MainWindow):
# dictChanged = pyqtSignal(str)
Expand Down Expand Up @@ -181,6 +181,11 @@ def __init__(self):

# self.loadProject(os.path.join(appPath(), "test_project.zip"))

# Automated spellcheck dictionary control mechanism
# (Coordinates between character model and spellchecker to
# automatically add and remove character names from the dictionary)
self.namesSpellchecker = SpellcheckNames(self.refreshSpellcheck)

def updateDockVisibility(self, restore=False):
"""
Saves the state of the docks visibility. Or if `restore` is True,
Expand Down Expand Up @@ -638,6 +643,10 @@ def loadProject(self, project, loadFromFile=True):
self.saveTimerNoChanges.timeout.connect(self.saveDatas)
self.saveTimerNoChanges.stop()

# Also set the custom names spellchecker to update when character names are changed
self.namesSpellchecker.onCharacterModelChanged(self.mdlCharacter)
self.mdlCharacter.dataChanged.connect(self.namesSpellchecker.onCharacterModelUpdated)

# UI
for i in [self.actOpen, self.menuRecents]:
i.setEnabled(False)
Expand Down Expand Up @@ -1438,6 +1447,9 @@ def setDictionary(self):
for w in self.findChildren(textEditView, QRegExp(".*"),
Qt.FindChildrenRecursively):
w.setDict(settings.dict)

# Trigger the special names custom dictionary to update
self.namesSpellchecker.onDictionaryChanged(Spellchecker.getDictionary(settings.dict))

def openSpellcheckWebPage(self, lib):
F.openURL(Spellchecker.getLibraryURL(lib))
Expand All @@ -1449,6 +1461,15 @@ def toggleSpellcheck(self, val):
for w in self.findChildren(textEditView, QRegExp(".*"),
Qt.FindChildrenRecursively):
w.toggleSpellcheck(val)

def refreshSpellcheck(self):
"""
Refresh the spellchecking in all text views (such as
after a new word is added to the dictionary)
"""
if settings.spellcheck:
self.toggleSpellcheck(settings.spellcheck)


###############################################################################
# SETTINGS
Expand Down