Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Swing

Swing Based GUIs in JRuby

4.50/5 (3 votes)
23 Jun 2014CPOL9 min read 22K   178  
How to create Swing based GUIs with JRuby

Introduction

Despite it being a scripting language, there is no shortage of Ruby GUI toolkits. Apart of Tk, its standard toolkit, many other GUI toolkits have Ruby bindings (like FXRuby, qtRuby and wxRuby). Ohers are listed here:

Ruby Programming/GUI Toolkit Modules

Except maybe for Shoes (designed by _why) other GUI toolkits are developed independently from Ruby and their respective Ruby bindings may or may not implement the GUI toolkit latest features. For instance, qtRuby, is available for QT4, but not for QT5, the toolkit's latest version.

Java ads another set of options for Ruby GUI development. With JRuby you can access Java standard GUI modules, Swing and SWT as well as Java FX, the latest addition to the Java GUI family.

The advantage of using Swing for your Ruby GUI is that it will run whereever Java is installed. Another advantage is you get all Java libraries available to your Ruby scripts for free. The disadvantage is that you will need to install JRuby, the Ruby version that runs on JVM.

Once you decide to use Java for your Ruby application GUI you still have several options to choose from. You can use plain Swing or one of its JRuby wrappers (like MonkeyBars or Cheri - these wrappers are found at various levels of completeness). You can use raw SWT. Lastly, you can use projects like Limelight, Profligacy or Swiby - all these projects attempt to make JRuby GUI development a more pleasant experience, but are also at various levels of completeness.

For the Web application people there is Rails, of cource.

Using raw Swing in your Ruby scripts can be intimidating for a programmer without experience in UI programming, on the other hand, I believe that people that have had experience in other GUI frameworks like Windows Forms will not have much trouble picking it up.

The basic structure of a JRuby Swing script

A possible outline of JRuby Swing script could be:

  • use java_import to import all the necessary Java classes. For brevity, put all imports in a separate file called 'java_imports.rb' and require that file in each of your scripts.
  • Create a subclass of the JFrame Java class named TopFrame as top application container. In the TopFrame#initialize you need to call super(), otherwise JFrame doesn't get properly initialized. Application components are initialized in a separate method, TopFrame#init_components(). set_default_close_operation() tells the application to exit when the TopFrame instance is closed.
  • Back in TopFrame#initialize() you call init_components(), pack() and set_visible().
  • Use SwingUtilities#invoke_later() to create a new TopFrame instance on the Swing Event Dispatch Thread.

Here is the 'java_imports.rb' code:

# java_imports.rb

java_import javax.swing.JFrame
java_import javax.swing.SwingUtilities
java_import java.awt.Dimension
java_import javax.swing.JSplitPane
java_import javax.swing.JTextArea
java_import java.awt.BorderLayout
java_import javax.swing.JMenuBar
java_import javax.swing.JMenu
java_import javax.swing.JMenuItem
java_import java.lang.System
java_import javax.swing.JToolBar
java_import javax.swing.JButton
java_import javax.swing.ImageIcon
java_import java.awt.Toolkit
java_import java.awt.datatransfer.StringSelection
java_import java.awt.datatransfer.Transferable
java_import java.awt.datatransfer.DataFlavor
java_import javax.swing.JScrollPane
java_import javax.swing.JTree
java_import javax.swing.tree.DefaultTreeModel
java_import javax.swing.tree.DefaultMutableTreeNode
java_import javax.swing.tree.TreeModel
java_import javax.swing.event.TreeWillExpandListener
java_import javax.swing.tree.TreeSelectionModel
java_import javax.swing.JFileChooser
java_import javax.swing.JProgressBar
java_import java.awt.event.ActionListener
java_import  javax.swing.UIManager
JavaFile = java.io.File
JavaThread = java.lang.Thread

You'll notice that there are more Java classes imported that needed in the below JRuby script - this is because I use the same imports file for all my Ruby scripts in this project.

01_swing_template.rb code:

#01_swing_template.rb

require 'java_imports.rb'

class TopFrame < JFrame

  def initialize
    super
    init_components()
    pack()
    set_visible(true)
  end
  
  def init_components()
    set_default_close_operation(JFrame::EXIT_ON_CLOSE)
    set_preferred_size(Dimension.new(400, 300))
  end
  
