PyQt - How to turn on/off spellcheck highlighting - python

I have a button which sets/unsets spellcheck highlighting in a QTextEdit box (ref PyQt - How to turn on/off spellchecking) which works fine.
Then I added a language selection QComboBox and tied its signal to the button's property but its highlighting set/unset doesn't work on changing the language. It drives me nuts, there may be something small and stupid I've done, but for the sake of it I can't find anything wrong with it.
The button (action rather) is
self.actionSpellCheck = QAction(QIcon(self.icon_spellcheck),
"Auto &Spellcheck", self,
shortcut=Qt.CTRL + Qt.SHIFT + Qt.Key_O,
triggered=self.spellcheck, checkable=True)
The combobox is
self.cb_lang = QComboBox(tb)
tb.addWidget(self.cb_lang)
lang_list = self.dict_broker.list_languages()
self.cb_lang.addItems(lang_list)
self.cb_lang.currentIndexChanged.connect(self.spellcheck)
and the self.spellcheck is
def spellcheck(self):
pos = self.cursor.position()
if self.actionSpellCheck.isChecked():
lang = self.cb_lang.currentText()
self.dict = self.dict_broker.request_dict(lang)
self.highlighter.setDict(self.dict)
self.setHighlighterEnabled(True)
self.show_status("Spellcheck language is set to " + self.dict.tag, None)
else:
self.setHighlighterEnabled(False)
self.highlighter.setDict(None)
self.show_status("Spellcheck is turned off", None)
self.cursor.setPosition(pos, QTextCursor.MoveAnchor)
self.textEdit.setTextCursor(self.cursor)
self.textEdit.setFocus()
How come the highlighter gets set/unset on clicking the button, but nothing happens on selecting the language (it only happens after I start typing, not immediately on combobox selection)? Thank you.

If you look at the HighLighter.setDict method, you'lll see that it doesn't do much other than reassign the dict attribute.
Also, the SpellTextEdit.setHighlighterEnabled only resets the document.
So you're going to need a method to re-highlight the text whenever the dict changes. Fortunately, HighLighter is a subclass of QSyntaxHighlighter, which already has a rehighlight slot which does what is required.
So you just need to amend your spellcheck method as follows:
def spellcheck(self):
pos = self.cursor.position()
if self.actionSpellCheck.isChecked():
self.setHighlighterEnabled(True)
lang = self.cb_lang.currentText()
self.dict = self.dict_broker.request_dict(lang)
self.highlighter.setDict(self.dict)
self.highlighter.rehighlight()
else:
...

Related

How to run tkinter code after event? [duplicate]

