Skip to content

mechcoding/fxruby-enhancement

 
 

Repository files navigation

FXRuby Enhancement Table of Contents

fxruby-enhancement

Introduction

The fxruby library is an excellent wrapper for the FOX Toolkit. However, it reflects the C++-ness of FOX, rather than being more Ruby-like. As such, creating composed objects with it tends to be rather cumbersome, given its C++ roots. For every new component you create with fxruby, you are handed back a reference to that object, which you’ll need to store somewhere. And then all the subsequent child objects will need to be passed pointers to the parent objects.

So, if you need to redo a layout, it becomes a messy exercise.

fxruby-enhancement makes this a snap to do. You simply declare your GUI arrangement in a nested fashion. fxruby-enhancement will take care of passing parents to the nested children, and other issues as well. You can now focus on creating a good GUI layout that you can change on the fly without much fuss and bother.

fxruby-enhancement is basically a DSL of sorts, and every effort has been taken to make it intuitive to use. Once you get the hang of it, you should be able to look at the FXRuby API documentation and infer the DSL construct for fxruby-enhancement. Please also see the many examples.

Installation

To install the gem from commandline:

gem install fxruby-enhancement

In your Gemfile:

gem "fxruby-enhancement", "~> 0"

fxruby-enhacement depends on fxruby version 1.6, and will automatically include it. However fxruby has a c-extension that must compile properly on your system. Normally, this is not a concern, but it is something to be aware of.

Documentation

In General

fxruby-enhancement (which we will refer to as “Enhancement” from time to time) makes use of the singleton pattern in Ruby. There is basically no need to declare subclases off of most FXRuby classes. This is a very C++ish way, and the way the C++ Fox Toolkit works. It will make most hard-core Rubyists gnash their teeth.

Here, we do away with all of it. Also, the SEL_x variables – which maps to the C++ #defines of the same – is replaced with method declarations of the nature of sel_x – just the lowercase version of the same. For instance, in the straight fxruby, you would have to do something like:

