Fake Tk events

P

Phlip

Rubies:

I posted a fix for this a while ago, and then tk.rb upgraded.

The quest is to build a fake event and send it into a bound event handler.

This code creates a canvas, writes "hello world" on it, and fakes a
'Button-1' click on that text:

require 'tk'
top = TkRoot.new()
canvas = TkCanvas.new(top) {width(400);height(300) }
canvas.grid()
tx = TkcText.new(canvas, 100, 200) { text 'hello world' }

tx.bind('Button-1') { |e|
p e.x
p e.y
}

entry = tx.bindinfo('Button-1')[0][0]

entry.call( 21, "??", 1, 0, "??", true, 0, "i", 1, "NotifyNormal",
false, "PlaceOnTop", 0, 7595922, 0, 115, 200, "??", 269,
1, false, "??", 1, "0x0", "0x0", 4, nil, 269, 376 )

Tk.mainloop()

It faults, emitting this error message:

c:/ruby/lib/ruby/1.8/tk/event.rb:167:in `num_or_str': wrong argument type
Fixnum (expected String) (TypeError)
from c:/ruby/lib/ruby/1.8/tk/event.rb:167:in `call'
from c:/ruby/lib/ruby/1.8/tk/event.rb:167:in `scan_args'
from c:/ruby/lib/ruby/1.8/tk/event.rb:167:in `install_bind_for_event_class'
from c:/ruby/lib/ruby/1.8/tk/event.rb:163:in `call'
from c:/ruby/lib/ruby/1.8/tk.rb:1039:in `eval_cmd'
from c:/ruby/lib/ruby/1.8/tk.rb:1039:in `cb_eval'
from c:/ruby/lib/ruby/1.8/tk.rb:990:in `call'
from yo.rb:16

That error message does not describe which argument is amiss, or if there
are too many or too few. I have painstakingly matched the arguments to
KEY_TBL in event.rb.

How do I diagnose such error messages? And how do I fake events
 
M

Mike Hall

Phlip said:
And how do I fake events

Two Tcl-ish thoughts come to mind:
- some Tk widgets have an 'invoke' method (not text, I guess)
- use the 'event' command to create your own virtual events
that can trigger the binding

The Tcl/Tk docs or a good Tcl book should explain both.
 
P

Phlip

Mike said:
Two Tcl-ish thoughts come to mind:
- some Tk widgets have an 'invoke' method (not text, I guess)

Neither TkCanvas nor TkcText have them.
- use the 'event' command to create your own virtual events
that can trigger the binding

They only work if the window displays. I think.

If I relent and let the window display, a little bit, I get this:

require 'tk'
top = TkRoot.new()
canvas = TkCanvas.new(top) {width(400);height(300) }
canvas.grid()
tx = TkcText.new(canvas, 100, 200) { text 'hello world' }

tx.bind('Button-1') { |e|
puts 'event generated'
p e.x
p e.y
}

tx.after_idle {
puts 'generating event'
tx.event_generate('Button-1')
}
Tk.mainloop()

Its major problem is it does not work. The event_generate does not trigger
an event.

So, what's the most accurate way to trigger a bound callback?
 
H

Hidetoshi NAGAI

Hi,

From: "Phlip" <[email protected]>
Subject: Fake Tk events
Date: Sun, 15 Aug 2004 10:46:05 +0900
Message-ID: said:
tx.bind('Button-1') { |e|
p e.x
p e.y
}

entry = tx.bindinfo('Button-1')[0][0]

entry.call( 21, "??", 1, 0, "??", true, 0, "i", 1, "NotifyNormal",
false, "PlaceOnTop", 0, 7595922, 0, 115, 200, "??", 269,
1, false, "??", 1, "0x0", "0x0", 4, nil, 269, 376 )

Because Tcl/Tk treats all values as strings, Ruby/Tk wraps
a callback procedure in an argument converter.
The 'bindinfo' method returns the wrapping procedure.
So, each of arguments of the procedure must be a string.
TkComm.simplelist method may be useful when arguments
include null strings.