Every time a character is entered into a Text widget, I want to get the contents of that widget and subtract its length from a certain number (basically a "you have x characters left" deal).
But the StringVar() is always one event behind. This is, from what I gather, because the event is processed before the character is entered into the Text widget. This means that if I have 3 characters in the field and I enter a 4th, the StringVar is updated but is still 3 characters long, then it updates to 4 when I enter a 5th character.
Is there a way to keep the two in line?
Here's some code. I removed irrelevant parts.
def __init__(self, master):
self.char_count = StringVar()
self.char_count.set("140 chars left")
self.post_tweet = Text(self.master)
self.post_tweet.bind("<Key>", self.count)
self.post_tweet.grid(...)
self.char_count = Label(self.master, textvariable=self.foo)
self.char_count.grid(...)
def count(self):
self.x = len(self.post_tweet.get(1.0, END))
self.char_count.set(str(140 - self.x))
A simple solution is to add a new bindtag after the class binding. That way the class binding will fire before your binding. See this answer to the question How to bind self events in Tkinter Text widget after it will binded by Text widget? for an example. That answer uses an entry widget rather than a text widget, but the concept of bindtags is identical between those two widgets. Just be sure to use Text rather than Entry where appropriate.
Another solution is to bind on KeyRelease, since the default bindings happen on KeyPress.
Here's an example showing how to do it with bindtags:
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
self.post_tweet = tk.Text(self)
bindtags = list(self.post_tweet.bindtags())
bindtags.insert(2, "custom") # index 1 is where most default bindings live
self.post_tweet.bindtags(tuple(bindtags))
self.post_tweet.bind_class("custom", "<Key>", self.count)
self.post_tweet.grid()
self.char_count = tk.Label(self)
self.char_count.grid()
def count(self, event):
current = len(self.post_tweet.get("1.0", "end-1c"))
remaining = 140-current
self.char_count.configure(text="%s characters remaining" % remaining)
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(side="top", fill="both", expand=True)
root.mainloop()
Like most events in Tk, your <Key> handler is fired before the event is processed by the built-in bindings, rather than after. This allows you to, for example, prevent the normal processing from happening, or change what it does.
But this means that you can't access the new value (whether via a StringVar, or just by calling entry.get()), because it hasn't been updated yet.
If you're using Text, there's a virtual event <<Modified>> that gets fired after the "modified" flag changes. Assuming you weren't using that flag for another purpose (e.g., in a text editor, you might want to use it to mean "enable the Save button"), you can use it to do exactly what you want:
def count(self, event=None):
if not self.post_tweet.edit_modified():
return
self.post_tweet.edit_modified(False)
self.x = len(self.post_tweet.get(1.0, END))
self.char_count.set(str(140 - self.x))
# ...
self.post_tweet.bind("<<Modified>>", self.count)
Usually, when you want something like this, you want an Entry rather than a Text. Which provides a much nicer way to do this: validation. As with everything beyond the basics in Tkinter, there's no way you're going to figure this out without reading the Tcl/Tk docs (which is why the Tkinter docs link to them). And really, even the Tk docs don't describe validation very well. But here's how it works:
def count(self, new_text):
self.x = len(new_text)
self.char_count.set(str(140 - self.x))
return True
# ...
self.vcmd = self.master.register(self.count)
self.post_tweet = Edit(self.master, validate='key',
validatecommand=(self.vcmd, '%P'))
The validatecommand can take a list of 0 or more arguments to pass to the function. The %P argument gets the new value the entry will have if you allow it. See VALIDATION in the Entry manpage for more details.
If you want the entry to be rejected (e.g., if you want to actually block someone from entering more than 140 characters), just return False instead of True.
By the way, it's worth looking over the Tk wiki and searching for Tkinter recipes on ActiveState. It's a good bet someone's got wrappers around Text and Entry that hide all the extra stuff you have to do to make these solutions (or others) work so you just have to write the appropriate count method. There might even be a Text wrapper that adds Entry-style validation.
There are a few other ways you could do this, but they all have downsides.
Add a trace to hook all writes to a StringVar attached to your widget. This will get fired by any writes to the variable. I guarantee that you will get the infinite-recursive-loop problem the first time you try to use it for validation, and then you'll run into other more subtle problems in the future. The usual solution is to create a sentinel flag, which you check every time you come into the handler to make sure you're not doing it recursively, and then set while you're doing anything that can trigger a recursive event. (That wasn't necessary for the edit_modified example above because we could just ignore anyone setting the flag to False, and we only set it to False, so there's no danger of infinite recursion.)
You can get the new char (or multi-char string) out of the <Key> virtual event. But then, what do you do with it? You need to know where it's going to be added, which character(s) it's going to be overwriting, etc. If you don't do all the work to simulate Entry—or, worse, Text—editing yourself, this is no better than just doing len(entry.get()) + 1.

Readonly tkinter text widget

