Saturday, 6 November 2010

Monkeybars, a UI framework

Finding a decent UI for use with Ruby is something of an on-going quest. I blogged a couple of years ago about Ruby Shoes, which is a very neat idea, but is limited, and not really suited to big projects. I am not too sure how well it is being supported nowadays, though there is some activity at GitHub.

So recently I was looking at Monkeybars. Monkeybars is not a UI as such, but a framework for using Swing inside an IDE. It attempts to hide all the underlying Java, so you just design your interface through your IDE, and the rest is Ruby (specially, JRuby of course, for Swing).

There is a problem with Monkeybars; it does not work with recent versions of JRuby. I found an older version in an example file, and that works old (I have reported this as a bug here).

Installation

Installing is simple (this will also install the "rawr" gem):
gem install monkeybars


Create a project

To create a new project:
monkeybars my_project
cd my_project
rawr install

You than need to set it up in your IDE. Monkeybars was designed with NetBeans in mind, and that is the IDE I use too (it obviously needs both Ruby and Java included). It needs to be a Java project, as you are actually running a Java application that uses Ruby, so your new NetBeans project will be a "Java Project with Existing Sources". You need to point NetBeans to the source packages in the "my_project/src" folder. Once the project is created, right click on "Libraries" in the project browser, select "Add JAR/Folder" and select the .jar files in "my_project/lib/java".

When you first run your project, NetBeans will ask for the main file; select "org.rubyforge.rawr.Main"

Create your model-view-controller

Just like Rails, you can use a rake task to generate these (in ths example, called "main"):

rake generate ALL=src/main


This will generate the files:
* src/main/main_controller.rb
* src/main/main_view.rb
* src/main/main_model.rb

Create the UI

You actually need a fourth file, which is the UI itself. In the IDE, right click on the folder (in this case "main") in the project browser, and select New - JFrame Form, and give it a suitable name (I chose MainJFrame). You can now add components graphically, using your IDE. The important point to remember is that any component you want your Ruby code to interact with should have a Ruby-friendly name. A "Quit" option on the file menu might be "quit_menu_item", but the menu itself you can leave to the default.

To keep it simple, drag a label on to the dialog box, and set the variable name (found under the code tab) to "message".

Edit the view

The main_view.rb file is the glue between the Java UI and your Ruby code. The first thing it needs is the name of your Java UI. Then it needs to know how the components on the UI map (or more accurately, properties of the components) to the model. In this example, then we need only two lines.
class MainView < ApplicationView
set_java_class 'main.MainJFrame'
map :model => :message, :view => "message.text"
end

You can have as many map statements as you need, one for each component that displays data (not required for buttons, etc. that generate events only). The :view part means that one end of the link is to the text property of the JLabel that we called "message". The other end of the link is the message property in the model.

You can set a mapping to be one way, by adding a :using value. In our example, the label cannot be edited directly by the user, so we only want the data to go from the model to the UI component, not the other way around (you can flip "nil" and ":default" to have the data go the other way only, but your model needs to provide read and write access to the property even so, because of the way Monkeybars creates a new model with the view data, then transfers data from that to the real model).
map :model => :message, :view => "message.text", :using => [:default, nil]

You can also use the ":using" value to specify a method to convert the data; give the method name instead of :default.

Edit the controller

The controller needs to be told the name of the model, and the name of the view. You can also set an action for when the dialog close is clicked. All of this is done for you, and is all we need for this simple application.
class MainController < ApplicationController
set_model 'MainModel'
set_view 'MainView'
set_close_action :exit
end

However, if you have any way for the user to interact with your dialog box, that gets captured here. Let us suppose you have a "Quit" option on yor file menu, and you have set the variable name to "quit_menu_item". This method will respond to that menu item being selected, and ask for confirmation.
def quit_menu_item_action_performed
r = javax.swing.JOptionPane.showConfirmDialog(nil,
"Do you really want to quit?",
"Confirmation",
javax.swing.JOptionPane::YES_NO_OPTION,
javax.swing.JOptionPane::QUESTION_MESSAGE)
exit if r == javax.swing.JOptionPane::YES_OPTION
end

Here is another example for a button; the user presses the button and the text in two JTextAreas is used to update the model (the first JTextArea is called "text_area_1", but mapped to "text1" in the model, using map :model => :text1, :view => "text_area_1.text" in the view). The update_model method is used to transfer data to the model. Then the update method in the model is called (this is a method I have written, to do what I need in my model). Finally, update_view is called so the UI is updated to reflect the new state of the model.
def update_button_action_performed
model.text1 = view_state.model.text1
model.text2 = view_state.model.text2
model.update
update_view
end