end

SwingUtilities.invoke_later do
  TopFrame.new
end

JFrame::EXIT_ON_CLOSE lets your application exit when the JFrame instance is closed.

The pack() method sizes the frame so that all its contents are at or above their preferred sizes

set_visible() shows the JFrame on the screen.

If everything is all right running this script displays an empty Swing JFrame on the screen:

JRuby Swing JFrame

Why not forget about SwingUtilities.invoke_later() and just instantiate TopFrame right there in the main script thread?

There are numerous links about Swing threading issues, here are some of them:

To invokeLater() or not

I have made a couple of scripts, one that doesn't use SwingUtilities#invokeLater() and one that does. Here is the first one:

#02_without_swing_utilities.rb

require 'java_imports.rb'

puts '----------------------------------------'
puts 'Before JFrame.new():'
puts "Thread name: #{JavaThread.current_thread.name}"
puts "Is event dispatch thread: #{SwingUtilities.event_dispatch_thread?}"
JavaThread.current_thread.thread_group.list

frame = JFrame.new()

frame.set_default_close_operation(JFrame::EXIT_ON_CLOSE)
frame.set_preferred_size(Dimension.new(400, 300))

button = JButton.new('Button with action listener')
button.add_action_listener do |event|
  puts '----------------------------------------'
  puts 'In action listener:'
  puts "Thread name: #{JavaThread.current_thread.name}"
  puts "Is event dispatch thread: #{SwingUtilities.event_dispatch_thread?}"
  JavaThread.current_thread.thread_group.list
end

frame.get_content_pane.add(button, BorderLayout::WEST)
frame.pack()

puts '----------------------------------------'
puts 'Before JFrame.setVisible():'
puts "Thread name: #{JavaThread.current_thread.name}"
puts "Is event dispatch thread: #{SwingUtilities.event_dispatch_thread?}"
JavaThread.current_thread.thread_group.list

frame.set_visible(true)

The output is:

