Refactor SpellChecker

- use Gspell instead of GtkSpell, it seems to have alot less problems
and needs less code
This commit is contained in:
Philipp Hörist 2017-11-25 17:37:46 +01:00
parent f75dd8bcad
commit 7692b376ee
10 changed files with 93 additions and 188 deletions

View File

@ -18,7 +18,7 @@
- python3-crypto to enable End to end encryption - python3-crypto to enable End to end encryption
- python3-gnupg to enable GPG encryption - python3-gnupg to enable GPG encryption
- For zeroconf (bonjour) you need dbus-glib, python-avahi - For zeroconf (bonjour) you need dbus-glib, python-avahi
- gir1.2-gtkspell3-3.0 and aspell-LANG where lang is your locale eg. en, fr etc - gir1.2-gspell-1 and hunspell-LANG where lang is your locale eg. en, fr etc
- gir1.2-secret-1 for GNOME Keyring or KDE support as password storage - gir1.2-secret-1 for GNOME Keyring or KDE support as password storage
- D-Bus running to have gajim-remote working. Some distributions split dbus-x11, which is needed for dbus to work with Gajim. Version >= 0.80 is required. - D-Bus running to have gajim-remote working. Some distributions split dbus-x11, which is needed for dbus to work with Gajim. Version >= 0.80 is required.
- python3-dbus bindings (>=1.2.0) - python3-dbus bindings (>=1.2.0)

View File

@ -1173,7 +1173,6 @@ class ChatControl(ChatControlBase):
self.handlers[i].disconnect(i) self.handlers[i].disconnect(i)
del self.handlers[i] del self.handlers[i]
self.conv_textview.del_handlers() self.conv_textview.del_handlers()
self.remove_speller()
self.msg_textview.destroy() self.msg_textview.destroy()
# PluginSystem: calling shutdown of super class (ChatControlBase) to let # PluginSystem: calling shutdown of super class (ChatControlBase) to let
# it remove it's GUI extension points # it remove it's GUI extension points

View File

