How’s it used?

Profligacy is really easy to use, but you should still be referring to the Swing™ docs to learn how to actually use the components.

These examples are all in the project’s examples directory along with many others.

You should also check out the LEL instructions for a fully explained example using the Layout Expression Language.

The Book

These examples are translated from the book Swing: A Beginner’s Guide by Herbert Schildt and published by McGraw Hill.

What I did was translate the Java code examples to Profligacy and then cleaned them up so they are Ruby idiomatic. The new work is completely different looking compared to what’s in the book.

examples/swing1.rb

This example doesn’t really show off Profligacy but instead shows you what the raw Swing looks like with JRuby. As you can see it’s much nicer than doing it in Java already.

require 'java'
require 'profligacy/swing'

import 'javax.swing.JFrame'
import 'javax.swing.JLabel'

class SwingDemo
  def initialize
    jfrm = JFrame.new "A Simple Demo" 
    jfrm.setSize(275,100)
    jfrm.default_close_operation = JFrame::EXIT_ON_CLOSE
    jlab = JLabel.new " Swing powers the modern Java GUI" 
    jfrm.add jlab
    jfrm.pack
    jfrm.visible = true
  end
end

SwingUtilities.invoke_later proc { SwingDemo.new }.to_runnable

examples/swing2.rb

This is the first example that uses Profligacy to build the Swing GUI, but which doesn’t use LEL to do the layout. Instead it just uses a plain FlowLayout to organize the components.


require 'profligacy/swing'

module Buttons
  class ButtonDemo
    include_package 'javax.swing'
    include_package 'java.awt'
    include Profligacy

    def initialize
      @ui = Swing::Build.new JFrame, :first, :second, :lab do |c,i|
        c.lab = JLabel.new "Press A Button." 
        c.first = JButton.new "First" 
        c.second = JButton.new "Second" 

        i.first = { :action => proc {|t,e| c.lab.text = "First pressed." } }
        i.second = { :action => proc {|t,e| c.lab.text = "Second pressed." } }
      end

      @ui.layout = FlowLayout.new
      @ui.build("The Sample Layout").default_close_operation = JFrame::EXIT_ON_CLOSE
    end
  end
end

SwingUtilities.invoke_later proc { Buttons::ButtonDemo.new }.to_runnable

Notice also that you can use the to_runnable and the regular Ruby procs to do all your callbacks. This removes a huge amount of repetitive error prone Java code from doing listeners and runnables.

examples/swing3.rb

This example is a simple stop watch that shows how the callbacks can also be method(:name) callbacks. What Profligacy does is anything that’s registered as a callback has to_proc called on it. This means if you pass in a proc, that gets called. If you pass in a method, it gets converted to a proc.

This means that you can implement callbacks on other objects by simply getting their method. The signature for the method should be method(type,event) with type being a symbol for the Java method that was called, and event being the AWT Event object.

require 'profligacy/swing'

class StopWatch
  include_package 'javax.swing'
  import 'java.awt.FlowLayout'
  include Profligacy

  def initialize
    @ui = Swing::Build.new JFrame, :start, :stop, :lab do |c,i|
      c.start = JButton.new "Start" 
      c.stop = JButton.new "Stop" 
      c.lab = JLabel.new "Press Start to begin timing." 

      i.start = { :action => method(:start) }
      i.stop = { :action => method(:stop) }
    end

    @ui.layout = FlowLayout.new
    @ui.build("Stop Watch").default_close_operation = JFrame::EXIT_ON_CLOSE
  end

  def start(type, event)
    @ui.lab.text = "Stopwatch is running..." 
    @time = Time.new
  end

  def stop(type, event)
    if @time
      @ui.lab.text = "#{Time.now.to_i - @time.to_i} seconds." 
    else
      @ui.lab.text = "Start the watch first." 
    end
    @time = nil
  end
end

SwingUtilities.invoke_later proc { StopWatch.new }.to_runnable

The signature for methods has to be callback(type, event) and the type is a symbol that represents the method name Java tried to call. This lets you tailor the reactions based on the type without having to dig into the event. It’s also necessary because the name and types of the events are inconsistent from the method that’s called in Swing.

examples/swing4.rb

This example shows the real power of Ruby over Java. It’s dynamic nature means you can crank out tons of readable functionality with very little code. The Java version of this simply repeated calls to new JLabel("Left, Top") and then use the exact same names as constants to set that alignment.