----------------------------------------
Before JFrame.new():
Thread name: main
Is event dispatch thread: false
java.lang.ThreadGroup[name=main,maxpri=10]
    Thread[main,5,main]
    Thread[Ruby-0-JIT-1,1,main]
    Thread[Ruby-0-JIT-2,1,main]
    java.lang.ThreadGroup[name=Ruby Threads#11483240,maxpri=10]
----------------------------------------
Before JFrame.setVisible():
Thread name: main
Is event dispatch thread: false
java.lang.ThreadGroup[name=main,maxpri=10]
    Thread[main,5,main]
    Thread[Ruby-0-JIT-1,1,main]
    Thread[Ruby-0-JIT-2,1,main]
    Thread[AWT-EventQueue-0,6,main]
    java.lang.ThreadGroup[name=Ruby Threads#11483240,maxpri=10]
----------------------------------------
In action listener:
Thread name: AWT-EventQueue-0
Is event dispatch thread: true
java.lang.ThreadGroup[name=main,maxpri=10]
    Thread[AWT-EventQueue-0,6,main]
    Thread[DestroyJavaVM,5,main]
    java.lang.ThreadGroup[name=Ruby Threads#11483240,maxpri=10]

Before JFrame constructor call the current thread is main and there is no EDT. Before JFrame#setVisible() the current thread is still main, but the EDT appears in the list. In the button action listener EDT is the current thread.

The other script uses SwingUtilities#invoke_later() to initialize JFrame:

#03_with_swing_utilities.rb

require 'java_imports.rb'

class TopFrame < JFrame

  def initialize
    super
    init_components()
    
    pack()
    set_visible(true)
    
    puts '----------------------------------------'
    puts 'In JFrame constructor:'
    puts "Thread name: #{JavaThread.current_thread.name}"
    puts "Is event dispatch thread: #{SwingUtilities.event_dispatch_thread?}"
    JavaThread.current_thread.thread_group.list
    
  end
  
  def init_components()
    
    set_default_close_operation(JFrame::EXIT_ON_CLOSE)
    set_preferred_size(Dimension.new(400, 300))
      
    button = JButton.new('Button with action listener')
    button.add_action_listener do |event|
      puts '----------------------------------------'
      puts 'In action listener:'
      puts "Thread name: #{JavaThread.current_thread.name}"
      puts "Is event dispatch thread: #{SwingUtilities.event_dispatch_thread?}"
      JavaThread.current_thread.thread_group.list
    end
    
    get_content_pane.add(button, BorderLayout::EAST)
    
  end
  
end

puts '----------------------------------------'
puts 'Before SwingUtilties.invokeLater():'
puts "Thread name: #{JavaThread.current_thread.name}"
puts "Is event dispatch thread: #{SwingUtilities.event_dispatch_thread?}"
JavaThread.current_thread.thread_group.list

SwingUtilities.invoke_later do
  TopFrame.new
end

puts '----------------------------------------'
puts 'After SwingUtilities.invokeLater():'
puts "Thread name: #{JavaThread.current_thread.name}"
puts "Is event dispatch thread: #{SwingUtilities.event_dispatch_thread?}"
JavaThread.current_thread.thread_group.list

The output is:

----------------------------------------
Before SwingUtilties.invokeLater():
Thread name: main
Is event dispatch thread: false
java.lang.ThreadGroup[name=main,maxpri=10]
    Thread[main,5,main]
    Thread[Ruby-0-JIT-1,1,main]
    Thread[Ruby-0-JIT-2,1,main]
    java.lang.ThreadGroup[name=Ruby Threads#11483240,maxpri=10]
----------------------------------------
After SwingUtilities.invokeLater():
Thread name: main
Is event dispatch thread: false
java.lang.ThreadGroup[name=main,maxpri=10]
    Thread[main,5,main]
    Thread[Ruby-0-JIT-1,1,main]
    Thread[Ruby-0-JIT-2,1,main]
    Thread[AWT-EventQueue-0,6,main]
    java.lang.ThreadGroup[name=Ruby Threads#11483240,maxpri=10]
----------------------------------------
In JFrame constructor:
Thread name: AWT-EventQueue-0
Is event dispatch thread: true
java.lang.ThreadGroup[name=main,maxpri=10]
    Thread[AWT-EventQueue-0,6,main]
    Thread[DestroyJavaVM,5,main]
    java.lang.ThreadGroup[name=Ruby Threads#11483240,maxpri=10]
----------------------------------------
In action listener:
Thread name: AWT-EventQueue-0
Is event dispatch thread: true
java.lang.ThreadGroup[name=main,maxpri=10]
    Thread[AWT-EventQueue-0,6,main]
    Thread[DestroyJavaVM,5,main]
    java.lang.ThreadGroup[name=Ruby Threads#11483240,maxpri=10]

Before and after calling SwingUtilities#invoke_later() the current thread is main, but after SwingUtilities#invoke_later() the EDT appears in the list. Both on JFrame constructor and JButton action listener EDT is the current thread.

JRUby Swing application look and feel

Swing applications have several options regarding GUI elements, known as 'look and feel'. You can list available application skins using this script:

# 4_look_and_feel_options.rb

java_import  javax.swing.UIManager

UIManager.get_installed_look_and_feels().each do |info|
  puts info.get_name()
end

On my Linux machine the output is:

Metal
Nimbus
CDE/Motif
GTK+




 

You can use any of these names to set (and change!) your application skin. Here is how:

# 05_set_lool_and_feel.rb

java_import javax.swing.JFrame
java_import javax.swing.SwingUtilities
java_import java.awt.Dimension
java_import java.awt.BorderLayout
java_import javax.swing.JButton
java_import javax.swing .JLabel
java_import javax.swing.UIManager

class TopFrame < JFrame

  def initialize
    super
    init_components()
    pack()
    set_visible(true)
  end
  
  def init_components()
    set_default_close_operation(JFrame::EXIT_ON_CLOSE)
    set_preferred_size(Dimension.new(400, 300))
    button = JButton.new('Swing JButton look and feel')
    get_content_pane.add(button, BorderLayout::NORTH)
    label = JLabel.new(BorderLayout::NORTH)
    label.text = 'JLabel look and feel'
    get_content_pane.add(label)
  end
  
end

SwingUtilities.invoke_later do

  UIManager.get_installed_look_and_feels().each do |info|
    if info.name == 'GTK+'
      UIManager.set_look_and_feel(info.class_name)
    end
  end

  TopFrame.new
  
end

Here is how the above listed look and feel options look on my machine:

Adding Widgets

When it comes to adding widgets to your forms this is the standard procedure:

  • Create an instance of the widget you want to use on your form
  • Set desired properties for the widget to be added
  • Get the parent container content pane. In case of JFrame you obtain the reference to its content pane using JFrame#get_content_pane()
  • Use the content panes add() method to add your witget to the parent container

As for creating widget instances and their scope this depends on what you need. If your widget is never to be changed or used after it has had been added to the container you can place its constructor call directly in the add() method. If you need to set or modify some of your widget attributes you can store a widget instance into a local variable, then customize its properties and finally add the local variable representing the widget using the add() method. If the widgets on your form will need to communicate among each other you can make them instance variables of your class that inherits JFrame.

Here is a simple example of adding two widgets, a JButton and a JLabel to a JFrame:

# 06_adding_widgets.rb

require 'java_imports.rb'

class TopFrame < JFrame

  attr_accessor :button, :label

  def initialize
    super
    init_components()
    pack()
    set_visible(true)
  end
  
  def init_components()
  
    set_default_close_operation(JFrame::EXIT_ON_CLOSE)   
    content_pane = get_content_pane()
    
    @button = JButton.new('Button text')
    @label = JLabel.new('Label text')
    
    content_pane.add(@button, BorderLayout::NORTH)
    content_pane.add(@label, BorderLayout::SOUTH)
    
  end
  
end

SwingUtilities.invoke_later do
  TopFrame.new
end

Here is what the form looks like:

The TopFrame class inherits JFrame and has two instance variables @button and @label (just for show - there is no interaction between them). All the widgets are set up in TopFrame#init_components().

JFrame.content_pane has a layout manager that helps you place widgets on it. The default layout manager is BorderLayout - you can use it to place widgets to the NORTH, EAST, SOUTH, WEST and CENTER of the frame. There are other, more flexible layout managers.

Jruby does some conversions of Java methods to let you write more Ruby-like code. For instance, Java's JFrame.getContentPane() becomes get_content_pane() in JRuby.  In the same vein Java's SwingUtilities.invokeLater() becomes SwingUtilities.invoke_later() in Ruby. This is not a rule - you can use the option you prefer.

Adding a menu to your application

Menus are ubiquotus in desktop (and not just desktop) applications. Adding a menu bar to a Swing application is a bit different than adding other widgets. Here is the code:

# 7_menu_bar.rb

require 'java_imports.rb'

class TopFrame < JFrame

  attr_accessor :main_menu_bar

  def initialize
    super
    init_components()
    pack()
    set_visible(true)
  end
  
  def init_components()
  
    set_default_close_operation(JFrame::EXIT_ON_CLOSE)
    set_preferred_size(Dimension.new(600, 400))

    @main_menu_bar = MainMenuBar.new()
    set_jmenu_bar(@main_menu_bar)

  end
  
end

class MainMenuBar < JMenuBar

  attr_accessor :file_menu, :new_menu_item, :exit_menu_item,
    :edit_menu, :cut_menu_item, :copy_menu_item 
  
  def initialize  
    super()
    init_components()
  end
  
  def init_components
    
    @file_menu = JMenu.new('File')
    
    @new_menu_item = JMenuItem.new('New')
    @new_menu_item.add_action_listener do |event|
      puts 'the New menu item clicked'
    end
    @file_menu.add(@new_menu_item)
         
    @exit_menu_item =  JMenuItem.new('Exit')
    @exit_menu_item.add_action_listener do |event|
      System.exit(0)
    end
    @file_menu.add(@exit_menu_item)

    add(@file_menu)
            
    @edit_menu = JMenu.new('Edit')
    
    @cut_menu_item = JMenuItem.new('Cut')
    @cut_menu_item.add_action_listener do |event|
      puts 'The Cut menu item clicked'
      puts 'Source: ' + event.source.to_s
      puts 'Action command: ' + event.action_command
    end
    @edit_menu.add(@cut_menu_item)
    
    @copy_menu_item = JMenuItem.new('Copy')
    @copy_menu_item.add_action_listener do |event|
      puts 'The Copy menu item clicked'
    end
    @edit_menu.add(@copy_menu_item)

    add(@edit_menu)

  end
  
end

SwingUtilities.invoke_later do
  TopFrame.new
end

Jframe with the main menu:

As you can see you use JFrame#setJMenuBar() instead of JFrame#getComponentPane#add(). The rest is as expected. JMenuItem instances (like 'Open', 'Copy' or 'Exit') are added to JMenu instances (like 'File' or 'Edit') which in turn are added to JMenuBar to construct the meny hierarchy.

More on JRuby fiddling with Java methods:

In TopFrame#init_components() set_default_close_operation() is a Java setter method just as set_preferred_size(). JRuby lets you use getter and setter methods like properties and in that case that snippet of code would like:

def init_components()

  self.default_close_operation = JFrame::EXIT_ON_CLOSE
  self.preferred_size = Dimension.new(600, 400)

  @main_menu_bar = MainMenuBar.new()
  self.jmenu_bar = @main_menu_bar

end

I've qualified the method calls with self and I can use them as properties. All the lookups (jmeny_bar => set_jmenu_bar() => setJMenuBar()) are done by JRuby.

Another thing JRuby will do to help you is to convert its code blocks to anonymous Java classes. For instance the JButton#addActionListener() expects a class that implements the ActionListener Java interface. ActionListener is specified to have just one method: actionPerformed() . Instead of creating a separate JRuby class that implements ActionListener and passing its instance to JButton, just pass a code block to the add_action_listener():

@cut_menu_item.add_action_listener do |event|
  puts 'The Cut menu item clicked'
  puts 'Source: ' + event.source.to_s
  puts 'Action command: ' + event.action_command
end

The code between do and end becomes actionPerformed method body and |event| of an ActionEvent instance. Note that the method name is not specified.

Toolbars

Toolbars are also common elements of a desktop application. Here is an example:

# 08_toolbar.rb

require 'java_imports.rb'

class TopFrame < JFrame

  attr_accessor :main_toolbar, :text_area

  def initialize
    super
    init_components()
    pack()
    set_visible(true)
  end
  
  def init_components()
  
    set_default_close_operation(JFrame::EXIT_ON_CLOSE)
    set_preferred_size(Dimension.new(600, 400))

    content_pane = get_content_pane()
    puts get_layout().class
    
    @text_area = JTextArea.new()
    content_pane.add(@text_area, BorderLayout::CENTER)
    
    @main_toolbar = MainToolBar.new(@text_area)
    content_pane.add(@main_toolbar, BorderLayout::NORTH)    

  end
  
end

class MainToolBar < JToolBar

  attr_accessor :copy_button, :paste_button, :text_container, :clipboard

  def initialize(text_container)
    super()
    init_components()
    set_floatable(false)
    @text_container = text_container
    @clipboard = Toolkit.default_toolkit.system_clipboard
  end
  
  def init_components()
    
    copy_icon = ImageIcon.new('assets/edit-copy.png')
    @copy_button = JButton.new(copy_icon)
    @copy_button.add_action_listener do |event|
      selection = StringSelection.new(@text_container.selected_text)
      @clipboard.set_contents(selection, selection)
    end
    add(@copy_button)
    
    paste_icon = ImageIcon.new('assets/edit-paste.png')
    @paste_button = JButton.new(paste_icon)
    @paste_button.add_action_listener do |event|
      transferable = @clipboard.get_contents(nil)
      if transferable.is_data_flavor_supported(DataFlavor::stringFlavor)
        text = transferable.get_transfer_data(DataFlavor::stringFlavor)
        position = @text_container.caret_position
        @text_container.insert(text, position)
      end
    end
    add(@paste_button)
    
  end

end

SwingUtilities.invoke_later do
  TopFrame.new
end

Toolbars in Java are represented by the JToolBar class. JToolBar is a container and can contain other widgets like JButtons, JCheckBoxes, JRadioButtons, JLabels, JComboBoxes etc. The above toolbar has two buttons for which I used icons from Tango Icon LIbrary. The icons are located in the /assets subdirectory. The first toolbar button (@copy_button) copies the selected text from the JTextArea located on the right hand side of the JFrame to the system clipboard. The second toolbar button (@paste_button) copies the clipboard contents to the JTextArea. Here is what it looks like.

Using JTree

The JTree widget is on of more complex Swing components - it lets you represent hierarchical data. The real strength of JTree is its underlying model. Here is a custom model that can be used with a JTree instance:

require 'java_imports.rb'

class FileSystemTreeModel 
  include TreeModel

  attr_accessor :root
  
  def initialize(path)
    @root = JavaFile.new(path)
  end
  
  def get_root()
    return @root
  end
  
  def is_leaf(node)
    return node.file?
  end
  
  def get_child_count(parent)
    children = parent.list
    if children == nil
      return 0
    else
      return children.length
    end
  end
  
  def get_child(parent, index)
    children = parent.list
    if children == nil or children.length <= index
      return nil
    else
      return parent.list_files[index]
    end
  end
  
  def get_index_of_child(parent, child)
    entries = parent.list
    hash = Hash[entries.map.with_index.to_a]
    puts hash[child]
    return hash[child]
  end
  
  def value_for_path_changed(path, new_value)
  end
  
  def add_tree_model_listener(listener)
  end
  
  def remove_tree_model_listener(listener)
  end

end

The model implements the TreeModel interface to reflect a file system tree on a computer disk. The first method the class has to implement is #get_root(). I implemented root as public so that it can be changed after the model instantiation, but this is not a requirement. The next implemented method returns true if a model node is lief (i.e. has no subnodes). In case a node is a file then it is also a leaf node because it has no children.

#get_child_count() returns subnode count on a given node. This allows for lazy JTree initialization. #get_child(parent, index) returns a child node by index and get_index_of_child(parent, child) returns a childs index. The last  three methods are not implemented because this JTree is not changeable.

Note that the FileSystemTreeModel class doesn't explicitly implement recursion needed to traverse the file system.

The JTree class itself is simple enough:

require 'pathname'
require 'java_imports.rb'
require 'file_system_tree_model.rb'

class TopFrame < JFrame

  attr_accessor :fs_tree, :scroll_pane

  def initialize
    super
    init_components()
    pack()
    set_visible(true)
  end
  
  def init_components()
  
    set_default_close_operation(JFrame::EXIT_ON_CLOSE)
    set_preferred_size(Dimension.new(300, 400))

    content_pane = get_content_pane()
        
    @fs_tree = FileSystemTree.new()
    @scroll_pane = JScrollPane.new(@fs_tree)
    content_pane.add(@scroll_pane, BorderLayout::CENTER)

  end
  
end

class FileSystemTree < JTree

  def initialize
    super()    
    init_components()
  end
  
  def init_components()
    home_dir = System.get_property('user.home')
    model = FileSystemTreeModel.new(home_dir)
    set_model(model)
  end
  
  def convertValueToText(value, selected, expanded, leaf, row, has_focus)
    return value.name
  end
  
end

SwingUtilities.invoke_later do
  UIManager.get_installed_look_and_feels().each do |info|
    if info.name == 'GTK+'
      UIManager.set_look_and_feel(info.class_name)
    end
  end
  TopFrame.new
end

JTree is embedded in a JScrollPane. One thing to mention is that the FileSystemTree overrides the JTree#convertValueToText() method to display a file name as a node text (instead of its path). In this case the method first argument is Java File class and you can use value.name() to display the underlying file name as the node text instead of using the full path.

Here is the JTree in action:

A more involved example

Lastly, a more complex example, a JFrame that has a menu bar, a toolbar, a split pane with a scrool pane on the left hand side and a text area on the right hand side. A JTree is embedded in the scroll pane.

# 10_hand_made_frame.rb

require 'java_imports.rb'
require 'file_system_tree_model.rb'

class TopFrame < JFrame

  attr_accessor :split_pane, :main_menu_bar, :toolbar, :clipboard

  def initialize
    super
    init_components()
    pack()
    set_visible(true)
  end
  
  def init_components()
  
    set_default_close_operation(JFrame::EXIT_ON_CLOSE)
    set_preferred_size(Dimension.new(600, 400))

    content_pane = get_content_pane()
    
    @main_menu_bar = MainMenuBar.new()
    set_jmenu_bar(@main_menu_bar)  
        
    @split_pane = TreeSplitPane.new()
    content_pane.add(@split_pane, BorderLayout::CENTER)
    
    @toolbar = MainToolBar.new()
    content_pane.add(@toolbar, BorderLayout::NORTH)
    
    @clipboard = Toolkit.default_toolkit.system_clipboard    
    
    copy_listener = ActionListener.impl do |event|
      selection = StringSelection.new(@split_pane.text_area.text)
      @clipboard.set_contents(selection, selection)
    end
      
    @toolbar.copy_button.add_action_listener(copy_listener)
    @main_menu_bar.copy_menu_item.add_action_listener(copy_listener)
    
    open_directory_listener = ActionListener.impl do |event|
      
      file_chooser = JFileChooser.new()
      file_chooser.file_selection_mode = JFileChooser::DIRECTORIES_ONLY
      file_chooseraccept_all_file_filter_used = false
      
      return_value = file_chooser.show_open_dialog(self)
      
      if return_value == JFileChooser::APPROVE_OPTION
        root_dir = file_chooser.selected_file.path
        model = FileSystemTreeModel.new(root_dir)
        @split_pane.tree.set_model(model)
      end
      
    end
    
    @main_menu_bar.open_menu_item.add_action_listener(open_directory_listener)
    @toolbar.open_button.add_action_listener(open_directory_listener)  
    
  end
  
end

class TreeSplitPane < JSplitPane

  attr_accessor :tree, :text_area, :scroll_pane

  def initialize
    
    super()
    init_components()    
    
    set_left_component(@scroll_pane)
    set_right_component(@text_area)
    java_send :setDividerLocation, [Java::int], 250
    
  end
  
  def init_components()
  
    @tree = FileSystemTree.new()
    @tree.selection_model.selection_mode = TreeSelectionModel::SINGLE_TREE_SELECTION

    
    @tree.add_tree_selection_listener do |event|
      node = @tree.last_selected_path_component
      if node.instance_of?(JavaFile)
        info = <<-INFO
  Path: #{node.path}
  Name: #{node.name}
  Size: #{node.length}
            INFO
      else
        info = ''
      end
      @text_area.text = info
      
    end
      
    @scroll_pane = JScrollPane.new(@tree)
    
    @text_area = JTextArea.new()
    @text_area.editable = false
    
  end

end

class MainMenuBar < JMenuBar

  attr_accessor :file_menu, :open_menu_item, :exit_menu_item,
    :edit_menu, :copy_menu_item 
  
  def initialize  
    super()
    init_components()
  end
  
  def init_components
    
    @file_menu = JMenu.new('File')
    
    @open_menu_item = JMenuItem.new('Open')
    @file_menu.add(@open_menu_item)
         
    @exit_menu_item =  JMenuItem.new('Exit')
    @exit_menu_item.add_action_listener do |event|
      System.exit(0)
    end
    @file_menu.add(@exit_menu_item)

    add(@file_menu)
            
    @edit_menu = JMenu.new('Edit')
    
    @copy_menu_item = JMenuItem.new('Copy')
    @edit_menu.add(@copy_menu_item)

    add(@edit_menu)

  end
  
end

class MainToolBar < JToolBar

  attr_accessor :open_button, :copy_button

  def initialize()
    super()
    init_components()
    set_floatable(false)    
  end
  
  def init_components()
    
    open_file_icon = ImageIcon.new('assets/document-open.png')
    @open_button = JButton.new(open_file_icon)
    add(@open_button)
    
    copy_icon = ImageIcon.new('assets/edit-copy.png')
    @copy_button = JButton.new(copy_icon)
    add(@copy_button)
        
  end

end

class FileSystemTree < JTree

  def initialize
    super()    
    init_components()
  end
  
  def init_components()
    home_dir = System.get_property('user.home')
    model = FileSystemTreeModel.new(home_dir)
    set_model(model)
  end
  
  def convertValueToText(value, selected, expanded, leaf, row, has_focus)
    return value.name
  end
  
end

SwingUtilities.invoke_later do
  UIManager.get_installed_look_and_feels().each do |info|
    if info.name == 'GTK+'
      UIManager.set_look_and_feel(info.class_name)
    end
  end
  TopFrame.new
end

Here is the frame:

Most of the features are seen in the previous examples, the only thing to mention is:

java_send :setDividerLocation, [Java::int], 250

to resolve ambiguous Java method call. Note that in this case you need to use the Java method name verbatim.

Designer generated forms

Many people prefer using WYSIWYG designer tools for designing Swing forms. It is possible to use a tool such as Eclipse Window Builder to design a form, compile the generated code into a class file, include the file in your JRuby script.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)