TkComm.simplelist('21 ?? 1 0 ?? true 0 i 1 NotifyNormal false PlaceOnTop 0 7595922 0 115 200 ?? 269 1 false ?? 1 0x0 0x0 4 {} 269 376')
#==> ["21", "??", "1", "0", "??", "true", "0", "i", "1", "NotifyNormal", "false", "PlaceOnTop", "0", "7595922", "0", "115", "200", "??", "269", "1", "false", "??", "1", "0x0", "0x0", "4", "", "269", "376"]

Of course, if you know the body of the callback procedure,
you can call the procedure with a created TkEvent::Event object.

cb_proc = proc{|e| p e.x; p e.y}
tx.bind('Button-1', cb_proc)
cb_proc.call(TkEvent::Event.new( 21, "??", 1, 0, "??", true, 0,
"i", 1, "NotifyNormal", false,
"PlaceOnTop", 0, 7595922, 0,
115, 200, "??", 269, 1, false,
"??", 1, "0x0", "0x0", 4, nil,
269, 376))

However, in this case, it seems that you need only 2 arguments, x and y.
It it is true, it is worthless to give the other arguments.

cb_proc = proc{|x, y| p x; p y}
tx.bind('Button-1', cb_proc, '%x %y')
entry = tx.bindinfo('Button-1')[0][0]
entry.call(*%w(115 200))

# In this case, Ruby 1.8.2 preview returns a 'nil' for
# tx.bindinfo('Button-1')[0][1].
# It is a bug. I'll fix it.

BTW, when callback arguments are given by '%' substitution,
you can give some extra arguments. For example,

cb_proc = proc{|x, y, sft, btn| p x; p y; p sft; p btn}
tx.bind('Button-1', cb_proc, '%x %y no-shift 1')
tx.bind('Button-2', cb_proc, '%x %y no-shift 2')
tx.bind('Shift-Button-1', cb_proc, '%x %y shift 1')
tx.bind('Shift-Button-2', cb_proc, '%x %y shift 2')
 
P

Phlip

Hidetoshi said:
cb_proc = proc{|x, y| p x; p y}
tx.bind('Button-1', cb_proc, '%x %y')
entry = tx.bindinfo('Button-1')[0][0]
entry.call(*%w(115 200))

# In this case, Ruby 1.8.2 preview returns a 'nil' for
# tx.bindinfo('Button-1')[0][1].
# It is a bug. I'll fix it.

Horalez! Viva la revolution, hombres!!! Muchisimas gracias!

BTW 3 Tk books at Fry's had NOTHING about event generation, send, etc.
 
H

Hidetoshi NAGAI

Hi,

From: "Phlip" <[email protected]>
Subject: Re: Fake Tk events
Date: Wed, 18 Aug 2004 15:40:56 +0900
Message-ID: said:
BTW 3 Tk books at Fry's had NOTHING about event generation, send, etc.

One of the examples of remote control with Tcl's send command
is 'ext/tk/lib/remote-tk.rb' (included in 1.8.2 preview).
The library can create a remote interpreter object to control
a Tk interpreter on the other process.
'ext/tk/sample/remote-ip_sample*.rb' are samples of the library.

About 'event generation', I wrote an example in my Ruby/Tk book
(Japanese). The following is the example (but a little changed).
It is also an example of 'Tk.callback_continue/callback_break',
'bindtag' and 'virtual event'.
--------------------------------------------------------
require 'tk'

root = TkRoot.new{ title('Hello Message') }

v = TkVariable.new('someone')
f1 = TkFrame.new.pack('side'=>'top', 'anchor'=>'w')

TkLabel.new(f1, 'text'=>'Input Your Name : ',
'font'=>'times').pack('side'=>'left')

