Originally this article formed one article with Ruby Inox Part 6: Widget as everything had to evolve concurrently of each other. But decided later to split them up into separated article to underline the importance of the xxxBase line of classes and there major difference to the xxxNative line of classes.
xxxBase
The priority of these classes is to be as general and uniform as possible so they can be all used in much the same way and give a great relieve to the developer that will use them. That’s one major factor for speeding up the development process.
By general I mean that every class should solve one specific problem and introduce as few properties and actions as required to solve that problem.
Uniform a widget should behave like his nearest existing cousin. This is enforced by the code reuse within the xxxBase class hierarchy, but still to keep in mind.
xxxNative
A small subset of general, uniform, reusable widgets is better than haveing to write 10 different classes for 10 different toolkits 10 times for every project your write.
Instead you get a small general toolkit to give face to your ruby script, on every supported toolkit.
So these xxxNative classes focus on the end user experience. These classes implements the xxxBase and should produce exactly the same behavior across different native solutions. Hereby we limit our choice of native Controls/Widgets to a small subset which are well understood and supported on every native toolkit.
In this article I present the implementation of ScreenBase, ApplicationBase, WindowBase and ButtonBase for Cocoa, Qt.
Screen
For Cocoa:
class Screen < ScreenBase
def native
OSX::NSScreen.mainScreen
end
protected
def create!
self.frame = native.frame
end
end
For Qt:
class Screen < ScreenBase
def native
Qt::Application.desktop
end
protected
def create
self.frame = native.screenGeometry
end
end
That's the whole magic for now. We have a Native Screen class with the size of the screen we will output our windows. For now this is a simplified approch that we need to expand to acknowledge multiple displays, multiple desktops and so on.
Screen.instance returns the singleton instance for a screen and the frame is the size of the screen. So we respect the uniform way of WidgetBase.
Application
The implementation for Cocoa is straightforward. As we find a one to one mapping of the functionality we are expecting. Cocoa expects application to be a singleton, and in the future we have to adapt this behavior of cocoa to the behavior we want of multiple application per interpreter.
class Application < ApplicationBase
def native
OSX::NSApplication.sharedApplication()
end
protected
def create!; native.activateIgnoringOtherApps(true);
def run!; native.run; end
def dispose!
# exception to the rule, terminate first the children an than the parent.
super
self.native.terminate(nil)
end
end
Qt gives us much the same idioms as Cocoa and doesn't limit us to one instance of application. Here the Qt code:
class Application < ApplicationBase
def native; @app; end
protected
def create!
@app = Qt::Application.new(ARGV)
end
def run!; native.exec(); end
def dispose!
# exception to the rule, terminate first the children an than the parent.
super
self.native.quit()
end
end
Window
Let's start with the window code for Cocoa:
# We reverse Cocoa's inverse coordinate style
class FlippedView < OSX::NSView
def isFlipped
true
end
end
# responds to action of NSWindow
class WindowDelegate < OSX::NSObject
def setTarget(target)
@target = target
end
def windowWillClose(notification)
@target.send :dispose
end
end
class Window < WindowBase
def create!
styleMask = OSX::NSTitledWindowMask + OSX::NSClosableWindowMask +
OSX::NSMiniaturizableWindowMask + OSX::NSResizableWindowMask
@window = OSX::NSWindow.alloc.initWithContentRect_styleMask_backing_defer(
self.frame.to_a, styleMask, OSX::NSBackingStoreBuffered, false)
self.tile = self.class.properties[:title].default
delegate = WindowDelegate.alloc.init
delegate.setTarget self
@window.setDelegate(delegate)
@window.setContentView(FlippedView.alloc.initWithFrame(@window.contentView.frame))
end
def native
@window
end
protected
def child_added!(obj)
super(obj)
@window.contentView.addSubview(obj.native)
end
def get_title; @window.title.to_s; end
def set_title(v); @window.setTitle(v); end
def visible_changed!
if self.visible then
@window.makeKeyAndOrderFront(@window)
else
@window.orderOut(nil)
end
end
def frame_changed!
@window.setFrame_display(self.frame.to_a,true)
super
end
end
TODO create a more general approach for wrapping Cocoa's voodoo delegates call and event notifications. But beside that this code is working. And behaves like we want a window to behave.
I won't go into details for Cocoa as this code will hopefully change and get cleaner. So next is the Qt class:
class Window < WindowBase
def native; @window; end
protected
def create!
@window = Qt::Widget.new(nil)
t = self.class.properties[:title].default
@window.setGeometry(frame.origin.x, frame.origin.y, frame.size.width, frame.size.height)
@window.setWindowTitle(t)
end
def dispose!; @window.delete; super; end
def child_added!(obj)
super(obj)
obj.native.setParent(@window)
end
def get_title; @window.windowTitle.to_s; end
def set_title(v); @window.setWindowTitle(v); end
def visible_changed!
if self.visible then
@window.show()
else
@window.hide()
end
end
def frame_changed!
@window.setGeometry(frame.origin.x, frame.origin.y, frame.size.width, frame.size.height)
super
end
end
Perhaps it is only me but I find that Qt still reflects better our intentions and deliver a more compact code than Cocoa. Whatever the two implmentations above works.
Button
First cocoa:
class ButtonDelegate < OSX::NSObject
def setTarget(target)
@target = target
end
def clicked(sender)
@target.send :clicked
end
end
class Button < ButtonBase
def native; @ button; end
def create!
@button = OSX::NSButton.alloc.initWithFrame(self.frame.to_a)
del = ButtonDelegate.alloc.init
del.setTarget self
with @button do
setAction :clicked
setTarget del
setBezelStyle OSX::NSRoundedBezelStyle
end
end
def get_title; native.title.to_s; end
def set_title(v); native.setTitle(v); end
def dispose!; native.release; super; end
def visible_changed!(ov, v)
native.setHidden(v) if ov != v
end
def frame_changed!
native.setFrame(self.frame.to_a)
end
end
Slowly we see a pattern emerge how to wrap Cocoa Objects into Widgets and this pattern will be catched in metaprogramming in a following article, but for now this code works as expected and produces the same result as the Qt code belove:
class ButtonDelegate < Qt::Widget
slots 'perform_click()'
def setTarget(obj); @target = obj; end
def perform_click; @target.clicked; end
end
class Button < ButtonBase
def native; @button; end
protected
def create!
@button = Qt::PushButton.new('Button', nil)
delegate = ButtonDelegate.new
delegate.setTarget(self)
Qt::Object.connect @button, SIGNAL('clicked()'), delegate, SLOT('perform_click()')
end
def get_title; native.title.to_s; end
def set_title(v); native.setWindowTitle(v); end
def dispose!; native.delete; super; end
def visible_changed!(ov, v); native.setHidden(v) if ov != v; end
def frame_changed!; native.setGeometry(*self.frame.to_a); end
end
The goal we set out to reach is attained. An with a little bit of sugar,
# special case for application as it doe #
def application(&block)
app = Application.instance
with(app, &block)
app
end
def self.define_sugar_for(*syms)
syms.each do |sym|
class_eval %{
def #{sym.to_s.downcase.to_sym}(&block)
obj = #{sym}.new(self)
with(obj, &block)
self.add_child(obj) if self.container?
obj
end
}
end
end
define_sugar_for :Window, :Button # :Label, :Edit
we can already do amazing constructs.
First working example
The Hello World example with a button:
require 'inox'
include Inox
# Evaluates the block within the singleton instance Application
application {
# creates a window and evalutes the block within its instance
window {
# set the title by method calling
# setting the title by assignment not possible due
# to the duality of local variables
title 'Hello World'
# evaluates the block within the instance of the property frame
# equivalent of doing something like:
#
# f = self.frame
# f.size = [200, 120]
# f.center = parent.bounds.center
# self.frame = f
#
# because f is of class Rect we need to get or assign an whole Rect
# for the frame property to get updated.
frame {
# assignement by method call not possible since size/center
# are not properties
self.size = [200, 120]
self.center = $parent.bounds.center
}
# create a button and evaluates within its instance
button {
title 'Quit'
frame {
self.center = $parent.bounds.center
}
# callback called when button is clicked
on_clicked { Application.instance.dispose }
}.show
}.show
}.run
To be continued
Hope this article has show the fun we are heading to. But there is still a lot of work to be done before Inox reaches a useable state. It reaches completeness when it supports at least: Cocoa, Qt, Kde, Gtk2, Windows(somehow), Agnostic(general doit ourself way), basic dialogs support for Ncurse.
The first targeted application written with Inox should be a GUI editor for Inox, which can produce and manage Inox code, in script style and MVC style.
But for the next article there is still a more humble step to be taken first. A great deal of code cleanup and re-factoring to settle down on a definitive core code so that you can download and tryout the first examples.
Ruby Inox on GitHub
This file is part of the Ruby Inox project. Inox is a work in progress and this file might not be up to date. The status and latest files are available on GitHub
Related posts:
- Ruby Inox Part 6: Widget We are steering toward a graphical user interface. In this...
- Ruby Inox Part 5: Component Something promised is something due. Only two days have passed...
- Ruby Inox Part 4.1: Property In Ruby Inox Part 4: Property I described I would...
- Unified Ruby GUI Ruby is incredibly adaptable to a multitude of environments, operating...
- Ruby Inox Part 4: Property Last time, we took a look at Actions. A standard...
Tags: Component, GUI, Inox, OO, Part 5, Ruby, Ruby Inox, Toolkit