Skip to content
Switch branches/tags
Go to file
Cannot retrieve contributors at this time
442 lines (331 sloc) 13.3 KB
# FrameInRuby
# Copyright, Keith Bennett, 2009
require 'java'
# In Java, classes in the java.lang package do not need to be imported.
# In JRuby, they do, so that JRuby can distinguish it from a Ruby constant.
import java.lang.Double
import java.lang.NumberFormatException
# In the Java version of the program, it was necessary to import Dimension
# because it was needed to define the type of a variable. In Ruby, it is
# not necessary to specify the type of a variable's value, so we never
# use the term 'Dimension', and there is no need to import it.
import java.awt.BorderLayout
import java.awt.Event
import java.awt.GridLayout
import java.awt.Toolkit
import java.awt.event.KeyEvent
import javax.swing.AbstractAction
import javax.swing.Action
import javax.swing.BorderFactory
import javax.swing.JButton
import javax.swing.JFrame
import javax.swing.JLabel
import javax.swing.JMenu
import javax.swing.JMenuBar
import javax.swing.JPanel
import javax.swing.JTextField
import javax.swing.KeyStroke
# In Java, all classes specified in the same package as the file being compiled
# will be found by the compiler without explicitly importing them. In contrast,
# in Ruby, we need to "require" files, even though they are in the same
# directory as the file being interpreted.
# =======================================================================
# TemperatureConversion module
# =======================================================================
module TemperatureConversion
# Converts a temperature from Fahrenheit to Celsius.
def f2c(f)
((f - 32) * 5.0 / 9.0)
# Converts a temperature from Celsius to Fahrenheit.
def c2f(c)
(c * 9.0 / 5.0) + 32
# =======================================================================
# FrameInRuby class
# =======================================================================
# Main application frame containing menus, text fields for the temperatures,
# and buttons. The menu items and buttons are driven by shared actions,
# which are disabled and enabled based on the state of the text fields.
# Temperatures can be converted in either direction, Fahrenheit to Celsius
# or Celsius to Fahrenheit. The convert actions (F2C, C2F) are each enabled
# when the respective source text field (F for F2C, C for C2F) contains text
# that can successfully be parsed to a Double.
# The Clear action is enabled when there is any text in either of the text fields.
class FrameInRuby < JFrame
include TemperatureConversion
attr_accessor :fahr_text_field, :cels_text_field
# These actions will be shared by menu items and buttons.
attr_accessor :f2c_action, :c2f_action, :clear_action, :exit_action
# Sets up frame with all components, centers on screen,
# and sets it up to exit the program when closed.
def initialize
super "Fahrenheit <--> Celsius Converter"
# In Ruby, the double colon is used to refer to static data members
# (class variables as opposed to instance variables):
getContentPane.add create_converters_panel, BorderLayout::CENTER
getContentPane.add create_buttons_panel, BorderLayout::SOUTH
setJMenuBar create_menu_bar
getContentPane.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12))
# Creates the Fahrenheit and Celsius text fields.
def create_text_fields
# Note that in JRuby we can use lambdas as lightweight nested functions
# to increase DRYness:
create_field = lambda do
f =;
f.setToolTipText "Input a temperature."
self.fahr_text_field =
self.cels_text_field =
# Creates the panel containing the Fahrenheit and Celsius labels
# and text fields.
def create_converters_panel
# Another lambda as lightweight nested function:
create_an_inner_panel = lambda {, 1, 5, 5))}
labelPanel =
labelPanel.add("Fahrenheit: "))
labelPanel.add("Celsius: "))
textFieldPanel =
panel =
panel.add(labelPanel, BorderLayout::WEST)
panel.add(textFieldPanel, BorderLayout::CENTER)
# Creates the menu bar with File, Edit, and Convert menus.
def create_menu_bar
menubar =
file_menu = "File"
file_menu.add exit_action
menubar.add file_menu
edit_menu = "Edit"
edit_menu.add clear_action
menubar.add edit_menu
convert_menu = "Convert"
convert_menu.add f2c_action
convert_menu.add c2f_action
menubar.add convert_menu
# Sets up the listeners that will determine the enabled states of the
# temperature conversion (f2c and c2f) and clear actions.
def setup_text_field_listeners
# In Java, a separate class is required for each type
# of action. In contrast, Ruby supports code blocks, lambdas, and procs.
# This allows us to use instances of the same SimpleDocumentListener
# class simply creating them with different lambdas.
clear_enabler = lambda {
ctext = cels_text_field.getText
ftext = fahr_text_field.getText
should_enable =
(ctext && ctext.length > 0) ||
(ftext && ftext.length > 0)
clear_action.setEnabled should_enable
fahr_text_field.getDocument.addDocumentListener( do
f2c_action.setEnabled double_string_valid?(fahr_text_field.getText)
cels_text_field.getDocument.addDocumentListener( do
c2f_action.setEnabled double_string_valid?(cels_text_field.getText)
clear_document_listener = &clear_enabler
fahr_text_field.getDocument.addDocumentListener clear_document_listener
cels_text_field.getDocument.addDocumentListener clear_document_listener
# Sets up the temperature conversion, clear, and exit actions, including
# name, behavior, tooltip, and accelerator key.
def setup_actions
self.f2c_action ="Fahr --> Cels",
Action::SHORT_DESCRIPTION => "Convert from Fahrenheit to Celsius",
KeyStroke.getKeyStroke(KeyEvent::VK_S, Event::CTRL_MASK),
f2c_action.setEnabled false
self.c2f_action ="Cels --> Fahr",
Action::SHORT_DESCRIPTION => "Convert from Celsius to Fahrenheit",
KeyStroke.getKeyStroke(KeyEvent::VK_T, Event::CTRL_MASK),
& c2f_behavior)
c2f_action.setEnabled false
self.exit_action ="Exit",
Action::SHORT_DESCRIPTION => "Exit this program",
KeyStroke.getKeyStroke(KeyEvent::VK_X, Event::CTRL_MASK))\
do |event|
java.lang.System::exit 0
self.clear_action ="Clear",
Action::SHORT_DESCRIPTION => "Reset to empty the temperature fields",
KeyStroke.getKeyStroke(KeyEvent::VK_L, Event::CTRL_MASK),
clear_action.setEnabled false
# Creates the button panel laid out such that the buttons will always
# stay at the right side of the window.
def create_buttons_panel
innerPanel =, 0, 5, 5))
innerPanel.add( f2c_action)
innerPanel.add( c2f_action)
innerPanel.add( clear_action)
innerPanel.add( exit_action)
outerPanel =
outerPanel.add innerPanel, BorderLayout::EAST
outerPanel.setBorder(BorderFactory.createEmptyBorder(10, 0, 0, 0))
# Defines and returns the behavior for the Fahrenheit to Celsius conversion.
def f2c_behavior
lambda do |event|
text = fahr_text_field.getText
if text != nil and text.length > 0
fahr = Double::parseDouble(text)
cels = f2c fahr
cels_text = Double::toString(cels)
cels_text_field.setText cels_text
# Defines and returns the behavior for the Celsius to Fahrenheit conversion.
def c2f_behavior
lambda do |event|
text = cels_text_field.getText
if text != nil and text.length > 0
cels = Double::parseDouble(text)
fahr = c2f cels
fahr_text = Double::toString(fahr)
fahr_text_field.setText fahr_text
# Defines and returns the behavior for the clear action.
def clear_behavior
lambda do |event|
fahr_text_field.setText ''
cels_text_field.setText ''
# A nice touch in Ruby is the ability to name functions with names
# that end in "?" to indicate that they return a boolean value.
def double_string_valid?(str)
Double::parseDouble(str) # convert but discard converted value
is_valid = true
rescue NumberFormatException
is_valid = false
# Centers the window on the screen based on the graphical information
# reported by the java.awt.Toolkit. Note that in some cases, such as
# use of multiple nonmirrored displays, the position may be odd, since
# the toolkit may report the sum of all display space across all available
# displays.
def centerOnScreen
screenSize = Toolkit.getDefaultToolkit().getScreenSize()
componentSize = getSize()
new_x = (screenSize.getWidth() - componentSize.getWidth()) / 2
new_y = (screenSize.getHeight() - componentSize.getHeight()) / 2
setLocation(new_x, new_y)
# =======================================================================
# SimpleDocumentListener class
# =======================================================================
# Simple implementation of javax.swing.event.DocumentListener that
# enables specifying a single code block that will be called
# when any of the three DocumentListener methods are called.
# Note that unlike Java, where it is necessary to subclass the abstract
# Java class SimpleDocumentListener, we can merely create an instance of
# the Ruby class SimpleDocumentListener with the code block we want
# executed when a DocumentEvent occurs. This code can be in the form of
# a code block, lambda, or proc.
import javax.swing.event.DocumentListener
class SimpleDocumentListener
# This is how we declare that this class implements the Java
# DocumentListener interface in JRuby:
include DocumentListener
attr_accessor :behavior
def initialize(&behavior)
self.behavior = behavior
def changedUpdate(event); event; end
def insertUpdate(event); event; end
def removeUpdate(event); event; end
# =======================================================================
# SwingAction class
# =======================================================================
# When running FrameInRuby, this will generate a warning because
# it is already imported in FrameInRuby. In JRuby, unfortunately,
# imports of Java classes are not confined to the file
# in which they are specified; once you import a class,
# it will be imported for other classes as well.
# This may be fixed in a future version of JRuby.
import javax.swing.AbstractAction
# This class enables the specification of a Swing action
# in a format natural to Ruby.
# It takes and stores a code block, lambda, or proc as the
# action's behavior, so there is no need to define a new class
# for each behavior. Also, it allows the optional specification
# of the action's properties via the passing of hash entries,
# which are effectively named parameters.
class SwingAction < AbstractAction
attr_accessor :behavior
# Creates the action object with a behavior, name, and options:
# behavior - a behavior can be a code block, lambda,
# or a Proc.
# name - this is the name that will be used for the menu option,
# button caption, etc. Note that if an app is internationalized,
# the name will vary by locale, so it is better to identify an action
# by the action instance itself rather than its name.
# options - these are hash entries that will be passed to
# AbstractAction.putValue(). Keys should be constants from the
# javax.swing.Action interface, such as Action.SHORT_DESCRIPTION.
# Ruby allows hash entries to passed as the last parameters to a
# function, and they can be accessed inside the method as a single
# hash object.
# Example:
# self.exit_action =
# "Exit",
# Action::SHORT_DESCRIPTION => "Exit this program",
# KeyStroke.getKeyStroke(KeyEvent::VK_X, Event::CTRL_MASK)) do
# System.exit 0
# end
def initialize(name, options=nil, &behavior)
super name
options.each { |key, value| putValue key, value } if options
self.behavior = behavior
def actionPerformed(action_event) action_event
# =======================================================================
# Program entry point:
# ======================================================================= true