Event Streams
This a very arcane and specialized capabilty. Basically you can get low level events like click or key press and chose to send the event to normal Shoes GUI handlers. For example you could disable all button clicks in another window
Shoes as we know it is a little graphical toolkit but combined with some wits can seamlessly manage hundreds of elements and events.
Abstract definition
Without further ado here are 2 snippets with the minimum code you need to create simple event management.
Approach 1
Shoes.app do
stack do
image "#{DIR}/static/shoes-icon.png"
click do
if @clickable
para "hey"
end
end
end
button "enable" do
@clickable = true
end
button "disable" do
@clickable = false
end
start do
@clickable = true
end
end
Approach 2
Shoes.app do
@b = proc do
para "hey"
end
@s = stack do
image "#{DIR}/static/shoes-icon.png"
click &@b
end
button "enable" do
@s.click &@b
end
button "disable" do
@s.click do; end
end
end
Practical example
Define the task
Create an application with numerous items that trigger events. Events interaction can be disabled on demand for one or many groups of objects.
Begin with something small
There are a bunch of pictures in a slot that the user wants to trigger click event drawing a text that specifies the group the pictures are in and its order in the group.
Shoes.app do
@box = edit_box "", left: 50, top: 400, width: 200, height: 50
@groups = stack left: 0, top: 0, width: 100, height: 300 do
background green
image "#{DIR}/static/shoes-icon.png", width: 70 do
click do
@box.text = "Green group, picture 1"
end
end
image "#{DIR}/static/shoes-icon-blue.png", width: 70 do
click do
@box.text = "Green group, picture 2"
end
end
image "#{DIR}/static/shoes-icon-federales.png", width: 70 do
click do
@box.text = "Green group, picture 3"
end
end
image "#{DIR}/static/shoes-icon-red.png", width: 70 do
click do
@box.text = "Green group, picture 4"
end
end
end
end
Try the app. Notice how clicking on each picture change the text in the edit box.
The code is a bit repetitive isn't it? Use loop.
Shoes.app do
@box = edit_box "", left: 50, top: 400, width: 200, height: 50
@group = stack left: 0, top: 0, width: 100, height: 300 do
background green
image "#{DIR}/static/shoes-icon.png", width: 70
image "#{DIR}/static/shoes-icon-blue.png", width: 70
image "#{DIR}/static/shoes-icon-federales.png", width: 70
image "#{DIR}/static/shoes-icon-red.png", width: 70
end
@group.contents[1..-1].each_with_index do |c, i|
c.click do
@box.text = "Green group, picture #{i+1}"
end
end
end
Same simple example, just increase the scale
Same result, easy to read code.
Make a second group of similar elements this time with yellow background.
Shoes.app do
@groups = []
@box = edit_box "", left: 50, top: 400, width: 200, height: 50
@groups << (stack left: 0, top: 0, width: 100, height: 300 do
background green
image "#{DIR}/static/shoes-icon.png", width: 70
image "#{DIR}/static/shoes-icon-blue.png", width: 70
image "#{DIR}/static/shoes-icon-federales.png", width: 70
image "#{DIR}/static/shoes-icon-red.png", width: 70
end)
@groups << (stack left: 270, top: 0, width: 100, height: 300 do
background yellow
image "#{DIR}/static/shoes-icon.png", width: 70
image "#{DIR}/static/shoes-icon-blue.png", width: 70
image "#{DIR}/static/shoes-icon-federales.png", width: 70
image "#{DIR}/static/shoes-icon-red.png", width: 70
end)
@groups[0].contents[1..-1].each_with_index do |c, i|
c.click do
@box.text = "Group Green, picture #{i+1}"
end
end
@groups[1].contents[1..-1].each_with_index do |c, i|
c.click do
@box.text = " group Yellow, picture #{i+1}"
end
end
end
It becomes a bit repetitive again. Move the group creating process into a method and loop it. Use a container (@groups) to save the newly created groups. Use similar technique to call them back and set click event.
Now we can create as much groups as we want.
Shoes.app do
@groups = []
@box = edit_box "", left: 50, top: 400, width: 200, height: 50
def set_group colour, i
group = stack left:0 + 120*i, top: 0, width: 100, height: 300 do
background colour
image "#{DIR}/static/shoes-icon.png", width: 70
image "#{DIR}/static/shoes-icon-blue.png", width: 70
image "#{DIR}/static/shoes-icon-federales.png", width: 70
image "#{DIR}/static/shoes-icon-red.png", width: 70
end
return group
end
colours = [green, yellow, red, orange ]
colour_names = ["green", "yellow", "red", "orange" ]
colours.each_with_index do |clr, i|
@groups << (set_group clr, i)
end
@groups.each_with_index do |g, i|
g.contents[1..-1].each_with_index do |c, ii|
c.click do
@box.text = "Group #{colour_names[i]}, picture #{ii+1}"
end
end
end
end
Where the magic lies - The hash gateway
When certain circumstances yellow and red groups elements events should be disabled. For this particular example the circumstances will be a button click.
-
Create a gate keeper - This is an instance variable hash (event handler) that serves as a switch for group events.
-
Setting gate keeper keys and values - For the purpose of this event colours will be used as a group names. Hash will consist of group names for keys which will be equal to true (events enabled) and false ( events disabled ). Set values to true for all hash elements.
-
Add a gatekeeper condition when event is triggered - Next in the click event we add a condition that says only if particular hash value is set to true trigger the event.
-
Set the open/close gate switch - Create a button that, when activated, sets Yellow and Red groups hash values to False.
Open the app and test click events on all groups.
Use the button and test again - red and yellow pictures will not trigger events.
Shoes.app do
@groups = []
@box = edit_box "", left: 150, top: 400, width: 200, height: 50
def set_group colour, i
group = stack left:0 + 120*i, top: 0, width: 100, height: 300 do
background colour
image "#{DIR}/static/shoes-icon.png", width: 70
image "#{DIR}/static/shoes-icon-blue.png", width: 70
image "#{DIR}/static/shoes-icon-federales.png", width: 70
image "#{DIR}/static/shoes-icon-red.png", width: 70
end
return group
end
colours = [green, yellow, red, orange ]
colour_names = ["green", "yellow", "red", "orange" ]
@event_handler = { "green" => true,
"yellow" => true,
"red" => true,
"orange" => true }
colours.each_with_index do |clr, i|
@groups << (set_group clr, i)
end
@groups.each_with_index do |g, i|
g.contents[1..-1].each_with_index do |c, ii|
c.click do
if @event_handler[colour_names[i]] == true then
@box.text = "Group #{colour_names[i]}, picture #{ii+1}"
end
end
end
end
button "Disable Red and Yellow groups events", left: 80, top: 350 do
@event_handler["red"] = false
@event_handler["yellow"] = false
end
end
=============================================================================== Credits: @Backorder, @dredknight
Event Streams
Think of events as a stream of user clicks, mouse moves, keypress and such.Shoes just passes them on to your click block or the keypress block, if you have them. Dont't have those 'event handlers' defined in your script? You won't be notified. But, those events are seen by Shoes and it knows your script did not ask for click events (for example) to be sent to it. How does Shoes know that? You did't set a click handler (or keypress handler).
So, you have a stream of events (clicks, mouse moves, keypress). If your script asks to be notified of those events then your click or keypress block is called, buttons get pressed, characters enter edit_lines and you have Shoes working normally.
What if you wanted to spy on all events, or prevent or modify events before the normal processing. Why would you want to do that? Perhaps you want to create a GUI builder app - you don't all clicks going to the being-built layout. Or you could write a program to capture all the events as you use a Shoes app and then later play them back - for testing or presentations?
Let me be very clear: Only Shoes Experts will survive poking inside the event stream!
Shoes 3.3.5 introduced the ShoesEvent class and the 'event' block
Shoes.app do
button "one" do
puts "button one"
end
click do |button,x,y,mods|
puts "click at #{x},#{y} for button #{button} with modifiers #{mods}"
end
# Get's gnarly here
event do |evt|
evt.accept = true
end
end
That gnarly event block will be called before the button block gets run and before that click block is called. As the writer of the event block, you control whether to pass the event on (accept it) for normal processing or don't pass it on (evt.accept = false).
If you don't have an event block everything behaves as it always did. Once you add the event block you will play in a different sandbox and some of the rules are strict - you will always do an evt.accept = t/f Failure to do so will just confuse you. I use evt
as the block arg so it doesn't confuse anybody reading this. event
is the Shoes level method. evt
is the block argument and is an object of Class ShoesEvent.
If you have that event block in your script, Shoes will start creating ShoeEvents objects for it to deal with and you have to deal with them all - accept = t/f. You might imagine thats there's a lot of object creation and garbage collect is going to run sooner and more often. Correct. This is not something for the normal script writer to use.
Setting an event block
You can set an event
block into a different window. It's not intuitive but you can do it and you should consider using it in your design.
ShoesEvent class
It's a C struct with Ruby getters and setters methods for each of the fields in the struct. You probably won't be creating these object in your code. Shoes does it and gives them to your event block
accept
You've already seen that there is the accept
field and I've warned you to alway set it before leaving the event block. Yes, the event is created with a default of true, pass it it on, but you should be explicit.
type
This where the fun begins. This is a Ruby symbol, not a string. Although it prints like it was a string, it's not a string. Not all possible events are currently passed to an event block. Tests/events/event6.rb is an example
Shoes.app do
event do |evt|
# do not trigger new events here unless you can handle them recursively
# which is harder than you think.
case evt.type
when :click
$stderr.puts "click handler called: #{evt.type} #{evt.button}, #{evt.x} #{evt.y} #{evt.modifiers}"
evt.accept = true
when :keypress
$stderr.puts "keypress: #{evt.key}"
evt.accept = true
when :keydown
$stderr.puts "keydown for #{evt.key}"
evt.accept = $ck.checked?
when :keyup
$stderr.puts "keyup for #{evt.key}"
evt.accept = $ck.checked?
when :motion
evt.accept = false
when :release
evt.accept = false
when :wheel
$stderr.puts "wheel handler called: #{evt.type} #{evt.button}, #{evt.x} #{evt.y} #{evt.modifiers}"
evt.accept = true
else
puts "Other: #{evt.type.inspect}"
evt.accept = true
end
end
stack do
para "Key Tests"
flow do
$ck = check checked: true; para "Enable up/down"
end
@eb = edit_box width: 500, height: 350
end
keypress do |key|
@eb.append "press: #{key}\n"
end
keyup do |key|
@eb.append "up: #{key}\n"
end
keydown do |key|
@eb.append "down: #{key}\n"
end
wheel do |d, x, y, mods|
@eb.append "wheel dir: #{d} at #{x},#{y}, with #{mods}\n"
end
end
Other events maybe included over time but the above sample shows that not a fields are valid for all events. They won't crash anything but they aren't useful. Keypress for example does report x,y and pressing on a button won't tell you which button was pressed. Slippery rules, all different.
Native Button events are not :clicks, they are :btn_activate when sent to the event block/handler.
object Read/Only
This gets set for native widgets like a button, checkbox or radio. x and y are valid as is height and width. but keep reading. You will have to keep track of the widget in the window, Shoes just notifies you that the use clicked this one. For non native widgets like Image, Svg, and Plot your event block can get modifiers like 'shift' and 'control'
Object is also valid if you click in a textblock (para, title, inscription...) and you can use the x,y,w,h. Remember that checkboxes are usually two things in a flow slot, the box and the text are two different Shoes objects.
x Read/Only
Most mouse related events like click have a valid x. This is the x of the widget in window co-ordinate. Not relative to the slot. For native widgets this will be left most position of the widget and not where the mouse was within the button. For non native widgets (Image, Svg, Plot) it might be the real mouse location.
y Read/Only
Most mouse related events like click have a valid y. This is the y of the widget in window co-ordinate. Not relative to the slot. For native widgets this will be left most position of the widget and not where the mouse was within the button. For non native widgets (Image, Svg, Plot) it might be the real mouse location.
width Read/Only
Clicks on a Widget (native or non-native) will report a valid width for the widget.
height Read/Only
Clicks on a Widget (native or non-native) will report a valid height for the widget.
button Read/Only
The button that was clicked. Highly platform and hardware dependent. Don't make assumptions about any number > 3. In wheel created events the button is actually the direction of the scroll. 1 for scrolling up and 0 for scrolling down. This may be changeable in the OS and Theme so you should discount it's usefulness.
key
This is only valid for keyup, keydown, and keypress events and they don't report the same. Press ^R and key down and key up will provide 'r' and keypress will provide 'control_r'. I'm certain that depression is ahead for anyone trying to 'front run' that mess in an event block. Use Keypress if you have to
key =
Currently you can change the key before the event is passed to normal Shoes handling. Likely to be not allowed in the future.
modifiers Read/Only
Many Mouse based events handlers like click, release, motion and wheel handlers have a new modifiers argument available in Shoes 3.3.5 (native widgets don't). So, the event block and the ShoesEvent object passed to it needs to provide them. There is only 3 possible: 'control
, shift
and control_shift
, 4 states if you count nil.
Check the source
Tests/events/*.rb is the real source of what event handlers can do. There is capture/replay that works with samples/simple/chipmunk.rb as a proof of concept for GUI testing There is an undocumented and unfinished app.replay_event hash
method.