TkInter: how to get actual text modification? - python-3.x

I want to know where and what was changed by user in tkinter's Text widget.
I've found how to get that text was somehow modified by using <<Modified>>event but I can't get actual changes:
from tkinter import *
def reset_modified():
global resetting_modified
resetting_modified = True
text.tk.call(text._w, 'edit', 'modified', 0)
resetting_modified = False
def on_change(ev=None):
if resetting_modified: return
print ("Text now:\n%s" % text.get("1.0", END))
if False: # ????
print ("Deleted [deleted substring] from row %d col %d")
if False: # ????
print ("Inserted [inserted substring] at row %d col %d")
reset_modified()
resetting_modified = False
root = Tk()
text = Text(root)
text.insert(END, "Hello\nworld")
text.pack()
text.bind("<<Modified>>", on_change)
reset_modified()
root.mainloop()
For example, if I select 'ello' part from "hello\nworld" in Text widget then I press 'E', then I want to see
"Deleted [ello] from row 0 col 1" followed by "Inserted [E] at row 0 col 1"
is it possible to get such changes (or at least their coordinates) or I have basically to diff text on each keystroke if I want to detect changes run time?

Catching the low level inserts and deletes performed by the underlying tcl/tk code is the only good way to do what you want. You can use something like WidgetRedirector or you can do your own solution if you want more control.
Writing your own proxy command to catch all internal commands is quite simple, and takes just a few lines of code. Here's an example of a custom Text widget that prints out every internal command as it happens:
from __future__ import print_function
import Tkinter as tk
class CustomText(tk.Text):
def __init__(self, *args, **kwargs):
"""A text widget that report on internal widget commands"""
tk.Text.__init__(self, *args, **kwargs)
self._orig = self._w + "_orig"
self.tk.call("rename", self._w, self._orig)
self.tk.createcommand(self._w, self.proxy)
def proxy(self, command, *args):
# this lets' tkinter handle the command as usual
cmd = (self._orig, command) + args
result = self.tk.call(cmd)
# here we just echo the command and result
print(command, args, "=>", result)
# Note: returning the result of the original command
# is critically important!
return result
if __name__ == "__main__":
root = tk.Tk()
CustomText(root).pack(fill="both", expand=True)
root.mainloop()

After digging around, I've found that idlelib has WidgetRedirector which can redirect on inserted/deleted events:
from tkinter import *
from idlelib.WidgetRedirector import WidgetRedirector
def on_insert(*args):
print ("INS:", text.index(args[0]))
old_insert(*args)
def on_delete(*args):
print ("DEL:", list(map(text.index, args)))
old_delete(*args)
root = Tk()
text = Text(root)
text.insert(END, "Hello\nworld")
text.pack()
redir = WidgetRedirector(text)
old_insert=redir.register("insert", on_insert)
old_delete=redir.register("delete", on_delete)
root.mainloop()
Though it seems hacky. Is there a more natural way?

Related

How to remove the option to select what's inside a text widget, (not by using state = disabled)

I have tried using exportselection = False
this is the code I use to get the input from the user, if the user is highlighting the text widget (while inputting their answer), they are able to edit where the input get's printed
import tkinter as tk
from tkinter import ttk
window = tk.Tk()
numb_of_times = 5
window.geometry('1920x1080')
window.configure(bg = 'blue')
input_board = tk.Text(window,state = "disabled")
input_board.pack()
input_board.place(x = 100,y = 40)
def send():
input_board.configure(state="normal")
input_board.insert(tk.INSERT, '%s\n' % user_input)
input_board.configure(state="disabled")
for i in range(numb_of_times):
user_input = input()
print(user_input)
send()
window.mainloop()
I have tried using exportselection = False
When the selection changes in the text widget, it emits a <<Selection>> event. You can bind to that event and remove the selection. This should prevent any text from being selected.
The selection is represented by the tag "sel", which you can pass to tag_remove.
The solution might look something like this:
def remove_selection(event):
event.widget.tag_remove("sel", "1.0", "end")
input_board.bind("<<Selection>>", remove_selection)

Python3, difficulty with classes and tkinter