I want to use tkinter text widget as a readonly widget. It should act as a transcript area. My idea is to keep this transcript in a file and whenever the user writes anything, just remove all the contents of the widget, and rewrite it again.
The code will look like:
transcript_entry = SimpleEditor() # SimpleEditor is inherited from ScrolledText
transcript_entry.text.delete("1.0", END)
# this is just a test string, it should be the contents of the transcript file
transcript_entry.text.insert("1.0", "This is test transcript")
transcript_entry.text.bind("<KeyPress>", transcript_entry.readonly)
And readonly function will look like:
def readonly(self, event):
self.text.delete("1.0", END)
# this is just a test string, it should be the contents of the transcript file
self.text.insert("1.0", "This is test transcript")
The bug here is that the last character entered by the user is added to the transcript. I suspect the reason is that the readonly function is called, then the user input is wrote to the widget. How to reverse this order & let the readonly function be called after the user input is wrote to the widget?
Any hints?
The reason that the last character is inserted is because the default bindings (which causes the insert) happens after custom bindings you put on the widget. So your bindings fire first and then the default binding inserts the characters. There are other questions and answers here that discuss this in more depth. For example, see https://stackoverflow.com/a/11542200/
However, there is a better way to accomplish what you are trying to do. If you want to create a readonly text widget, you can set the state attribute to "disabled". This will prevent all inserts and deletes (and means you need to revert the state whenever you want to programmatically enter data).
On some platforms it will seem like you can't highlight and copy text, but that is only because the widget won't by default get focus on a mouse click. By adding a binding to set the focus, the user can highlight and copy text but they won't be able to cut or insert.
Here's an example using python 2.x; for 3.x you just have to change the imports:
import Tkinter as tk
from ScrolledText import ScrolledText
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
t = ScrolledText(self, wrap="word")
t.insert("end", "Hello\nworld")
t.configure(state="disabled")
t.pack(side="top", fill="both", expand=True)
# make sure the widget gets focus when clicked
# on, to enable highlighting and copying to the
# clipboard.
t.bind("<1>", lambda event: t.focus_set())
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
Please do not delete and reinsert your text :
It is huge performance issue.
It will remove any tags and marks set on the text
This will be visible to the user, and users don't like flickering interfaces
This is not necessary, Tkinter is customizable enough to just not allow the user change the content.
The best way I found to create a read only Text is to disable all the bindings leading to a text change.
My solution is to create a new Widget binding map containing only "read only commands". Then, just reconfigure your widget to use the new RO binding map instead of the default one :
from Tkinter import *
# This is the list of all default command in the "Text" tag that modify the text
commandsToRemove = (
"<Control-Key-h>",
"<Meta-Key-Delete>",
"<Meta-Key-BackSpace>",
"<Meta-Key-d>",
"<Meta-Key-b>",
"<<Redo>>",
"<<Undo>>",
"<Control-Key-t>",
"<Control-Key-o>",
"<Control-Key-k>",
"<Control-Key-d>",
"<Key>",
"<Key-Insert>",
"<<PasteSelection>>",
"<<Clear>>",
"<<Paste>>",
"<<Cut>>",
"<Key-BackSpace>",
"<Key-Delete>",
"<Key-Return>",
"<Control-Key-i>",
"<Key-Tab>",
"<Shift-Key-Tab>"
)
class ROText(Text):
tagInit = False
def init_tag(self):
"""
Just go through all binding for the Text widget.
If the command is allowed, recopy it in the ROText binding table.
"""
for key in self.bind_class("Text"):
if key not in commandsToRemove:
command = self.bind_class("Text", key)
self.bind_class("ROText", key, command)
ROText.tagInit = True
def __init__(self, *args, **kwords):
Text.__init__(self, *args, **kwords)
if not ROText.tagInit:
self.init_tag()
# Create a new binding table list, replace the default Text binding table by the ROText one
bindTags = tuple(tag if tag!="Text" else "ROText" for tag in self.bindtags())
self.bindtags(bindTags)
text = ROText()
text.insert("1.0", """A long text with several
lines
in it""")
text.pack()
text.mainloop()
Note that just the bindings are changed. All the Text command (as insert, delete, ...) are still usable.
I recently worked a different, slightly simpler solution. Rather than changing all the bindings, one can add a function to delete all input characters as soon as they are written:
def read_only(self, event):
if event.char is not '': # delete only if the key pressed
# corresponds to an actual character
self.text.delete('insert-1c')
and just bind it to any event:
root.bind('<Key>', self.read_only)

Tkinter: set StringVar after <Key> event, including the key pressed

