diff --git a/README.adoc b/README.adoc index c3378b0..e6f00d8 100644 --- a/README.adoc +++ b/README.adoc @@ -28,6 +28,10 @@ Following packages are *required*: * Python 2.x >= 2.6 * PySide (recommended, packages: python.pyside.*) or PyQt4 (python-qt4) +Following packages are *optional*: + +* PyEnchant (for spell check) + === Install via source distribution ---- diff --git a/qweechat/input.py b/qweechat/input.py index 5bde922..73b977c 100644 --- a/qweechat/input.py +++ b/qweechat/input.py @@ -21,12 +21,13 @@ # import qt_compat +from inputlinespell import InputLineSpell QtCore = qt_compat.import_module('QtCore') QtGui = qt_compat.import_module('QtGui') -class InputLineEdit(QtGui.QLineEdit): +class InputLineEdit(InputLineSpell): """Input line.""" bufferSwitchPrev = qt_compat.Signal() @@ -34,53 +35,65 @@ class InputLineEdit(QtGui.QLineEdit): textSent = qt_compat.Signal(str) def __init__(self, scroll_widget): - QtGui.QLineEdit.__init__(self) + InputLineSpell.__init__(self, False) self.scroll_widget = scroll_widget self._history = [] self._history_index = -1 - self.returnPressed.connect(self._input_return_pressed) def keyPressEvent(self, event): key = event.key() modifiers = event.modifiers() - bar = self.scroll_widget.verticalScrollBar() + scroll = self.scroll_widget.verticalScrollBar() + newline = (key == QtCore.Qt.Key_Enter or key == QtCore.Qt.Key_Return) if modifiers == QtCore.Qt.ControlModifier: if key == QtCore.Qt.Key_PageUp: self.bufferSwitchPrev.emit() elif key == QtCore.Qt.Key_PageDown: self.bufferSwitchNext.emit() else: - QtGui.QLineEdit.keyPressEvent(self, event) + InputLineSpell.keyPressEvent(self, event) elif modifiers == QtCore.Qt.AltModifier: if key in (QtCore.Qt.Key_Left, QtCore.Qt.Key_Up): self.bufferSwitchPrev.emit() elif key in (QtCore.Qt.Key_Right, QtCore.Qt.Key_Down): self.bufferSwitchNext.emit() elif key == QtCore.Qt.Key_PageUp: - bar.setValue(bar.value() - (bar.pageStep() / 10)) + scroll.setValue(scroll.value() - (scroll.pageStep() / 10)) elif key == QtCore.Qt.Key_PageDown: - bar.setValue(bar.value() + (bar.pageStep() / 10)) + scroll.setValue(scroll.value() + (scroll.pageStep() / 10)) elif key == QtCore.Qt.Key_Home: - bar.setValue(bar.minimum()) + scroll.setValue(scroll.minimum()) elif key == QtCore.Qt.Key_End: - bar.setValue(bar.maximum()) + scroll.setValue(scroll.maximum()) else: - QtGui.QLineEdit.keyPressEvent(self, event) + InputLineSpell.keyPressEvent(self, event) elif key == QtCore.Qt.Key_PageUp: - bar.setValue(bar.value() - bar.pageStep()) + scroll.setValue(scroll.value() - scroll.pageStep()) elif key == QtCore.Qt.Key_PageDown: - bar.setValue(bar.value() + bar.pageStep()) - elif key == QtCore.Qt.Key_Up: - self._history_navigate(-1) - elif key == QtCore.Qt.Key_Down: - self._history_navigate(1) + scroll.setValue(scroll.value() + scroll.pageStep()) + elif key == QtCore.Qt.Key_Up or key == QtCore.Qt.Key_Down: + # Compare position, optionally only nativate history if no change: + pos1 = self.textCursor().position() + InputLineSpell.keyPressEvent(self, event) + pos2 = self.textCursor().position() + if pos1 == pos2: + if key == QtCore.Qt.Key_Up: + # Add to history if there is text like curses weechat: + txt = self.toPlainText().encode('utf-8') + if txt != "" and len(self._history) == self._history_index: + self._history.append(txt) + self._history_navigate(-1) + elif key == QtCore.Qt.Key_Down: + self._history_navigate(1) + elif newline and modifiers != QtCore.Qt.ShiftModifier: + self._input_return_pressed() else: - QtGui.QLineEdit.keyPressEvent(self, event) + InputLineSpell.keyPressEvent(self, event) def _input_return_pressed(self): - self._history.append(self.text().encode('utf-8')) + self._history.append(self.toPlainText().encode('utf-8')) self._history_index = len(self._history) - self.textSent.emit(self.text()) + self.textSent.emit(self.toPlainText()) self.clear() def _history_navigate(self, direction): @@ -94,3 +107,7 @@ class InputLineEdit(QtGui.QLineEdit): self.clear() return self.setText(self._history[self._history_index]) + # End of line: + text_cursor = self.textCursor() + text_cursor.setPosition(len(self._history[self._history_index])) + self.setTextCursor(text_cursor) diff --git a/qweechat/inputlinespell.py b/qweechat/inputlinespell.py new file mode 100644 index 0000000..62f82cb --- /dev/null +++ b/qweechat/inputlinespell.py @@ -0,0 +1,255 @@ +# -*- coding: utf-8 -*- +# +# inputlinespell.py - single line edit with spellcheck for qweechat +# +# Copyright (C) Ricky Brent +# Copyright for auto-resizing portions of code are held by Kamil Úliwak as +# part of git@github.com:cameel/auto-resizing-text-edit.git and for +# spellcheck portions by John Schember, both under the MIT license. +# +# This file is part of QWeeChat, a Qt remote GUI for WeeChat. +# +# QWeeChat 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. +# +# QWeeChat 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 QWeeChat. If not, see . +# +import functools +import re +import config +import qt_compat +import weechat.color as color + +# Spell checker support +try: + import enchant +except ImportError: + enchant = None +QtCore = qt_compat.import_module('QtCore') +QtGui = qt_compat.import_module('QtGui') + + +class InputLineSpell(QtGui.QTextEdit): + """Chat area.""" + + def __init__(self, debug, *args): + QtGui.QTextEdit.__init__(*(self,) + args) + self.debug = debug + self.setFontFamily('monospace') + + self._textcolor = self.textColor() + self._bgcolor = QtGui.QColor('#FFFFFF') + self._setcolorcode = { + 'F': (self.setTextColor, self._textcolor), + 'B': (self.setTextBackgroundColor, self._bgcolor) + } + self._setfont = { + '*': self.setFontWeight, + '_': self.setFontUnderline, + '/': self.setFontItalic + } + self._fontvalues = { + False: { + '*': QtGui.QFont.Normal, + '_': False, + '/': False + }, + True: { + '*': QtGui.QFont.Bold, + '_': True, + '/': True + } + } + self._color = color.Color(config.color_options(), self.debug) + self.initDict() + # Set height to one line: + font_metric = QtGui.QFontMetrics(self.currentFont()) + self.setMinimumHeight(font_metric.height() + 8) + size_policy = self.sizePolicy() + size_policy.setHeightForWidth(True) + size_policy.setVerticalPolicy(QtGui.QSizePolicy.Preferred) + self.setSizePolicy(size_policy) + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.textChanged.connect(lambda: self.updateGeometry()) + + @staticmethod + def hasHeightForWidth(): + return True + + def heightForWidth(self, width): + margins = self.contentsMargins() + + if width >= margins.left() + margins.right(): + document_width = width - margins.left() - margins.right() + else: + # If specified width can't even fit the margin, no space left. + document_width = 0 + # Cloning seems wasteful but is the preferred way to in Qt >= 4. + document = self.document().clone() + document.setTextWidth(document_width) + + return margins.top() + document.size().height() + margins.bottom() + + def sizeHint(self): + original_hint = super(InputLineSpell, self).sizeHint() + return QtCore.QSize(original_hint.width(), + self.heightForWidth(original_hint.width())) + + def scroll_bottom(self): + scroll = self.verticalScrollBar() + scroll.setValue(scroll.maximum()) + + def initDict(self, lang=None): + if enchant: + if lang is None: + # Default dictionary based on the current locale. + try: + self.spelldict = enchant.Dict() + except enchant.DictNotFoundError: + self.spelldict = None + else: + self.spelldict = enchant.Dict(lang) + else: + self.spelldict = None + self.highlighter = SpellHighlighter(self.document()) + if self.spelldict: + self.highlighter.setDict(self.spelldict) + self.highlighter.rehighlight() + + def toggleDict(self, label=None): + if self.spelldict: + self.killDict() + else: + self.initDict() + + def killDict(self): + self.highlighter.setDocument(None) + self.highlighter.setDict(None) + self.spelldict = None + + def mousePressEvent(self, event): + if event.button() == QtCore.Qt.RightButton: + # Rewrite the mouse event to a left button event so the cursor + # is moved to the location of the pointer. + event = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress, + event.pos(), QtCore.Qt.LeftButton, + QtCore.Qt.LeftButton, + QtCore.Qt.NoModifier) + QtGui.QTextEdit.mousePressEvent(self, event) + + def contextMenuEvent(self, event): + popup_menu = self.createStandardContextMenu() + + # Select the word under the cursor. + cursor = self.textCursor() + cursor.select(QtGui.QTextCursor.WordUnderCursor) + self.setTextCursor(cursor) + + # Check if the selected word is misspelled and offer spelling + # suggestions if it is. + if enchant and self.spelldict: + top_action = popup_menu.actions()[0] + if self.textCursor().hasSelection(): + text = unicode(self.textCursor().selectedText()) + if not self.spelldict.check(text): + suggestions = self.spelldict.suggest(text) + if len(suggestions) != 0: + popup_menu.insertSeparator(popup_menu.actions()[0]) + for suggest in suggestions: + self._menu_action(suggest, popup_menu, + self.correctWord, after=top_action) + popup_menu.insertSeparator(top_action) + add_action = QtGui.QAction("Add to dictionary", self) + add_action.triggered.connect(lambda: self.addWord(text)) + popup_menu.insertAction(top_action, add_action) + spell_menu = QtGui.QMenu(popup_menu) + spell_menu.setTitle('Spellcheck') + popup_menu.insertMenu(top_action, spell_menu) + for lang in enchant.list_languages(): + self._menu_action(lang, spell_menu, self.initDict, + checked=(lang == self.spelldict.tag)) + toggle = self._menu_action('Check spelling', spell_menu, + self.toggleDict, + checked=(self.spelldict is not False)) + spell_menu.insertSeparator(toggle) + elif enchant: + toggle = self._menu_action('Check spelling', popup_menu, + self.toggleDict, checked=False) + popup_menu.insertSeparator(toggle) + popup_menu.exec_(event.globalPos()) + + @staticmethod + def _menu_action(text, menu, method, after=None, checked=None): + action = QtGui.QAction(text, menu) + action.connect( + action, + QtCore.SIGNAL("triggered()"), + functools.partial(method, text) + ) + if checked is not None: + action.setCheckable(True) + action.setChecked(checked) + if after is not None: + menu.insertAction(after, action) + else: + menu.addAction(action) + return action + + def addWord(self, word): + self.spelldict.add(word) + self.highlighter.rehighlight() + + def correctWord(self, word): + ''' + Replaces the selected text with word. + ''' + cursor = self.textCursor() + cursor.beginEditBlock() + + cursor.removeSelectedText() + cursor.insertText(word) + + cursor.endEditBlock() + + @staticmethod + def list_languages(): + if enchant: + return enchant.list_languages() + return [] + + +class SpellHighlighter(QtGui.QSyntaxHighlighter): + + WORDS = r'(?iu)[\w\']+' + + def __init__(self, *args): + QtGui.QSyntaxHighlighter.__init__(self, *args) + + self.spelldict = None + self._mispelled = QtGui.QTextCharFormat() + self._mispelled.setUnderlineColor(QtGui.QColor('red')) + self._mispelled.setUnderlineStyle(QtGui.QTextCharFormat.DotLine) + + def setDict(self, spelldict): + self.spelldict = spelldict + + def highlightBlock(self, text): + if not self.spelldict: + return + + text = unicode(text) + + for word_object in re.finditer(self.WORDS, text): + if not self.spelldict.check(word_object.group()): + word_len = word_object.end() - word_object.start() + self.setFormat(word_object.start(), + word_len, self._mispelled)