In the Ruby example below, I use an array of symbols and then just simply construct both the label and the alignment from them.

require 'profligacy/swing'

class AlignLabelDemo
  include_package 'javax.swing'
  include_package 'java.awt'
  include_package 'javax.swing.border'
  include SwingConstants
  include Profligacy

  def initialize
    border = BorderFactory.createEtchedBorder

    @ui = Swing::Build.new JFrame, :labels do |c,i|
      c.labels = [ 
        [:LEFT, :TOP], [:CENTER, :TOP], [:RIGHT, :TOP],
        [:LEFT, :CENTER], [:CENTER, :CENTER], [:RIGHT, :CENTER],
        [:LEFT, :BOTTOM], [:CENTER, :BOTTOM], [:RIGHT, :BOTTOM]
      ].collect do |horiz,vert| 
        lab = JLabel.new "#{horiz},#{vert}", AlignLabelDemo.const_get(horiz) 
        lab.border = border
        lab.vertical_alignment = AlignLabelDemo.const_get(vert)
        lab 
      end
    end

    @ui.layout = GridLayout.new(3,3,4,4)
    frame = @ui.build("Label Play")
    frame.content_pane.border = BorderFactory.createEmptyBorder 4,4,4,4
    frame.default_close_operation = JFrame::EXIT_ON_CLOSE
  end
end

SwingUtilities.invoke_later proc { AlignLabelDemo.new }.to_runnable

This sample also shows you how, because Profligacy uses yield rather than instance_eval to have you build the components you can use the full power of Ruby to help build things. No need to worry about how the contents of this block is interpreted because it’s not.

examples/swing5.rb

This sample shows how you can access the components in the returned Swing::Build object in your callbacks. In this example we’ve saved a @ui object and then we simply reference the components by their names in the toggled callback. This helps organize and structure the code. Rather than flood the class with a mixture of UI component variables and variables that actually are needed for the UI’s logic, we just have them in a handy single location with consistent names.

require 'profligacy/swing'

class ToggleButtonDemo
  include_package 'javax.swing'
  include_package 'java.awt'
  include_package 'javax.swing.border'
  include Profligacy

  def initialize
    @ui = Swing::Build.new JFrame, :label, :button do |c,i|
      c.label = JLabel.new "Button is off." 
      c.button = JToggleButton.new "On/Off" 

      i.button = { :item => method(:toggled) }
    end

    @ui.layout = FlowLayout.new
    @ui.build("Toggle Demo").default_close_operation = JFrame::EXIT_ON_CLOSE
  end

  def toggled(type, event)
    if @ui.button.selected?
      @ui.label.text = "Button is on." 
    else
      @ui.label.text = "Button is off." 
    end
  end
end

SwingUtilities.invoke_later proc { ToggleButtonDemo.new }.to_runnable

You of course don’t have to do this at all, it’s just proven to reduce code repetition since you already named these variables in another place, used them there, and will need them again later.

No point then storing these components in variables again.

examples/swing6.rb

This sample simply shows handling various events from components. One thing that hasn’t been discussed is how you register a callback. The two variables passed to your block are the components and interactions (here done as c,i). You put components on the c variable and they get loaded into the layout and JFrame (or JPanel or any container) when you call build.

The interactions are just your proc or method that you attach to the i variable. The problem is many components have different callbacks that fire off. Some answer actionListener others answer changedListener. The solution is you attach a Ruby Hash that maps the action to the callback to make. You do as many as you want to handle, and when build is called they get appropriately translated.

require 'profligacy/swing'

class TwoTFDemo
  include_package 'javax.swing'
  include_package 'java.awt'
  include_package 'javax.swing.border'
  include Profligacy

  def initialize
    @ui = Swing::Build.new JFrame, :texts, :label do |c,i|
      c.texts = [ JTextField.new(10), JTextField.new(10)]
      c.texts.each_with_index {|t,n| t.action_command = "text#{n}" }
      c.label = JLabel.new "Something will show up here." 

      i.texts = {:action => method(:text_action) }
    end

    @ui.layout = FlowLayout.new
    @ui.build("Two Text Fields Demo").default_close_operation = JFrame::EXIT_ON_CLOSE
  end

  def text_action(type, event)
    puts "EVENT: #{type} #{event.action_command}" 
    if event.action_command == "text0" 
      @ui.label.text = "ENTER pressed in first text" 
    else
      @ui.label.text = "ENTER pressed in second text" 
    end
  end