e = TkEntry.new(f1, 'textvariable'=>v).pack('side'=>'left')
asc_only = TkBindTag.new
asc_only.bind('Key', proc{|a|
if a.kind_of?(String) &&
( a == ' ' || a[0] < 0x20 ||
(a[0] >= ?a && a[0] <= ?z) ||
(a[0] >= ?A && a[0] <= ?Z) )
Tk.callback_continue
end
Tk.callback_break
}, '%A')
e.bindtags(e.bindtags.unshift(asc_only))
ev_EXIT = TkVirtualEvent.new(['Control-x', 'Control-c'],
'Control-q', ['Escape', 'q'], 'Alt-q')

# generate Bitmap
exit_bmp = TkBitmapImage.new('data'=><<'END')
#define exit-door_width 35
#define exit-door_height 22
static unsigned char exit-door_bits[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xff, 0x01, 0x00, 0x00,
0x7c, 0x00, 0x01, 0x00, 0x00, 0x7c, 0x00, 0x01, 0x00, 0x00, 0x7c, 0x00,
0x01, 0x00, 0x00, 0x7c, 0x00, 0x01, 0x00, 0x08, 0x7c, 0x00, 0x01, 0x00,
0x18, 0x7c, 0x00, 0x01, 0x00, 0x38, 0x7c, 0x00, 0x01, 0xe9, 0x7f, 0x7c,
0x00, 0x01, 0xd2, 0xff, 0x7c, 0x03, 0x01, 0xa4, 0xff, 0x7d, 0x03, 0x01,
0xd2, 0xff, 0x7c, 0x00, 0x01, 0xe9, 0x7f, 0x7c, 0x00, 0x01, 0x00, 0x38,
0x7c, 0x00, 0x01, 0x00, 0x18, 0x7c, 0x00, 0x01, 0x00, 0x08, 0x7c, 0x00,
0x01, 0x00, 0x00, 0x7c, 0x00, 0x01, 0x00, 0x00, 0x7c, 0x00, 0x01, 0x00,
0x00, 0x7c, 0x00, 0x01, 0x00, 0x00, 0xfc, 0xff, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00};
END
TkButton.new(nil, 'image'=>exit_bmp, 'anchor'=>'e',
'command'=>proc{TkRoot.new.destroy}) {|b|
root.bind(ev_EXIT, proc{b.flash; b.invoke})

# create a label widget on a button widget
txt = TkLabel.new(b, 'text'=>'EXIT',
'padx'=>2).place('anchor'=>'w', 'x'=>2, 'rely'=>0.5)

# set button width to 'padding of place' + 'label width' + 'image width'
width (2 + TkWinfo.reqwidth(txt) + exit_bmp.width)

# transfer events on the label to the button
bindtags.each{|tags|
tags.bindinfo.each{|ev|
txt.bind(ev, proc{Tk.event_generate(b, ev)})
}
}

# control 'Enter' and 'Leave' event
# NOT send 'Enter' and 'Leave' on the label to the button
txt.bind_remove('Enter')
txt.bind_remove('Leave')
# change the labels background when 'Enter' or 'Leave' on the button
b.bind_append('Enter', proc{txt.background(b.activebackground)})
b.bind_append('Leave', proc{txt.background(b.background)})
}.pack('side'=>'bottom', 'anchor'=>'e')

font = TkFont.new(['courier', 16, ['italic']])
TkFrame.new{|f2|
TkLabel.new(f2, 'text'=>'Hello, ', 'font'=>font).pack('side'=>'left')
TkLabel.new(f2, 'text'=>v.value, 'font'=>font){|l|
e.bind('Return', proc{l.text v.value})
}.pack('side'=>'left')
TkLabel.new(f2, 'text'=>'!!', 'font'=>font).pack('side'=>'left')
}.pack('side'=>'bottom')

Tk.mainloop

print "Bye, #{v.value} ...\n"
 
P

Phlip

Hidetoshi said:
One of the examples of remote control with Tcl's send command
is 'ext/tk/lib/remote-tk.rb' (included in 1.8.2 preview).
The library can create a remote interpreter object to control
a Tk interpreter on the other process.
'ext/tk/sample/remote-ip_sample*.rb' are samples of the library.

