I intended to write about the Java Swing layout manager that I now use as my next post; however I got sidetracked on a small experiment that I wanted to post about first.
Almost 5 years ago to the day, I had posted on my previous blog about how to create a Cocoa based calculator using the now defunct Cocoa-Java bridge. At that time, I was working on a commercial tool called XMLFace that allowed you to define your GUI (view) using XML, write your controller code in Java and then run your application (assuming you used standard controls) using either Swing or SWT. The Cocoa/Java calculator example was my first experiments into researching the viability of adding Cocoa bindings to XMLFace. Luckily, due to the lifespan and robustness (or lack thereof) of the Cocoa/Java bridge, I didn't spend much more time on it.
Recently, I've been intrigued with the idea that I could use Ruby to write cross platform applications instead of Java; where the controller logic is in Ruby, and the view, while implemented in Ruby, is platform specific so that the application feels (or actually is) native. Also, this would give me an opportunity to learn a new language after working almost exclusively with Java and C# in the recent years.
I decided last week, waiting in an airport for a delayed flight to arrive, to try to implement that Calculator example that I had written using the Cocoa/Java bridge using MacRuby. It took me the duration of the flight, and a few hours at home afterwards to get it up and running (mostly due to my lack of Ruby experience and due to an initial lack of understand of how MacRuby wraps the Cocoa framework); however I did get it working rather painlessly. Here is the obligitory screenshot:
If your curious, you can download the source code to the original Java source code here: Calculator.java.
The source code to the ruby version is below; however if you want to download it, you can download it here: Calculator.rb.
Note: I've only run this on the version of MacRuby available in their SVN trunk. I'm not sure if it works with the downloadable version (0.2) available on the website.
-
# This is an example of how to use MacRuby to programatically
-
# build a Cocoa application without using InterfaceBuilder.
-
#
-
# Author:: Jon Lipsky (jon.lipsky [at] elevenworks.com)
-
# Copyright:: Copyright (c) 2008, Jon Lipsky
-
# License:: Apache Software License
-
-
framework 'Cocoa'
-
-
class Calculator
-
-
# Define the constants for the operations
-
NONE = -1
-
ADDITION = 0
-
SUBTRACTION = 1
-
MULTIPLICATION = 2
-
DIVISION = 3
-
-
# Define the constants for the window and buttons sizes
-
WINDOW_WIDTH = 220
-
WINDOW_HEIGHT = 280
-
BUTTON_COLUMNS = 4
-
BUTTON_ROWS = 6
-
BUTTON_SPACING = 2
-
WINDOW_MARGIN = 8
-
BUTTON_WIDTH = (WINDOW_WIDTH - (WINDOW_MARGIN * 2) - ((BUTTON_COLUMNS + 1) * BUTTON_SPACING)) / BUTTON_COLUMNS
-
BUTTON_HEIGHT = (WINDOW_HEIGHT - (WINDOW_MARGIN * 2) - ((BUTTON_ROWS + 1) * BUTTON_SPACING)) / BUTTON_ROWS
-
-
# Define other constants
-
MAX_PRECISION = 10
-
-
def initialize
-
@editing = false
-
@result = 0.0
-
@entered_number = 0.0
-
@fractional_digits = 0
-
@operation = 0
-
@decimal_separator = false
-
-
@application = NSApplication.sharedApplication()
-
self.setup
-
@application.run
-
end
-
-
def setup
-
# Create the text field to display the current value
-
x = BUTTON_SPACING + WINDOW_MARGIN
-
y = BUTTON_SPACING * 6 + (BUTTON_HEIGHT * 5) + WINDOW_MARGIN + BUTTON_SPACING * 2
-
-
width = BUTTON_WIDTH * 4 + (BUTTON_SPACING * 3)
-
height = BUTTON_HEIGHT - (BUTTON_SPACING * 2)
-
-
@display = NSTextField.alloc.initWithFrame [x,y,width,height]
-
@display.setEditable(false)
-
@display.setBezeled(true)
-
@display.setBezelStyle(NSBezelBorder)
-
@display.setDrawsBackground(true)
-
@display.setAlignment(NSRightTextAlignment)
-
-
# Create the buttons
-
buttons = Array.new
-
-
buttons <<create_digit_button(0, 0, 0)
-
buttons <<create_button(2, 0, 1, 1, "handle_decimal:", ".", ".", -1)
-
buttons <<create_button(3, 0, 1, 2, "handle_equals:", "=", "=", -1)
-
-
buttons <<create_digit_button(0, 1, 1)
-
buttons <<create_digit_button(1, 1, 2)
-
buttons <<create_digit_button(2, 1, 3)
-
-
buttons <<create_digit_button(0, 2, 4)
-
buttons <<create_digit_button(1, 2, 5)
-
buttons <<create_digit_button(2, 2, 6)
-
buttons <<create_button(3, 2, 1, 1, "handle_operation:", "+", "+", ADDITION)
-
-
buttons <<create_digit_button(0, 3, 7)
-
buttons <<create_digit_button(1, 3, 8)
-
buttons <<create_digit_button(2, 3, 9)
-
buttons <<create_button(3, 3, 1, 1, "handle_operation:", "-", "-", SUBTRACTION)
-
-
buttons <<create_button(0, 4, 1, 1, "handle_clear:", "CL", "c", -1)
-
buttons <<create_button(1, 4, 1, 1, "handle_square_root:", "SQR", "s", -1)
-
buttons <<create_button(2, 4, 1, 1, "handle_operation:", "/", "/", DIVISION)
-
buttons <<create_button(3, 4, 1, 1, "handle_operation:", "*", "*", MULTIPLICATION)
-
-
# Create the window
-
win = NSWindow.alloc.initWithContentRect [100,100,WINDOW_WIDTH,WINDOW_HEIGHT], :styleMask => (NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask), :backing => NSBackingStoreBuffered, :defer=> false
-
win.setTitle("Calculator")
-
win.setDelegate(self)
-
-
# Add the controls to the window
-
buttons.each do |button|
-
win.contentView.addSubview(button)
-
end
-
win.contentView.addSubview(@display)
-
-
win.center
-
win.makeKeyAndOrderFront(win)
-
end
-
-
# Create a digit button for the calculator panel
-
#
-
# aCellX - The X position of the button in the grid
-
# aCellY - The Y position of the button in the grid
-
def create_digit_button(aCellX, aCellY, aDigit)
-
create_button(aCellX, aCellY, aDigit==0?2:1, 1, "handle_digit:", aDigit.to_s, aDigit.to_s, aDigit)
-
end
-
-
# Create a a NSButton positioned in the grid. The grid starts in the
-
# lower left and extends right and up. (0,0) is in the lower left
-
# hand corner.
-
#
-
# aCellX - The X position of the button in the grid
-
# aCellY - The Y position of the button in the grid
-
# aColumns - The number of columns the button should span
-
# aRows - The number of rows the button should span
-
# aMethod - The method name to call when the action occurs
-
# aTitle - The title of the button
-
# aKeyEquivalent - The key equivalent of the button.
-
# aTag - The tag of the button
-
def create_button(aCellX, aCellY, aColumns, aRows, aMethod, aTitle, aKeyEquivalent, aTag)
-
-
x = (BUTTON_SPACING * (aCellX + 1)) + (BUTTON_WIDTH * aCellX) + WINDOW_MARGIN
-
y = (BUTTON_SPACING * (aCellY + 1)) + (BUTTON_HEIGHT * aCellY) + WINDOW_MARGIN
-
-
button_width = BUTTON_WIDTH * aColumns + (BUTTON_SPACING * (aColumns - 1))
-
button_height = BUTTON_HEIGHT * aRows + (BUTTON_SPACING * (aRows - 1))
-
-
vButton = NSButton.alloc.initWithFrame [x, y, button_width, button_height]
-
-
vButton.setTarget(self)
-
vButton.setAction(aMethod)
-
vButton.setTitle(aTitle)
-
vButton.setKeyEquivalent(aKeyEquivalent)
-
vButton.setTag(aTag)
-
vButton.setButtonType(NSMomentaryPushInButton)
-
vButton.setImagePosition(NSNoImage)
-
vButton.setBezelStyle(NSRegularSquareBezelStyle)
-
-
return vButton
-
end
-
-
def windowWillClose(a_notification)
-
Process.exit
-
end
-
-
# Handle that the decimal button was pressed
-
#
-
# @param aButton The button that was pressed
-
def handle_decimal(sender)
-
if !@editing
-
@entered_number = 0
-
@decimal_separator = false
-
@fractional_digits = 0
-
@editing = true
-
end
-
if !@decimal_separator
-
@decimal_separator = true
-
update_display(@fractional_digits)
-
end
-
end
-
-
def handle_equals(sender)
-
case @operation
-
when NONE
-
@result = @entered_number
-
@entered_number = 0
-
@decimal_separator = false
-
@fractional_digits = 0
-
return
-
when ADDITION
-
@result = @result + @entered_number
-
when SUBTRACTION
-
@result = @result - @entered_number
-
when MULTIPLICATION
-
@result = @result * @entered_number
-
when DIVISION
-
if @entered_number == 0
-
report_error("Error")
-
else
-
@result = @result / @entered_number
-
end
-
end
-
-
@entered_number = @result
-
update_display(MAX_PRECISION)
-
-
@operation = NONE
-
@editing = false
-
end
-
-
# Handle that an operation button was pressed
-
#
-
# sender The button that was pressed
-
def handle_operation(sender)
-
if (@operation == NONE)
-
@result = @entered_number
-
@entered_number = 0
-
@decimal_separator = false
-
@fractional_digits = 0
-
@operation = sender.tag
-
else
-
handle_equals(nil)
-
handle_operation(sender)
-
end
-
end
-
-
#Handle that the square root button was pressed
-
#
-
#sender The button that was pressed
-
def handle_square_root(sender)
-
if (@operation == NONE)
-
@result = Math.sqrt(@entered_number)
-
@entered_number = @result
-
-
@editing = true
-
update_display(MAX_PRECISION)
-
@editing = false
-
@operation = NONE
-
else
-
handle_equals(nil)
-
handle_square_root(sender)
-
end
-
end
-
-
# Handle that a digit button was pressed
-
#
-
# sender The button that was pressed
-
def handle_digit(sender)
-
digit = sender.tag.to_i
-
puts("digit pressed: #{digit}")
-
-
if !@editing
-
@entered_number = 0
-
@decimal_separator = false
-
@fractional_digits = 0
-
@editing = true
-
end
-
-
if @decimal_separator
-
@entered_number = @entered_number + digit * (0.1**(1 + @fractional_digits))
-
@fractional_digits+=1
-
else
-
@entered_number = @entered_number * 10 + digit
-
-
# Check overflow
-
if (@entered_number> 10**15)
-
report_error("Overflow Error")
-
return
-
end
-
end
-
-
update_display(@fractional_digits)
-
end
-
-
# Handle that the clear button was pressed
-
#
-
# sender The button that was pressed
-
def handle_clear(sender)
-
-
@result = 0
-
@enteredNumber = 0
-
@operation = NONE
-
@fractionalDigits = 0
-
@decimalSeparator = false
-
@editing = false
-
-
update_display(@fractional_digits)
-
end
-
-
#Update the number displayed in the text field
-
def update_display(a_fractional_digits)
-
if !@editing
-
value=""
-
elsif (@decimal_separator) || (a_fractional_digits>0)
-
if a_fractional_digits == 0
-
value = "#{@entered_number}."
-
else
-
value = @entered_number.to_s
-
end
-
else
-
value = @entered_number.to_i.to_s
-
end
-
-
puts("display=#{value}")
-
@display.setStringValue(value)
-
end
-
-
# Report an error to the user
-
#
-
# a_message The message to display in the text input field
-
def report_error(a_message)
-
@display.setStringValue(a_message)
-
end
-
end
-
-
## Create a new calculator when this ruby script is run
-
Calculator.new
Since this process was rather painless, I'm thinking about attempting to port an existing Java Swing application that I wrote for my personal use over to Cocoa + MacRuby. In the end, I think the hardest part will be giving up the nice code completion and language support I get from IntelliJ when doing Java development. For this experiment I used Netbeans as my Ruby editor; however I might give a few others a try. (I did try XCode shortly, but discarded it since I didn't want/need to use Interface Builder.)
I'm sure I'll have more to post on this subject as time goes on.









