From: Tom Sawyer Date: 2002-07-25T10:47:58+09:00 Subject: Re: GUI's and the Rouge, Part III (grand finally) 2/2 figures. as soon as post this i figure it out. never mind. On Wed, 2002-07-24 at 19:37, Tom Sawyer wrote: > has anyone gotten a chance to take a look at the GUtopIa code yet? > > there's something very strange about it. something i can't figure out. i > mean i wrote it and all, but i never explictly told it how to update the > widget when the object changes. yet it does! how? i keep coming back to > this and looking for some place in the code that does it, but i can't > find it. very spooky. > > ~transami > > > On Tue, 2002-07-23 at 18:31, Tom Sawyer wrote: > > 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 :-) > > > > > > ---- > > > > > # GUtopIa Example - Application Layer > > # Copyright (c) 2002 Thomas Sawyer, Ruby License > > > > class FruitApp > > attr_accessor :appname > > attr_reader :fruitbasket, :fruit, :basketname > > def initialize > > @appname = 'Fruity Fun' > > @fruitbasket = FruitBasket.new > > @fruit = Fruit.new > > end > > def pickafruit > > puts "You got a #{@fruit.isa}!" > > end > > end > > > > class FruitBasket > > attr_accessor :name, :contains > > def initialize > > @name = 'Fruit Basket' > > @contains = ["None", "Apple", "Orange", "Banana"] > > end > > end > > > > class Fruit > > attr_accessor :isa > > def initialize > > @isa = "None" > > end > > end > > ---- > > > > > # GUtopIa Daemon Example - Client > > # Copyright (c) 2002 Thomas Sawyer, Ruby License > > > > require 'drb' > > require 'example-app' > > require 'example-pre' > > > > > > # Configure interface > > > > INTERFACE = 'PDA' > > puts "\nINTERFACE: #{INTERFACE}\n" > > > > > > # Make GUI App > > > > fruitapp = FruitApp.new > > > > DRb.start_service() > > mygui = DRbObject.new(nil, 'druby://localhost:8080') > > > > 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 > > > > > > # 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=#{fruitapp.fruit.isa}" > > puts "widget=#{mygui.widgets[fruitapp.fruitbasket.name].value}" > > puts > > puts "Changing widget to: #{mygui.widgets[fruitapp.fruitbasket.name].value = 'Banana'}" > > puts "fruit=#{fruitapp.fruit.isa}" > > puts > > puts "Changing fruit to: #{fruitapp.fruit.isa = 'Orange'}" > > puts "widget=#{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 = 'Apple'}" > > puts "widget=#{mygui.widgets[fruitapp.fruitbasket.name].value}" > > puts > > puts "Changing widget to: #{mygui.widgets[fruitapp.fruitbasket.name].value = 'Banana'}" > > puts "fruit=#{fruitapp.fruit.isa}" > > > > puts > > ---- > > > > > # 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 { > > > > # RadioBox > > r = gui.widgetFactory(:RadioBox, 'aradio') > > > > # Window > > w = gui.widgetFactory(:Window, 'awindow') > > w.width = 640 > > w.height = 400 > > w.grid = [ [ r ] ] > > > > } > > > > end > > > > # PDA model using parallel notation (faster) > > def MyModels.pda_model(gui) > > > > Proc.new { > > > > # RadioBox > > r = gui.widgetFactory(:RadioBox, 'aradio') > > > > # Window > > w = gui.widgetFactory(:Window, 'awindow', > > :width => 320, > > :height => 240, > > :grid => [ [ r ] ] > > ) > > > > } > > > > end > > > > end # MyModels > > ---- > > > > > # GUtopIa Daemon Example - Client > > # Copyright (c) 2002 Thomas Sawyer, Ruby License > > > > require 'romp/romp' > > require 'example-app' > > require 'example-pre' > > > > > > # Configure interface > > > > INTERFACE = 'PDA' > > puts "\nINTERFACE: #{INTERFACE}\n" > > > > > > # Make GUI App > > > > fruitapp = FruitApp.new > > > > client = ROMP::Client.new('tcpromp://localhost:8080') > > mygui = client.resolve('gutopia-romp') > > > > 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 > > > > > > # 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=#{fruitapp.fruit.isa}" > > puts "widget=#{mygui.widgets[fruitapp.fruitbasket.name].value}" > > puts > > puts "Changing widget to: #{mygui.widgets[fruitapp.fruitbasket.name].value = 'Banana'}" > > puts "fruit=#{fruitapp.fruit.isa}" > > puts > > puts "Changing fruit to: #{fruitapp.fruit.isa = 'Orange'}" > > puts "widget=#{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 = 'Apple'}" > > puts "widget=#{mygui.widgets[fruitapp.fruitbasket.name].value}" > > puts > > puts "Changing widget to: #{mygui.widgets[fruitapp.fruitbasket.name].value = 'Banana'}" > > puts "fruit=#{fruitapp.fruit.isa}" > > > > puts > > ---- > > > > > # GUtopIa Example - Presentation > > # Copyright (c) 2002 Thomas Sawyer, Ruby License > > > > require 'gutopia' > > require 'example-app' > > require 'example-pre' > > > > > > # Configure interface > > > > INTERFACE = 'PDA' > > puts "\nINTERFACE: #{INTERFACE}\n" > > > > > > # Make GUI App > > > > 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 > > > > > > # 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=#{fruitapp.fruit.isa}" > > puts "widget=#{mygui.widgets[fruitapp.fruitbasket.name].value}" > > puts > > puts "Changing widget to: #{mygui.widgets[fruitapp.fruitbasket.name].value = 'Banana'}" > > puts "fruit=#{fruitapp.fruit.isa}" > > puts > > puts "Changing fruit to: #{fruitapp.fruit.isa = 'Orange'}" > > puts "widget=#{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 = 'Apple'}" > > puts "widget=#{mygui.widgets[fruitapp.fruitbasket.name].value}" > > puts > > puts "Changing widget to: #{mygui.widgets[fruitapp.fruitbasket.name].value = 'Banana'}" > > puts "fruit=#{fruitapp.fruit.isa}" > > > > puts > > ---- > > > > > # GUtopIa - ROMP Daemon > > # Copyright (c) 2002 Thomas Sawyer, Ruby License > > > > require 'drb' > > require 'gutopia' > > > > gutopia_drb = GUtopIa::GUI.new('DRb') > > > > DRb.start_service('druby://localhost:8080', gutopia_drb) > > DRb.thread.join > > ---- > > > > > # GUtopIa - ROMP Daemon > > # Copyright (c) 2002 Thomas Sawyer, Ruby License > > > > require 'romp/romp' > > require 'gutopia' > > > > gutopiad = GUtopIa::GUI.new('Romp') > > > > server = ROMP::Server.new('tcpromp://localhost:8080', nil, true) > > server.bind(gutopiad, 'gutopia-romp') > > server.thread.join > > ---- > > > > > # 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 = name > > # > > @widgets = {} > > @names = {} > > @bindings = {} > > @transactions = [] > > end > > > > # Method used to prefab GUI > > def transaction(bindings={}, names={}, &model) > > @names = names > > @bindings = bindings > > @transactions << Proc.new { > > #commit > > #begin > > model.call #(self) > > #commit > > #rescue ScriptError > > #rollback > > #rescue StandardError > > #rollback > > #end > > } > > end > > > > # 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 > > > > # Widget Factory > > # Returns a new widget object of specialized widget class > > def widgetFactory(widget, name, attributes={}) > > > > # a widget string name will work too > > widget = widget.intern if widget.is_a?(String) > > > > # makes an anoynomous class as subclass of desired widget > > widgetClass = Class.new(GUtopIa.const_get(widget)) > > > > # 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 > > > > w = widgetClass.new(attributes) > > w.name = @names[name] || name > > @widgets[w.name] = w > > > > return w > > > > end > > > > # > > def alert(msg) > > puts msg > > end > > > > # > > def stop > > puts "stopping...#{@name}" > > exit > > end > > > > # Useful Class Methods? > > > > def GUI.screen_realestate > > # { :height => , :width => , :depth => } > > end > > > > end # GUI > > > > > > # Widgets > > > > # ToDo: add a whole bunch more super widgets :-) > > # attributes require validation > > > > class Widget > > attr_accessor :name > > end > > > > > > class Window < Widget > > > > attr_accessor :height, :width, :colordepth, :flags > > attr_accessor :caption, :icon, :background, :grid > > > > def initialize(attributes={}) > > @height = attributes[:height] ? attributes[:height] : 200 > > @width = attributes[:width] ? attributes[:width] : 200 > > @colordepth = attributes[:colordepth] ? attributes[:colordepth] : 16 > > @flags = attributes[:flags] ? attributes[:flags] : 0 > > @caption = attributes[:caption] ? attributes[:caption] : nil > > @icon = attributes[:icon] ? attributes[:icon] : nil > > @background = attributes[:background] ? attributes[:background] : nil > > @grid = attributes[:grid] ? attributes[:grid] : [] > > # > > super() > > end > > > > def show > > puts "showing window....#{@caption}" > > end > > > > def hide > > puts "hiding window...#{@caption}" > > end > > > > end # Window > > > > > > class Panel < Widget > > > > attr_accessor :height, :width > > attr_accessor :background, :grid > > > > def initialize(attributes={}) > > @height = attributes[:height] ? attributes[:height] : 200 > > @width = attributes[:width] ? attributes[:width] : 200 > > @background = attributes[:background] ? attributes[:background] : nil > > @grid = attributes[:grid] ? attributes[:grid] : [] > > # > > super() > > end > > > > end # Panel > > > > > > class Label < Widget > > > > attr_accessor :text > > > > def initialize(attributes={}) > > @text = attributes[:text] ? attributes[:text] : nil > > # > > super() > > end > > > > end # Label > > > > > > class TextField < Widget > > > > attr_accessor :text, :value > > > > def initialize(attributes={}) > > @text = attributes[:text] ? attributes[:text] : nil > > @value = attributes[:value] ? attributes[:value] : nil > > # > > super() > > end > > > > end # TextField > > > > > > class TextArea < Widget > > > > attr_accessor :text, :value, :cols, :lines > > > > def initialize(attributes={}) > > @text = attributes[:text] ? attributes[:text] : nil > > @value = attributes[:value] ? attributes[:value] : nil > > @cols = attributes[:cols] ? attributes[:cols] : nil > > @lines = attributes[:lines] ? attributes[:lines] : nil > > # > > super() > > end > > > > end # TextArea > > > > > > class Button < Widget > > > > attr_accessor :text > > attr_accessor :event_pressed > > > > def initialize(attributes={}) > > @text = attributes[:text] ? attributes[:text] : nil > > # events > > @event_pressed = attributes[:event_pressed] ? attributes[:event_pressed] : nil > > # > > super() > > end > > > > end # Button > > > > > > class RadioBox < Widget > > > > attr_accessor :value, :items > > attr_accessor :event_change > > > > def initialize(attributes={}) > > @value = attributes[:value] ? attributes[:value] : nil > > @items = attributes[:content] ? attributes[:content] : nil > > # events > > @event_change = attributes[:event_change] ? attributes[:event_change] : nil > > # > > super() > > end > > > > end # RadioBox > > > > > > class MenuBar < Widget > > > > attr_accessor :items > > > > def initialize(attributes={}) > > @items = attributes[:items] ? attributes[:items] : nil > > # > > super() > > end > > > > end # MenuBar > > > > > > class Menu < Widget > > > > attr_accessor :text, :items > > > > def initialize(attributes={}) > > @text = attributes[:text] ? attributes[:text] : nil > > @items = attributes[:items] ? attributes[:items] : nil > > # > > super() > > end > > > > end # Menu > > > > > > class MenuItem < Widget > > > > attr_accessor :text > > attr_accessor :event_selected > > > > def initialize(attributes={}) > > @text = attributes[:text] ? attributes[:text] : nil > > # events > > @event_selected = attributes[:event_selected] ? attributes[:event_selected] : nil > > # > > super() > > end > > > > end # MenuItem > > > > end # GUtopIa > > ---- > > > > > # GUtopIa Example - Presentation > > # Copyright (c) 2002 Thomas Sawyer, Ruby License > > > > require 'gutopia' > > > > app = GUtopIa::GUI.new('My Application') > > > > # here you must refer to widgets with app.widgets['widget's name'] > > bindings = { > > 'quit' => { > > :event_selected => Proc.new { > > app.stop > > } > > }, > > 'about' => { > > :event_selected => Proc.new { > > app.widgets['aboutWindow'].show > > } > > }, > > 'submit' => { > > :event_pressed => Proc.new { > > app.alert("And the fruit is...#{app.widgets['favoriteFruit'].text}") > > app.widgets['mainWindow'].hide > > app.stop > > } > > }, > > 'clear' => { > > :event_pressed => Proc.new { > > app.widgets['favoriteFruit'].text = nil > > } > > }, > > 'close' => { > > :event_pressed => 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) { > > > > quit = app.widgetFactory(:MenuItem, 'quit', > > :text => 'Quit' > > ) > > about = app.widgetFactory(:MenuItem, 'about', > > :text => 'About' > > ) > > file = app.widgetFactory(:Menu, 'file', > > :text => 'File', > > :items => [ app.widgets['quit'] ] > > ) > > help = app.widgetFactory(:Menu, 'help', > > :text => 'Help', > > :items => [ about ] > > ) > > menu = app.widgetFactory(:MenuBar, 'menu', > > :items => [ file, help ] > > ) > > firstName = app.widgetFactory(:TextField, 'firstName', > > :label => 'First Name', > > :label_position => :left > > ) > > lastName = app.widgetFactory(:TextField, 'lastName', > > :label => 'Last Name', > > :label_position => :left > > ) > > favoriteFruit = app.widgetFactory(:TextArea, 'favoriteFruit', > > :text => 'Banana', > > :cols => 5, > > :lines => 4, > > :readonly => false, > > :label => 'Favorite Fruit', > > :label_position => :top > > ) > > submit = app.widgetFactory(:Button, 'submit', > > :text => 'Update', > > :image => 'check.png' > > ) > > clear = app.widgetFactory(:Button, 'clear', > > :text => 'Clear' > > ) > > fruit_panel = app.widgetFactory(:Panel, 'fruit_panel', > > :grid => [ [ firstName ], > > [ lastName ], > > [ favoriteFruit ], > > [ submit ], > > [ clear ] ] > > ) > > mainWindow = app.widgetFactory(:Window, 'mainWindow', > > :caption => 'My Cool Window', > > :icon => 'myIcon.ico', > > :grid => [ [ menu ], > > [ fruit_panel ] ] > > ) > > aboutLbl = app.widgetFactory(:Label, 'aboutLbl', > > :text => <<-DATA > > This is a very cool application > > that I created using the GUtopIa > > GUI API. > > DATA > > ) > > close = app.widgetFactory(:Button, 'close', > > :text => 'Close' > > ) > > aboutWindow = app.widgetFactory(:Window, 'aboutWindow', > > :caption => 'About My Cool App', > > :grid => [ [ aboutLbl ], > > [ close ] ] > > ) > > > > } > > > > 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 > -- > ~transami > > -- ~transami