You are on page 1of 56

Code Tour - Part 2

Ruby / JSPanel / MineOPS


Garth Klee, Software Engineer
18th September, 2017
Code Tour Agenda

1. Ruby
a) MineOPS
b) JSPanel
c) Painters
d) Icons and images
2. Ruby-Gnome
a) gtk_widgets.rb
b) rb.xml files
c) Common patterns

2 Confidential
Ruby

Important Source Locations


Ruby Code Locations

/opt/Jigsaw/Library…
• jminegraph/app/views
• jminegraph/app/controllers
• jminegraph/app/widgets
• jminegraph/app/widgets/painters
• jmineops
• ruby/models
• ruby/jspanel

4 Confidential
General MineOPS Views

/opt/Jigsaw/Library…
• jminegraph/app/views
• jminegraph/app/controllers
• jminegraph/app/widgets
• jminegraph/app/widgets/painters
• jmineops
• ruby/models
• ruby/jspanel

5 Confidential
MineOPS Mine Map / Playback

/opt/Jigsaw/Library…
• jminegraph/app/views
• jminegraph/app/controllers
• jminegraph/app/widgets
• jminegraph/app/widgets/painters
• jmineops
• ruby/models
• ruby/jspanel

6 Confidential
JSPanel Code Locations

/opt/Jigsaw/Library…
• jminegraph/app/views
• jminegraph/app/controllers
• jminegraph/app/widgets
• jminegraph/app/widgets/painters
• jmineops
• ruby/models
• ruby/jspanel

7 Confidential
Database Code Locations

/opt/Jigsaw/Library…


jminegraph/app/views
jminegraph/app/controllers
Migration scripts
• jminegraph/app/widgets SQL stored procedures
• jminegraph/app/widgets/painters
• jmineops Database views
• ruby/models
• ruby/jspanel

8 Confidential
Ruby

MineOPS Views and GTK


General MineOPS Views

/opt/Jigsaw/Library…
• jminegraph/app/views
• jminegraph/app/controllers
• jminegraph/app/widgets
• jminegraph/app/widgets/painters
• jmineops
• ruby/models
• ruby/jspanel

View: layout of visual elements


Controller: Logic
Widgets: Elements eg. buttons
Models: Data being shown

10 Confidential
General MineOPS Views

TruckView

EquipmentView

EditorView

UtilityView

View

11 Confidential
General MineOPS Views

TruckView

EquipmentView

EditorView

UtilityView

View

12 Confidential
Table Tab
General MineOPS Views Detail Tab

TruckView

EquipmentView

EditorView

UtilityView

View

13 Confidential
General MineOPS Views

Configuration for truck_view

Most attrs are configured


rather than set in code

14 Confidential
General MineOPS Views
View: view is based on the truck_view Class
Class Name: view will show Truck objects (data)

Configuration for truck_view

Truck/Equipment-specific
settings (mostly)

15 Confidential
General MineOPS Views

Filter Expr: Hide objects that match this pattern

Configuration for truck_view

Actions: Buttons at bottom of view


Auto Apply: User changes apply immediately

16 Confidential
General MineOPS Views

To seeConfiguration for truck_view


what settings are available take a look in:
• Library/jminegraph/app/views/editor_view.rb
• Library/jminegraph/app/views/equipment_view.rb
• Library/jminegraph/app/views/truck_view.rb
• …and the rest… shift_views… menu_views… etc.

17 Confidential
Gnome UI Widgets (gtk_widgets)
Generally applies to both MineOPS and JSPanel…
The points of difference are:

• Source locations:
• Library/jminegraph/app/widgets/gtk_widgets.rb (MineOPS)
• Library/ruby/jspanel/gtk_widgets.rb (JSPanel)
• MineOPS is plain Ruby code
• JSPanel is mostly rb.xml
• MineOPS uses the display_selectors table

18 Confidential
Gnome UI Widgets (gtk_widgets)
We don’t directly instantiate Gnome/GTK objects.
We create instances from the classes defined in the gtk_widgets.rb files

19 Confidential
Ruby

JSPanel and rb.xml files


JSPanel & rb.xml files

Rb.xml files are XML-based tags that are used to help layout the widgets
within the panel.
Some logic is defined in these files, but generally, we prefer to keep logic
in Objective-C rather than rb.xml.
• Objective-C is much faster
• We’d like to get rid of Ruby/Gnome UI at some stage in the future