Every time a character is entered into a Text widget, I want to get the contents of that widget and subtract its length from a certain number (basically a "you have x characters left" deal).
But the StringVar() is always one event behind. This is, from what I gather, because the event is processed before the character is entered into the Text widget. This means that if I have 3 characters in the field and I enter a 4th, the StringVar is updated but is still 3 characters long, then it updates to 4 when I enter a 5th character.
Is there a way to keep the two in line?
Here's some code. I removed irrelevant parts.
def __init__(self, master):
self.char_count = StringVar()
self.char_count.set("140 chars left")
self.post_tweet = Text(self.master)
self.post_tweet.bind("<Key>", self.count)
self.post_tweet.grid(...)
self.char_count = Label(self.master, textvariable=self.foo)
self.char_count.grid(...)
def count(self):
self.x = len(self.post_tweet.get(1.0, END))
self.char_count.set(str(140 - self.x))
A simple solution is to add a new bindtag after the class binding. That way the class binding will fire before your binding. See this answer to the question How to bind self events in Tkinter Text widget after it will binded by Text widget? for an example. That answer uses an entry widget rather than a text widget, but the concept of bindtags is identical between those two widgets. Just be sure to use Text rather than Entry where appropriate.
Another solution is to bind on KeyRelease, since the default bindings happen on KeyPress.
Here's an example showing how to do it with bindtags:
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
self.post_tweet = tk.Text(self)
bindtags = list(self.post_tweet.bindtags())
bindtags.insert(2, "custom") # index 1 is where most default bindings live
self.post_tweet.bindtags(tuple(bindtags))
self.post_tweet.bind_class("custom", "<Key>", self.count)
self.post_tweet.grid()
self.char_count = tk.Label(self)
self.char_count.grid()
def count(self, event):
current = len(self.post_tweet.get("1.0", "end-1c"))
remaining = 140-current
self.char_count.configure(text="%s characters remaining" % remaining)
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(side="top", fill="both", expand=True)
root.mainloop()
Like most events in Tk, your <Key> handler is fired before the event is processed by the built-in bindings, rather than after. This allows you to, for example, prevent the normal processing from happening, or change what it does.
But this means that you can't access the new value (whether via a StringVar, or just by calling entry.get()), because it hasn't been updated yet.
If you're using Text, there's a virtual event <<Modified>> that gets fired after the "modified" flag changes. Assuming you weren't using that flag for another purpose (e.g., in a text editor, you might want to use it to mean "enable the Save button"), you can use it to do exactly what you want:
def count(self, event=None):
if not self.post_tweet.edit_modified():
return
self.post_tweet.edit_modified(False)
self.x = len(self.post_tweet.get(1.0, END))
self.char_count.set(str(140 - self.x))
# ...
self.post_tweet.bind("<<Modified>>", self.count)
Usually, when you want something like this, you want an Entry rather than a Text. Which provides a much nicer way to do this: validation. As with everything beyond the basics in Tkinter, there's no way you're going to figure this out without reading the Tcl/Tk docs (which is why the Tkinter docs link to them). And really, even the Tk docs don't describe validation very well. But here's how it works:
def count(self, new_text):
self.x = len(new_text)
self.char_count.set(str(140 - self.x))
return True
# ...
self.vcmd = self.master.register(self.count)
self.post_tweet = Edit(self.master, validate='key',
validatecommand=(self.vcmd, '%P'))
The validatecommand can take a list of 0 or more arguments to pass to the function. The %P argument gets the new value the entry will have if you allow it. See VALIDATION in the Entry manpage for more details.
If you want the entry to be rejected (e.g., if you want to actually block someone from entering more than 140 characters), just return False instead of True.
By the way, it's worth looking over the Tk wiki and searching for Tkinter recipes on ActiveState. It's a good bet someone's got wrappers around Text and Entry that hide all the extra stuff you have to do to make these solutions (or others) work so you just have to write the appropriate count method. There might even be a Text wrapper that adds Entry-style validation.
There are a few other ways you could do this, but they all have downsides.
Add a trace to hook all writes to a StringVar attached to your widget. This will get fired by any writes to the variable. I guarantee that you will get the infinite-recursive-loop problem the first time you try to use it for validation, and then you'll run into other more subtle problems in the future. The usual solution is to create a sentinel flag, which you check every time you come into the handler to make sure you're not doing it recursively, and then set while you're doing anything that can trigger a recursive event. (That wasn't necessary for the edit_modified example above because we could just ignore anyone setting the flag to False, and we only set it to False, so there's no danger of infinite recursion.)
You can get the new char (or multi-char string) out of the <Key> virtual event. But then, what do you do with it? You need to know where it's going to be added, which character(s) it's going to be overwriting, etc. If you don't do all the work to simulate Entry—or, worse, Text—editing yourself, this is no better than just doing len(entry.get()) + 1.

How do you modify the current selection length in a Tkinter Text widget?