First of all to kick it off,
I'm not great at programming,
I have difficulty with understanding most basics,
I always try doing my uterly best to solve things like this myself.
I'm trying to create a simple gui that makes json files. Everything works fine. Fine in the sense that I'm able to create the files.
Now I wanted to get my code cleaned up and to the next level. I have added tabs to the tkinter screen and that is where the troubles starts. Because when I'm, on a differend tab, the function doesn't get the current selected items, so I added buttons to save that list and then move to different tab.
I have a function(Save_List_t), which looks at the selected items from the listbox(a_lsb1) and saves them to a list(choice_list_t). This function runs when I press button(a_button).
After doing that I got a problem, I don't want to use "global" but I need the list in a other function(Mitre_Gen_Techs) to generate the files. This function runs when I press a button on the third tab.(c.button1)
To tackel this problem, I saw a post where someone uses a class to fix it. However even after reading to the documentation about classes I still don't truely get it.
Now I'm stuck and get the error. Which I don't find that strange, it makes sense to me why it gives the error but what am I doing wrong or how do I solve this issue.
The error:
Exception in Tkinter callback
Traceback (most recent call last):
File "C:\Users\thans\AppData\Local\Programs\Python\Python38-32\lib\tkinter\__init__.py", line 1883, in __call__
return self.func(*args)
TypeError: Save_List_t() missing 1 required positional argument: 'self'
The code I wrote:
from tkinter import *
from attackcti import attack_client
from mitretemplategen import *
from tkinter import ttk
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# Mitre ATT&Ck
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
ac = attack_client()
groups = ac.get_groups()
groups = ac.remove_revoked(groups)
techs = ac.get_enterprise_techniques()
techs = ac.remove_revoked(techs)
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# Tkinter screen setup
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
root = Tk()
root.title("Mitre Att&ck")
root.minsize(900, 800)
root.wm_iconbitmap('')
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# Functions / classes
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
class Screen(object):
def __init__(self):
self.choice_list_t = []
self.choice_list_g = []
def Save_List_t(self):
for item in a_lsb2.curselection():
self.choice_list_t.append(a_lsb2.get(item))
print(self.choice_list_t)
def Save_List_g(self):
choice_list_g = []
for item in b_lsb1.curselection():
self.choice_list_g.append(b_lsb1.get(item))
print(self.choice_list_g)
def Mitre_Gen_Techs(self):
# Gen the json file
# mitre_gen_techs(self.choice_list_t, techs)
#testing class
print(self.choice_list_t)
def Mitre_Gen_Groups(self):
# Gen the json file
# mitre_gen_groups(self.choice_list_g, groups)
#testing class
print(self.choice_list_g)
def main():
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# First Tkinter tab
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
rows = 0
while rows < 50:
root.rowconfigure(rows, weight=1)
root.columnconfigure(rows, weight=1)
rows += 1
# Notebook creating tabs
nb = ttk.Notebook(root)
nb.grid(row=1, column=0, columnspan=50, rowspan=50, sticky='NESW')
# Create the differend tabs on the notebook
tab_one = Frame(nb)
nb.add(tab_one, text='APG')
tab_two = Frame(nb)
nb.add(tab_two, text='Actors')
tab_gen = Frame(nb)
nb.add(tab_gen, text='test')
# =-=- First Tab -=-=
# List box 1
a_lsb1 = Listbox(tab_one, height=30, width=30, selectmode=MULTIPLE)
# List with techs
a_lsb2 = Listbox(tab_one, height=30, width=30, selectmode=MULTIPLE)
for t in techs:
a_lsb2.insert(END, t['name'])
# Save list, to later use in Screen.Mitre_Gen_Techs
a_button = Button(tab_one, text="Save selected", command=Screen.Save_List_t)
# =-=- Second Tab -=-=
# List with TA's
b_lsb1 = Listbox(tab_two, height=30, width=30, selectmode=MULTIPLE)
for g in groups:
b_lsb1.insert(END, g['name'])
# Save list, to later use in Screen.Mitre_Gen_Groups
b_button = Button(tab_two, text="Save selected", command=Screen.Save_List_g)
# =-=- Third tab -=-=
c_button = Button(tab_gen, text="Print group json", command=Screen.Mitre_Gen_Groups)
c_button1 = Button(tab_gen, text="Print techs json", command=Screen.Mitre_Gen_Techs)
# Placing the items on the grid
a_lsb1.grid(row=1, column=1)
a_lsb2.grid(row=1, column=2)
b_lsb1.grid(row=1, column=1)
a_button.grid(row=2, column=1)
b_button.grid(row=2, column=1)
c_button.grid(row=2, column=1)
c_button1.grid(row=2, column=2)
root.mainloop()
# If main file then run: main()
if __name__ == "__main__":
main()
The application:
Image
I found someone who explained what was wrong.
Credits to Scriptman ( ^ , ^ ) /
simply adding:
sc = Screen()
And changing:
Button(tab_apg, text="Save selected", command=sc.Save_List_t)
Resolved the issue.