21 Confidential
JSPanel & rb.xml files

The interpreter for rb.xml is:


/opt/Jigsaw/Library/ruby/jspanel/xml_loader.rb.xml

The output of the interpreter is Ruby.

The generated Ruby outputs are cached so that subsequent panel startup
times are accelerated (Library/ruby/jspanel/cache).

22 Confidential
JSPanel & rb.xml files

The interpreter for rb.xml is:


/opt/Jigsaw/Library/ruby/jspanel/xml_loader.rb.xml

The output of the interpreter is Ruby.

The generated Ruby outputs are cached so that subsequent panel startup
times are accelerated (Library/ruby/jspanel/cache).

23 Confidential
JSPanel layout (rb.xml)
<frame name="w0" font_size="20" undecorated="true" visible="true“
location="0,0" maximized="true" size="1024,768">
<menubar name="mn" size="160,92">
<menu name="jigsaw" image="puzzle_82x82.png" label="" size="10,92">
<include name="system_menu"/>
<include name="truck_activity_menu" not_config="disable_truck_activity_menu"/>
<include name="truck_status_menu"/>
<include name="operations_menu"/>
<include name="road_conditions_menu" config="road_condition_enabled” />
<include name="message_menu"/>
<include name="truck_help_menu" not_config="disable_truck_help_menu"/>
<include name="emergency_menu"/>
</menu>

w0 (Frame)
w0.mn (Menubar)
w0.mn.jigsaw (Menu)

24 Confidential
JSPanel layout (rb.xml)
<frame name="w0" font_size="20" undecorated="true" visible="true“
location="0,0" maximized="true" size="1024,768">
<menubar name="mn" size="160,92">
<menu name="jigsaw" image="puzzle_82x82.png" label="" size="10,92">
<include name="system_menu"/>
<include name="truck_activity_menu" not_config="disable_truck_activity_menu
<include name="truck_status_menu"/>
<include name="operations_menu"/>
<include name="road_conditions_menu" config="road_condition_enabled” />
<include name="message_menu"/>
<include name="truck_help_menu" not_config="disable_truck_help_menu"/>
<include name="emergency_menu"/>
</menu>

The road conditions item is missing

25 Confidential
JSPanel layout (rb.xml)
<frame name="w0" font_size="20" undecorated="true" visible="true“
location="0,0" maximized="true" size="1024,768">
<menubar name="mn" size="160,92">
<menu name="jigsaw" image="puzzle_82x82.png" label="" size="10,92">
<include name="system_menu"/>
<include name="truck_activity_menu" not_config="disable_truck_activity_menu"/>
<include name="truck_status_menu"/>
<include name="operations_menu"/>
<include name="road_conditions_menu" config="road_condition_enabled” />
<include name="message_menu"/>
<include name="truck_help_menu" not_config="disable_truck_help_menu"/>
<include name="emergency_menu"/>
</menu>

Q: What does <include/> do?

Q: What does config= do?

26 Confidential
JSPanel layout (rb.xml)
<frame name="w0" font_size="20" undecorated="true" visible="true“
location="0,0" maximized="true" size="1024,768">
<menubar name="mn" size="160,92">
<menu name="jigsaw" image="puzzle_82x82.png" label="" size="10,92">
<include name="system_menu"/>
<include name="truck_activity_menu" not_config="disable_truck_activity_menu"/>
<include name="truck_status_menu"/>
<include name="operations_menu"/>
<include name="road_conditions_menu" config="road_condition_enabled” />
<include name="message_menu"/>
<include name="truck_help_menu" not_config="disable_truck_help_menu"/>
<include name="emergency_menu"/>
</menu>

Q: What does <include/> do?


A: Includes another rb.xml file from Library/ruby/jspanel/xml

Q: What does config= do?


A: Makes the entire tag (and children) conditional based on configuration

27 Confidential
JSPanel layout (truck_status_menu.rb.xml)
<menu name="status" display_label="STATUS" size="230,70">
<menuitem display_label="READY" image="truck_ready_82x82.png" size="220,70" background="green" change_colors="false">
status_selection "ready"
</menuitem>
<menuitem display_label="DOWN" image="truck_down_82x82.png" size="220,70" background="red" change_colors="false">
status_selection "down"
</menuitem>
<menuitem display_label="DELAY" image="truck_delay_82x82.png" size="220,70" background="orange" change_colors="false">
status_selection "delay"
</menuitem>
<menuitem display_label="STANDBY" image="truck_spare_82x82.png“
size="220,70" background="blue" change_colors="false">
status_selection "standby"
</menuitem>
<menuitem display_label="SHIFT CHANGE“
size="220,70" change_colors="false" config="enable_shift_change_status">
status_selection "shift_change"
</menuitem>
</menu>