I would like to be able to double click on test,
in a Tkinter Text widget, and have it select test (and exclude the comma).
Here is what I've tried:
import Tkinter as tk
def selection_mod(event=None):
result = aText.selection_get().find(',')
if result > 0:
try:
aText.tag_add("sel", "sel.first", "sel.last-1c")
except tk.TclError:
pass
lord = tk.Tk()
aText = tk.Text(lord, font=("Georgia", "12"))
aText.grid()
aText.bind("<Double-Button-1>", selection_mod)
lord.mainloop()
The first issue is that <Double-Button-1> seems to trigger the handler before the selection is made, producing:
TclError: PRIMARY selection doesn't exist or form "STRING" not defined
The second issue is that even when using a binding that works,
my selection tag doesn't seem to do anything.
It doesn't even raise an error, and I've tried without the except tk.TclError:.
Your binding is happening before the default bindings occur. Thus, the selection doesn't yet exist when your binding fires. Because your binding tries to get the selection, it fails with the error that you see.
You will need to arrange for your binding to happen after the class bindings. A cheap hack is to use after to execute your code once the default bindings have a chance to work. Or, you can use the bindtag feature to make sure your binding fires after the default bindings.
The second problem is that you don't clear the old selection before setting the new. You'll want to do tag_remove to first remove the existing selection. Otherwise, the comma (if it was somehow selected) will remain selected since all you're doing is re-applying the tag to text that already has the tag.
However, double-click doesn't normally capture the comma so I don't quite understand then point of your code. At least, when I test it on OSX it doesn't include the comma.
Here is what I came up with thanks to Bryan's answer:
import Tkinter as tki # tkinter in Python 3
def selection_mod(event=None):
result = txt.selection_get().find(',')
if result > 0:
fir, sec = txt.tag_ranges("sel")
txt.tag_remove("sel", "sel.first", "sel.last")
txt.tag_add("sel", fir, str(sec)+"-1c")
root = tki.Tk()
txt = tki.Text(root, font=("Georgia", "12"))
txt.grid()
txt.bind("<Double-Button-1>", lambda x: root.after(20, selection_mod))
root.mainloop()
It's worth noting that I'm using Windows 7, and according to Bryan,
OSX doesn't include the comma when you double click a word.

How to tweak my tooltips in wxpython?

I was trying to add a tooltip to show the full content of a truncated ObjectListView, until it turned out it had such a feature built-in:
I tried making my own tool tips using wx.TipWindow, wx.PopupWindow and SuperToolTip, but none of them looked as 'native' as this one.
While I'm aware of this wiki article that supposedly enables the tooltip for truncated wx.Listrctrls, I didn't really understand how to get it working. I also expect that it only works when something is truncated, whereas I'd like to be able to use it to display some more information.
I guess the SuperToolTip comes close, but when you remove the 'header' it leaves it with empty space at the top, rather than centering the text in the middle of the tooltip and making it fit.
I tried looking through the source code of ObjectListView, SuperToolTip and wxpython to try and find how tooltips are being created, but I can't really find the low level parts that make it happen.
So how can I tweak tooltips so they look more like native tooltips?
The code to generate my current popups was:
text = "I'm a popup"
class PopUp(wx.TipWindow):
def __init__(self, parent, text):
wx.TipWindow.__init__(self, parent, text)
class PopUp2(wx.PopupWindow):
def __init__(self, parent, text):
wx.PopupWindow.__init__(self, parent)
st = wx.StaticText(self, parent, text)
# Import `from agw import supertooltip as STT`
popup3 = STT.SuperToolTip(text)
I'm not sure if we have a way to create a native Win7 tooltip yet, as you've seen wx.TipWindow looks like the tooltips from older versions of Windows, so there are probably some newer APIs that we should be using instead. Please create a ticket at trac.wxwidgets.org to find out for sure or to request the change if it's not possible some other way that I'm not thinking of at the moment.
Even if you can't create and pop up a native tooltip from scratch, you can still assign the entire ListCtrl a tooltip when you create it, and then change the text to whatever you want based on the item under the mouse pointer. It doesn't position the tooltip neatly over the list item like ObjectListView does, but I think it still accomplishes what you're asking.
self.lc = wx.ListCtrl(self, style=wx.LC_REPORT)
# ...
self.lc.Bind(wx.EVT_MOTION, self.OnMouseMotion)
def OnMouseMotion(self, evt):
pos = self.lc.ScreenToClient(wx.GetMousePosition())
item_index, flag = self.lc.HitTest(pos)
tip = self.lc.GetToolTip()
if flag == wx.LIST_HITTEST_ONITEMLABEL:
tip.SetTip('Some information about ' + self.lc.GetItemText(item_index))
else:
tip.SetTip('')
evt.Skip()

Categories