end

SwingUtilities.invoke_later proc { TwoTFDemo.new }.to_runnable

The way you figure out the symbol to use is from the Swing documentation. Swing uses a method signature of addSomethingListener. For :action it’s addActionListener. All you do is drop the add and Listener and what’s in the middle is what you set in the Hash. For example, if you had a component with an addJavaIsRetardedListener you’d use the symbol javaIsRetarded to set the callback.

Using Arrays Of Components

You should also notice another handy feature of Profligacy shown in demo swing6.rb. Notice this code:

      ...
      c.texts = [ JTextField.new(10), JTextField.new(10)]
      c.texts.each_with_index {|t,n| t.action_command = "text#{n}" }
      ...

When you set a component to an Array Profligacy assumes you want them to be added in order and to have the same callback applied to them all.

This cuts down on having to use tons of names in the builder for what’s effectively the same component repeated. The alternative would be to have a c.text1, c.text2, etc. It also saves you time since Profligacy will take the i.texts = {:action => method(:text_action)} and apply that same callback to all the components in that Array.

examples/swing7.rb

This sample shows more advanced callback handling and again shows how Ruby cleans up the code tremendously. The Java version had repetitive if/else blocks and nasty String construction to do the same thing. In Ruby we just use each and build the string we want on the fly.

require 'profligacy/swing'

class ChangeDemo
  include_package 'javax.swing'
  include_package 'java.awt'
  include_package 'javax.swing.border'
  include Profligacy

  def initialize
    @ui = Swing::Build.new JFrame, :button, :label do |c,i|
      c.label = JLabel.new
      c.button = JButton.new "Press for Change Event Test" 

      i.button = { :change => method(:change_check) }
    end

    @ui.layout = FlowLayout.new
    frame = @ui.build("Change Demo")
    frame.set_size(250,160)
    frame.default_close_operation = JFrame::EXIT_ON_CLOSE
  end

  def change_check(type, event)
    model = event.source.model
    what = []
    [:enabled?, :rollover?, :armed?, :pressed?].each do |test|
      if model.send(test)
        what << "#{test}<br>" 
      end
    end

    @ui.label.text = "<html>Current state:<br>#{what.join(" ")}" 
  end
end

SwingUtilities.invoke_later proc { ChangeDemo.new }.to_runnable

examples/swing8.rb

This is probably the most complex demo since it actually works as a phone book (although a really small one). No, that’s not my real number.

Again this shows how Ruby just destroys the complexity in these GUIs. By using a case/when block and built-in regex usage we can just do the lookup in about 4 lines of code.

require 'profligacy/swing'

class PhoneBookDemo
  include_package 'javax.swing'
  include_package 'java.awt'
  include_package 'javax.swing.border'
  include Profligacy

  def initialize
    children = [:lab_name, :name, :lab_number, :number, :lab_options,
      :ignore_case, :exact, :starts, :ends]

    @search_style = :exact

    @ui = Swing::Build.new(JFrame, *children) do |c,i|
      c.lab_name = JLabel.new "Name" 
      c.lab_number = JLabel.new "Number" 
      c.lab_options = JLabel.new "Search Options" 
      c.name = JTextField.new 10
      c.number = JTextField.new 10

      c.exact = JRadioButton.new("Exact Match", true)
      c.starts = JRadioButton.new("Starts With")
      c.ends = JRadioButton.new("Ends With")

      b = ButtonGroup.new
      [c.exact, c.starts, c.ends].each {|x| b.add(x) }

      i.name   = {:action => proc {|t,e| @ui.number.text = find(@names, e.source.text) } }
      i.number = {:action => proc {|t,e| @ui.name.text = find(@numbers, e.source.text) } }
      i.exact  = {:action => proc {|t,e| @search_style = :exact } }
      i.starts = {:action => proc {|t,e| @search_style = :starts } }
      i.ends   = {:action => proc {|t,e| @search_style = :ends } }
    end

    @ui.layout = GridLayout.new 0,1
    @ui.build("Phone Book").default_close_operation = JFrame::EXIT_ON_CLOSE

    @names = { 
    "Zed A. Shaw" => "917-555-5555",
    "Frank Blank" => "212-554-5555" 
    }

    @numbers = { 
     "917-555-5555" => "Zed A. Shaw",
     "212-554-5555" => "Frank Blank" 
    }
  end

  protected
  def find(inside, text)
    results = case @search_style
    when :exact; inside.keys.grep /^#{text}$/
    when :starts; inside.keys.grep /^#{text}/
    when :ends; inside.keys.grep /#{text}$/
    end

    inside[results[0]] || "" 
  end