28 Confidential
JSPanel layout (truck_status_menu.rb.xml)

<menuitem display_label="READY" image="truck_ready_82x82.png" size="220,70" background="green" change_colors="false">


status_selection "ready"
</menuitem>

So where are all these things defined?


gtk_widgets.rb.xml

29 Confidential
JSPanel layout (truck_status_menu.rb.xml)
<menuitem display_label="READY" image="truck_ready_82x82.png" size="220,70" background="green" change_colors="false">
status_selection "ready"
</menuitem>

class Menuitem < Menuitembase


def sensitive=(bool)
@sensitive = bool
self.foreground = JSPanel::Application.application.system_foreground
end

def foreground=(color)
super
if @sensitive.nil? || @sensitive
c = get_color(color)
else
c = get_color("gray")
end
Gtk.post_update do
@gtk_object.child.modify_fg(Gtk::STATE_NORMAL, c) if @gtk_object
@label.modify_fg(Gtk::STATE_NORMAL, c) if @label
end
end
end

30 Confidential
JSPanel layout (truck_status_menu.rb.xml)
<menuitem display_label="READY" image="truck_ready_82x82.png" size="220,70" background="green" change_colors="false">
status_selection "ready"
</menuitem>

class Menuitembase < GuiWidgetContainer



# display label will be automatically translated
def display_label=(label)
set_label(tr(label))
end

def background=(color)
return if @background == color
super
c = get_color(color)
Gtk.post_update do
@contents.modify_bg(Gtk::STATE_NORMAL, c) if @contents
@contents.modify_bg(Gtk::STATE_INSENSITIVE, c) if @contents
end
end

end

31 Confidential
Gtk_widgets: creating UI widgets
class Button < GuiWidget

# initialize widget GtkWidgets::Button
def create
@image_size=[82,82]
b = Gtk::Button.new Gtk::Button
b.set_label("")

# Don't allow the visible focus to remain on a clicked button


b.can_focus = false

set_gtk_object(b)

# Changed to handle a 'pressed' [Emitted when the button is pressed] event


# as opposed to a 'clicked' [Emitted when the button has been activated (pressed and released).] event
# as it does not seem necessary to wait around for a button release on a touch screen.
@gtk_object.signal_connect('pressed') do |w,e|
performAction e
end
@persist_properties = true
create_gui_widget
end

32 Confidential
Gtk_widgets: creating UI widgets
class Button < GuiWidget

# initialize widget GtkWidgets::Button
def create These are
@image_size=[82,82] not the same
b = Gtk::Button.new Gtk::Button
b.set_label("")

# Don't allow the visible focus to remain on a clicked button


b.can_focus = false

set_gtk_object(b)

# Changed to handle a 'pressed' [Emitted when the button is pressed] event


# as opposed to a 'clicked' [Emitted when the button has been activated (pressed and released).] event
# as it does not seem necessary to wait around for a button release on a touch screen.
@gtk_object.signal_connect('pressed') do |w,e|
performAction e
end
@persist_properties = true
create_gui_widget
end

33 Confidential
More examples with buttons - actions
@gtk_object.signal_connect('pressed') do |w,e|
performAction e
end

<panel layout="grid 0 1" size="100,160" shadow="none">


<button name="zoom_in" stock_image="gtk-zoom-in" size="80,80">
w0.side.zoom_by(20)
update_side_view_info
</button>
<button name="zoom_out" stock_image="gtk-zoom-out" size="80,80">
w0.side.zoom_by(-20)
update_side_view_info
</button>
<button stock_image="gtk-go-up" size="80,80">
w0.side.move_up
</button>
<button stock_image="gtk-go-down" size="80,80">
w0.side.move_down
</button>
</panel>

34 Confidential
More examples with buttons - cloning