Someone needs to say to me "send" sends commands over a network.

Nobody has said that yet. If they did, I would not try to use it.
About 'event generation', I wrote an example in my Ruby/Tk book
(Japanese). The following is the example (but a little changed).
It is also an example of 'Tk.callback_continue/callback_break',
'bindtag' and 'virtual event'.

I will try that next. But I'm stuck again, here:

require 'tk'
require 'test/unit'

class TestGrapher < Test::Unit::TestCase


def click(what, how = 'Button-1')
x1,y1,x2,y2 = what.bbox
cx = (x2 + x1) / 2
cy = (y2 - y1) / 2
Tk.update()
what.event_generate(how, :x => cx, :y => cy)
end

def test_simulateMouseClick()
@canvas = TkCanvas.new()
@canvas.configure('width', 600)
@canvas.configure('height', 600)
@canvas.grid()
TkcOval.new(@canvas, 5.0, 12.0, 59.0, 48.0, :fill=>'red')
oval = @canvas.find_all()[0]
assert_equal(oval.class.name(), 'TkcOval')
eventWasCalled = false

oval.bind('Button-1') { |event|
eventWasCalled = true
}

click(oval)
assert(eventWasCalled)

@canvas.destroy()
Tk.restart()
end

def test_selectNode()
@canvas = TkCanvas.new()
@canvas.configure('width', 600)
@canvas.configure('height', 600)
@canvas.grid()

TkcOval.new(@canvas, 5.0, 12.0, 59.0, 48.0, :fill=>'red')
oval = @canvas.find_all()[0]
assert_equal('TkcOval', oval.class.name())
weGotClicked = false

oval.bind('Button-1') { |event|
weGotClicked = true
}
click(oval)
assert(weGotClicked)
@canvas.destroy()
Tk.restart()

end

end

Those tests fail. They try to create a canvas, click on it, destroy it,
restart Tk, create a canvas, and click on it again. Whichever test case runs
last, based on its name, fails. The assert(weGotClicked) is false.

Is this because I call Tk.update() twice in a row?
 
H

Hidetoshi NAGAI

Hi,

From: "Phlip" <[email protected]>
Subject: Re: Fake Tk events
Date: Fri, 20 Aug 2004 01:27:10 +0900
Message-ID: said:
I will try that next. But I'm stuck again, here:

Hmmm...
Probably, to generate events on canvas items, the canvas requires
to know the mouse position by 'Enter' or 'Motion' event.
Please try to add one line to your script such as the following.
 
P

Phlip

Hidetoshi said:
Probably, to generate events on canvas items, the canvas requires
to know the mouse position by 'Enter' or 'Motion' event.

BTW I keep lecturing about clean code, and using Ruby driving TkCanvas as a
sterling example...

....and I just peeked inside tkCanvas.c, and it is a bucket of shit. One of
these functions is 1400 lines long! Compilers should just refuse to go over
99; they'd probably run faster on that internal optimization.
Please try to add one line to your script such as the following.

Noop. It no worky. Don't sweat this - nobody pushes libraries where I push
them. Thanks again for the help.

I'm going with this:

def click(what, how = 'Button-1')

cb_entry = what.bindinfo(how)[0][0]

cb_entry.call()

end


One deciding factor is the code may come to use x or y, and you showed how
to slip them in without risking friction at library upgrade time.
 
H

Hidetoshi NAGAI

Hi,

From: "Phlip" <[email protected]>
Subject: Re: Fake Tk events
Date: Sun, 22 Aug 2004 05:15:47 +0900
Message-ID: said:
Noop. It no worky. Don't sweat this - nobody pushes libraries where I push
them. Thanks again for the help.

Even if you change 'Motion' to 'Enter', do you get the same result?
How about adding what.event_generate('Enter', :x => cx, :y => cy)
before the 'Motion' line?