@canvas.connect(SEL_PAINT) { |sender, sel, evt|
  FXDCWindow.new(sender, evt) { |dc|
    dc.drawImage(@backBuffer, 0, 0)
  }

but with Enhancement, you do it thusly:

instance { |c|
  c.sel_paint { |sender, sel, event|
    FXDCWindow.new(sender, event) { |dc|
      dc.drawImage(ref(:back_buffer), 0, 0)
    }
  }
}

And here we illustrate something else, the instance declaration. Why do we do it this way? Because Enhancement is multi-phase. First, we declare the GUI layout with Enhancement. At this time, none of the underlying FXRuby objects exist yet, but need to be referenced anyway. So we defer that part where references need to be resolved to the instance claus, which, as you can well imagine, means the FXRuby object instances have been instantiated.

You will also note the use of the ref clause, as in:

dc.drawImage(ref(:back_buffer), 0, 0)

When the :back_buffer object was declared, it was done thusly:

fx_image(:back_buffer) { opts IMAGE_KEEP }

So when the actual FXImage object is instantiated, it is associated to the :back_buffer tag, which then is found by ref() and can be used anywhere in the instantiation phase.

Execution Phases of fxruby-enhancement

This represents the work flow, in the order stated:

StateDescription
DeclarativeThe basic GUI layout is declared by the DSL, but it is not instantiated yet.
FXRuby instantiationAll the basic underlying FXRuby object are instantiatied, but the foundational FOX Toolkit Objects are not instantiated yet.
FOX Toolkit instantiationThe FOX Toolkit C++-level objects are now alive and kicking.

Declarative

This phase, under the proverbial hood, ceates the component objects, which are just place-holders for the underlying FXRuby objects.

When the FXRuby object is created, it is assigned to its place holder component object, and can be references as comp.inst. In most cases, you will almost never need to touch the component objects directly.

FXRuby Instantiation

During the FXRuby instantiantion stage, all of the FXRuby objects are instantiated and stored in their respective component objects. If they are tagged, the instantiated object may be referenced with ref(), and the component object itself may be referenced via refc(). There is almost never a case where you would need to go after the component object directly.

FOX Toolkit instantiation

All of the FOX Toolkit C++ objects, resources, etc. that correspond to the FXRuby objects are now set up, and activated. With the all-important “show PLACEMENT_SCREEN” command, the FOX GUI should now be visible.

Events from other Threads

In handling interfacing to databases, AMQPs like RabbitMQ, network connections, or just about anything else that might otherwise slow down the GUI (Fox) thread and make it non-responsive, there needs to be a clean way to get data into and out of the GUI thread.

Fox provides some mechanisms specifically for sockets or system-level IO, but these are too specific, and would require some awkard workarounds to make them work in the general context.

And so we provide a means to accomplish that in a clean – to you, anyway – manner. We make use of queue_ding queues for passing messages into and out of the FXRuby (and therefore FXRuby Enhancement) space. This will allow you to keep the GUI thread responsive and also to maintain a seperation of concerns.

The Queue_Ding Queues

Queue Ding is an enhancement for doing queing across threads in Ruby, and we offer it here to allow external events to be funneled into and out of the Fox GUI thread. Usage is easy and straightforard. When removing entries from Queue Ding using #next, the queue will block until the next entry arrives. Since Queue Ding is really derived from ::Array, you may also do thing like #empty? to check to see if entries are availabe to avoid blocking.

Enhancement.ingress

To get messages objects into fxruby_enhacement, simply #push or #<< it into the queue as shown:

Enhancement.ingress << [:some_tag, some_payload]

In the DSL, you must set up a handler for the ingress,

ingress_handler :status do |tag, payload|
  puts "received #{tag} => #{payload}"
end

And so your handler will most likely act as a dispatcher for the payloads received. For example:

ingress_handler :log_info, :log_error do |tag, logline|
  puts "received #{tag} => #{payload}"
  case tag
  when :log_info
    ref(:logging_info).appendItem logline
  when :log_error
    ref(:logging_error).appendItem logline
  end
end

Note that this ingress handler is responding to two tags. You can have as many tags as you like for your ingress handler, and as many ingress handlers as you like.

Currently, all the tags should be unique. Later we may support having multiple blocks associated with the same tag. Please feel free to generate an issue if you want this!!!

Enhancement.egress

Wnen your Fox application needs to send a message to other listening threads, You simply push your payload onto the egress queue thusly:

Enhancement.egress << [:button_clicked, "I was clicked!"]

and your Ruby thread external to Fox would simply do:

...
message = Enhancement.egress.next
...

where you’ll block pending the arrival of the next message. If you do not wish to block, you may do:

...
unless Enhancement.egress.empty?
  message = Enhancement.egress.next 
else
  # some action to take
end
...

API & DSL

ref(), refc() and tagging your objects

In an effort to eliminate the fuss and bother with scoping issues and object reference, ref(:some_tag) will retrive the FXRuby instance object so tagged with :some_tag.

You may have anonymous, i.e., untagged objects, and those will not be findable by ref(). It is not necessary to tag all objects, either.

refc() is similar to ref(), except it retrives the underlying component object insted. Indeed, the following are equivalent operations:

ref(:some_tag)
refc(:some_tag).inst

There may be some edge cases where you might want to reference the underlying component ahead of FXRuby object instaniation, but in the vast majority of the cases, it should be unnecessary. My goal is to enable to do what you need, not to restrict you. You may need it for debugging, etc.

Underlying, the component object is really a subclass of OpenScript. While you may like to stuff some additional data there, this is frowned upon because it might conflict with Enhancement. If you have a need for this, please do a issue in GitHub.

fox_component and fox_instance

fox_component and fox_instance are roughly the equivalent of refc() and ref(), respecively. The difference mainly being that fox_component does no sanity checking, and is therefore slightly faster.

At some point, they may be merged, but for now don’t count on it.

To initialize and run your app, you customairly do the following:

fox_component :app do |app|
  app.launch
end

Which presumes your fx_app declaration was tagged with :app as follows:

fx_app :app do
  app_name "Your Amazingly Cool Application"
  vendor_name "YouDaMan"
  ...
end

This is the only time you will reference the component object directly for the obvious reason that you must start from someonere.

fx_app

To begin the declaration of your app, you must do the following somewhere:

fx_app :app do
  app_name "The Forbin Project"
  vendor_name "Colossus"
  ...
end

Typeically you’d do this inside of a module, but you could do it also in a class body. Please see the examples.

instance

Inside of your component declaration, you will undoubtly want to specify what you want to do once the FXRuby object is actually instantiated. This is what the instance clause will allow you to do. Your code block there will be passed a reference to the FXRuby object, allowing you to set up connections, change the component state, etc.

There are some added benefits as well. When making a connection, with the normal FXRuby, you would do something like this:

...
aButton.connect(SEL_COMMAND)  { |sender, selector, data|
  ... code to handle this event ...
}

But with Enhancement, you would be able to do it thusly:

fx_button(:my_button) {
  ... configs for this FXButton object ...
  instance { |button|
    button.sel_command { |sender, selector, data|
      ... code to handle this event ...
    }
  }
}

which will make it feel more Ruby-like and less C++-like.

ingress_handler

ingress_handler will allow you to set up the handler for messages coming in from an external source to FXRuby thread, such as RabbitMQ, network connections, databases, or anything else. It allows you to do clean multhreaded Ruby without the normal worries of semaphores and synchronization and the like – it is all handled for you “magically” behind the scenes!

You may have as many ingress_handlers specified as you like, as each one needs to have a tag, and the tags are used to dispatch the messages.

Here is an example taken from RubyNEAT Panel:

ingress_handler :status do |type, status|
  suc, st = status.response
 
  wlist = ref :ov_conn_neaters_widget_list
  wlist.clearItems
  st[:neaters].each { |neater| wlist.appendItem neater }

  nlist = ref :ov_conn_neurons_list
  nlist.clearItems
  st[:neurons].each { |neuron| nlist.appendItem neuron}
end

Here you can see that a status message has been dispatched to this ingress_handler, and that the message contains a list of ‘neaters’ and ‘neurons’ that are being sent to the wlist and nlist list (:ov_conn_neaters_widget_list and :ov_conn:_neurons_list), respecively.

You may declare your ingress_handler anywhere in your code and have the expected happen.

igress_handler may also be specified with more than one tag, for instance:

ingress_handler :warn, :info, :error do |type, log|
  case type
  when :warn
    ...
  when :info
    ...
  when :error
    ...
  else
    raise "Unknown log type"
  end
end

The same block is assigned to all the given tags of :warn, :info, and :error.

deferred_setup

Mapping between fx_* declarations and the FX* FXRuby objects

binding.fx

This is a way to split up your layouts into different .fx “modules”, purely for organizational reasons. For example,

binding.fx "overview"

will load the overview.fx portion of the GUI, which happens to be a tab contents in the tab book, which in our case looks like:

# Overview Tab

fx_tab_item { text "&Overview" }
fx_horizontal_frame (:overview_info) {
  opts STD_FRAME|LAYOUT_FILL_Y
  
  fx_group_box (:ov_connections_group) {
    text "Connections"
    opts STD_GROUPBOX|LAYOUT_FILL_Y
    
    fx_vertical_frame {
      opts LAYOUT_FILL_Y|LAYOUT_FILL_X #|PACK_UNIFORM_HEIGHT
 
      fx_group_box (:ov_conn_rabbitmq) {
...

Examples

Because this is a spinoff project of the ongoing RubyNEAT effort, there is a splendid RubyNEAT Panel example, that is still in the works. However, you are free to look at the code that is there to get good ideas.

https://github.com/flajann2/rubyneat-panel/tree/master/lib/rubyneat-panel

Class-based Enhancement (this is currently not supported!!!):

class Main < FXMainWindow
  compose :my_window do
    title "RubyNEAT Panel"
    show PLACEMENT_SCREEN
    width 700
    height 400
    fx_tab_book :my_book do |tab_book_ob|
      x 0
      y 0
      width 500
      height 100
      pad_bottom 10
      fx_text :my_text1, :my_window { |text_ob|
        width 200
        height 100
        text_ob.target my_window: :on_click
      }
      fx_text :my_text2, :my_window { |text_ob|
        width 200
        height 100
        text_ob { |t| puts "called after object initialization" }
      }
    end
  end

  def on_click
    ...
  end
end    

Class-free Enhancement (strongly recommended):

mw = fx_main_window :my_window do 
    title "RubyNEAT Panel"
    width 700
    height 400
    opts DECOR_ALL
    x 10
    y 10
    instance { show PLACEMENT_SCREEN }
    fx_tab_book :my_book do |tab_book_ob|
      x 0
      y 0
      width 500
      height 100
      pad_bottom 10
      fx_text :my_text1, :my_window { |text_ob|
        width 200
        height 100
        instance my_window: :on_click
      }
      fx_text :my_text2, :my_window { 
        width 200
        height 100
        instance { |t| puts "called after object initialization" }
      }
    end
  end

  def mw.on_click
    ...
  end
end    

Hello World example (full) the Enhancement Way

#!/usr/bin/env ruby
require 'fxruby-enhancement'

include Fox
include Fox::Enhancement::Mapper

fx_app :app do
  app_name "Hello"
  vendor_name "Example"

  fx_main_window(:main) {
    title "Hello"
    opts DECOR_ALL

    fx_button {
      text "&Hello, World"
      selector FXApp::ID_QUIT
      
      instance { |b|
        b.target = ref(:app)
      }
    }

    instance { |w|
      w.show PLACEMENT_SCREEN
    }
  }
end

# alias for fox_component is fxc
fox_component :app do |app|
  app.launch
end

Hello World the old fxruby way:

#!/usr/bin/env ruby

require 'fox16'

include Fox

application = FXApp.new("Hello", "FoxTest")
main = FXMainWindow.new(application, "Hello", nil, nil, DECOR_ALL)
FXButton.new(main, "&Hello, World!", nil, application, FXApp::ID_QUIT)
application.create()
main.show(PLACEMENT_SCREEN)
application.run()

Even though the old way has a slightly smaller line count, you can see how messy it can be assigning each newly-created object to a variable, and then having to pass that variable to the children. Perhaps this example is too small, but perhaps the next one will more illustrative.

Bouncing Ball example (full):

#!/usr/bin/env ruby
require 'fxruby-enhancement'

include Fox
include Fox::Enhancement::Mapper

ANIMATION_TIME = 20

class Ball
  attr_reader :color
  attr_reader :center
  attr_reader :radius
  attr_reader :dir
  attr_reader :x, :y
  attr_reader :w, :h
  attr_accessor :worldWidth
  attr_accessor :worldHeight

  
  def initialize r
    @radius = r
    @w = 2*@radius
    @h = 2*@radius
    @center = FXPoint.new(50, 50)
    @x = @center.x - @radius
    @y = @center.y - @radius
    @color = FXRGB(255, 0, 0) # red
    @dir = FXPoint.new(-1, -1)
    setWorldSize(1000, 1000)
  end
  
  # Draw the ball into this device context
  def draw(dc)
    dc.setForeground(color)
    dc.fillArc(x, y, w, h, 0, 64*90)
    dc.fillArc(x, y, w, h, 64*90, 64*180)
    dc.fillArc(x, y, w, h, 64*180, 64*270)
    dc.fillArc(x, y, w, h, 64*270, 64*360)
  end

  def bounce_x
    @dir.x=-@dir.x
  end

  def bounce_y
    @dir.y=-@dir.y
  end

  def collision_y?
    (y<0 && dir.y<0) || (y+h>worldHeight && dir.y>0)
  end

  def collision_x?
    (x<0 && dir.x<0) || (x+w>worldWidth && dir.x>0)
  end

  def setWorldSize(ww, wh)
    @worldWidth = ww
    @worldHeight = wh
  end
  
  def move(units)
    dx = dir.x*units
    dy = dir.y*units
    center.x += dx
    center.y += dy
    @x += dx
    @y += dy
    if collision_x?
      bounce_x
      move(units)
    end
    if collision_y?
      bounce_y
      move(units)
    end
  end
end

fx_app :app do
  app_name "Bounce"
  vendor_name "Example"

  fx_image(:back_buffer) { opts IMAGE_KEEP }
  
  fx_main_window(:bounce_window) {
    title "Bounce Demo"
    opts DECOR_ALL
    width 400
    height 300
    
    instance { |w|
      def w.ball
        @ball ||= Ball.new(20)
      end
      
      def w.drawScene(drawable)
        FXDCWindow.new(drawable) { |dc|
          dc.setForeground(FXRGB(255, 255, 255))
          dc.fillRectangle(0, 0, drawable.width, drawable.height)
          ball.draw(dc)
        }
      end
      
      def w.updateCanvas
        ball.move(10)
        drawScene(ref(:back_buffer))
        ref(:canvas).update
      end
      
      #
      # Handle timeout events
      #
      def w.onTimeout(sender, sel, ptr)
        # Move the ball and re-draw the scene
        updateCanvas
        
        # Re-register the timeout
        ref(:app).addTimeout(ANIMATION_TIME, ref(:bounce_window).method(:onTimeout))
        
        # Done
        return 1
      end
      
      w.show PLACEMENT_SCREEN
      ref(:app).addTimeout(ANIMATION_TIME, w.method(:onTimeout))
    }
    
    fx_canvas(:canvas) {
      opts LAYOUT_FILL_X|LAYOUT_FILL_Y
      
      instance { |c|
        c.sel_paint { |sender, sel, event|
          FXDCWindow.new(sender, event) { |dc|
            dc.drawImage(ref(:back_buffer), 0, 0)
          }
        }

        c.sel_configure{ |sender, sel, event|
          bb = ref(:back_buffer)
          bb.create unless bb.created?
          bb.resize(sender.width, sender.height)
          ref(:bounce_window) do |bw|
            bw.ball.setWorldSize(sender.width, sender.height)
            bw.drawScene(bb)
          end
        }
      }
    }
  }
end

if __FILE__ == $0
  # alias for fox_component is fxc
  fox_component :app do |app|
    app.launch
  end
end

Bouncing Ball the old fxruby way:

require 'fox16'

include Fox

# How long to pause between updates (in milliseconds)
ANIMATION_TIME = 20

class Ball

  attr_reader :color
  attr_reader :center
  attr_reader :radius
  attr_reader :dir
  attr_reader :x, :y
  attr_reader :w, :h
  attr_accessor :worldWidth
  attr_accessor :worldHeight

  # Returns an initialized ball
  def initialize(r)
    @radius = r
    @w = 2*@radius
    @h = 2*@radius
    @center = FXPoint.new(50, 50)
    @x = @center.x - @radius
    @y = @center.y - @radius
    @color = FXRGB(255, 0, 0) # red
    @dir = FXPoint.new(-1, -1)
    setWorldSize(1000, 1000)
  end

  # Draw the ball into this device context
  def draw(dc)
    dc.setForeground(color)
    dc.fillArc(x, y, w, h, 0, 64*90)
    dc.fillArc(x, y, w, h, 64*90, 64*180)
    dc.fillArc(x, y, w, h, 64*180, 64*270)
    dc.fillArc(x, y, w, h, 64*270, 64*360)
  end

  def bounce_x
    @dir.x=-@dir.x
  end

  def bounce_y
    @dir.y=-@dir.y
  end

  def collision_y?
    (y<0 && dir.y<0) || (y+h>worldHeight && dir.y>0)
  end

  def collision_x?
    (x<0 && dir.x<0) || (x+w>worldWidth && dir.x>0)
  end

  def setWorldSize(ww, wh)
    @worldWidth = ww
    @worldHeight = wh
  end

  def move(units)
    dx = dir.x*units
    dy = dir.y*units
    center.x += dx
    center.y += dy
    @x += dx
    @y += dy
    if collision_x?
      bounce_x
      move(units)
    end
    if collision_y?
      bounce_y
      move(units)
    end
  end
end

class BounceWindow < FXMainWindow

  include Responder

  def initialize(app)
    # Initialize base class first
    super(app, "Bounce", :opts => DECOR_ALL, :width => 400, :height => 300)

    # Set up the canvas
    @canvas = FXCanvas.new(self, :opts => LAYOUT_FILL_X|LAYOUT_FILL_Y)

    # Set up the back buffer
    @backBuffer = FXImage.new(app, nil, IMAGE_KEEP)

    # Handle expose events (by blitting the image to the canvas)
    @canvas.connect(SEL_PAINT) { |sender, sel, evt|
      FXDCWindow.new(sender, evt) { |dc|
        dc.drawImage(@backBuffer, 0, 0)
      }
    }

    # Handle resize events
    @canvas.connect(SEL_CONFIGURE) { |sender, sel, evt|
      @backBuffer.create unless @backBuffer.created?
      @backBuffer.resize(sender.width, sender.height)
      @ball.setWorldSize(sender.width, sender.height)
      drawScene(@backBuffer)
    }

    @ball = Ball.new(20)
  end

  #
  # Draws the scene into the back buffer
  #
  def drawScene(drawable)
    FXDCWindow.new(drawable) { |dc|
      dc.setForeground(FXRGB(255, 255, 255))
      dc.fillRectangle(0, 0, drawable.width, drawable.height)
      @ball.draw(dc)
    }
  end

  def updateCanvas
    @ball.move(10)
    drawScene(@backBuffer)
    @canvas.update
  end

  #
  # Handle timeout events
  #
  def onTimeout(sender, sel, ptr)
    # Move the ball and re-draw the scene
    updateCanvas

    # Re-register the timeout
    getApp().addTimeout(ANIMATION_TIME, method(:onTimeout))

    # Done
    return 1
  end

  #
  # Create server-side resources
  #
  def create
    # Create base class
    super

    # Create the image used as the back-buffer
    @backBuffer.create

    # Draw the initial scene into the back-buffer
    drawScene(@backBuffer)

    # Register the timer used for animation
    getApp().addTimeout(ANIMATION_TIME, method(:onTimeout))

    # Show the main window
    show(PLACEMENT_SCREEN)
  end
end

if __FILE__ == $0
  FXApp.new("Bounce", "FXRuby") do |theApp|
    BounceWindow.new(theApp)
    theApp.create
    theApp.run
  end
end

The Ball class is the same, but the actual Fox-related code should clearly illustrate the power of Enhancement.

More examples can be found HERE.

Release Notes

VersionDateNotes
0.0.22017-01-11Initial release
0.0.32017-01-15Needed to require fox16/colors for FXColor to be loaded
0.0.42017-01-16ingress_handler now handles multiple tags.

Known Issues

VersionDateIssues
0.0.22017-01-11Not enough example code!!! Need more documentation!!!

Contributing to fxruby-enhancement

  • Check out the latest master to make sure the feature hasn’t been implemented or the bug hasn’t been fixed yet.
  • Check out the issue tracker to make sure someone already hasn’t requested it and/or contributed it.
  • Fork the project.
  • Start a feature/bugfix branch.
  • Commit and push until you are happy with your contribution.
  • Make sure to add tests for it. This is important so I don’t break it in a future version unintentionally.
  • Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.

Copyright and Licensing

Copyright (c) 2016-2017 Fred Mitchell. See MIT License for further details.

The Junkyard / Scratchpad

These are my personal notes, not meant for anyone else. You may see some interesting tidbits here, but I am not gauranteeing anything to be useful or reliable in this section. YOU HAVE BEEN WARNED.

Genesis of the meta-meta programming, whereby brain goes boom

    class FXToolBar # monkey patch
      include Enhancement
      attr_accessor :_o
    end

    def fx_tool_bar name, &block # DSL
      o = OStruct.new
      o.title = "default title"
      ...

      def o.title t 
        @title = t
      end    

      def o.instance a, &block
        o.instance_time_block = block
      end
      f = FXToolBar.new ...
      f._o = o
    end

<% for @class, @details in @api %>
   #<%= @class %> < <%= @details[:class][1] %>
   <% unless @details[:initialize].nil? %>
      <% for @iniparams in @details[:initialize] %>
         #<%= @iniparams %>   
      <% end %>
   <% else %>
      #No initializer
   <% end %>
<% end %>

About

FOX Toolkit Wrapper for Rubyists.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Ruby 99.1%
  • HTML 0.9%