35 Confidential
More examples with buttons - cloning
<dialog name="blast_dialog" display_title="" location="0,0" size="1024,768" undecorated="true">
<panel layout="ybox">
<field height="100" display_label="Select your blast zone" font_size="30"/>
<scrollpane height="560" policy="never,auto" vscrollbar_size="60,60">
<panel name="panel" layout="grid 0 3">
<button name="list" source_label="name" font_size="20" width="100">
set_selected_blast active_widget.object
blast_dialog.unpost
</button>
</panel>
</scrollpane>
<button name="cancel" display_label="CANCEL" font_size="24" height="100">
blast_dialog.unpost
</button>
</panel>
</dialog>

36 Confidential
More examples with buttons - cloning
<dialog name="blast_dialog" display_title="" location="0,0" size="1024,768" undecorated="true">
<panel layout="ybox">
<field height="100" display_label="Select your blast zone" font_size="30"/>
<scrollpane height="560" policy="never,auto" vscrollbar_size="60,60">
<panel name="panel" layout="grid 0 3">
<button name="list" source_label="name" font_size="20" width="100">
set_selected_blast active_widget.object
blast_dialog.unpost
</button>
</panel>
</scrollpane>
<button name="cancel" display_label="CANCEL" font_size="24" height="100">
blast_dialog.unpost
</button>
</panel>
</dialog>

Then elsewhere this happens…


blast_list = Location.find(:all, :conditions =&gt; "type='Blast'", :order =&gt; "name") # Get Array of blasts

blast_dialog.panel.list.reset_source(blast_list) # Create buttons for the blasts
blast_dialog.post # Show the blast selection dialog

37 Confidential
Panels
<dialog name="blast_dialog" display_title="" location="0,0" size="1024,768" undecorated="true">
<panel layout="ybox">
<field height="100" display_label="Select your blast zone" font_size="30"/>
<scrollpane height="560" policy="never,auto" vscrollbar_size="60,60">
<panel name="panel" layout="grid 0 3">
<button name="list" source_label="name" font_size="20" width="100">
set_selected_blast active_widget.object
blast_dialog.unpost
</button>
</panel>
</scrollpane>
<button name="cancel" display_label="CANCEL" font_size="24" height="100">
blast_dialog.unpost
</button>
</panel>
</dialog>

Every panel needs to have a layout.

38 Confidential
Panels
<dialog name="kp" title="Keypad">
<panel layout="ybox">
<label name="hint" size="240,40" font_size="20.0"/>
<panel layout="xbox">
<field name="d" size="500,100" font_size="24.0" background="white"/>
</panel>
<panel layout="grid 0 3">
<button label="1" size="100,100" font_size="20.0">kp.d.append("1")</button>
<button label="2" size="100,100" font_size="20.0">kp.d.append("2")</button>
<button label="3" size="100,100" font_size="20.0">kp.d.append("3")</button>
<button label="4" size="100,100" font_size="20.0">kp.d.append("4")</button>
<button label="5" size="100,100" font_size="20.0">kp.d.append("5")</button>
<button label="6" size="100,100" font_size="20.0">kp.d.append("6")</button>
<button label="7" size="100,100" font_size="20.0">kp.d.append("7")</button>
<button label="8" size="100,100" font_size="20.0">kp.d.append("8")</button>
<button label="9" size="100,100" font_size="20.0">kp.d.append("9")</button>
<button label="+/-" size="100,100" font_size="20.0">
if kp.d.text.to_f &gt; 0.0
kp.d.write("-"+kp.d.text)
elsif kp.d.text.to_f &lt; 0.0
kp.d.write(kp.d.text.gsub("-",""))
end
</button>
<button label="0" size="100,100" font_size="20.0">kp.d.append("0")</button>
<button label="." size="100,100" font_size="20.0">kp.d.append(".")</button>
</panel>
<panel layout="xbox" size="240,100">
<button display_label="DEL" font_size="20.0" width="125">kp.d.delete</button>
<button display_label="CLR" font_size="20.0" width="125">kp.d.clear</button>
<button display_label="Cancel" width="125">kp.cancel_and_unpost</button>
<button display_label="OK" font_size="20.0" width="125">kp.doit</button>
</panel>
</panel>
</dialog>

