From: Tom Sawyer Date: 2002-07-24T09:31:19+09:00 Subject: Re: GUI's and the Rouge, Part III (grand finally) 2/2 --=-TqJqe3LH5O9FHN0zIzFn Content-Type: text/plain Content-Transfer-Encoding: 7bit well, i just read over my last post. sorry about all the typos. i needed a break and didn't bother to proofread. but i'm sure you can make out what i was trying to say where i blurbed. okay, now to the good stuff! i spent the last few days hacking out the first steps in the creation of a super-duper ruby gui api, which i have dubbed GUtopIa (thanks rich). just to kick things off here's a small clip of one of my examples (note MyModels is drawn from an unshown require): INTERFACE = 'PDA' fruitapp = FruitApp.new mygui = GUtopIa::GUI.new('Fruity Fun Time') names = { 'awindow' => fruitapp.appname, 'aradio' => fruitapp.fruitbasket.name } bindings = { fruitapp.fruitbasket.name => { :value => Proc.new { fruitapp.fruit.isa }, :value= => Proc.new { |x| fruitapp.fruit.isa = x }, :items => Proc.new { fruitapp.fruitbasket.contains }, :event_change => Proc.new { fruitapp.pickafruit } } } model = MyModels.pick_model(mygui, INTERFACE) mygui.transaction(bindings, names, &model) mygui.build the idea is that you take an instance of your application (FruitApp in this case), completely untouched --you don't have to add or change a single line of code (unless you want too ;-) you then create a model. a model (not shown in example) is an executable description of the fixed aspects of your gui. it has no references to any part of your app (although you can do so if you don't care about SOC) it is a completely independent entity and as such can be reused in other fitting contexts. you can also draw on multiple sub-models. then you create a binding (as seen above). this describes how an instance of your application "interfaces" with your model. and then presto out comes you gui-ized app. also note that GUtopIa uses a transaction mechinism. this effectivly bundles an executable model into a single execution. not only does this increase speed, but also offers a means to rollback the effects of a transaction should there be an error, adding much greater stability to your gui-ized application. some other things to note: bindings are actually integrated directly into the class models of the widgets. when a widget is created it is subclassed into a specialized class of that widget which includes methods to react to the bindings described. i've attached my work thus far so you can checkout all of this in detail. be sure to run example.rb (the snipet above) and check out orig-example.rb, which is a non-SOC version of the original Rouge/Utopia example. then check out gutopia.rb which is the heart of the matter. so that's the the word on implementation. i've taken the advice of others and i have worked out what i think is shaping up to be the best gui api design in existence. Curt Hibbs has offered me a wiki for this project. (thanks curt) i will take him up on it. now i hope i can get YOU enthused enough to help. there's real potential here, to not only create an awsome gui api for ruby that is unique and powerful, but also create a full-fledge game sdk! this thing looks to be awesome indeed. i need people to take on a few tasks. a ruby wrapper needs to be created for ClanLib and we need to build a Themes and Behaviors manager. someone needs to hack out the communications layer (i've tried it with DRb and ROMP but they bomb). and i could use some help finishing off the main api. if we can pull together on this, ruby, our favorite little scripting language, will have its very own GUI to rival all others! as you might be able to tell, i'm very excited! i hope you are too! thanks for your time and hope to hear from you, ~transami :-) --=-TqJqe3LH5O9FHN0zIzFn Content-Disposition: attachment; filename=example-app.rb Content-Transfer-Encoding: quoted-printable Content-Type: text/x-ruby; name=example-app.rb; charset=ANSI_X3.4-1968 # GUtopIa Example - Application Layer # Copyright (c) 2002 Thomas Sawyer, Ruby License class FruitApp attr_accessor :appname attr_reader :fruitbasket, :fruit, :basketname def initialize @appname =3D 'Fruity Fun' @fruitbasket =3D FruitBasket.new @fruit =3D Fruit.new end def pickafruit puts "You got a #{@fruit.isa}!" end end class FruitBasket attr_accessor :name, :contains def initialize @name =3D 'Fruit Basket' @contains =3D ["None", "Apple", "Orange", "Banana"] end end class Fruit attr_accessor :isa def initialize @isa =3D "None" end end --=-TqJqe3LH5O9FHN0zIzFn Content-Disposition: attachment; filename=example-drb.rb Content-Transfer-Encoding: quoted-printable Content-Type: text/x-ruby; name=example-drb.rb; charset=ANSI_X3.4-1968 # GUtopIa Daemon Example - Client # Copyright (c) 2002 Thomas Sawyer, Ruby License require 'drb' require 'example-app' require 'example-pre' # Configure interface INTERFACE =3D 'PDA' puts "\nINTERFACE: #{INTERFACE}\n" # Make GUI App fruitapp =3D FruitApp.new DRb.start_service() mygui =3D DRbObject.new(nil, 'druby://localhost:8080') names =3D { 'awindow' =3D> fruitapp.appname, 'aradio' =3D> fruitapp.fruitbasket.name } bindings =3D { fruitapp.fruitbasket.name =3D> {=20 :value =3D> Proc.new { fruitapp.fruit.isa }, :value=3D =3D> Proc.new { |x| fruitapp.fruit.isa =3D x }, :items =3D> Proc.new { fruitapp.fruitbasket.contains }, :event_change =3D> Proc.new { fruitapp.pickafruit } } } model =3D MyModels.pick_model(mygui, INTERFACE) mygui.transaction(bindings, names, &model) mygui.build # Run some tests puts puts "MYGUI " p mygui puts puts "MYGUI WIDGETS" mygui.widgets.each_key do |k| puts k end puts puts "THE WINDOW" p mygui.widgets[fruitapp.appname].class.superclass puts "name: #{fruitapp.appname}" puts "width: #{mygui.widgets[fruitapp.appname].width}" puts "height: #{mygui.widgets[fruitapp.appname].height}" puts puts "THE RADIO" p mygui.widgets[fruitapp.fruitbasket.name].class.superclass puts "name: #{fruitapp.fruitbasket.name}" puts "items: #{mygui.widgets[fruitapp.fruitbasket.name].items.join(', ')}" puts "value: #{mygui.widgets[fruitapp.fruitbasket.name].value}" puts puts puts "TRY OUT!" puts puts "Current state of fruit and its widget:" puts "fruit=3D#{fruitapp.fruit.isa}" puts "widget=3D#{mygui.widgets[fruitapp.fruitbasket.name].value}" puts puts "Changing widget to: #{mygui.widgets[fruitapp.fruitbasket.name].value = =3D 'Banana'}" puts "fruit=3D#{fruitapp.fruit.isa}" puts puts "Changing fruit to: #{fruitapp.fruit.isa =3D 'Orange'}" puts "widget=3D#{mygui.widgets[fruitapp.fruitbasket.name].value}" puts puts "Trigger event_change event:" mygui.widgets[fruitapp.fruitbasket.name].event_change puts puts "Changing fruit to: #{fruitapp.fruit.isa =3D 'Apple'}" puts "widget=3D#{mygui.widgets[fruitapp.fruitbasket.name].value}" puts puts "Changing widget to: #{mygui.widgets[fruitapp.fruitbasket.name].value = =3D 'Banana'}" puts "fruit=3D#{fruitapp.fruit.isa}" puts --=-TqJqe3LH5O9FHN0zIzFn Content-Disposition: attachment; filename=example-pre.rb Content-Transfer-Encoding: quoted-printable Content-Type: text/x-ruby; name=example-pre.rb; charset=ANSI_X3.4-1968 # GUtopIa Example - Models # Copyright (c) 2002 Thomas Sawyer, Ruby License require 'gutopia' module MyModels def MyModels.pick_model(gui, which) case which when 'PC' return MyModels.pc_model(gui) when 'PDA' return MyModels.pda_model(gui) end end private # PC model using serial notatation (slower) def MyModels.pc_model(gui) Proc.new { =20 # RadioBox r =3D gui.widgetFactory(:RadioBox, 'aradio') # Window w =3D gui.widgetFactory(:Window, 'awindow') w.width =3D 640 w.height =3D 400 w.grid =3D [ [ r ] ] =20 } end # PDA model using parallel notation (faster) def MyModels.pda_model(gui) Proc.new { =20 # RadioBox r =3D gui.widgetFactory(:RadioBox, 'aradio') # Window w =3D gui.widgetFactory(:Window, 'awindow', :width =3D> 320, :height =3D> 240, :grid =3D> [ [ r ] ] ) =20 } =20 end end # MyModels --=-TqJqe3LH5O9FHN0zIzFn Content-Disposition: attachment; filename=example-romp.rb Content-Transfer-Encoding: quoted-printable Content-Type: text/x-ruby; name=example-romp.rb; charset=ANSI_X3.4-1968 # GUtopIa Daemon Example - Client # Copyright (c) 2002 Thomas Sawyer, Ruby License require 'romp/romp' require 'example-app' require 'example-pre' # Configure interface INTERFACE =3D 'PDA' puts "\nINTERFACE: #{INTERFACE}\n" # Make GUI App fruitapp =3D FruitApp.new client =3D ROMP::Client.new('tcpromp://localhost:8080') mygui =3D client.resolve('gutopia-romp') names =3D { 'awindow' =3D> fruitapp.appname, 'aradio' =3D> fruitapp.fruitbasket.name } bindings =3D { fruitapp.fruitbasket.name =3D> {=20 :value =3D> Proc.new { fruitapp.fruit.isa }, :value=3D =3D> Proc.new { |x| fruitapp.fruit.isa =3D x }, :items =3D> Proc.new { fruitapp.fruitbasket.contains }, :event_change =3D> Proc.new { fruitapp.pickafruit } } } model =3D MyModels.pick_model(mygui, INTERFACE) mygui.transaction(bindings, names, &model) mygui.build # Run some tests puts puts "MYGUI " p mygui puts puts "MYGUI WIDGETS" mygui.widgets.each_key do |k| puts k end puts puts "THE WINDOW" p mygui.widgets[fruitapp.appname].class.superclass puts "name: #{fruitapp.appname}" puts "width: #{mygui.widgets[fruitapp.appname].width}" puts "height: #{mygui.widgets[fruitapp.appname].height}" puts puts "THE RADIO" p mygui.widgets[fruitapp.fruitbasket.name].class.superclass puts "name: #{fruitapp.fruitbasket.name}" puts "items: #{mygui.widgets[fruitapp.fruitbasket.name].items.join(', ')}" puts "value: #{mygui.widgets[fruitapp.fruitbasket.name].value}" puts puts puts "TRY OUT!" puts puts "Current state of fruit and its widget:" puts "fruit=3D#{fruitapp.fruit.isa}" puts "widget=3D#{mygui.widgets[fruitapp.fruitbasket.name].value}" puts puts "Changing widget to: #{mygui.widgets[fruitapp.fruitbasket.name].value = =3D 'Banana'}" puts "fruit=3D#{fruitapp.fruit.isa}" puts puts "Changing fruit to: #{fruitapp.fruit.isa =3D 'Orange'}" puts "widget=3D#{mygui.widgets[fruitapp.fruitbasket.name].value}" puts puts "Trigger event_change event:" mygui.widgets[fruitapp.fruitbasket.name].event_change puts puts "Changing fruit to: #{fruitapp.fruit.isa =3D 'Apple'}" puts "widget=3D#{mygui.widgets[fruitapp.fruitbasket.name].value}" puts puts "Changing widget to: #{mygui.widgets[fruitapp.fruitbasket.name].value = =3D 'Banana'}" puts "fruit=3D#{fruitapp.fruit.isa}" puts --=-TqJqe3LH5O9FHN0zIzFn Content-Disposition: attachment; filename=example.rb Content-Transfer-Encoding: quoted-printable Content-Type: text/x-ruby; name=example.rb; charset=ANSI_X3.4-1968 # GUtopIa Example - Presentation # Copyright (c) 2002 Thomas Sawyer, Ruby License require 'gutopia' require 'example-app' require 'example-pre' # Configure interface INTERFACE =3D 'PDA' puts "\nINTERFACE: #{INTERFACE}\n" # Make GUI App fruitapp =3D FruitApp.new mygui =3D GUtopIa::GUI.new('Fruity Fun Time') names =3D { 'awindow' =3D> fruitapp.appname, 'aradio' =3D> fruitapp.fruitbasket.name } bindings =3D { fruitapp.fruitbasket.name =3D> {=20 :value =3D> Proc.new { fruitapp.fruit.isa }, :value=3D =3D> Proc.new { |x| fruitapp.fruit.isa =3D x }, :items =3D> Proc.new { fruitapp.fruitbasket.contains }, :event_change =3D> Proc.new { fruitapp.pickafruit } } } model =3D MyModels.pick_model(mygui, INTERFACE) mygui.transaction(bindings, names, &model) mygui.build # Run some tests puts puts "MYGUI " p mygui puts puts "MYGUI WIDGETS" mygui.widgets.each_key do |k| puts k end puts puts "THE WINDOW" p mygui.widgets[fruitapp.appname].class.superclass puts "name: #{fruitapp.appname}" puts "width: #{mygui.widgets[fruitapp.appname].width}" puts "height: #{mygui.widgets[fruitapp.appname].height}" puts puts "THE RADIO" p mygui.widgets[fruitapp.fruitbasket.name].class.superclass puts "name: #{fruitapp.fruitbasket.name}" puts "items: #{mygui.widgets[fruitapp.fruitbasket.name].items.join(', ')}" puts "value: #{mygui.widgets[fruitapp.fruitbasket.name].value}" puts puts puts "TRY OUT!" puts puts "Current state of fruit and its widget:" puts "fruit=3D#{fruitapp.fruit.isa}" puts "widget=3D#{mygui.widgets[fruitapp.fruitbasket.name].value}" puts puts "Changing widget to: #{mygui.widgets[fruitapp.fruitbasket.name].value = =3D 'Banana'}" puts "fruit=3D#{fruitapp.fruit.isa}" puts puts "Changing fruit to: #{fruitapp.fruit.isa =3D 'Orange'}" puts "widget=3D#{mygui.widgets[fruitapp.fruitbasket.name].value}" puts puts "Trigger event_change event:" mygui.widgets[fruitapp.fruitbasket.name].event_change puts puts "Changing fruit to: #{fruitapp.fruit.isa =3D 'Apple'}" puts "widget=3D#{mygui.widgets[fruitapp.fruitbasket.name].value}" puts puts "Changing widget to: #{mygui.widgets[fruitapp.fruitbasket.name].value = =3D 'Banana'}" puts "fruit=3D#{fruitapp.fruit.isa}" puts --=-TqJqe3LH5O9FHN0zIzFn Content-Disposition: attachment; filename=gutopia-drb.rb Content-Transfer-Encoding: quoted-printable Content-Type: text/x-ruby; name=gutopia-drb.rb; charset=ANSI_X3.4-1968 # GUtopIa - ROMP Daemon # Copyright (c) 2002 Thomas Sawyer, Ruby License require 'drb' require 'gutopia' gutopia_drb =3D GUtopIa::GUI.new('DRb') DRb.start_service('druby://localhost:8080', gutopia_drb) DRb.thread.join --=-TqJqe3LH5O9FHN0zIzFn Content-Disposition: attachment; filename=gutopia-romp.rb Content-Transfer-Encoding: quoted-printable Content-Type: text/x-ruby; name=gutopia-romp.rb; charset=ANSI_X3.4-1968 # GUtopIa - ROMP Daemon # Copyright (c) 2002 Thomas Sawyer, Ruby License require 'romp/romp' require 'gutopia' gutopiad =3D GUtopIa::GUI.new('Romp') server =3D ROMP::Server.new('tcpromp://localhost:8080', nil, true) server.bind(gutopiad, 'gutopia-romp') server.thread.join --=-TqJqe3LH5O9FHN0zIzFn Content-Disposition: attachment; filename=gutopia.rb Content-Transfer-Encoding: quoted-printable Content-Type: text/x-ruby; name=gutopia.rb; charset=ANSI_X3.4-1968 # GUtopIa API # Copyright (c)2002 Thomas Sawyer, Ruby License module GUtopIa # Returns a GUIFactory object class GUI attr_reader :name, :widgets def initialize(name) @name =3D name # @widgets =3D {} @names =3D {} @bindings =3D {} @transactions =3D [] end # Method used to prefab GUI def transaction(bindings=3D{}, names=3D{}, &model) @names =3D names @bindings =3D bindings @transactions << Proc.new { #commit #begin model.call #(self) #commit #rescue ScriptError #rollback #rescue StandardError #rollback #end } end =20 # Method used to build GUI # This runs the all pending transactions def build @transactions.each do |transaction| transaction.call(@bindings) end @transactions.clear # all done with those end =20 # Widget Factory # Returns a new widget object of specialized widget class def widgetFactory(widget, name, attributes=3D{}) =20 # a widget string name will work too widget =3D widget.intern if widget.is_a?(String) =20 # makes an anoynomous class as subclass of desired widget widgetClass =3D Class.new(GUtopIa.const_get(widget)) =20 # specialize class via bindings if @bindings[@names[name] || name] @bindings[@names[name] || name].each do |k, v| widgetClass.class_eval { define_method(k, v) } end end =20 w =3D widgetClass.new(attributes) w.name =3D @names[name] || name @widgets[w.name] =3D w =20 return w =20 end # def alert(msg) puts msg end # def stop puts "stopping...#{@name}" exit end # Useful Class Methods? =20 def GUI.screen_realestate # { :height =3D> , :width =3D> , :depth =3D> } end =20 end # GUI =20 =20 # Widgets =20 # ToDo: add a whole bunch more super widgets :-) # attributes require validation =20 class Widget attr_accessor :name end =20 =20 class Window < Widget =20 attr_accessor :height, :width, :colordepth, :flags attr_accessor :caption, :icon, :background, :grid =20 def initialize(attributes=3D{}) @height =3D attributes[:height] ? attributes[:height] : 200 @width =3D attributes[:width] ? attributes[:width] : 200 @colordepth =3D attributes[:colordepth] ? attributes[:colordepth] : 1= 6 @flags =3D attributes[:flags] ? attributes[:flags] : 0 @caption =3D attributes[:caption] ? attributes[:caption] : nil @icon =3D attributes[:icon] ? attributes[:icon] : nil @background =3D attributes[:background] ? attributes[:background] : n= il @grid =3D attributes[:grid] ? attributes[:grid] : [] # super() end =20 def show puts "showing window....#{@caption}" end =20 def hide puts "hiding window...#{@caption}" end =20 end # Window class Panel < Widget =20 attr_accessor :height, :width attr_accessor :background, :grid =20 def initialize(attributes=3D{}) @height =3D attributes[:height] ? attributes[:height] : 200 @width =3D attributes[:width] ? attributes[:width] : 200 @background =3D attributes[:background] ? attributes[:background] : n= il @grid =3D attributes[:grid] ? attributes[:grid] : [] # super() end =20 end # Panel class Label < Widget =20 attr_accessor :text =20 def initialize(attributes=3D{}) @text =3D attributes[:text] ? attributes[:text] : nil # super() end =20 end # Label =20 class TextField < Widget =20 attr_accessor :text, :value =20 def initialize(attributes=3D{}) @text =3D attributes[:text] ? attributes[:text] : nil @value =3D attributes[:value] ? attributes[:value] : nil # super() end =20 end # TextField class TextArea < Widget =20 attr_accessor :text, :value, :cols, :lines =20 def initialize(attributes=3D{}) @text =3D attributes[:text] ? attributes[:text] : nil @value =3D attributes[:value] ? attributes[:value] : nil @cols =3D attributes[:cols] ? attributes[:cols] : nil @lines =3D attributes[:lines] ? attributes[:lines] : nil # super() end =20 end # TextArea class Button < Widget =20 attr_accessor :text attr_accessor :event_pressed =20 def initialize(attributes=3D{}) @text =3D attributes[:text] ? attributes[:text] : nil # events @event_pressed =3D attributes[:event_pressed] ? attributes[:event_pre= ssed] : nil # super() end =20 end # Button class RadioBox < Widget =20 attr_accessor :value, :items attr_accessor :event_change =20 def initialize(attributes=3D{}) @value =3D attributes[:value] ? attributes[:value] : nil @items =3D attributes[:content] ? attributes[:content] : nil # events @event_change =3D attributes[:event_change] ? attributes[:event_chang= e] : nil # super() end =20 end # RadioBox =20 class MenuBar < Widget =20 attr_accessor :items =20 def initialize(attributes=3D{}) @items =3D attributes[:items] ? attributes[:items] : nil # super() end =20 end # MenuBar =20 =20 class Menu < Widget =20 attr_accessor :text, :items =20 def initialize(attributes=3D{}) @text =3D attributes[:text] ? attributes[:text] : nil @items =3D attributes[:items] ? attributes[:items] : nil # super() end =20 end # Menu class MenuItem < Widget =20 attr_accessor :text attr_accessor :event_selected =20 def initialize(attributes=3D{}) @text =3D attributes[:text] ? attributes[:text] : nil # events @event_selected =3D attributes[:event_selected] ? attributes[:event_s= elected] : nil # super() end =20 end # MenuItem end # GUtopIa --=-TqJqe3LH5O9FHN0zIzFn Content-Disposition: attachment; filename=orig-example.rb Content-Transfer-Encoding: quoted-printable Content-Type: text/x-ruby; name=orig-example.rb; charset=ANSI_X3.4-1968 # GUtopIa Example - Presentation # Copyright (c) 2002 Thomas Sawyer, Ruby License require 'gutopia' app =3D GUtopIa::GUI.new('My Application') # here you must refer to widgets with app.widgets['widget's name'] bindings =3D { 'quit' =3D> { :event_selected =3D> Proc.new { app.stop } }, 'about' =3D> { :event_selected =3D> Proc.new {=20 app.widgets['aboutWindow'].show } }, 'submit' =3D> {=20 :event_pressed =3D> Proc.new {=20 app.alert("And the fruit is...#{app.widgets['favoriteFruit'].text}") app.widgets['mainWindow'].hide app.stop } }, 'clear' =3D> {=20 :event_pressed =3D> Proc.new { app.widgets['favoriteFruit'].text =3D nil } }, 'close' =3D> { :event_pressed =3D> Proc.new { app.widgets['aboutWindow'].hide } } } # here you can refer to widgets with local name # or the app.widgets['widget's name'] app.transaction(bindings) { =20 quit =3D app.widgetFactory(:MenuItem, 'quit', :text =3D> 'Quit' ) about =3D app.widgetFactory(:MenuItem, 'about', :text =3D> 'About' ) file =3D app.widgetFactory(:Menu, 'file', :text =3D> 'File', :items =3D> [ app.widgets['quit'] ] ) help =3D app.widgetFactory(:Menu, 'help', :text =3D> 'Help', :items =3D> [ about ] ) menu =3D app.widgetFactory(:MenuBar, 'menu', :items =3D> [ file, help ] ) firstName =3D app.widgetFactory(:TextField, 'firstName', :label =3D> 'First Name', :label_position =3D> :left ) lastName =3D app.widgetFactory(:TextField, 'lastName', :label =3D> 'Last Name', :label_position =3D> :left ) favoriteFruit =3D app.widgetFactory(:TextArea, 'favoriteFruit', :text =3D> 'Banana', :cols =3D> 5, :lines =3D> 4, :readonly =3D> false, :label =3D> 'Favorite Fruit', :label_position =3D> :top ) submit =3D app.widgetFactory(:Button, 'submit', :text =3D> 'Update', :image =3D> 'check.png' ) clear =3D app.widgetFactory(:Button, 'clear', :text =3D> 'Clear' ) fruit_panel =3D app.widgetFactory(:Panel, 'fruit_panel', :grid =3D> [ [ firstName ], [ lastName ], [ favoriteFruit ], [ submit ], [ clear ] ] ) mainWindow =3D app.widgetFactory(:Window, 'mainWindow', :caption =3D> 'My Cool Window', :icon =3D> 'myIcon.ico', :grid =3D> [ [ menu ], [ fruit_panel ] ] ) aboutLbl =3D app.widgetFactory(:Label, 'aboutLbl',=20 :text =3D> <<-DATA This is a very cool application that I created using the GUtopIa GUI API. DATA ) close =3D app.widgetFactory(:Button, 'close', :text =3D> 'Close' ) aboutWindow =3D app.widgetFactory(:Window, 'aboutWindow', :caption =3D> 'About My Cool App', :grid =3D> [ [ aboutLbl ], [ close ] ] ) =20 } =20 app.build puts puts "select about from menu" app.widgets['about'].event_selected puts "click on close" app.widgets['close'].event_pressed puts "click on submit" app.widgets['submit'].event_pressed puts --=-TqJqe3LH5O9FHN0zIzFn--