end

SwingUtilities.invoke_later proc { PhoneBookDemo.new }.to_runnable

examples/swing8_lel.rb

The last sample is the first demonstration of using LEL, but also an example of when you might not what to use Swing::LEL compared to Swing::Build. In this case, the code is almost the same and the results are nearly the same.

What you can see is that the layout has a bit more flavor to it, with components right justified, filled out, and better spacing. This is the primary advantage of LEL in this situation. It allows you to tweak and make the layout look nicer. It’s also better when the layout gets more complex.

However, don’t bother using LEL when a simple FlowLayout might work instead. Use the right tool for the job. See the LEL instructions for a good example of something that’s infinitely easier with LEL.

require 'profligacy/swing'
require 'profligacy/lel'

class PhoneBookDemo
  include_package 'javax.swing'
  include_package 'java.awt'
  include_package 'javax.swing.border'
  include Profligacy

  def initialize
    @search_style = :exact

    layout = "[>lab_name][*name][>lab_number][*number][*lab_options][<exact][<starts][<ends]" 

    @ui = Swing::LEL.new(JFrame, layout) do |c,i|
      c.lab_name = JLabel.new "Name" 
      c.lab_number = JLabel.new "Number" 
      c.lab_options = JLabel.new "Search Options" 
      c.name = JTextField.new 10
      c.number = JTextField.new 10

      c.exact = JRadioButton.new("Exact Match", true)
      c.starts = JRadioButton.new("Starts With")
      c.ends = JRadioButton.new("Ends With")

      b = ButtonGroup.new
      [c.exact, c.starts, c.ends].each {|x| b.add(x) }

      i.name   = {:action => proc {|t,e| @ui.number.text = find(@names, e.source.text) } }
      i.number = {:action => proc {|t,e| @ui.name.text = find(@numbers, e.source.text) } }
      i.exact  = {:action => proc {|t,e| @search_style = :exact } }
      i.starts = {:action => proc {|t,e| @search_style = :starts } }
      i.ends   = {:action => proc {|t,e| @search_style = :ends } }
    end

    @ui.build(:args => "Phone Book")

    @names = { 
    "Zed A. Shaw" => "917-555-5555",
    "Frank Blank" => "212-554-5555" 
    }

    @numbers = { 
     "917-555-5555" => "Zed A. Shaw",
     "212-554-5555" => "Frank Blank" 
    }
  end

  protected
  def find(inside, text)
    results = case @search_style
    when :exact; inside.keys.grep /^#{text}$/
    when :starts; inside.keys.grep /^#{text}/
    when :ends; inside.keys.grep /#{text}$/
    end

    inside[results[0]] || "" 
  end
end

SwingUtilities.invoke_later proc { PhoneBookDemo.new }.to_runnable

examples/layout_test.rb

To see this example, go to the LEL instructions which is also a great step-by-step set of instructions for using LEL and Profligacy.

MY GOD THAT’S SO UGLY!

Alright, calm down moron. Yes, 10 years ago AWT and Swing were ugly as hell. Now they have these things called “look-and-feel” plugins that make it look however you want. Drag your head out of your tightly clinched ass, update your damn information, and take a look at a few screenshots here:

That’s what just a raw prototype GUI looks like on OSX. Still using the native buttons, still looks pretty close to normal, just a few tweaks and Apple specific settings needed to improve it and keep the Jobsians happy.

Here’s a LAF called Substance that’s in default mode:

This one is very configurable, and if you want to try it you have to do the following:

  1. Grab the substance.jar.
  2. export CLASSPATH=.:substance.jar
  3. jruby -J-Dswing.defaultlaf=org.jvnet.substance.SubstanceLookAndFeel -Ilib examples/utu_main.rb

The utu_main.rb file is now included in the examples directory of the profligacy.gem.

So, quit your whining, you can make it look however you want.