39 Confidential
Panels
<dialog name="kp" title="Keypad">
<panel layout="ybox">
<label name="hint" size="240,40" font_size="20.0"/>
<panel layout="xbox">
<field name="d" size="500,100" font_size="24.0" background="white"/>
</panel>
<panel layout="grid 0 3">
<button label="1" size="100,100" font_size="20.0">kp.d.append("1")</button>
<button label="2" size="100,100" font_size="20.0">kp.d.append("2")</button>
<button label="3" size="100,100" font_size="20.0">kp.d.append("3")</button>
<button label="4" size="100,100" font_size="20.0">kp.d.append("4")</button>
<button label="5" size="100,100" font_size="20.0">kp.d.append("5")</button>
<button label="6" size="100,100" font_size="20.0">kp.d.append("6")</button>
<button label="7" size="100,100" font_size="20.0">kp.d.append("7")</button>
<button label="8" size="100,100" font_size="20.0">kp.d.append("8")</button>
<button label="9" size="100,100" font_size="20.0">kp.d.append("9")</button>
<button label="+/-" size="100,100" font_size="20.0">
if kp.d.text.to_f &gt; 0.0
kp.d.write("-"+kp.d.text)
elsif kp.d.text.to_f &lt; 0.0
kp.d.write(kp.d.text.gsub("-",""))
end
</button>
<button label="0" size="100,100" font_size="20.0">kp.d.append("0")</button>
<button label="." size="100,100" font_size="20.0">kp.d.append(".")</button>
</panel>
<panel layout="xbox" size="240,100">
<button display_label="DEL" font_size="20.0" width="125">kp.d.delete</button>
<button display_label="CLR" font_size="20.0" width="125">kp.d.clear</button>
<button display_label="Cancel" width="125">kp.cancel_and_unpost</button>
<button display_label="OK" font_size="20.0" width="125">kp.doit</button>
</panel>
</panel>
</dialog>

40 Confidential
Panels
<dialog name="kp" title="Keypad">
<panel layout="ybox"> hint
<label name="hint" size="240,40" font_size="20.0"/>
<panel layout="xbox"> d
<field name="d" size="500,100" font_size="24.0" background="white"/>
</panel>
<panel layout="grid 0 3">
<button label="1" size="100,100" font_size="20.0">kp.d.append("1")</button> grid
<button label="2" size="100,100" font_size="20.0">kp.d.append("2")</button>
<button label="3" size="100,100" font_size="20.0">kp.d.append("3")</button>
<button label="4" size="100,100" font_size="20.0">kp.d.append("4")</button>
<button label="5" size="100,100" font_size="20.0">kp.d.append("5")</button>
<button label="6" size="100,100" font_size="20.0">kp.d.append("6")</button>
<button label="7" size="100,100" font_size="20.0">kp.d.append("7")</button>
<button label="8" size="100,100" font_size="20.0">kp.d.append("8")</button>
<button label="9" size="100,100" font_size="20.0">kp.d.append("9")</button>
<button label="+/-" size="100,100" font_size="20.0">
if kp.d.text.to_f &gt; 0.0
kp.d.write("-"+kp.d.text)
elsif kp.d.text.to_f &lt; 0.0
kp.d.write(kp.d.text.gsub("-",""))
end
</button>
<button label="0" size="100,100" font_size="20.0">kp.d.append("0")</button>
<button label="." size="100,100" font_size="20.0">kp.d.append(".")</button>
</panel>
<panel layout="xbox" size="240,100">
<button display_label="DEL" font_size="20.0" width="125">kp.d.delete</button> xbox
<button display_label="CLR" font_size="20.0" width="125">kp.d.clear</button>
<button display_label="Cancel" width="125">kp.cancel_and_unpost</button>
<button display_label="OK" font_size="20.0" width="125">kp.doit</button>
</panel>
</panel>
</dialog>

41 Confidential
Macros, Symbols, Timers
<symbol name="panel_color_scheme"/>
<symbol name="system_background_colour"/>
<symbol name="system_foreground_colour"/>

<macro name="day">
bground = config(:day_background) || @default_bg_color || "white"
fground = config(:day_foreground) || "black"
Symbols are like self.backgrounds = bground
global variables self.foregrounds = fground
system_background_colour.value = bground
system_foreground_colour.value = fground
panel_color_scheme.value = :day
</macro>

<macro name="night">
bground = config(:night_background) || "#282828"
fground = config(:night_foreground) || "white"

system_background_colour.value = bground
system_foreground_colour.value = fground
self.backgrounds = bground
self.foregrounds = fground
panel_color_scheme.value = :night
</macro>

