forked from Mirrors/apostrophe
Migrate to gspell
gtkspellcheck is abandoned; Use gspell instead. Closes https://github.com/UberWriter/uberwriter/issues/93github/fork/yochananmarqos/patch-1
parent
181af445e6
commit
e0cea3654a
2
PKGBUILD
2
PKGBUILD
|
@ -6,7 +6,7 @@ pkgdesc='A distraction free Markdown editor for GNU/Linux made with GTK+'
|
|||
arch=('any')
|
||||
url='http://uberwriter.github.io/uberwriter/'
|
||||
license=('GPL3')
|
||||
depends=('gtk3' 'pandoc' 'python-gtkspellcheck')
|
||||
depends=('gtk3' 'pandoc' 'gspell')
|
||||
makedepends=('python-setuptools')
|
||||
optdepends=('texlive-core' 'otf-fira-mono: Recommended font')
|
||||
provides=("$_pkgname")
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"app-id": "de.wolfvollprecht.UberWriter",
|
||||
"runtime": "org.gnome.Platform",
|
||||
"runtime-version": "3.32",
|
||||
"sdk": "org.gnome.Sdk",
|
||||
"command": "uberwriter",
|
||||
"finish-args": [
|
||||
"app-id":"de.wolfvollprecht.UberWriter",
|
||||
"runtime":"org.gnome.Platform",
|
||||
"runtime-version":"3.32",
|
||||
"sdk":"org.gnome.Sdk",
|
||||
"command":"uberwriter",
|
||||
"finish-args":[
|
||||
"--socket=x11",
|
||||
"--socket=wayland",
|
||||
"--share=ipc",
|
||||
|
@ -16,86 +16,99 @@
|
|||
"--env=DCONF_USER_CONFIG_DIR=.config/dconf",
|
||||
"--env=PATH=/app/extensions/TexLive/bin:/app/extensions/TexLive/2018/bin/x86_64-linux:/app/bin:/usr/bin"
|
||||
],
|
||||
"build-options" : {
|
||||
"env": {
|
||||
"PYTHON": "python3"
|
||||
"build-options":{
|
||||
"env":{
|
||||
"PYTHON":"python3"
|
||||
}
|
||||
},
|
||||
"add-extensions": {
|
||||
"de.wolfvollprecht.UberWriter.Plugin": {
|
||||
"directory": "extensions",
|
||||
"version": "stable",
|
||||
"subdirectories": true,
|
||||
"no-autodownload": true,
|
||||
"autodelete": true
|
||||
}
|
||||
"add-extensions":{
|
||||
"de.wolfvollprecht.UberWriter.Plugin":{
|
||||
"directory":"extensions",
|
||||
"version":"stable",
|
||||
"subdirectories":true,
|
||||
"no-autodownload":true,
|
||||
"autodelete":true
|
||||
}
|
||||
},
|
||||
"modules": [
|
||||
"modules":[
|
||||
{
|
||||
"name": "enchant",
|
||||
"config-opts": ["--disable-static", "--with-myspell-dir=/usr/share/hunspell"],
|
||||
"cleanup": [
|
||||
"/bin"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type" : "archive",
|
||||
"url" : "https://github.com/AbiWord/enchant/releases/download/enchant-1-6-1/enchant-1.6.1.tar.gz",
|
||||
"sha256" : "bef0d9c0fef2e4e8746956b68e4d6c6641f6b85bd2908d91731efb68eba9e3f5"
|
||||
}
|
||||
]
|
||||
"name":"enchant",
|
||||
"config-opts":[
|
||||
"--disable-static",
|
||||
"--with-myspell-dir=/usr/share/hunspell"
|
||||
],
|
||||
"cleanup":[
|
||||
"/bin"
|
||||
],
|
||||
"sources":[
|
||||
{
|
||||
"type":"archive",
|
||||
"url":"https://github.com/AbiWord/enchant/releases/download/enchant-1-6-1/enchant-1.6.1.tar.gz",
|
||||
"sha256":"bef0d9c0fef2e4e8746956b68e4d6c6641f6b85bd2908d91731efb68eba9e3f5"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "pandoc",
|
||||
"only-arches": [
|
||||
"x86_64"
|
||||
],
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"mkdir -p /app/bin",
|
||||
"cp bin/pandoc /app/bin/pandoc",
|
||||
"cp bin/pandoc-citeproc /app/bin/pandoc-citeproc"
|
||||
],
|
||||
"sources": [
|
||||
"name":"gspell",
|
||||
"sources":[
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/jgm/pandoc/releases/download/2.2/pandoc-2.2-linux.tar.gz",
|
||||
"sha256": "06ecd882e42ef9b7390b1c82e1e71b3ea48679181289b9b810a8797825bed8ed"
|
||||
"type":"archive",
|
||||
"url":"https://download.gnome.org/sources/gspell/1.8/gspell-1.8.1.tar.xz",
|
||||
"sha256":"819a1d23c7603000e73f5e738bdd284342e0cd345fb0c7650999c31ec741bbe5"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"pandoc",
|
||||
"only-arches":[
|
||||
"x86_64"
|
||||
],
|
||||
"buildsystem":"simple",
|
||||
"build-commands":[
|
||||
"mkdir -p /app/bin",
|
||||
"cp bin/pandoc /app/bin/pandoc",
|
||||
"cp bin/pandoc-citeproc /app/bin/pandoc-citeproc"
|
||||
],
|
||||
"sources":[
|
||||
{
|
||||
"type":"archive",
|
||||
"url":"https://github.com/jgm/pandoc/releases/download/2.2/pandoc-2.2-linux.tar.gz",
|
||||
"sha256":"06ecd882e42ef9b7390b1c82e1e71b3ea48679181289b9b810a8797825bed8ed"
|
||||
}
|
||||
]
|
||||
},
|
||||
"de.wolfvollprecht.UberWriter.pipdeps.json",
|
||||
{
|
||||
"name": "fonts",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"mkdir -p /app/share/fonts/",
|
||||
"cp ttf/* /app/share/fonts/"
|
||||
"name":"fonts",
|
||||
"buildsystem":"simple",
|
||||
"build-commands":[
|
||||
"mkdir -p /app/share/fonts/",
|
||||
"cp ttf/* /app/share/fonts/"
|
||||
],
|
||||
"sources": [
|
||||
"sources":[
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://github.com/mozilla/Fira",
|
||||
"tag": "4.202"
|
||||
"type":"git",
|
||||
"url":"https://github.com/mozilla/Fira",
|
||||
"tag":"4.202"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "uberwriter",
|
||||
"sources": [
|
||||
{
|
||||
"type" : "git",
|
||||
"url" : "../",
|
||||
"branch" : "various-improvements"
|
||||
}
|
||||
],
|
||||
"build-commands": [
|
||||
"install -Dm644 data/de.wolfvollprecht.UberWriter.appdata.xml /app/share/appdata/de.wolfvollprecht.UberWriter.appdata.xml "
|
||||
],
|
||||
"post-install": [
|
||||
"name":"uberwriter",
|
||||
"sources":[
|
||||
{
|
||||
"type":"git",
|
||||
"url":"../",
|
||||
"branch":"various-improvements"
|
||||
}
|
||||
],
|
||||
"build-commands":[
|
||||
"install -Dm644 data/de.wolfvollprecht.UberWriter.appdata.xml /app/share/appdata/de.wolfvollprecht.UberWriter.appdata.xml "
|
||||
],
|
||||
"post-install":[
|
||||
"glib-compile-schemas /app/share/glib-2.0/schemas",
|
||||
"install -d /app/extensions"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
regex
|
||||
enchant
|
||||
python-gtkspellcheck
|
||||
pypandoc==1.4
|
||||
|
|
1
setup.py
1
setup.py
|
@ -62,7 +62,6 @@ setup(
|
|||
# "": '/opt/uberwriter/'
|
||||
},
|
||||
packages=[
|
||||
"uberwriter.gtkspellcheck",
|
||||
"uberwriter.pylocales",
|
||||
# "uberwriter.pressagio",
|
||||
"uberwriter",
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2012, Maximilian Köhl <linuxmaxi@googlemail.com>
|
||||
# Copyright (C) 2012, Carlos Jenkins <carlos@jenkins.co.cr>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
__version__ = '4.0.5'
|
||||
__project__ = 'Python GTK Spellcheck'
|
||||
__short_name__ = 'pygtkspellcheck'
|
||||
__authors__ = 'Maximilian Köhl & Carlos Jenkins'
|
||||
__emails__ = 'linuxmaxi@googlemail.com & carlos@jenkins.co.cr'
|
||||
__website__ = 'http://koehlma.github.com/projects/pygtkspellcheck.html'
|
||||
__download_url__ = 'https://github.com/koehlma/pygtkspellcheck/tarball/master'
|
||||
__source__ = 'https://github.com/koehlma/pygtkspellcheck/'
|
||||
__vcs__ = 'git://github.com/koehlma/pygtkspellcheck.git'
|
||||
__copyright__ = '2012, Maximilian Köhl & Carlos Jenkins'
|
||||
__desc_short__ = ('a simple but quite powerful Python spell checking library '
|
||||
'for GtkTextViews based on Enchant')
|
||||
__desc_long__ = ('A simple but quite powerful spellchecking library written in '
|
||||
'pure Python for Gtk based on Enchant. It supports PyGObject '
|
||||
'as well as PyGtk for Python 2 and 3 with automatic switching '
|
||||
'and binding detection. For automatic translation of the user '
|
||||
'interface it can use Gedit’s translation files.')
|
||||
|
||||
__metadata__ = {'__version__' : __version__,
|
||||
'__project__' : __project__,
|
||||
'__short_name__' : __short_name__,
|
||||
'__authors__' : __authors__,
|
||||
'__emails__' : __emails__,
|
||||
'__website__' : __website__,
|
||||
'__download_url__' : __download_url__,
|
||||
'__source__' : __source__,
|
||||
'__vcs__' : __vcs__,
|
||||
'__copyright__' : __copyright__,
|
||||
'__desc_short__' : __desc_short__,
|
||||
'__desc_long__' : __desc_long__}
|
||||
|
||||
from .spellcheck import (SpellChecker, NoDictionariesFound,
|
||||
NoGtkBindingFound)
|
|
@ -1,294 +0,0 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2012, Carlos Jenkins <carlos@jenkins.co.cr>
|
||||
# Copyright (C) 2012-2016, Maximilian Köhl <mail@koehlma.de>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
This module extracts the .dic and .aff (Hunspell) dictionaries from any given
|
||||
.oxt extension.
|
||||
|
||||
Extensions could be found at:
|
||||
|
||||
http://extensions.services.openoffice.org/dictionary
|
||||
"""
|
||||
|
||||
import functools
|
||||
import gettext
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import warnings
|
||||
import xml.dom.minidom
|
||||
import xml.parsers.expat
|
||||
import zipfile
|
||||
|
||||
# enable deprecation warnings
|
||||
warnings.simplefilter('always', DeprecationWarning)
|
||||
|
||||
# public objects
|
||||
__all__ = ['extract_oxt', 'batch_extract', 'BadXml', 'BadExtensionFile',
|
||||
'ExtractPathIsNoDirectory', 'BATCH_SUCCESS', 'BATCH_ERROR',
|
||||
'BATCH_WARNING']
|
||||
|
||||
# logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# translation
|
||||
locale_name = 'py{}gtkspellcheck'.format(sys.version_info.major)
|
||||
_ = gettext.translation(locale_name, fallback=True).gettext
|
||||
|
||||
class BadXml(Exception):
|
||||
"""
|
||||
The XML dictionary registry is not valid XML.
|
||||
"""
|
||||
|
||||
class BadExtensionFile(Exception):
|
||||
"""
|
||||
The extension has a wrong file format, should be a ZIP file.
|
||||
"""
|
||||
|
||||
class ExtractPathIsNoDirectory(Exception):
|
||||
"""
|
||||
The given `extract_path` is no directory.
|
||||
"""
|
||||
|
||||
|
||||
def find_dictionaries(registry):
|
||||
def oor_name(name, element):
|
||||
return element.attributes['oor:name'].value.lower() == name
|
||||
|
||||
def get_property(name, properties):
|
||||
property = list(filter(functools.partial(oor_name, name),
|
||||
properties))
|
||||
if property:
|
||||
return property[0].getElementsByTagName('value')[0]
|
||||
|
||||
result = []
|
||||
|
||||
# find all "node" elements which have "dictionaries" as "oor:name" attribute
|
||||
for dictionaries in filter(functools.partial(oor_name, 'dictionaries'),
|
||||
registry.getElementsByTagName('node')):
|
||||
# for all "node" elements in this dictionary nodes
|
||||
for dictionary in dictionaries.getElementsByTagName('node'):
|
||||
# get all "prop" elements
|
||||
properties = dictionary.getElementsByTagName('prop')
|
||||
# get the format property as text
|
||||
format = get_property('format', properties).firstChild.data.strip()
|
||||
if format and format == 'DICT_SPELL':
|
||||
# find the locations property
|
||||
locations = get_property('locations', properties)
|
||||
# if the location property is text:
|
||||
# %origin%/dictionary.aff %origin%/dictionary.dic
|
||||
if locations.firstChild.nodeType == xml.dom.Node.TEXT_NODE:
|
||||
locations = locations.firstChild.data
|
||||
locations = locations.replace('%origin%/', '').strip()
|
||||
result.append(locations.split())
|
||||
# otherwise:
|
||||
# <i>%origin%/dictionary.aff</i> <i>%origin%/dictionary.dic</i>
|
||||
else:
|
||||
locations = [item.firshChild.data.replace('%origin%/', '') \
|
||||
.strip() for item in
|
||||
locations.getElementsByTagName('it')]
|
||||
result.append(locations)
|
||||
|
||||
return result
|
||||
|
||||
def extract(filename, target, override=False):
|
||||
"""
|
||||
Extract Hunspell dictionaries out of LibreOffice ``.oxt`` extensions.
|
||||
|
||||
:param filename: path to the ``.oxt`` extension
|
||||
:param target: path to extract Hunspell dictionaries to
|
||||
:param override: override existing files in the target directory
|
||||
:rtype: list of the extracted dictionaries
|
||||
|
||||
This function extracts the Hunspell dictionaries (``.dic`` and ``.aff``
|
||||
files) from the given ``.oxt`` extension found to ``target``.
|
||||
|
||||
Extensions could be found at:
|
||||
|
||||
http://extensions.services.openoffice.org/dictionary
|
||||
"""
|
||||
# TODO 5.0: remove this function
|
||||
warnings.warn(('call to deprecated function "{}", '
|
||||
'moved to separate package "oxt_extract", '
|
||||
'will be removed in pygtkspellcheck 5.0').format(extract.__name__),
|
||||
category=DeprecationWarning)
|
||||
try:
|
||||
with zipfile.ZipFile(filename, 'r') as extension:
|
||||
files = extension.namelist()
|
||||
|
||||
registry = 'dictionaries.xcu'
|
||||
if not registry in files:
|
||||
for filename in files:
|
||||
if filename.lower().endswith(registry):
|
||||
registry = filename
|
||||
|
||||
if registry in files:
|
||||
registry = xml.dom.minidom.parse(extension.open(registry))
|
||||
dictionaries = find_dictionaries(registry)
|
||||
extracted = []
|
||||
for dictionary in dictionaries:
|
||||
for filename in dictionary:
|
||||
dict_file = os.path.join(target,
|
||||
os.path.basename(filename))
|
||||
if (not os.path.exists(dict_file)
|
||||
or (override and os.path.isfile(dict_file))):
|
||||
if filename in files:
|
||||
with open(dict_file, 'wb') as _target:
|
||||
with extension.open(filename, 'r') as _source:
|
||||
extracted.append(os.path.basename(filename))
|
||||
_target.write(_source.read())
|
||||
else:
|
||||
logger.warning('dictionary exists in registry '
|
||||
'but not in the extension zip')
|
||||
else:
|
||||
logging.warning(('dictionary file "{}" already exists '
|
||||
'and not overriding it'
|
||||
).format(dict_file))
|
||||
return extracted
|
||||
except zipfile.BadZipfile:
|
||||
raise BadExtensionFile('extension is not a valid ZIP file')
|
||||
except xml.parsers.expat.ExpatError:
|
||||
raise BadXml('dictionary registry is not valid XML')
|
||||
|
||||
BATCH_SUCCESS = 'success'
|
||||
BATCH_ERROR = 'error'
|
||||
BATCH_WARNING = 'warning'
|
||||
|
||||
def batch_extract(oxt_path, extract_path, override=False, move_path=None):
|
||||
"""
|
||||
Uncompress, read and install LibreOffice ``.oxt`` dictionaries extensions.
|
||||
|
||||
:param oxt_path: path to a directory containing the ``.oxt`` extensions
|
||||
:param extract_path: path to extract Hunspell dictionaries files to
|
||||
:param override: override already existing files
|
||||
:param move_path: optional path to move the ``.oxt`` files after processing
|
||||
:rtype: generator over all extensions, yielding result, extension name,
|
||||
error, extracted dictionaries and translated error message - result
|
||||
would be :const:`BATCH_SUCCESS` for success, :const:`BATCH_ERROR` if
|
||||
some error happened or :const:`BATCH_WARNING` which contain some warning
|
||||
messages instead of errors
|
||||
|
||||
This function extracts the Hunspell dictionaries (``.dic`` and ``.aff``
|
||||
files) from all the ``.oxt`` extensions found on ``oxt_path`` directory to
|
||||
the ``extract_path`` directory.
|
||||
|
||||
Extensions could be found at:
|
||||
|
||||
http://extensions.services.openoffice.org/dictionary
|
||||
|
||||
In detail, this functions does the following:
|
||||
|
||||
1. find all the ``.oxt`` extension files within ``oxt_path``
|
||||
2. open (unzip) each extension
|
||||
3. find the dictionary definition file within (*dictionaries.xcu*)
|
||||
4. parse the dictionary definition file and locate the dictionaries files
|
||||
5. uncompress those files to ``extract_path``
|
||||
|
||||
|
||||
By default file overriding is disabled, set ``override`` parameter to True
|
||||
if you want to enable it. As additional option, each processed extension can
|
||||
be moved to ``move_path``.
|
||||
|
||||
Example::
|
||||
|
||||
for result, name, error, dictionaries, message in oxt_extract.batch_extract(...):
|
||||
if result == oxt_extract.BATCH_SUCCESS:
|
||||
print('successfully extracted extension "{}"'.format(name))
|
||||
elif result == oxt_extract.BATCH_ERROR:
|
||||
print('could not extract extension "{}"'.format(name))
|
||||
print(message)
|
||||
print('error {}'.format(error))
|
||||
elif result == oxt_extract.BATCH_WARNING:
|
||||
print('warning during processing extension "{}"'.format(name))
|
||||
print(message)
|
||||
print(error)
|
||||
|
||||
"""
|
||||
|
||||
# TODO 5.0: remove this function
|
||||
warnings.warn(('call to deprecated function "{}", '
|
||||
'moved to separate package "oxt_extract", '
|
||||
'will be removed in pygtkspellcheck 5.0').format(extract.__name__),
|
||||
category=DeprecationWarning)
|
||||
|
||||
# get the real, absolute and normalized path
|
||||
oxt_path = os.path.normpath(os.path.abspath(os.path.realpath(oxt_path)))
|
||||
|
||||
# check that the input directory exists
|
||||
if not os.path.isdir(oxt_path):
|
||||
return
|
||||
|
||||
# create extract directory if not exists
|
||||
if not os.path.exists(extract_path):
|
||||
os.makedirs(extract_path)
|
||||
|
||||
# check that the extract path is a directory
|
||||
if not os.path.isdir(extract_path):
|
||||
raise ExtractPathIsNoDirectory('extract path is not a valid directory')
|
||||
|
||||
# get all .oxt extension at given path
|
||||
oxt_files = [extension for extension in os.listdir(oxt_path)
|
||||
if extension.lower().endswith('.oxt')]
|
||||
|
||||
for extension_name in oxt_files:
|
||||
extension_path = os.path.join(oxt_path, extension_name)
|
||||
|
||||
try:
|
||||
dictionaries = extract(extension_path, extract_path, override)
|
||||
yield BATCH_SUCCESS, extension_name, None, dictionaries, ''
|
||||
except BadExtensionFile as error:
|
||||
logger.error(('extension "{}" is not a valid ZIP file'
|
||||
).format(extension_name))
|
||||
yield (BATCH_ERROR, extension_name, error, [],
|
||||
_('extension "{}" is not a valid ZIP file'
|
||||
).format(extension_name))
|
||||
except BadXml as error:
|
||||
logger.error(('extension "{}" has no valid XML dictionary registry'
|
||||
).format(extension_name))
|
||||
yield (BATCH_ERROR, extension_name, error, [],
|
||||
_('extension "{}" has no valid XML dictionary registry'
|
||||
).format(extension_name))
|
||||
|
||||
# move the extension after processing if user requires it
|
||||
if move_path is not None:
|
||||
# create move path if it doesn't exists
|
||||
if not os.path.exists(move_path):
|
||||
os.makedirs(move_path)
|
||||
# move to the given path only if it is a directory and target
|
||||
# doesn't exists
|
||||
if os.path.isdir(move_path):
|
||||
if (not os.path.exists(os.path.join(move_path, extension_name))
|
||||
or override):
|
||||
shutil.move(extension_path, move_path)
|
||||
else:
|
||||
logger.warning(('unable to move extension, file with same '
|
||||
'name exists within move_path'))
|
||||
yield (BATCH_WARNING, extension_name,
|
||||
('unable to move extension, file with same name '
|
||||
'exists within move_path'), [],
|
||||
_('unable to move extension, file with same name '
|
||||
'exists within move_path'))
|
||||
else:
|
||||
logger.warning(('unable to move extension, move_path is not a '
|
||||
'directory'))
|
||||
yield (BATCH_WARNING, extension_name,
|
||||
('unable to move extension, move_path is not a '
|
||||
'directory'), [],
|
||||
_('unable to move extension, move_path is not a '
|
||||
'directory'))
|
|
@ -1,660 +0,0 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2012, Maximilian Köhl <linuxmaxi@googlemail.com>
|
||||
# Copyright (C) 2012, Carlos Jenkins <carlos@jenkins.co.cr>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
A simple but quite powerful spellchecking library written in pure Python for Gtk
|
||||
based on Enchant. It supports PyGObject as well as PyGtk for Python 2 and 3 with
|
||||
automatic switching and binding detection. For automatic translation of the user
|
||||
interface it can use Gedit’s translation files.
|
||||
"""
|
||||
|
||||
import enchant
|
||||
import gettext
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
|
||||
from uberwriter.pylocales import code_to_name as _code_to_name
|
||||
from uberwriter.pylocales import LanguageNotFound, CountryNotFound
|
||||
|
||||
# public objects
|
||||
__all__ = ['SpellChecker', 'NoDictionariesFound', 'NoGtkBindingFound']
|
||||
|
||||
# logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class NoDictionariesFound(Exception):
|
||||
"""
|
||||
There aren't any dictionaries installed on the current system so
|
||||
spellchecking could not work in any way.
|
||||
"""
|
||||
|
||||
class NoGtkBindingFound(Exception):
|
||||
"""
|
||||
Could not find any loaded Gtk binding.
|
||||
"""
|
||||
|
||||
if sys.version_info.major == 3:
|
||||
_py3k = True
|
||||
else:
|
||||
_py3k = False
|
||||
|
||||
if _py3k:
|
||||
# there is only the gi binding for Python 3
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk as gtk
|
||||
_pygobject = True
|
||||
else:
|
||||
# find any loaded gtk binding
|
||||
if 'gi.repository.Gtk' in sys.modules:
|
||||
gtk = sys.modules['gi.repository.Gtk']
|
||||
_pygobject = True
|
||||
elif 'gtk' in sys.modules:
|
||||
gtk = sys.modules['gtk']
|
||||
_pygobject = False
|
||||
else:
|
||||
raise NoGtkBindingFound('could not find any loaded Gtk binding')
|
||||
|
||||
# select base list class
|
||||
try:
|
||||
from collections import UserList
|
||||
_list = UserList
|
||||
except ImportError:
|
||||
_list = list
|
||||
|
||||
|
||||
|
||||
# select base string
|
||||
if _py3k:
|
||||
basestring = str
|
||||
|
||||
# map between Gedit's translation and PyGtkSpellcheck's
|
||||
_GEDIT_MAP = {'Languages' : 'Languages',
|
||||
'Ignore All' : 'Ignore _All',
|
||||
'Suggestions' : 'Suggestions',
|
||||
'(no suggestions)' : '(no suggested words)',
|
||||
'Add "{}" to Dictionary' : 'Add w_ord',
|
||||
'Unknown' : 'Unknown'}
|
||||
|
||||
# translation
|
||||
if gettext.find('gedit'):
|
||||
_gedit = gettext.translation('gedit', fallback=True).gettext
|
||||
_ = lambda message: _gedit(_GEDIT_MAP[message]).replace('_', '')
|
||||
else:
|
||||
locale_name = 'py{}gtkspellcheck'.format(sys.version_info.major)
|
||||
_ = gettext.translation(locale_name, fallback=True).gettext
|
||||
|
||||
def code_to_name(code, separator='_'):
|
||||
try:
|
||||
return _code_to_name(code, separator)
|
||||
except (LanguageNotFound, CountryNotFound):
|
||||
return '{} ({})'.format(_('Unknown'), code)
|
||||
|
||||
class SpellChecker(object):
|
||||
"""
|
||||
Main spellchecking class, everything important happens here.
|
||||
|
||||
:param view: GtkTextView the SpellChecker should be attached to.
|
||||
:param language: The language which should be used for spellchecking.
|
||||
Use a combination of two letter lower-case ISO 639 language code with a
|
||||
two letter upper-case ISO 3166 country code, for example en_US or de_DE.
|
||||
:param prefix: A prefix for some internal GtkTextMarks.
|
||||
:param collapse: Enclose suggestions in its own menu.
|
||||
:param params: Dictionary with Enchant broker parameters that should be set
|
||||
e.g. `enchant.myspell.dictionary.path`.
|
||||
|
||||
.. attribute:: languages
|
||||
|
||||
A list of supported languages.
|
||||
|
||||
.. function:: exists(language)
|
||||
|
||||
Checks if a language exists.
|
||||
|
||||
:param language: language to check
|
||||
"""
|
||||
FILTER_WORD = 'word'
|
||||
FILTER_LINE = 'line'
|
||||
FILTER_TEXT = 'text'
|
||||
|
||||
DEFAULT_FILTERS = {FILTER_WORD : [r'[0-9.,]+'],
|
||||
FILTER_LINE : [(r'(https?|ftp|file):((//)|(\\\\))+[\w\d:'
|
||||
r'#@%/;$()~_?+-=\\.&]+'),
|
||||
r'[\w\d]+@[\w\d.]+'],
|
||||
FILTER_TEXT : []}
|
||||
|
||||
class _LanguageList(_list):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if sys.version_info.major == 3:
|
||||
super().__init__(*args, **kwargs)
|
||||
else:
|
||||
_list.__init__(self, *args, **kwargs)
|
||||
self.mapping = dict(self)
|
||||
|
||||
@classmethod
|
||||
def from_broker(cls, broker):
|
||||
return cls(sorted([(language, code_to_name(language))
|
||||
for language in broker.list_languages()],
|
||||
key=lambda language: language[1]))
|
||||
|
||||
def exists(self, language):
|
||||
return language in self.mapping
|
||||
|
||||
class _Mark():
|
||||
def __init__(self, buffer, name, start):
|
||||
self._buffer = buffer
|
||||
self._name = name
|
||||
self._mark = self._buffer.create_mark(self._name, start, True)
|
||||
|
||||
@property
|
||||
def iter(self):
|
||||
return self._buffer.get_iter_at_mark(self._mark)
|
||||
|
||||
@property
|
||||
def inside_word(self):
|
||||
return self.iter.inside_word()
|
||||
|
||||
@property
|
||||
def word(self):
|
||||
start = self.iter
|
||||
if not start.starts_word():
|
||||
start.backward_word_start()
|
||||
end = self.iter
|
||||
if end.inside_word():
|
||||
end.forward_word_end()
|
||||
return start, end
|
||||
|
||||
def move(self, location):
|
||||
self._buffer.move_mark(self._mark, location)
|
||||
|
||||
def __init__(self, view, language='en', prefix='gtkspellchecker',
|
||||
collapse=True, params={}):
|
||||
self._view = view
|
||||
self.collapse = collapse
|
||||
self._view.connect('populate-popup',
|
||||
lambda entry, menu:self._extend_menu(menu))
|
||||
self._view.connect('popup-menu', self._click_move_popup)
|
||||
self._view.connect('button-press-event', self._click_move_button)
|
||||
self._prefix = prefix
|
||||
self._broker = enchant.Broker()
|
||||
for param, value in params.items(): self._broker.set_param(param, value)
|
||||
self.languages = SpellChecker._LanguageList.from_broker(self._broker)
|
||||
if self.languages.exists(language):
|
||||
self._language = language
|
||||
elif self.languages.exists('en'):
|
||||
logger.warning(('no installed dictionary for language "{}", '
|
||||
'fallback to english'.format(language)))
|
||||
self._language = 'en'
|
||||
else:
|
||||
if self.languages:
|
||||
self._language = self.languages[0][0]
|
||||
logger.warning(('no installed dictionary for language "{}" '
|
||||
'and english, fallback to first language in'
|
||||
'language list ("{}")').format(language,
|
||||
self._language))
|
||||
else:
|
||||
logger.critical('no dictionaries found')
|
||||
raise NoDictionariesFound()
|
||||
self._dictionary = self._broker.request_dict(self._language)
|
||||
self._deferred_check = False
|
||||
self._filters = dict(SpellChecker.DEFAULT_FILTERS)
|
||||
self._regexes = {SpellChecker.FILTER_WORD : re.compile('|'.join(
|
||||
self._filters[SpellChecker.FILTER_WORD])),
|
||||
SpellChecker.FILTER_LINE : re.compile('|'.join(
|
||||
self._filters[SpellChecker.FILTER_LINE])),
|
||||
SpellChecker.FILTER_TEXT : re.compile('|'.join(
|
||||
self._filters[SpellChecker.FILTER_TEXT]),
|
||||
re.MULTILINE)}
|
||||
self._enabled = True
|
||||
self.buffer_initialize()
|
||||
|
||||
@property
|
||||
def language(self):
|
||||
"""
|
||||
The language used for spellchecking.
|
||||
"""
|
||||
return self._language
|
||||
|
||||
@language.setter
|
||||
def language(self, language):
|
||||
if language != self._language and self.languages.exists(language):
|
||||
self._language = language
|
||||
self._dictionary = self._broker.request_dict(language)
|
||||
self.recheck()
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
"""
|
||||
Enable or disable spellchecking.
|
||||
"""
|
||||
return self._enabled
|
||||
|
||||
@enabled.setter
|
||||
def enabled(self, enabled):
|
||||
if enabled and not self._enabled:
|
||||
self.enable()
|
||||
elif not enabled and self._enabled:
|
||||
self.disable()
|
||||
|
||||
def buffer_initialize(self):
|
||||
"""
|
||||
Initialize the GtkTextBuffer associated with the GtkTextView. If you
|
||||
have associated a new GtkTextBuffer with the GtkTextView call this
|
||||
method.
|
||||
"""
|
||||
if _pygobject:
|
||||
self._misspelled = gtk.TextTag.new('{}-misspelled'\
|
||||
.format(self._prefix))
|
||||
else:
|
||||
self._misspelled = gtk.TextTag('{}-misspelled'.format(self._prefix))
|
||||
self._misspelled.set_property('underline', 4)
|
||||
self._buffer = self._view.get_buffer()
|
||||
self._buffer.connect('insert-text', self._before_text_insert)
|
||||
self._buffer.connect_after('insert-text', self._after_text_insert)
|
||||
self._buffer.connect_after('delete-range', self._range_delete)
|
||||
self._buffer.connect_after('mark-set', self._mark_set)
|
||||
start = self._buffer.get_bounds()[0]
|
||||
self._marks = {'insert-start' : SpellChecker._Mark(self._buffer,
|
||||
'{}-insert-start'.format(self._prefix), start),
|
||||
'insert-end' : SpellChecker._Mark(self._buffer,
|
||||
'{}-insert-end'.format(self._prefix), start),
|
||||
'click' : SpellChecker._Mark(self._buffer,
|
||||
'{}-click'.format(self._prefix), start)}
|
||||
self._table = self._buffer.get_tag_table()
|
||||
self._table.add(self._misspelled)
|
||||
self.ignored_tags = []
|
||||
def tag_added(tag, *args):
|
||||
if hasattr(tag, 'spell_check') and not getattr(tag, 'spell_check'):
|
||||
self.ignored_tags.append(tag)
|
||||
def tag_removed(tag, *args):
|
||||
if tag in self.ignored_tags:
|
||||
self.ignored_tags.remove(tag)
|
||||
self._table.connect('tag-added', tag_added)
|
||||
self._table.connect('tag-removed', tag_removed)
|
||||
self._table.foreach(tag_added, None)
|
||||
self.no_spell_check = self._table.lookup('no-spell-check')
|
||||
if not self.no_spell_check:
|
||||
if _pygobject:
|
||||
self.no_spell_check = gtk.TextTag.new('no-spell-check')
|
||||
else:
|
||||
self.no_spell_check = gtk.TextTag('no-spell-check')
|
||||
self._table.add(self.no_spell_check)
|
||||
self.recheck()
|
||||
|
||||
def recheck(self):
|
||||
"""
|
||||
Rechecks the spelling of the whole text.
|
||||
"""
|
||||
start, end = self._buffer.get_bounds()
|
||||
self.check_range(start, end, True)
|
||||
|
||||
def disable(self):
|
||||
"""
|
||||
Disable spellchecking.
|
||||
"""
|
||||
self._enabled = False
|
||||
start, end = self._buffer.get_bounds()
|
||||
self._buffer.remove_tag(self._misspelled, start, end)
|
||||
|
||||
def enable(self):
|
||||
"""
|
||||
Enable spellchecking.
|
||||
"""
|
||||
self._enabled = True
|
||||
self.recheck()
|
||||
|
||||
def append_filter(self, regex, filter_type):
|
||||
"""
|
||||
Append a new filter to the filter list. Filters are useful to ignore
|
||||
some misspelled words based on regular expressions.
|
||||
|
||||
:param regex: The regex used for filtering.
|
||||
:param filter_type: The type of the filter.
|
||||
|
||||
Filter Types:
|
||||
|
||||
:const:`SpellChecker.FILTER_WORD`: The regex must match the whole word
|
||||
you want to filter. The word separation is done by Pango's word
|
||||
separation algorithm so, for example, urls won't work here because
|
||||
they are split in many words.
|
||||
|
||||
:const:`SpellChecker.FILTER_LINE`: If the expression you want to match
|
||||
is a single line expression use this type. It should not be an open
|
||||
end expression because then the rest of the line with the text you
|
||||
want to filter will become correct.
|
||||
|
||||
:const:`SpellChecker.FILTER_TEXT`: Use this if you want to filter
|
||||
multiline expressions. The regex will be compiled with the
|
||||
`re.MULTILINE` flag. Same with open end expressions apply here.
|
||||
"""
|
||||
self._filters[filter_type].append(regex)
|
||||
if filter_type == SpellChecker.FILTER_TEXT:
|
||||
self._regexes[filter_type] = re.compile('|'.join(
|
||||
self._filters[filter_type]), re.MULTILINE)
|
||||
else:
|
||||
self._regexes[filter_type] = re.compile('|'.join(
|
||||
self._filters[filter_type]))
|
||||
|
||||
def remove_filter(self, regex, filter_type):
|
||||
"""
|
||||
Remove a filter from the filter list.
|
||||
|
||||
:param regex: The regex which used for filtering.
|
||||
:param filter_type: The type of the filter.
|
||||
"""
|
||||
self._filters[filter_type].remove(regex)
|
||||
if filter_type == SpellChecker.FILTER_TEXT:
|
||||
self._regexes[filter_type] = re.compile('|'.join(
|
||||
self._filters[filter_type]), re.MULTILINE)
|
||||
else:
|
||||
self._regexes[filter_type] = re.compile('|'.join(
|
||||
self._filters[filter_type]))
|
||||
|
||||
def append_ignore_tag(self, tag):
|
||||
"""
|
||||
Appends a tag to the list of ignored tags. A string will be automatic
|
||||
resolved into a tag object.
|
||||
|
||||
:param tag: Tag object or tag name.
|
||||
"""
|
||||
if isinstance(tag, basestring):
|
||||
tag = self._table.lookup(tag)
|
||||
self.ignored_tags.append(tag)
|
||||
|
||||
def remove_ignore_tag(self, tag):
|
||||
"""
|
||||
Removes a tag from the list of ignored tags. A string will be automatic
|
||||
resolved into a tag object.
|
||||
|
||||
:param tag: Tag object or tag name.
|
||||
"""
|
||||
if isinstance(tag, basestring):
|
||||
tag = self._table.lookup(tag)
|
||||
self.ignored_tags.remove(tag)
|
||||
|
||||
def add_to_dictionary(self, word):
|
||||
"""
|
||||
Adds a word to user's dictionary.
|
||||
|
||||
:param word: The word to add.
|
||||
"""
|
||||
self._dictionary.add_to_pwl(word)
|
||||
self.recheck()
|
||||
|
||||
def ignore_all(self, word):
|
||||
"""
|
||||
Ignores a word for the current session.
|
||||
|
||||
:param word: The word to ignore.
|
||||
"""
|
||||
self._dictionary.add_to_session(word)
|
||||
self.recheck()
|
||||
|
||||
def check_range(self, start, end, force_all=False):
|
||||
"""
|
||||
Checks a specified range between two GtkTextIters.
|
||||
|
||||
:param start: Start iter - checking starts here.
|
||||
:param end: End iter - checking ends here.
|
||||
"""
|
||||
if not self._enabled:
|
||||
return
|
||||
if end.inside_word(): end.forward_word_end()
|
||||
if not start.starts_word() and (start.inside_word() or
|
||||
start.ends_word()):
|
||||
start.backward_word_start()
|
||||
self._buffer.remove_tag(self._misspelled, start, end)
|
||||
cursor = self._buffer.get_iter_at_mark(self._buffer.get_insert())
|
||||
precursor = cursor.copy()
|
||||
precursor.backward_char()
|
||||
highlight = (cursor.has_tag(self._misspelled) or
|
||||
precursor.has_tag(self._misspelled))
|
||||
if not start.get_offset():
|
||||
start.forward_word_end()
|
||||
start.backward_word_start()
|
||||
word_start = start.copy()
|
||||
while word_start.compare(end) < 0:
|
||||
word_end = word_start.copy()
|
||||
word_end.forward_word_end()
|
||||
in_word = ((word_start.compare(cursor) < 0) and
|
||||
(cursor.compare(word_end) <= 0))
|
||||
if in_word and not force_all:
|
||||
if highlight:
|
||||
self._check_word(word_start, word_end)
|
||||
else:
|
||||
self._deferred_check = True
|
||||
else:
|
||||
self._check_word(word_start, word_end)
|
||||
self._deferred_check = False
|
||||
word_end.forward_word_end()
|
||||
word_end.backward_word_start()
|
||||
if word_start.equal(word_end):
|
||||
break
|
||||
word_start = word_end.copy()
|
||||
|
||||
def _languages_menu(self):
|
||||
def _set_language(item, code):
|
||||
self.language = code
|
||||
if _pygobject:
|
||||
menu = gtk.Menu.new()
|
||||
group = []
|
||||
else:
|
||||
menu = gtk.Menu()
|
||||
group = gtk.RadioMenuItem()
|
||||
connect = []
|
||||
for code, name in self.languages:
|
||||
if _pygobject:
|
||||
item = gtk.RadioMenuItem.new_with_label(group, name)
|
||||
group.append(item)
|
||||
else:
|
||||
item = gtk.RadioMenuItem(group, name)
|
||||
if code == self.language:
|
||||
item.set_active(True)
|
||||
connect.append((item, code))
|
||||
menu.append(item)
|
||||
for item, code in connect:
|
||||
item.connect('activate', _set_language, code)
|
||||
return menu
|
||||
|
||||
def _suggestion_menu(self, word):
|
||||
menu = []
|
||||
suggestions = self._dictionary.suggest(word)
|
||||
if not suggestions:
|
||||
if _pygobject:
|
||||
item = gtk.MenuItem.new()
|
||||
label = gtk.Label.new('')
|
||||
else:
|
||||
item = gtk.MenuItem()
|
||||
label = gtk.Label()
|
||||
try:
|
||||
label.set_halign(gtk.Align.LEFT)
|
||||
except AttributeError:
|
||||
label.set_alignment(0.0, 0.5)
|
||||
label.set_markup('<i>{text}</i>'.format(text=_('(no suggestions)')))
|
||||
item.add(label)
|
||||
menu.append(item)
|
||||
else:
|
||||
for suggestion in suggestions:
|
||||
if _pygobject:
|
||||
item = gtk.MenuItem.new()
|
||||
label = gtk.Label.new('')
|
||||
else:
|
||||
item = gtk.MenuItem()
|
||||
label = gtk.Label()
|
||||
label.set_markup('<b>{text}</b>'.format(text=suggestion))
|
||||
try:
|
||||
label.set_halign(gtk.Align.LEFT)
|
||||
except AttributeError:
|
||||
label.set_alignment(0.0, 0.5)
|
||||
item.add(label)
|
||||
item.connect('activate', self._replace_word, word, suggestion)
|
||||
menu.append(item)
|
||||
if _pygobject:
|
||||
menu.append(gtk.SeparatorMenuItem.new())
|
||||
item = gtk.MenuItem.new_with_label(
|
||||
_('Add "{}" to Dictionary').format(word))
|
||||
else:
|
||||
menu.append(gtk.SeparatorMenuItem())
|
||||
item = gtk.MenuItem(_('Add "{}" to Dictionary').format(word))
|
||||
item.connect('activate', lambda *args: self.add_to_dictionary(word))
|
||||
menu.append(item)
|
||||
if _pygobject:
|
||||
item = gtk.MenuItem.new_with_label(_('Ignore All'))
|
||||
else:
|
||||
item = gtk.MenuItem(_('Ignore All'))
|
||||
item.connect('activate', lambda *args: self.ignore_all(word))
|
||||
menu.append(item)
|
||||
return menu
|
||||
|
||||
def _extend_menu(self, menu):
|
||||
if not self._enabled:
|
||||
return
|
||||
if _pygobject:
|
||||
separator = gtk.SeparatorMenuItem.new()
|
||||
else:
|
||||
separator = gtk.SeparatorMenuItem()
|
||||
separator.show()
|
||||
menu.prepend(separator)
|
||||
if _pygobject:
|
||||
languages = gtk.MenuItem.new_with_label(_('Languages'))
|
||||
else:
|
||||
languages = gtk.MenuItem(_('Languages'))
|
||||
languages.set_submenu(self._languages_menu())
|
||||
languages.show_all()
|
||||
menu.prepend(languages)
|
||||
if self._marks['click'].inside_word:
|
||||
start, end = self._marks['click'].word
|
||||
if start.has_tag(self._misspelled):
|
||||
if _py3k:
|
||||
word = self._buffer.get_text(start, end, False)
|
||||
else:
|
||||
word = self._buffer.get_text(start, end,
|
||||
False).decode('utf-8')
|
||||
items = self._suggestion_menu(word)
|
||||
if self.collapse:
|
||||
if _pygobject:
|
||||
suggestions = gtk.MenuItem.new_with_label(
|
||||
_('Suggestions'))
|
||||
submenu = gtk.Menu.new()
|
||||
else:
|
||||
suggestions = gtk.MenuItem(_('Suggestions'))
|
||||
submenu = gtk.Menu()
|
||||
for item in items:
|
||||
submenu.append(item)
|
||||
suggestions.set_submenu(submenu)
|
||||
suggestions.show_all()
|
||||
menu.prepend(suggestions)
|
||||
else:
|
||||
items.reverse()
|
||||
for item in items:
|
||||
menu.prepend(item)
|
||||
menu.show_all()
|
||||
|
||||
def _click_move_popup(self, *args):
|
||||
self._marks['click'].move(self._buffer.get_iter_at_mark(
|
||||
self._buffer.get_insert()))
|
||||
return False
|
||||
|
||||
def _click_move_button(self, widget, event):
|
||||
if event.button == 3:
|
||||
if self._deferred_check: self._check_deferred_range(True)
|
||||
x, y = self._view.window_to_buffer_coords(2, int(event.x),
|
||||
int(event.y))
|
||||
iter = self._view.get_iter_at_location(x, y)
|
||||
if isinstance(iter, tuple):
|
||||
iter = iter[1]
|
||||
self._marks['click'].move(iter)
|
||||
return False
|
||||
|
||||
def _before_text_insert(self, textbuffer, location, text, length):
|
||||
self._marks['insert-start'].move(location)
|
||||
|
||||
def _after_text_insert(self, textbuffer, location, text, length):
|
||||
start = self._marks['insert-start'].iter
|
||||
self.check_range(start, location)
|
||||
self._marks['insert-end'].move(location)
|
||||
|
||||
def _range_delete(self, textbuffer, start, end):
|
||||
self.check_range(start, end)
|
||||
|
||||
def _mark_set(self, textbuffer, location, mark):
|
||||
if mark == self._buffer.get_insert() and self._deferred_check:
|
||||
self._check_deferred_range(False)
|
||||
|
||||
def _replace_word(self, item, old_word, new_word):
|
||||
start, end = self._marks['click'].word
|
||||
offset = start.get_offset()
|
||||
self._buffer.begin_user_action()
|
||||
self._buffer.delete(start, end)
|
||||
self._buffer.insert(self._buffer.get_iter_at_offset(offset), new_word)
|
||||
self._buffer.end_user_action()
|
||||
self._dictionary.store_replacement(old_word, new_word)
|
||||
|
||||
def _check_deferred_range(self, force_all):
|
||||
start = self._marks['insert-start'].iter
|
||||
end = self._marks['insert-end'].iter
|
||||
self.check_range(start, end, force_all)
|
||||
|
||||
def _check_word(self, start, end):
|
||||
if start.has_tag(self.no_spell_check):
|
||||
return
|
||||
for tag in self.ignored_tags:
|
||||
if start.has_tag(tag):
|
||||
return
|
||||
if _py3k:
|
||||
word = self._buffer.get_text(start, end, False).strip()
|
||||
else:
|
||||
word = self._buffer.get_text(start, end, False).decode('utf-8').strip()
|
||||
if not word:
|
||||
return
|
||||
if len(self._filters[SpellChecker.FILTER_WORD]):
|
||||
if self._regexes[SpellChecker.FILTER_WORD].match(word):
|
||||
return
|
||||
if len(self._filters[SpellChecker.FILTER_LINE]):
|
||||
line_start = self._buffer.get_iter_at_line(start.get_line())
|
||||
line_end = end.copy()
|
||||
line_end.forward_to_line_end()
|
||||
if _py3k:
|
||||
line = self._buffer.get_text(line_start, line_end, False)
|
||||
else:
|
||||
line = self._buffer.get_text(line_start, line_end,
|
||||
False).decode('utf-8')
|
||||
for match in self._regexes[SpellChecker.FILTER_LINE].finditer(line):
|
||||
if match.start() <= start.get_line_offset() <= match.end():
|
||||
start = self._buffer.get_iter_at_line_offset(
|
||||
start.get_line(), match.start())
|
||||
end = self._buffer.get_iter_at_line_offset(start.get_line(),
|
||||
match.end())
|
||||
self._buffer.remove_tag(self._misspelled, start, end)
|
||||
return
|
||||
if len(self._filters[SpellChecker.FILTER_TEXT]):
|
||||
text_start, text_end = self._buffer.get_bounds()
|
||||
if _py3k:
|
||||
text = self._buffer.get_text(text_start, text_end, False)
|
||||
else:
|
||||
text = self._buffer.get_text(text_start, text_end,
|
||||
False).decode('utf-8')
|
||||
for match in self._regexes[SpellChecker.FILTER_TEXT].finditer(text):
|
||||
if match.start() <= start.get_offset() <= match.end():
|
||||
start = self._buffer.get_iter_at_offset(match.start())
|
||||
end = self._buffer.get_iter_at_offset(match.end())
|
||||
self._buffer.remove_tag(self._misspelled, start, end)
|
||||
return
|
||||
if not self._dictionary.check(word):
|
||||
self._buffer.apply_tag(self._misspelled, start, end)
|
|
@ -8,7 +8,8 @@ from uberwriter.text_view_drag_drop_handler import DragDropHandler, TARGET_URI,
|
|||
from uberwriter.scroller import Scroller
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, Gdk, GObject, GLib
|
||||
gi.require_version('Gspell', '1')
|
||||
from gi.repository import Gtk, Gdk, GObject, GLib, Gspell
|
||||
|
||||
import logging
|
||||
LOGGER = logging.getLogger('uberwriter')
|
||||
|
@ -52,6 +53,10 @@ class TextView(Gtk.TextView):
|
|||
self.get_buffer().connect('changed', self.on_text_changed)
|
||||
self.connect('size-allocate', self.on_size_allocate)
|
||||
|
||||
# Spell checking
|
||||
self.gspell_view = Gspell.TextView.get_from_gtk_text_view(self)
|
||||
self.gspell_view.basic_setup()
|
||||
|
||||
# Undo / redo
|
||||
self.undo_redo = UndoRedoHandler()
|
||||
self.get_buffer().connect('insert-text', self.undo_redo.on_insert_text)
|
||||
|
@ -115,6 +120,7 @@ class TextView(Gtk.TextView):
|
|||
and the surrounding text is greyed out."""
|
||||
|
||||
self.focus_mode = focus_mode
|
||||
self.gspell_view.set_inline_spell_checking(not focus_mode)
|
||||
self.update_vertical_margin()
|
||||
self.markup.apply()
|
||||
self.scroll_to()
|
||||
|
|
|
@ -38,7 +38,6 @@ import cairo
|
|||
from uberwriter import helpers
|
||||
from uberwriter.theme import Theme
|
||||
from uberwriter.helpers import get_builder
|
||||
from uberwriter.gtkspellcheck import SpellChecker
|
||||
|
||||
from uberwriter.sidebar import Sidebar
|
||||
from uberwriter.search_and_replace import SearchAndReplace
|
||||
|
@ -223,8 +222,6 @@ class Window(Gtk.ApplicationWindow):
|
|||
|
||||
focus_mode = state.get_boolean()
|
||||
self.text_view.set_focus_mode(focus_mode)
|
||||
if self.spell_checker:
|
||||
self.spell_checker._misspelled.set_property('underline', 0 if focus_mode else 4)
|
||||
self.text_view.grab_focus()
|
||||
|
||||
def set_hemingway_mode(self, state):
|
||||
|
@ -482,39 +479,9 @@ class Window(Gtk.ApplicationWindow):
|
|||
status {gtk bool} -- Desired status of the spellchecking
|
||||
"""
|
||||
|
||||
if state.get_boolean():
|
||||
try:
|
||||
self.spell_checker.enable()
|
||||
except:
|
||||
try:
|
||||
self.spell_checker = SpellChecker(
|
||||
self.text_view, locale.getdefaultlocale()[0],
|
||||
collapse=False)
|
||||
if self.auto_correct:
|
||||
self.auto_correct.set_language(self.spell_checker.language)
|
||||
self.spell_checker.connect_language_change( # pylint: disable=no-member
|
||||
self.auto_correct.set_language)
|
||||
except:
|
||||
self.spell_checker = None
|
||||
dialog = Gtk.MessageDialog(self,
|
||||
Gtk.DialogFlags.MODAL \
|
||||
| Gtk.DialogFlags.DESTROY_WITH_PARENT,
|
||||
Gtk.MessageType.INFO,
|
||||
Gtk.ButtonsType.NONE,
|
||||
_("You can not enable the Spell Checker.")
|
||||
)
|
||||
dialog.format_secondary_text(
|
||||
_("Please install 'hunspell' or 'aspell' dictionaries"
|
||||
+ " for your language from the software center."))
|
||||
_response = dialog.run()
|
||||
return
|
||||
return
|
||||
else:
|
||||
try:
|
||||
self.spell_checker.disable()
|
||||
except:
|
||||
pass
|
||||
return
|
||||
self.text_view.gspell_view\
|
||||
.set_inline_spell_checking(state.get_boolean()
|
||||
and not self.text_view.focus_mode)
|
||||
|
||||
def toggle_gradient_overlay(self, state):
|
||||
"""Toggle the gradient overlay
|
||||
|
|
Loading…
Reference in New Issue