Tkinter ttk Treeview binding responds to past events!

2023-09-11 Thread John O'Hagan via Python-list
I was surprised that the code below prints 'called' three times.


from tkinter import *
from tkinter.ttk import *

root=Tk()

def callback(*e):
    print('called')

tree = Treeview(root)
tree.pack()

iid = tree.insert('', 0, text='test')

tree.selection_set(iid)
tree.selection_remove(iid)
tree.selection_set(iid)

tree.bind('<>', callback)

mainloop()

In other words, selection events that occurred _before_ the callback
function was bound to the Treeview selections are triggering the
function upon binding. AFAIK, no other tk widget/binding combination
behaves this way (although I haven't tried all of them).

This was a problem because I wanted to reset the contents of the
Treeview without triggering a relatively expensive bound function, but
found that temporarily unbinding didn't prevent the calls.

I've worked around this by using a regular button-click binding for
selection instead, but I'm curious if anyone can cast any light on
this.

Cheers

John
-- 
https://mail.python.org/mailman/listinfo/python-list


Re: Tkinter ttk Treeview binding responds to past events!

2023-09-12 Thread John O'Hagan via Python-list
On Mon, 2023-09-11 at 22:25 +0200, Mirko via Python-list wrote:
> Am 11.09.23 um 14:30 schrieb John O'Hagan via Python-list:
> > I was surprised that the code below prints 'called' three times.
> > 
> > 
> > from tkinter import *
> > from tkinter.ttk import *
> > 
> > root=Tk()
> > 
> > def callback(*e):
> >      print('called')
> > 
> > tree = Treeview(root)
> > tree.pack()
> > 
> > iid = tree.insert('', 0, text='test')
> > 
> > tree.selection_set(iid)
> > tree.selection_remove(iid)
> > tree.selection_set(iid)
> > 
> > tree.bind('<>', callback)
> > 
> > mainloop()
> > 
> > In other words, selection events that occurred _before_ the
> > callback
> > function was bound to the Treeview selections are triggering the
> > function upon binding. AFAIK, no other tk widget/binding
> > combination
> > behaves this way (although I haven't tried all of them).
> > 
> > This was a problem because I wanted to reset the contents of the
> > Treeview without triggering a relatively expensive bound function,
> > but
> > found that temporarily unbinding didn't prevent the calls.
> > 
> > I've worked around this by using a regular button-click binding for
> > selection instead, but I'm curious if anyone can cast any light on
> > this.
> > 
> > Cheers
> > 
> > John
> 
> 
> AFAIK (it's been quite some time, since I used Tk/Tkinter):
> 
> These selection events are not triggered upon binding, but after the 
> mainloop has startet. Tk's eventloop is queue-driven, so the 
> tree.selection_{set,remove}() calls just place the events on the 
> queue. After that, you setup a callback and when the mainloop 
> starts, it processes the events from the queue, executing the 
> registered callback.
> 
> I seem to remember, that I solved a similar issue by deferring the 
> callback installation using root.after().
> 
> 
> from tkinter import *
> from tkinter.ttk import *
> 
> root=Tk()
> 
> def callback(*e):
>  print('called')
> 
> tree = Treeview(root)
> tree.pack()
> 
> iid = tree.insert('', 0, text='test')
> 
> tree.selection_set(iid)
> tree.selection_remove(iid)
> tree.selection_set(iid)
> 
> root.after(100, lambda: tree.bind('<>', callback))
> 
> mainloop()
> 
> 
> 
> This does not print "called" at all after startup (but still selects 
> the entry), because the callback has not been installed when the 
> mainloop starts. But any subsequent interaction with the list 
> (clicking) will print it (since the callback is then setup).
> 
> HTH


Thanks for your reply. However, please see the example below, which is
more like my actual use-case. The selection events take place when a
button is pressed, after the mainloop has started but before the
binding. This also prints 'called' three times. 

from tkinter import *
from tkinter.ttk import *

class Test:

   def __init__(self):
  root=Tk()
  self.tree = Treeview(root)
  self.tree.pack()
  self.iid = self.tree.insert('', 0, text='test')
  Button(root, command=self.temp_unbind).pack()
  mainloop()

   def callback(self, *e):
  print('called')
   
   def temp_unbind(self):
  self.tree.unbind('<>')
  self.tree.selection_set(self.iid)
  self.tree.selection_remove(self.iid)
  self.tree.selection_set(self.iid)
  self.tree.bind('<>', self.callback)
  #self.tree.after(0, lambda: self.tree.bind('<>',
  self.callback))
  
c=Test()

It seems the events are still queued, and then processed by a later
bind?

However, your solution still works, i.e. replacing the bind call with
the commented line. This works even with a delay of 0, as suggested in
Rob Cliffe's reply. Does the call to after clear the event queue
somehow?

My issue is solved, but I'm still curious about what is happening here.

Regards

John

-- 
https://mail.python.org/mailman/listinfo/python-list


Re: Tkinter ttk Treeview binding responds to past events!

2023-09-12 Thread John O'Hagan via Python-list
On Tue, 2023-09-12 at 20:51 +0200, Mirko via Python-list wrote:
> Am 12.09.23 um 07:43 schrieb John O'Hagan via Python-list:
> 
> > My issue is solved, but I'm still curious about what is happening
> > here.
> 
> MRAB already said it: When you enter the callback function, Tk's 
> mainloop waits for it to return. So what's happening is:
> 
> 1. Tk's mainloop pauses
> 2. temp_unbind() is called
> 3. TreeviewSelect is unbound
> 4. events are queued
> 5. TreeviewSelect is bound again
> 6. temp_unbind() returns
> 7. Tk's mainloop continues with the state:
>  - TreeviewSelect is bound
>  - events are queued
> 
> [. . .]

Thanks (also to others who have explained), now I get it!


> FWIW, here's a version without after(), solving this purely on the 
> python side, not by temporarily unbinding the event, but by 
> selectively doing nothing in the callback function.
> 
> from tkinter import *
> from tkinter.ttk import *
> 
> class Test:
>  def __init__(self):
>  self.inhibit = False
>  root=Tk()
>  self.tree = Treeview(root)
>  self.tree.pack()
>  self.iid = self.tree.insert('', 0, text='test')
>  Button(root, command=self.temp_inhibit).pack()
>  mainloop()
> 
>  def callback(self, *e):
>  if not self.inhibit:
>  print('called')
> 
>  def temp_inhibit(self):
>  self.inhibit = True
>  self.tree.selection_set(self.iid)
>  self.tree.selection_remove(self.iid)
>  self.tree.selection_set(self.iid)
>  self.inhibit = False
>  self.callback()
> 
> c=Test()
> 


I like this solution better - it's much more obvious to me what it's
doing.

Regards

John


-- 
https://mail.python.org/mailman/listinfo/python-list


Re: Tkinter ttk Treeview binding responds to past events!

2023-09-12 Thread John O'Hagan via Python-list
On Wed, 2023-09-13 at 01:33 +0100, MRAB via Python-list wrote:
> On 2023-09-13 00:40, John O'Hagan via Python-list wrote:
> > On Tue, 2023-09-12 at 20:51 +0200, Mirko via Python-list wrote:
> > > Am 12.09.23 um 07:43 schrieb John O'Hagan via Python-list:
> > > 

[...]


> > 
> > 
> > > FWIW, here's a version without after(), solving this purely on
> > > the 
> > > python side, not by temporarily unbinding the event, but by 
> > > selectively doing nothing in the callback function.
> > > 
> > > from tkinter import *
> > > from tkinter.ttk import *
> > > 
> > > class Test:
> > >  def __init__(self):
> > >  self.inhibit = False
> > >  root=Tk()
> > >  self.tree = Treeview(root)
> > >  self.tree.pack()
> > >  self.iid = self.tree.insert('', 0, text='test')
> > >  Button(root, command=self.temp_inhibit).pack()
> > >  mainloop()
> > > 
> > >  def callback(self, *e):
> > >  if not self.inhibit:
> > >  print('called')
> > > 
> > >  def temp_inhibit(self):
> > >  self.inhibit = True
> > >  self.tree.selection_set(self.iid)
> > >  self.tree.selection_remove(self.iid)
> > >  self.tree.selection_set(self.iid)
> > >  self.inhibit = False
> > >  self.callback()
> > > 
> > > c=Test()
> > > 
> > 
> > 
> > I like this solution better - it's much more obvious to me what
> > it's
> > doing.
> > 
> That code is not binding at all, it's just calling 'temp_inhibit'
> when 
> the button is clicked.
> 
> You can remove all uses of self.inhibit and rename 'temp_inhibit' to 
> something more meaningful, like 'delete_item'.

You're right of course, not as obvious as I thought!

-- 
https://mail.python.org/mailman/listinfo/python-list


SOLVED: Tkinter button-motion event behaves differently under Windows and Linux

2024-11-11 Thread John O'Hagan via Python-list
I'm posting this in case anyone else encounters the same problem, and
to ask for suggestions, if any, about a better way to do it.

I'm implementing a method for dragging embedded widgets on a Text
widget. When the left mouse button is held down over an embedded widget
and the mouse is dragged across other widgets embedded in the same
parent, the original widget moves to the new positions until the button
is released.

Here's some minimal code:

---

from tkinter import *
text = Text(Tk())
text.pack()

def drag(e):
print(e.widget)
target = e.widget.winfo_containing(e.x_root, e.y_root)
if target and target not in (text, e.widget):
if text.compare(target, '>', e.widget):
target = f'{target} + 1 char'
text.window_create(target, window=e.widget)

for n in ('1', '2', '3'):
l=Label(text, text=n, width=10)
#l.bind('', lambda e:e.widget.grab_set())
l.bind('', drag)
#l.bind('', lambda e:e.widget.grab_release())
text.window_create(END, window=l)

mainloop()

---

This works as intended for me on Windows (11). The print statement
always shows the name of the first selected widget while the button is
held down, regardless of where the mouse is dragged to.

But on Linux (Debian testing with Gnome), for me the above code only
moves the widget to the first new position it is dragged to, any
further dragging is ineffective, and the print statement shows the
names of the subsequently-traversed widgets.

There is a further issue in the real code (not shown here) also on
Linux only, where if the cursor traverses any other widgets bound to
'', they are also triggered, which is not intended.

Just a guess, but it seems that on Linux, the focus switches to
whatever widget is under the cursor even during dragging, so any
bindings on the originally-clicked widget are no longer triggered,
whereas Windows maintains focus on the originally-clicked widget during
dragging until the button is released.

If that's the case (and I never thought I'd say this), I think Windows
is right! But for all I know it might be the window manager or
something else.

I eventually figured out that the commented lines calling grab_set and
grab_release solved the issue for me on Linux.

I haven't found this documented anywhere and I'm interested to know if
anyone can cast any light on it.

Thanks 


-- 
https://mail.python.org/mailman/listinfo/python-list