42 Confidential
Macros, Symbols, Timers
<timer name="time_display" timeout="1000">
begin
if self.respond_to?(:date_widget) &amp;&amp; self.date_widget.respond_to?(:label=)
self.date_widget.label=Time.now.strftime("%H:%M:%S")
end
rescue Exception =&gt; e
$stderr.puts "Time update failed : #{e}"
end
true
</timer>

# elsewhere...
timer_display.start

43 Confidential
Macros, Symbols, Timers
<timer name="time_display" timeout="1000">
begin
if self.respond_to?(:date_widget) &amp;&amp; self.date_widget.respond_to?(:label=)
self.date_widget.label=Time.now.strftime("%H:%M:%S")
end
rescue Exception =&gt; e
$stderr.puts "Time update failed : #{e}"
end
true
</timer>

# elsewhere...
timer_display.start

Side Note:
Timers are in a separate Thread

44 Confidential
Macros, Symbols, Timers
<timer name="time_display" timeout="1000">
begin
if self.respond_to?(:date_widget) &amp;&amp; self.date_widget.respond_to?(:label=)
self.date_widget.label=Time.now.strftime("%H:%M:%S")
end
rescue Exception =&gt; e
$stderr.puts "Time update failed : #{e}"
end
true
</timer>

What’s all this about?

45 Confidential
Ruby

Painters (MineOPS and JSPanel)


Painters

Painters are used to represent objects in the map (eg. Trucks, Dumps, Roads).
JSPanel and MineOPS share the same painters (codebase).

See: /opt/Jigsaw/Library/jminegraph/app/widgets/painters
Also: /opt/Jigsaw/Images/jminegraph/canvas

47 Confidential
Painters

A painter can have some limited logic to decide how to depict an object for it’s
given state. For example, most equipment painters rely on activity or status.

48 Confidential
Painters – MineOPS Mine Map
In MineOPS, the Mine Map uses LayerViews.
Each selects a mapping between an object
type (eg. Truck) and the most appropriate
painter to use for that object (eg. TruckPainter)

49 Confidential
Painters – MineOPS Mine Map

< Used for filtering

50 Confidential
Painters – JSPanel Mine Map

<!-- Machines -->


<!-- Trucks -->
<layer name="trucks" painter_class="Truck" painter="TruckPainter"
config="canvas_layers[trucks]=~enabled || canvas_layers[trucks]=~layermenu || .../>
<layer name="watertrucks" painter_class="WaterTruck" painter="WaterTruckPainter"
config="canvas_layers[watertrucks]=~enabled || canvas_layers[watertrucks]=~layermenu .../>
<!-- Diggers -->
<layer name="loaders" painter_class="Loader" painter="LoaderPainter"
config="canvas_layers[loaders]=~enabled || canvas_layers[loaders]=~layermenu .../>
<layer name="shovels" painter_class="Shovel" painter="ShovelPainter"
config="canvas_layers[shovels]=~enabled || canvas_layers[shovels]=~layermenu .../>
<layer name="surface_miners" painter_class="SurfaceMiner" painter="SurfaceMinerPainter"
config="canvas_layers[surface_miners]=~enabled || canvas_layers[surface_miners]=~layermenu .../>
<!-- Auxilliary Fleet -->
<layer name="aux_equipment" painter_class="AuxEquipment" painter="AuxEquipmentPainter"
config="canvas_layers[aux_equipment]=~enabled || canvas_layers[aux_equipment]=~layermenu .../>
<layer name="fueltrucks" painter_class="FuelTruck" painter="FuelTruckPainter"
config="canvas_layers[fueltrucks]=~enabled || canvas_layers[fueltrucks]=~layermenu .../>
<layer name="graders" painter_class="Grader" painter="GraderPainter"
config="canvas_layers[graders]=~enabled || canvas_layers[graders]=~layermenu .../>

51 Confidential
Ruby

Other things you want to know about


Ruby – Respond to database changes

Project.jams_notify(:insert){|p| projects_reload}
Project.jams_notify(:update){|p| projects_reload}
Project.jams_notify(:delete){|pid| projects_reload}

53 Confidential
Ruby – Respond to message from JAMS

conn = ActiveRecord::Base.connection

conn.listen('guiMessage'){|msg|
if "#{msg}" == "SILENT HORN"
show_silent_horn
else
show_gui_message msg
end
}

54 Confidential
Ruby – irb and ‘jams_connect’

55 Confidential
Exhausted?
56 Confidential

You might also like