It seems that the problem exists on Tcl/Tk side (on the opration of
canvas widget), because wish has the same problem. The canvas widget
accepts all of the generated event ( You can test it by setting a
binding for the canvas. And it shows that the 'event_generate' works
properly.), but sometimes fails to call the binding of the canvas
item. I'm sorry but I couldn't find the conditions to succeed or not.
I'm sorry I cannot help you.
 
P

Phlip

Hidetoshi said:
Even if you change 'Motion' to 'Enter', do you get the same result?
How about adding what.event_generate('Enter', :x => cx, :y => cy)
before the 'Motion' line?

That worked! Did it work for you?
It seems that the problem exists on Tcl/Tk side (on the opration of
canvas widget), because wish has the same problem. The canvas widget
accepts all of the generated event ( You can test it by setting a
binding for the canvas. And it shows that the 'event_generate' works
properly.), but sometimes fails to call the binding of the canvas
item. I'm sorry but I couldn't find the conditions to succeed or not.
I'm sorry I cannot help you.

In this source, without the line what.event_generate('Enter', :x => cx, :y
=> cy), the second test to run (in alpha order) fails, despite it's exactly
the same as the first.

The sequence of events is:

- create a canvas
- populate
- update
- generate a click
- detect the click
- remove all items
- populate
- update
- generate a click
- detect the click <--- failure

The fix is to push an 'Enter' event into the event queue.

require 'tk'
require 'test/unit'

def doc(anObject)
puts(anObject.class.name)

itsMethods = anObject.public_methods() -
Object.new().public_methods()

puts(itsMethods.sort()) if !itsMethods.nil?
end

class TestGrapher < Test::Unit::TestCase


def click(what, how = 'Button-1')
x1,y1,x2,y2 = what.bbox
cx = (x2 + x1) / 2
cy = (y2 - y1) / 2

Tk.update()
what.event_generate('Enter', :x => cx, :y => cy)
what.event_generate(how, :x => cx, :y => cy)

end

def test_simulateMouseClick()
Tk.restart()
root = Tk.root()
@canvas = TkCanvas.new(root)

@canvas = TkCanvas.new()
@canvas.configure('width', 600)
@canvas.configure('height', 600)
@canvas.pack()
TkcOval.new(@canvas, 5.0, 12.0, 59.0, 48.0, :fill=>'red')
oval = @canvas.find_all()[0]
assert_equal(oval.class.name(), 'TkcOval')
eventWasCalled = false

oval.bind('Button-1') { |event|
eventWasCalled = true
}

click(oval)
assert(eventWasCalled)
@canvas.destroy()
root.withdraw()


end

def test_selectNode()

Tk.restart()
root = Tk.root()
@canvas = TkCanvas.new(root)
@canvas.configure('width', 600)
@canvas.configure('height', 600)
@canvas.pack()

TkcOval.new(@canvas, 5.0, 12.0, 59.0, 48.0, :fill=>'red')
oval = @canvas.find_all()[0]
assert_equal('TkcOval', oval.class.name())
weGotClicked = false

oval.bind('Button-1') { |event|
weGotClicked = true
}
click(oval)
assert(weGotClicked)
@canvas.destroy()
root.withdraw()

end

end
 
H

Hidetoshi NAGAI

Hi,

From: "Phlip" <[email protected]>
Subject: Re: Fake Tk events
Date: Mon, 23 Aug 2004 23:50:45 +0900
Message-ID: said:
That worked! Did it work for you?

Yes. So I recommended you to do that. :)
In this source, without the line what.event_generate('Enter', :x => cx, :y
=> cy), the second test to run (in alpha order) fails, despite it's exactly
the same as the first.

A canvas widget changes its own internal status when receives 'Enter' or
'Motion' event. When I checked on my environment, 'Enter' and 'Motion'
gave the same effect. But on your environment, needs 'Enter' event.
It is obscure whether 'Enter' event is enough on all environment.
But I think that it is probably enough.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,769
Messages
2,569,580
Members
45,054
Latest member
TrimKetoBoost

Latest Threads

Top