@ -32,7 +32,6 @@ import time
from gi.repository import Gtk from gi.repository import Gtk
from gi.repository import Gdk from gi.repository import Gdk
from gi.repository import Pango from gi.repository import Pango
from gi.repository import GObject
from gi.repository import GLib from gi.repository import GLib
from gi.repository import Gio from gi.repository import Gio
@ -42,7 +41,6 @@ from gajim import message_control
from gajim import dialogs from gajim import dialogs
from gajim import history_window from gajim import history_window
from gajim import notify from gajim import notify
from gajim import gtkspell
import re import re
from gajim import emoticons from gajim import emoticons
@ -66,6 +64,9 @@ from gajim.command_system.implementation.middleware import CommandTools
from gajim.command_system.implementation import standard from gajim.command_system.implementation import standard
from gajim.command_system.implementation import execute from gajim.command_system.implementation import execute
if app.HAVE_SPELL:
from gi.repository import Gspell
################################################################################ ################################################################################
class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
@ -345,8 +346,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
self.set_emoticon_popover() self.set_emoticon_popover()
# Attach speller # Attach speller
self.spell = None self.spell_checker = None
self.spell_handlers = []
self.set_speller() self.set_speller()
self.conv_textview.tv.show() self.conv_textview.tv.show()
@ -471,44 +471,22 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
image.set_from_pixbuf(icon) image.set_from_pixbuf(icon)
def set_speller(self): def set_speller(self):
if not gtkspell.HAS_GTK_SPELL or not app.config.get('use_speller'): if not app.HAVE_SPELL or not app.config.get('use_speller'):
return return
def _on_focus_in(*args): gspell_lang = self.get_speller_language()
if self.spell is None: if gspell_lang is None:
return
self.spell.attach(self.msg_textview)
def _on_focus_out(*args):
if self.spell is None:
return
if not self.msg_textview.has_text():
self.spell.detach()
lang = self.get_speller_language()
if not lang:
return return
try:
self.spell = gtkspell.Spell(self.msg_textview, lang)
self.spell.connect('language_changed', self.on_language_changed)
handler_id = self.msg_textview.connect('focus-in-event',
_on_focus_in)
self.spell_handlers.append(handler_id)
handler_id = self.msg_textview.connect('focus-out-event',
_on_focus_out)
self.spell_handlers.append(handler_id)
except OSError:
dialogs.AspellDictError(lang)
app.config.set('use_speller', False)
def remove_speller(self): self.spell_checker = Gspell.Checker.new(gspell_lang)
if self.spell is None: spell_buffer = Gspell.TextBuffer.get_from_gtk_text_buffer(
return self.msg_textview.get_buffer())
self.spell.detach() spell_buffer.set_spell_checker(self.spell_checker)
for id_ in self.spell_handlers: spell_view = Gspell.TextView.get_from_gtk_text_view(self.msg_textview)
self.msg_textview.disconnect(id_) spell_view.set_inline_spell_checking(False)
self.spell_handlers.remove(id_) spell_view.set_enable_language_menu(True)
self.spell = None
self.spell_checker.connect('notify::language', self.on_language_changed)
def get_speller_language(self): def get_speller_language(self):
per_type = 'contacts' per_type = 'contacts'
@ -521,16 +499,20 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
lang = app.config.get('speller_language') lang = app.config.get('speller_language')
if not lang: if not lang:
lang = app.LANG lang = app.LANG
return lang or None gspell_lang = Gspell.language_lookup(lang)
if gspell_lang is None:
gspell_lang = Gspell.language_get_default()
return gspell_lang
def on_language_changed(self, spell, lang): def on_language_changed(self, checker, param):
gspell_lang = checker.get_language()
per_type = 'contacts' per_type = 'contacts'
if self.type_id == message_control.TYPE_GC: if self.type_id == message_control.TYPE_GC:
per_type = 'rooms' per_type = 'rooms'
if not app.config.get_per(per_type, self.contact.jid): if not app.config.get_per(per_type, self.contact.jid):
app.config.add_per(per_type, self.contact.jid) app.config.add_per(per_type, self.contact.jid)
app.config.set_per( app.config.set_per(per_type, self.contact.jid,
per_type, self.contact.jid, 'speller_language', lang) 'speller_language', gspell_lang.get_code())
def on_banner_label_populate_popup(self, label, menu): def on_banner_label_populate_popup(self, label, menu):
""" """

View File

