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.

  1. Create a gate keeper - This is an instance variable hash (event handler) that serves as a switch for group events.

  2. 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.

  3. 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.

  4. 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.