From fe28b526d7a52a6523ab5a9cca54b27fd4a804fa Mon Sep 17 00:00:00 2001 From: ricky Date: Sat, 21 Nov 2015 04:00:47 -0800 Subject: [PATCH] Added spellchecked and auto-resizing height input area. --- qweechat/input.py | 41 ++++-- qweechat/inputlinespell.py | 254 +++++++++++++++++++++++++++++++++++++ 2 files changed, 283 insertions(+), 12 deletions(-) create mode 100644 qweechat/inputlinespell.py diff --git a/qweechat/input.py b/qweechat/input.py index 371b287..6d5e6f4 100644 --- a/qweechat/input.py +++ b/qweechat/input.py @@ -21,11 +21,12 @@ # 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() @@ -33,11 +34,10 @@ 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() @@ -49,7 +49,7 @@ class InputLineEdit(QtGui.QLineEdit): 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() @@ -64,22 +64,35 @@ class InputLineEdit(QtGui.QLineEdit): elif key == QtCore.Qt.Key_End: bar.setValue(bar.maximum()) else: - QtGui.QLineEdit.keyPressEvent(self, event) + InputLineSpell.keyPressEvent(self, event) elif key == QtCore.Qt.Key_PageUp: bar.setValue(bar.value() - bar.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) + 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 ((key == QtCore.Qt.Key_Enter or key == QtCore.Qt.Key_Return) + 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): @@ -93,3 +106,7 @@ class InputLineEdit(QtGui.QLineEdit): self.clear() return self.setText(self._history[self._history_index]) + # End of line: + textCursor = self.textCursor() + textCursor.setPosition(len(self._history[self._history_index])) + self.setTextCursor(textCursor) diff --git a/qweechat/inputlinespell.py b/qweechat/inputlinespell.py new file mode 100644 index 0000000..80322c1 --- /dev/null +++ b/qweechat/inputlinespell.py @@ -0,0 +1,254 @@ +# -*- 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 qt_compat +QtCore = qt_compat.import_module('QtCore') +QtGui = qt_compat.import_module('QtGui') +import config +import re +import weechat.color as color + +# Spell checker support +try: + import enchant +except ImportError: + enchant = None + +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: + fm = QtGui.QFontMetrics(self.currentFont()) + self.setMinimumHeight(fm.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()) + + def hasHeightForWidth(self): + 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): + bar = self.verticalScrollBar() + bar.setValue(bar.maximum()) + + def initDict(self, lang=None): + if enchant: + if lang == 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 killDict(self): + self.highlighter.setDocument(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() + pal = QtGui.QApplication.instance().palette() + # This fixes Issue 20 + menu_style = """ * { background-color: %s; + color: %s;} + """%(unicode(pal.color(QtGui.QPalette.Button).name()), + unicode(pal.color(QtGui.QPalette.WindowText).name())) + popup_menu.setStyleSheet(menu_style) + + # 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: + if self.textCursor().hasSelection(): + text = unicode(self.textCursor().selectedText()) + #Add to dictionary + #Spell-checker options + if not self.spelldict.check(text): + suggestions = self.spelldict.suggest(text) + if len(suggestions) != 0: + popup_menu.insertSeparator(popup_menu.actions()[0]) + topAction = popup_menu.actions()[0] + for word in suggestions: + action = SpellAction(word, popup_menu) + action.correct.connect(self.correctWord) + popup_menu.insertAction(topAction, action) + popup_menu.insertSeparator(topAction) + add = SpellAddAction(text, popup_menu) + add.add.connect(self.addWord) + popup_menu.insertAction(topAction, add) + + # FIXME: add change dict and disable spellcheck options + + popup_menu.exec_(event.globalPos()) + + 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() + + def killDict(self): + self.highlighter.setDocument(None) + self.spelldict = None + + +class SpellHighlighter(QtGui.QSyntaxHighlighter): + + WORDS = u'(?iu)[\w\']+' + + def __init__(self, *args): + QtGui.QSyntaxHighlighter.__init__(self, *args) + + self.spelldict = None + + def setDict(self, spelldict): + self.spelldict = spelldict + + def highlightBlock(self, text): + if not self.spelldict: + return + + text = unicode(text) + + format = QtGui.QTextCharFormat() + format.setUnderlineColor(QtGui.QColor('red')) + format.setUnderlineStyle(QtGui.QTextCharFormat.DotLine) + + for word_object in re.finditer(self.WORDS, text): + if not self.spelldict.check(word_object.group()): + self.setFormat(word_object.start(), + word_object.end() - word_object.start(), format) + +class SpellAction(QtGui.QAction): + + ''' + A special QAction that returns the text in a signal. + ''' + + correct = qt_compat.Signal(unicode) + + def __init__(self, *args): + QtGui.QAction.__init__(self, *args) + + self.triggered.connect(lambda x: self.correct.emit( + unicode(self.text()))) + +class SpellAddAction(QtGui.QAction): + + ''' + An action to add the given word to a dictionary. + ''' + + add = qt_compat.Signal(unicode) + + def __init__(self, word, *args): + QtGui.QAction.__init__(self, "Add to dictionary", *args) + self._word = word + self.triggered.connect(lambda x: self.add.emit( + unicode(self._word))) +