Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added spellchecked and auto-resizing height input area. #3

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

----
Expand Down
55 changes: 36 additions & 19 deletions qweechat/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,66 +21,79 @@
#

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()
bufferSwitchNext = qt_compat.Signal()
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):
Expand All @@ -94,3 +107,7 @@ def _history_navigate(self, direction):
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)
255 changes: 255 additions & 0 deletions qweechat/inputlinespell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
# -*- coding: utf-8 -*-
#
# inputlinespell.py - single line edit with spellcheck for qweechat
#
# Copyright (C) Ricky Brent <ricky@rickybrent.com>
# 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 <http://www.gnu.org/licenses/>.
#
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)