Up and down arrow inserting unwanted character in tkinter.Entry widget

The following code produces an app with a single Entry widget. When run on MacOS using Python 3.7.3 from Homebrew, pressing the up or down arrow while inside the entry box causes a character 0xF701 to be inserted:
import tkinter as tk
root = tk.Tk()
app = tk.Frame(master=root)
app.pack()
entry = tk.Entry(app)
entry.pack()
app.mainloop()
This doesn't happen with Anaconda Python and I haven't been able to find anyone else having this issue.
By binding print to the up and down events I've been able to see that the character associated with these events is indeed 0xF700 and 0xF701.
entry.bind('<Down>', print)
entry.bind('<Up>', print)
Output after pressing up and down:
<KeyPress event state=Mod3|Mod4 keysym=Up keycode=8320768 char='\uf700' delta=8320768 x=-5 y=-50>
<KeyPress event state=Mod3|Mod4 keysym=Down keycode=8255233 char='\uf701' delta=8255233 x=-5 y=-50>
With the Anaconda Python version the output is slightly different:
<KeyPress event state=Mod3|Mod4 keysym=Up keycode=8320768 char='\uf700' x=-5 y=-50>
<KeyPress event state=Mod3|Mod4 keysym=Down keycode=8255233 char='\uf701' x=-5 y=-50>
Does anyone know of a simple solution to this problem?
Can validating the Entry help? The code below validates that the resulting string in Entry only contains characters in valid_chars. A more complex validation rule could be written if required.
import tkinter as tk
import re
valid_chars = re.compile(r'^[0-9A-Za-z ]*$') # Accept Alphanumeric and Space
class ValidateEntry(tk.Entry):
def __init__(self, parent, regex):
self.valid = regex
validate_cmd = (parent.register(self.validate),'%P') # %P pass the new string to validate
super().__init__( parent, validate = 'key', validatecommand = validate_cmd)
# validate = 'key' runs the validation at each keystroke.
def validate(self, new_str):
if self.valid.match(new_str): return True
return False
def do_key(ev):
print(ev.widget, ev, entry.get())
root= tk.Tk()
root.title("Validation")
fram = tk.Frame(root)
fram.grid()
entry = ValidateEntry(fram, valid_chars)
entry.grid()
entry.bind('<Down>', do_key)
entry.bind('<Up>', do_key)
root.mainloop()
This may be overkill but should work across all the platforms.
The release why you are getting those unknown characters in an Entry widget is because for some reason char codes of "Up" (\uf700) and "Down" (\uf701) arrows prints  when run from homebrew python but not with anaconda python not sure why is that.
You can try and see yourself by running this code with either of them.
root = Tk()
E = Entry(root)
E.bind('<Key>', lambda e: print(e.char))
E.pack()
root.mainloop()
The solution I come up with is to overwrite the main <Key> bind of Entry widget to ignore "Up" and "Down" arrows.
import tkinter as tk
class Entry(tk.Entry):
def __init__(self, master=None, cnf={}, **kw):
super(Entry, self).__init__(master=master, cnf=cnf, **kw)
self.bind_class('Entry', '<Key>', self.add_char)
def add_char(self, evt):
if evt.char != '\uf701' and evt.char != '\uf700':
self.insert('insert', evt.char)
self.xview_moveto(1)
if __name__ == "__main__":
root = tk.Tk()
E = Entry(root)
E.pack()
root.mainloop()

Bug appears when writing file from tkinter module

