Ruby Inox Part 7: Native

2009
05.29

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:
  1. Ruby Inox Part 6: Widget We are steering toward a graphical user interface. In this...
  2. Ruby Inox Part 5: Component Something promised is something due. Only two days have passed...
  3. Ruby Inox Part 4.1: Property In Ruby Inox Part 4: Property I described I would...
  4. Unified Ruby GUI Ruby is incredibly adaptable to a multitude of environments, operating...
  5. Ruby Inox Part 4: Property Last time, we took a look at Actions. A standard...

Tags: , , , , , , ,

Comments are closed.


Ironic Wolf is Digg proof thanks to caching by WP Super Cache