What happens is that calling view_state creates a new instance of the model, and this is populated with the values from the UI. You can then copy across the values you want into the real model. A convenience method, update_model, can be used instead.
def update_button_action_performed
update_model(view_state.model, :text1, text2)
model.update
update_view
end

This is how the controller will handle most events, first transfer the data from the UI to the model, then call a method in the model, then update the view, so you could have one method to create a whole set.
%w(up down edit cut paste insert).each do |action|
%w(button menu_item).each do |type|
class_eval <<-"END"
def #{action}_#{type}_action_performed
model.text = view_state.model.text
model.#{action}
update_view
end
END
end
end

You can set the value for set_close_action to :nothing, :exit, :close, :dispose or :hide. If you want other functionality, override one of those methods in the controller (I found I could override close, but not exit).

Edit the model

The model needs to include accessor methods for all the properties you mapped to in the view, and methods for all the actions. In our simple example, like this:
attr_accessor :message

You can set properties through properly defined methods, but that is something of a minefield, I have found, and better avoided.

There is no more to say about the model; Monkeybars makes no assumptions about it, and it has no parent class to inherit from (other than Object). This is where you do the real work.


Struggling with Ruby: Contents Page

8 comments:

James Britt said...

Thanks for trying out Monkeybars, and especially for taking the time to write such a detailed description of creating an app.

A few comments:

The most current version of Monkeybars is on Github: https://github.com/monkeybars/monkeybars-core

Because of issues with rubyforge.org being dropped in favor of rubygems.org and accounts not correctly transfered, the gem has not been updated. Best to install from source.

Bugs are tracked using Pivotal Tracker, not code.google.com (which should have been shut off long ago when bug tracking was moved to Kenai, but that was before I took over the project).

Monkeybars is not about the IDE. You can use it just fine without Netbeans or whatever. Not does it attempt to hide all the underlying Java. If you create a compiled Swing class using an IDE then you can probably avoid writing any Java, but if you go for inline Swing you may have to use some Java (though Neurogami::SwingSet can help there).

The main value in Monkeybars is decoupling model, view, and controller. Using certain naming conventions makes the use of compiled Swing code automatic. For anything non-trivial I would use an IDE + graphical UI editor, but it is not a requirement.

Some of the examples an tutorials include using Netbeans, which is handy but had the unfortunate effect of leadng people to think the two were coupled. My most recent Monkeybars apps have all been done using vim and SwingSet. And they seem to run just fine with the current JRuby.

Recent changes to JRuby did break some things (mostly in rawr, as a recall), but these should be fixed in monkeybars and rawr (https://github.com/rawr/rawr). I encourage you to get the latest code from github and see if it works for you. If not, please let me know.


James Britt / james@neurogami.com

Monkeybars/rawr project admin guy

Steev said...

Hey, thanks for mentioning Shoes! Just so you know, it is well supported, and Shoes does actually scale up to bigger projects well... I don't think there are any 100klok+ projects yet, mind you, but I maintain Hackety Hack, which is a few thousands lines, and it works quite well.

James Britt said...

I think the "scaling" issue with Shoes is that, last I saw, it did not provide a great variety of sophisticated, pre-built, UI controls, such as sortable tables, expandable trees, etc., the kind of things one takes for granted with Swing or SWT.

John said...

I am trying to follow this. I think I got most of it setup correctly.
I found that I need to add
require 'main_controller'
MainController.instance.open

in main.rb

but I get
Java::OrgJrubyExceptions::RaiseException - no such file to load -- main_controller

I have tried playing with $LOAD_PATH but have not had any luck.
Any suggestions?

James Britt said...

> require 'main_controller'

> MainController.instance.open

I'm assuming you have src/main/main_controller.rb, yes?

`src/main` should b automagicaly pushed onto $:

How are you running the code?

John said...

I got it going.
I had created the dir outside of netbeans and then used new java app with existing sources, so I don't think it could find them were they were and i probably never had the LOAD_PATH correct to find them.

I got it working by moving the main_controller/view/model in to the netbeans directory under src/main
and then i had to add to the manifest.rb
add_to_load_path "../src/main"

Are there any examples that show how to get info out of a list, tree, etc?
I can see from the examples I have looked at how to use _action_performed, but it's not clear how to work with other types of events

nPn said...

btw found the stuff on monkeybars.org
and I am looking at the flickrbrowser project, that looks like it has most of what i need to learn - thanks!

James Britt said...

The examples are now a bit dated, but while they might have been some API changes they should show some ways to interact with a few Swing objects.

Monkeybars doesn't wrap an Swing components, just helps make it easier to work with them in a structured way.

Unfortunately I've often had to root around Swing javadocs and Java examples to figure out how various components expose their behavior. In some cases I've needed to catch a mouse event and then check the state of a component.