import tkinter as tk
class Application(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
self.pack()
self.create_widgets()
def create_widgets(self):
self.hi_there = tk.Button(self)
self.hi_there["text"] = "Done!\n(click me)"
self.hi_there["command"] = self.say_hi
self.hi_there.pack(side="top")
self.entrythingy = tk.Entry()
self.entrythingy2 = tk.Entry()
self.entrythingy.pack()
self.entrythingy2.pack()
# here is the application variable
self.contents = tk.StringVar()
self.contents2 = tk.StringVar()
# set it to some value
self.contents.set("stdio")
self.contents2.set("script name")
# tell the entry widget to watch this variable
self.entrythingy["textvariable"] = self.contents
self.entrythingy2["textvariable"] = self.contents2
self.text = tk.Text()
self.text.pack()
# and here we get a callback when the user hits return.
# we will have the program print out the value of the
# application variable when the user hits return
self.entrythingy.bind('<Key-Return>',
self.print_contents)
self.quit = tk.Button(self, text="QUIT", fg="red",
command=root.destroy)
self.quit.pack(side="bottom")
def say_hi(self):
#print("hi there, everyone!")
self.fn = self.contents2.get()
self.body = self.text.get(1.0, tk.END).split('\n')
#print('Self.body:\n',self.body)
self.libs = self.contents.get().split(' ')
self.make_c()
def make_c(self):
lib_text = ''
for i in self.libs:
lib_text += "#include <lib.h>\n".replace('lib', i)
body_text = "int main() {\n\t"+"\n\t".join(self.body)+"return 0\n}"
print(lib_text+body_text)
with open(self.fn+'.c', 'w+') as f:
f.write(lib_text+body_text)
print('File written!')
from subprocess import call
call(['gcc',self.fn+'.c', '-o', self.fn])
def print_contents(self, event):
print("hi. contents of entry is now ---->",
self.contents.get())
#self.contents.set("")
#def
root = tk.Tk()
app = Application(master=root)
app.mainloop()
Those are the my code, which tries to make a c file and convert it. The problem is, when I convert it once, it is working fine, but when I change the content of the text box, the file doesn't change, and I don't understand why. I am sure that I put in the new file content, because it prints before it writes. Also, it appears that when I try to write files independent from tkinter, it works just the way I want it to.
I think there is some mechanism that I am not aware of in TK, or there is a bug. Please help me out, thanks.
I solved it. It doesn't compile again due to the error in it when I added return 0, without semicolon. So, when I click the executable file, it shows the old program. I added the semicolon, and now it is fine. Thx everyone!

How can I save output tho the same file that I have got the data from, in Python 3

I am trying to open a file, remove some characters (defined in dic) and then save it to the same the file.
I can print the output and it looks fine, but I cannot save it into the same file that the original text is being loaded from.
from tkinter import *
from tkinter.filedialog import askopenfilename
from tkinter.messagebox import showerror
import sys
import fileinput
dic = {'/':' ', '{3}':''};
def replace_all(text, dic):
for i, j in dic.items():
text = text.replace(i, j)
return text
class MyFrame(Frame):
def __init__(self):
Frame.__init__(self)
self.master.title("Example")
self.master.rowconfigure(5, weight=1)
self.master.columnconfigure(5, weight=1)
self.grid(sticky=W+E+N+S)
self.button = Button(self, text="Browse", command=self.load_file, width=10)
self.button.grid(row=1, column=0, sticky=W)
def load_file(self):
fname = askopenfilename(filetypes=(("Napisy", "*.txt"),
("All files", "*.*") ))
if fname:
try:
with open (fname, 'r+') as myfile: #here
data = myfile.read() #here
data2 = replace_all(data, dic) #here
print(data2) #here
data.write(data2) #and here should it happen
except:
showerror("Open Source File", "Failed to read file\n'%s'" % fname)
return
if __name__ == "__main__":
MyFrame().mainloop()
I have tried several commands but either I am receiving python errors or it is simply not working.
This is often implemented by writing to a temp file and then moving it to the original file's name.
Strings do not have a .write method. The following should work (I tried it): replace
data.write(data2) #and here should it happen
with
myfile.seek(0)
myfile.truncate()
myfile.write(data2)
The truncate() call is needed if data2 is shorter than data as otherwise, the tail end of data will be left in the file.

Resources