diff --git a/PKGBUILD b/PKGBUILD index b7b4cb2..24c2817 100644 --- a/PKGBUILD +++ b/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") diff --git a/flatpak/uberwriter.json b/flatpak/uberwriter.json index 5f62444..160f84f 100644 --- a/flatpak/uberwriter.json +++ b/flatpak/uberwriter.json @@ -29,7 +29,7 @@ "name":"enchant", "config-opts":[ "--disable-static", - "--with-myspell-dir=/usr/share/myspell" + "--with-myspell-dir=/usr/share/hunspell" ], "cleanup":[ "/bin" @@ -42,6 +42,16 @@ } ] }, + { + "name":"gspell", + "sources":[ + { + "type":"archive", + "url":"https://download.gnome.org/sources/gspell/1.8/gspell-1.8.1.tar.xz", + "sha256":"819a1d23c7603000e73f5e738bdd284342e0cd345fb0c7650999c31ec741bbe5" + } + ] + }, { "name":"fonts", "buildsystem":"simple", @@ -112,4 +122,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/requirements.txt b/requirements.txt index df91d82..0a7f6c0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ regex +enchant pypandoc==1.4 pyenchant pygtkspellcheck diff --git a/setup.py b/setup.py index 752e897..9345029 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,6 @@ setup( # "": '/opt/uberwriter/' }, packages=[ - "uberwriter.gtkspellcheck", "uberwriter.pylocales", # "uberwriter.pressagio", "uberwriter", diff --git a/uberwriter/gtkspellcheck/__init__.py b/uberwriter/gtkspellcheck/__init__.py deleted file mode 100644 index 3f30951..0000000 --- a/uberwriter/gtkspellcheck/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding:utf-8 -*- -# -# Copyright (C) 2012, Maximilian Köhl -# Copyright (C) 2012, Carlos Jenkins -# -# 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 . - -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) diff --git a/uberwriter/gtkspellcheck/oxt_extract.py b/uberwriter/gtkspellcheck/oxt_extract.py deleted file mode 100644 index ce1c45a..0000000 --- a/uberwriter/gtkspellcheck/oxt_extract.py +++ /dev/null @@ -1,294 +0,0 @@ -# -*- coding:utf-8 -*- -# -# Copyright (C) 2012, Carlos Jenkins -# Copyright (C) 2012-2016, Maximilian Köhl -# -# 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 . - -""" -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: - # %origin%/dictionary.aff %origin%/dictionary.dic - 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')) \ No newline at end of file diff --git a/uberwriter/gtkspellcheck/spellcheck.py b/uberwriter/gtkspellcheck/spellcheck.py deleted file mode 100644 index 0112fbf..0000000 --- a/uberwriter/gtkspellcheck/spellcheck.py +++ /dev/null @@ -1,660 +0,0 @@ -# -*- coding:utf-8 -*- -# -# Copyright (C) 2012, Maximilian Köhl -# Copyright (C) 2012, Carlos Jenkins -# -# 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 . - -""" -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('{text}'.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('{text}'.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) diff --git a/uberwriter/text_view.py b/uberwriter/text_view.py index 006c253..7436372 100644 --- a/uberwriter/text_view.py +++ b/uberwriter/text_view.py @@ -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() diff --git a/uberwriter/window.py b/uberwriter/window.py index 271fc82..10cdb97 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -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