@ -253,6 +253,21 @@ except Exception:
glog.info(_('Unable to load idle module')) glog.info(_('Unable to load idle module'))
HAVE_IDLE = False HAVE_IDLE = False
HAVE_SPELL = False
try:
spell_log = logging.getLogger('gajim.speller')
gi.require_version('Gspell', '1')
from gi.repository import Gspell
langs = Gspell.language_get_available()
for lang in langs:
spell_log.info('%s (%s) dict available',
lang.get_name(), lang.get_code())
if langs:
HAVE_SPELL = True
else:
spell_log.info('No dicts available')
except ImportError:
pass
gajim_identity = {'type': 'pc', 'category': 'client', 'name': 'Gajim'} gajim_identity = {'type': 'pc', 'category': 'client', 'name': 'Gajim'}
gajim_common_features = [nbxmpp.NS_BYTESTREAM, nbxmpp.NS_SI, nbxmpp.NS_FILE, gajim_common_features = [nbxmpp.NS_BYTESTREAM, nbxmpp.NS_SI, nbxmpp.NS_FILE,

View File

@ -50,7 +50,6 @@ from gajim import message_control
from gajim.chat_control_base import ChatControlBase from gajim.chat_control_base import ChatControlBase
from gajim import dataforms_widget from gajim import dataforms_widget
from gajim import gui_menu_builder from gajim import gui_menu_builder
from gajim import gtkspell
from gajim.common import helpers from gajim.common import helpers
from gajim.common import app from gajim.common import app
@ -66,6 +65,9 @@ try:
except (ImportError, ValueError): except (ImportError, ValueError):
HAS_GST = False HAS_GST = False
if app.HAVE_SPELL:
from gi.repository import Gspell
#---------- PreferencesWindow class -------------# #---------- PreferencesWindow class -------------#
class PreferencesWindow: class PreferencesWindow:
""" """
@ -187,7 +189,7 @@ class PreferencesWindow:
self.xml.get_object('xhtml_checkbutton').set_active(st) self.xml.get_object('xhtml_checkbutton').set_active(st)
# use speller # use speller
if gtkspell.HAS_GTK_SPELL: if app.HAVE_SPELL:
st = app.config.get('use_speller') st = app.config.get('use_speller')
self.xml.get_object('speller_checkbutton').set_active(st) self.xml.get_object('speller_checkbutton').set_active(st)
else: else:
@ -657,30 +659,22 @@ class PreferencesWindow:
if isinstance(ctrl, ChatControlBase): if isinstance(ctrl, ChatControlBase):
ctrl.set_speller() ctrl.set_speller()
def remove_speller(self):
for ctrl in self._get_all_controls():
if isinstance(ctrl, ChatControlBase):
if ctrl.spell is not None:
ctrl.remove_speller()
def on_speller_checkbutton_toggled(self, widget): def on_speller_checkbutton_toggled(self, widget):
active = widget.get_active() active = widget.get_active()
app.config.set('use_speller', active) app.config.set('use_speller', active)
if active: if not active:
lang = app.config.get('speller_language') return
if not lang: lang = app.config.get('speller_language')
lang = app.LANG gspell_lang = Gspell.language_lookup(lang)
if gspell_lang is None:
available = gtkspell.test_language(lang) gspell_lang = Gspell.language_get_default()
if not available: if gspell_lang is None:
dialogs.AspellDictError(lang) dialogs.AspellDictError(lang)
app.config.set('use_speller', False) app.config.set('use_speller', False)
widget.set_active(False) widget.set_active(False)
else:
app.config.set('speller_language', lang)
self.apply_speller()
else: else:
self.remove_speller() app.config.set('speller_language', gspell_lang.get_code())
self.apply_speller()
def on_positive_184_ack_checkbutton_toggled(self, widget): def on_positive_184_ack_checkbutton_toggled(self, widget):
self.on_checkbutton_toggled(widget, 'positive_184_ack') self.on_checkbutton_toggled(widget, 'positive_184_ack')

View File

@ -34,6 +34,7 @@ from gi.repository import Gdk
from gi.repository import GdkPixbuf from gi.repository import GdkPixbuf
from gi.repository import GObject from gi.repository import GObject
from gi.repository import GLib from gi.repository import GLib
import os import os
import nbxmpp import nbxmpp
import time import time
@ -42,7 +43,6 @@ from gajim import gtkgui_helpers
from gajim import vcard from gajim import vcard
from gajim import conversation_textview from gajim import conversation_textview
from gajim import dataforms_widget from gajim import dataforms_widget
from gajim import gtkspell
from random import randrange from random import randrange
from gajim.common import pep from gajim.common import pep
@ -64,6 +64,9 @@ from gajim.common import dataforms
from gajim.common.exceptions import GajimGeneralException from gajim.common.exceptions import GajimGeneralException
from gajim.common.connection_handlers_events import MessageOutgoingEvent from gajim.common.connection_handlers_events import MessageOutgoingEvent
if app.HAVE_SPELL:
from gi.repository import Gspell
import logging import logging
log = logging.getLogger('gajim.dialogs') log = logging.getLogger('gajim.dialogs')
@ -3077,15 +3080,19 @@ class SingleMessageWindow:
else: else:
self.to_entry.set_text(to) self.to_entry.set_text(to)
if app.config.get('use_speller') and gtkspell.HAS_GTK_SPELL and action == 'send': if app.config.get('use_speller') and app.HAVE_SPELL and action == 'send':
try: lang = app.config.get('speller_language')
lang = app.config.get('speller_language') gspell_lang = Gspell.language_lookup(lang)
if not lang: if gspell_lang is None:
lang = app.LANG
self.spell = gtkspell.Spell(self.message_textview, lang)
self.spell.attach(self.message_textview)
except OSError:
AspellDictError(lang) AspellDictError(lang)
else:
spell_buffer = Gspell.TextBuffer.get_from_gtk_text_buffer(
self.message_textview.get_buffer())
spell_buffer.set_spell_checker(Gspell.Checker.new(gspell_lang))
spell_view = Gspell.TextView.get_from_gtk_text_view(
self.message_textview)
spell_view.set_inline_spell_checking(True)
spell_view.set_enable_language_menu(True)
self.prepare_widgets_for(self.action) self.prepare_widgets_for(self.action)

View File

@ -63,8 +63,8 @@ class FeaturesWindow:
_('On Windows the Windows Credential Vault is used.')), _('On Windows the Windows Credential Vault is used.')),
_('Spell Checker'): (self.speller_available, _('Spell Checker'): (self.speller_available,
_('Spellchecking of composed messages.'), _('Spellchecking of composed messages.'),
_('Requires libgtkspell.'), _('Requires Gspell'),
_('Requires libgtkspell and libenchant.')), _('Requires Gspell')),
_('Automatic status'): (self.idle_available, _('Automatic status'): (self.idle_available,
_('Ability to measure idle time, in order to set auto status.'), _('Ability to measure idle time, in order to set auto status.'),
_('Requires libxss library.'), _('Requires libxss library.'),
@ -164,11 +164,7 @@ class FeaturesWindow:
return True return True
def speller_available(self): def speller_available(self):
try: return app.HAVE_SPELL
__import__('gajim.gtkspell')
except ValueError:
return False
return True
def idle_available(self): def idle_available(self):
from gajim.common import sleepy from gajim.common import sleepy

View File

@ -1,104 +0,0 @@
## src/gtkspell.py
##
## (C) 2008 Thorsten P. 'dGhvcnN0ZW5wIEFUIHltYWlsIGNvbQ==\n'.decode("base64")
## (C) 2015 Yann Leboulanger <asterix AT lagaule.org>
##
## This file is part of Gajim.
##
## Gajim 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; version 3 only.
##
## Gajim 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 Gajim. If not, see <http://www.gnu.org/licenses/>.
from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import GLib
import gi
try:
gi.require_version('GtkSpell', '3.0')
from gi.repository import GtkSpell
HAS_GTK_SPELL = True
except (ImportError, ValueError):
HAS_GTK_SPELL = False
def ensure_attached(func):
def f(self, *args, **kwargs):
if self.spell:
func(self, *args, **kwargs)
else:
raise RuntimeError("Spell object is already detached")
return f
class Spell(GObject.GObject):
__gsignals__ = {
'language_changed': (GObject.SignalFlags.RUN_FIRST, None, (str,))
}
def __init__(self, textview, language=None, create=True, jid=None,
per_type=None):
GObject.GObject.__init__(self)
if not isinstance(textview, Gtk.TextView):
raise TypeError("Textview must be derived from Gtk.TextView")
spell = GtkSpell.Checker.get_from_text_view(textview)
if create:
if spell:
raise RuntimeError("Textview has already a Spell obj attached")
self.spell = GtkSpell.Checker.new()
try:
self.spell.set_language(language)
except GLib.GError as error:
if error.domain == 'gtkspell-error-quark':
raise OSError("Unable to set language: '%s'" % language)
self.spell.connect('language-changed', self.on_language_changed)
else:
if spell:
self.spell = spell
else:
raise RuntimeError("Textview has no Spell object attached")
def on_language_changed(self, spell, lang):
self.emit('language_changed', lang)
@ensure_attached
def set_language(self, language):
if not self.spell.set_language(language):
raise OSError("Unable to set language: '%s'" % language)
@ensure_attached
def recheck_all(self):
self.spell.recheck_all()
def detach(self):
if self.spell is not None:
self.spell.detach()
def attach(self, textview):
spell = GtkSpell.Checker.get_from_text_view(textview)
if spell is None:
print('attached')
self.spell.attach(textview)
GObject.type_register(Spell)
def test_language(lang):
spell = GtkSpell.Checker.new()
try:
spell.set_language(lang)
except GLib.GError as error:
if error.domain == 'gtkspell-error-quark':
return False
return True

View File

@ -30,6 +30,10 @@ from gi.repository import Pango
from gajim.common import app from gajim.common import app
from gajim import gtkgui_helpers from gajim import gtkgui_helpers
if app.HAVE_SPELL:
from gi.repository import Gspell
class MessageTextView(Gtk.TextView): class MessageTextView(Gtk.TextView):
""" """
Class for the message textview (where user writes new messages) for Class for the message textview (where user writes new messages) for
@ -101,6 +105,7 @@ class MessageTextView(Gtk.TextView):
def _on_focus_in(self, *args): def _on_focus_in(self, *args):
if not self.has_text(): if not self.has_text():
self.get_buffer().set_text('') self.get_buffer().set_text('')
self.toggle_speller(True)
def _on_focus_out(self, *args): def _on_focus_out(self, *args):
buf = self.get_buffer() buf = self.get_buffer()
@ -109,6 +114,12 @@ class MessageTextView(Gtk.TextView):
if text == '': if text == '':
buf.insert_with_tags( buf.insert_with_tags(
start, self.PLACEHOLDER, self.placeholder_tag) start, self.PLACEHOLDER, self.placeholder_tag)
self.toggle_speller(False)
def toggle_speller(self, activate):
if app.HAVE_SPELL and app.config.get('use_speller'):
spell_view = Gspell.TextView.get_from_gtk_text_view(self)
spell_view.set_inline_spell_checking(activate)
def remove_placeholder(self): def remove_placeholder(self):
self._on_focus_in() self._on_focus_in()

View File

@ -75,7 +75,9 @@ function install_deps {
mingw-w64-"${ARCH}"-adwaita-icon-theme \ mingw-w64-"${ARCH}"-adwaita-icon-theme \
mingw-w64-"${ARCH}"-libwebp \ mingw-w64-"${ARCH}"-libwebp \
mingw-w64-"${ARCH}"-sqlite3 \ mingw-w64-"${ARCH}"-sqlite3 \
mingw-w64-"${ARCH}"-goocanvas mingw-w64-"${ARCH}"-goocanvas \
mingw-w64-"${ARCH}"-gspell \
mingw-w64-"${ARCH}"-hunspell
build_pip install setuptools_scm build_pip install setuptools_scm
@ -136,6 +138,10 @@ function install_gajim {
mkdir "${PACKAGE_DIR}"/gajim/data/plugins mkdir "${PACKAGE_DIR}"/gajim/data/plugins
7z x -o"${PACKAGE_DIR}"/gajim/data/plugins "${BUILD_ROOT}"/plugin_installer.zip 7z x -o"${PACKAGE_DIR}"/gajim/data/plugins "${BUILD_ROOT}"/plugin_installer.zip
# Install language dicts
curl -o "${BUILD_ROOT}"/speller_dicts.zip https://gajim.org/downloads/snap/win/build/speller_dicts.zip
7z x -o"${MINGW_ROOT}"/share "${BUILD_ROOT}"/speller_dicts.zip
# Install themes # Install themes
# rm -Rf "${MINGW_ROOT}"/etc # rm -Rf "${MINGW_ROOT}"/etc
# rm -Rf "${MINGW_ROOT}"/share/themes # rm -Rf "${MINGW_ROOT}"/share/themes
@ -194,7 +200,6 @@ function cleanup_install {
rm -Rf "${MINGW_ROOT}"/share/ffmpeg rm -Rf "${MINGW_ROOT}"/share/ffmpeg
rm -Rf "${MINGW_ROOT}"/share/vala rm -Rf "${MINGW_ROOT}"/share/vala
rm -Rf "${MINGW_ROOT}"/share/readline rm -Rf "${MINGW_ROOT}"/share/readline
rm -Rf "${MINGW_ROOT}"/share/xml
rm -Rf "${MINGW_ROOT}"/share/bash-completion rm -Rf "${MINGW_ROOT}"/share/bash-completion
rm -Rf "${MINGW_ROOT}"/share/common-lisp rm -Rf "${MINGW_ROOT}"/share/common-lisp
rm -Rf "${MINGW_ROOT}"/share/emacs rm -Rf "${MINGW_ROOT}"/share/emacs