Ruby / TK button event handler code not firing as expected - ruby

The following code creates 3 buttons and adds a handler to each one. The Quit button works, the Give Up button produces an error NameError unknown option settings_change and suggests that an object has been deleted. Same with the Next button. The code works ok when I put the event handlers outside the class.
It turns out that if a callback is created by first doing something like next_note_proc = proc {next_note}, then in the button creation do command next_note_proc. Why does this work??
Why do the callbacks work differently when inside or outside the class?
require 'tk'
require 'tkextlib/tile'
class App
def next_note
puts "Got next note"
end
def settings_change
puts "Got settings change"
end
def quit
puts "Got exit"
exit(1)
end
def initialize
$number_correct = TkVariable.new;
$mode = TkVariable.new
#root = TkRoot.new {title "Music Training"}
#content = Tk::Tile::Frame.new(#root) {padding "0 0 0 0"}.grid( :sticky => 'nsew')
#a = Tk::Tile::Button.new(#content) {text 'Next'; command {next_note}}.grid( :column => 1, :row => 1, :sticky => 'w')
#b = Tk::Tile::Button.new(#content) {text 'Give up'; command {settings_change}}.grid( :column => 2, :row => 1, :sticky => 'w')
#c = Tk::Tile::Button.new(#content) {text 'Quit'; command {quit}}.grid( :column => 2, :row => 2, :sticky => 'w')
TkWinfo.children(#content).each {|w| TkGrid.configure w, :padx => 0, :pady => 0}
#c.bind("1") {quit}
#a.bind("1") {next_note}
#b.bind("1") {settings_change}
puts "Starting up"
end
def run
Tk.mainloop
end
end
the_app = App.new
the_app.run

Commands executed from buttons run in the global context, but settings_change, quit and next_note are in the context of the class. When you use the proc command it creates a new Proc object which calls the method, and which can be called from other contexts.
The reason the quit command seems to work is probably because there is another quit command at the global scope that is getting called -- it is almost certainly not calling the quit method of the App object. You can verify that by adding a print statement in the quit method.

Related

puppet - unexpected result from 'each' in a custom function

I have a simple function which takes a JSON and 'does something' with it. The main part works good BUT the function returns not only what I want but additionally the result of .each loop!
The code:
module Puppet::Parser::Functions
newfunction(:mlh, :type => :rvalue) do |args|
lvm_default_hash = args[0]
lvm_additional_hash = args[1]
if lvm_additional_hash.keys.length == 1
if lvm_additional_hash.keys.include? 'logical_volumes'
# do stuff - we have only 'logical_volumes'
lvm_default_hash.keys.each do |key|
pv_array = Hash['physical_volumes' => lvm_default_hash[key]['physical_volumes']]
lv_hash = lvm_default_hash[key]['logical_volumes']
new_lv_hash = lvm_additional_hash['logical_volumes']
merged_lv_hash = Hash['logical_volumes' => lv_hash.merge(new_lv_hash)]
# this is what I want to return to init.pp
puts Hash[key => pv_array.merge(merged_lv_hash)]
end
end
end
end
end
Variables in the init.pp are:
$default_volume_groups = {
'sys' => {
'physical_volumes' => [
'/dev/sda2',
],
'logical_volumes' => {
'root' => {'size' => '4G'},
'swap' => {'size' => '256M'},
'var' => {'size' => '8G'},
'docker' => {'size' => '16G'},
},
},
}
and the second argument from a hieradata:
modified_volume_groups:
logical_volumes:
cloud_log:
size: '16G'
In the init.pp I have something like this to test it:
notice(mlh($default_volume_groups, $modified_volume_groups))
which gives me a result:
syslogical_volumesvarsize8Gdockersize16Gcloud_logsize16Gswapsize256Mrootsize4Gphysical_volumes/dev/sda2
Notice: Scope(Class[Ops_lvm]): sys
The "long" part before the Notice is the proper result from the puts but the Notice: Scope(): sys is this what I do not want to!
I know that this is the result of this each loop over the default_volumes_groups:
lvm_default_hash.keys.each do |key|
# some stuff
end
How to block of this unwanted result? It blows my puppet's logic because my init.pp sees this sys and not what I want.
Does someone knows how to handle such problem?
Thank you!
I found how to handle this problem but maybe someone could explain me why it works in this way :)
This does not work (short version):
module Puppet::Parser::Functions
newfunction(:mlh, :type => :rvalue) do |args|
lvm_default_hash = args[0]
lvm_additional_hash = args[1]
if lvm_additional_hash.keys.length == 1
if lvm_additional_hash.keys.include? 'logical_volumes'
lvm_default_hash.keys.each do |key|
pv_array = Hash['physical_volumes' => lvm_default_hash[key]['physical_volumes']]
lv_hash = lvm_default_hash[key]['logical_volumes']
new_lv_hash = lvm_additional_hash['logical_volumes']
merged_lv_hash = Hash['logical_volumes' => lv_hash.merge(new_lv_hash)]
puts Hash[key => pv_array.merge(merged_lv_hash)]
end
end
end
end
end
but this works:
module Puppet::Parser::Functions
newfunction(:mlh, :type => :rvalue) do |args|
lvm_default_hash = args[0]
lvm_additional_hash = args[1]
# empty Hash
hash_to_return = {}
if lvm_additional_hash.keys.length == 1
if lvm_additional_hash.keys.include? 'logical_volumes'
lvm_default_hash.keys.each do |key|
pv_array = Hash['physical_volumes' => lvm_default_hash[key]['physical_volumes']]
lv_hash = lvm_default_hash[key]['logical_volumes']
new_lv_hash = lvm_additional_hash['logical_volumes']
merged_lv_hash = Hash['logical_volumes' => lv_hash.merge(new_lv_hash)]
# assigned value in the 'each' loop we want to return to puppet
hash_to_return = Hash[key => pv_array.merge(merged_lv_hash)]
end
# returned Hash - instead of previous 'puts'
return hash_to_return
end
end
end
end
Now I have what I need!
Notice: Scope(Class[Ops_lvm]): sysphysical_volumes/de
You've got it -- the first one doesn't work because in Ruby, the return value of a block or function is the last evaluated statement. In the case of the one that didn't work, the last evaluated statement was the .each. As it turns out, each evaluates to the enumerable that it was looping through.
A simple example:
def foo
[1, 2, 3].each do |n|
puts n
end
end
If I were to run this, the return value of the function would be the array:
> foo
1
2
3
=> [1, 2, 3]
So what you have works, because the last thing evaluated is return hash_to_return. You could even just go hash_to_return and it'd work.
If you wanted to get rid of the return and clean that up a little bit (and if you're using Ruby 1.9 or above), you could replace your each line with:
lvm_default_hash.keys.each_with_object({}) do |key, hash_to_return|
This is because each_with_object evaluates to the "object" (in this case the empty hash passed into the method, and referred to as hash_to_return in the block params). If you do this you can remove the return as well as the initialization hash_to_return = {}.
Hope this helps!
Your custom function has rvalue type which means it needs to return value. If you don't specify return <something> by default, your last statement is implicitly your return.
In the example above, first one that does not work correctly, has last statement inside each block:
puts Hash[key => pv_array.merge(merged_lv_hash)]
Your second example is correct simply because you set value for hash_to_return in each block and then "return" it outside of each block. Not sure if this is the behavior you want since last assigned hash value (in last loop inside each block) will be the one that will be returned from this function.

How to write block in IRB?

For example, I want to implement this code in IRB, but it has single-line input so I can get how to write block there.
a = [3, 2, 1]
a[3] = a[2] - 1
a.each do |elt|
print elt+1
end
(Oh you mean IRB)
If you enter something that will be on multiple lines, ruby will wait until the final end is completed before running the code:
irb(main):001:0> def dostuff
irb(main):002:1> puts "things"
irb(main):003:1> end
=> :dostuff
irb(main):004:0> dostuff
things
=> nil
irb(main):005:0>
As you can see the number at the prompt changes depending on how deep the block-level is.

String with letter giving me a number in Ruby-Processing

I'm following this tutorial to learn about creating shapes and colors on a canvas. Here is the issue I'm running into: When I try to run the command in the run_command method and I take the first letter of my command (command[0]), it is returning the number 98 to me. I am trying to match the first letter of the command to a letter of the alphabet, but am unable to do so. What's strange though, is that when I remove the first letter with "command.delete "b"", the letter is removed and I'm free to use the rest of the string as I please.
Here is my code:
require 'ruby-processing'
class ProcessArtist < Processing::App
def setup
background(0, 0, 0)
end
def draw
# Do Stuff
end
def key_pressed
if #queue.nil?
#queue = ""
end
if key != "\n"
#queue = #queue + key
else
warn "Time to run the command: #{#queue}"
run_command(#queue)
#queue = ""
end
end
def run_command(command)
puts "Running command: #{command}"
puts command[0]
if command[0] == "b"
command.delete "b"
command.split(",")
background(command[0].to_i,command[1].to_i,command[2].to_i)
else
puts command[0]
command.delete "b"
command.split(",")
background(command[0].to_i,command[1].to_i,command[2].to_i)
end
end
end
ProcessArtist.new(:width => 800, :height => 800,
:title => "ProcessArtist", :full_screen => false)
Ah, I see what I did wrong. It should have been:
def run_command(command)
puts "Running command: #{command}"
puts command[0]
if command[0] = "b"
command.delete "b"
command.split(",")
background(command[0].to_i,command[1].to_i,command[2].to_i)
else
puts command[0]
command.delete "b"
command.split(",")
background(command[0].to_i,command[1].to_i,command[2].to_i)
end
end
It seems like you're using ruby version older than 1.9.
In old version of ruby (1.8-), String#\[\] return Fixnum object representing ASCII value, not String object.
>> RUBY_VERSION
=> "1.8.7"
>> 'bcd'[0]
=> 98
To get string back, use one of followings:
>> 'bcd'[0,1]
=> "b"
>> 'bcd'[0..0]
=> "b"
>> 'bcd'[0].chr # this will not work in Ruby 1.9+, so not recommended.
=> "b"
For comparison:
>> 'bcd'[0] == 'b'
=> false
>> 'bcd'[0] == ?b
=> true
>> 'bcd'.start_with? 'b'
=> true

list_box won't use default value on startup - Shoes.

I'm trying to use a list_box to select from different types of conversions. When I start the program and enter a number it does nothing, but if I click the next item in the list_box it works fine. This makes me think my method is not getting it's value from the list_box. Here's the code:
Shoes.app :title=> 'Temperature Converter', :width => 200, :height => 200 do
def convert(temp, unit='C')
if unit == "C"
(temp.to_i * 9.0 / 5.0) + 32.0
elsif unit == "F"
"Fail"
end
end
list_box :items => ["C", "F"], :choose => "C" do |item|
#unit.text = item.text
end
line1 = edit_line :width => 100
button 'Compute' do
#result.text = convert(line1.text, #unit.text)
end
#unit = para
#result = para
end
I tried setting 'C' as the default variable but that didn't work either. Is there a way to force the list_box to send it's value on startup?
Also, and unrelated, if I remove '#unit = para' from the end it won't print anything, even the #result. Why is that?
Any help would be awesome.
It probably won't print anything because your button and list_box are trying to call #unit.text, so you must continue to define #unit.
I think that is possibly the same reason that it won't choose "C" by default. At the time you choose "C", #unit is not defined.
Try this:
#list_box = list_box :items => ["C", "F"]
line1 = edit_line :width => 100
button 'Compute' do
#result.text = convert(line1.text, #unit.text)
end
#unit = para
#list_box.change{|item|
#unit.text = item.text
}
#list_box.choose("C")
I am unsure whether you need to separate the change method from the list box, but I have done so to be on the safe side.
EDIT: 2012-01-31 13:29
Shoes.app :title=> 'Temperature Converter', :width => 200, :height => 200 do
def convert(temp, unit='C')
#unit.text = unit
if unit == "C"
(temp.to_i * 9.0 / 5.0) + 32.0
elsif unit == "F"
"Fail"
end
end
#list_box = list_box :items => ["C", "F"]
line1 = edit_line :width => 100
button 'Compute' do
#result.text = convert(line1.text, #list_box.text)
end
#unit = para
#list_box.choose("C")
#result = para
end
The reason the op's code does not succeed in retrieving the initial value of the list box is because the block given to list_box() is only executed onchange, i.e. when the user changes the selection in the listbox. The button click in the op's code retrieves the current value of a para, horribly named "list_box"--but the para is only set when the onchcange event of the listbox fires, and the initial value of the para is set to nothing.
The way to get the initial value of the listbox when a button is clicked is by not relying on the onchange event. Instead, when the button is clicked, simply query the listbox for its current value, e.g. #my_listbox.text.
However, querying a listbox for its current value does not work directly inside the app block--apparently the listbox does not exist until after the app block has finished executing. In that case, you need to manually set the initial value of the list box:
#default_choice = "red"
#current_choice = "red"
list_box :items => ["blue", "red", "green"], :choose => #default_choice do |list|
#current_choice = list.text
end

Strange behavior with instance variables in Shoes

Hey, all. I'm working on making a GUI for a Ruby project using Shoes.
I've got a class called Manager (as in memory manager) that loads a 'process list' from a file, splits it up and assigns things to different 'pages' in memory when a certain execution call is made. I really don't think this part matters too much, though. It all works as a terminal application just fine.
However, Shoes is just baffling me. Here's what I've got so far:
Shoes.app(:title => "Paging Simulator", :width => 800, :height => 450) do
#manager = Manager.new
stack(:width => 200) do
#exec_list = stack {
title "Execution Queue", :size => 14
#exec_lines = para "click button to load", :size => 9
#file_button = button "Load Process List"
#file_button.click {
filename = ask_open_file
# #manager.set_exec_list filename
# alert "this makes no sense"
#exec_lines.text = #manager.exec_list.join "\n"
# exec_lines.text = File.read filename
}
}
end
end
What happens when I run this:
The program view loads as expected. I get a header, a paragraph that says "click button....", and a button. I click the button and I select the file. But this is where things get weird.
If I run the last commented line exec_lines.text = File.read filename it does as I would like, but my manager doesn't get any of the information it needs.
If I run the #manager.set_exec_list filename line, nothing from that line on in the block gets run, including the alert, or any other code I try to put in there.
if I run as shown above, however, I get the output I expect, but I don't get to set my data from the file that I select.
I've tried to figure this out from the Shoes Rules page, but this doesn't seem to be an issue that they address, and their "it changes/doesn't change self" I think I grasp, but it's confusing and I don't think it's exactly related to this problem.
Does anyone have any idea how to get this to work? I'm kind of down to crunch time on this project and I can't seem to get any other Ruby GUI toolkit to even run, so I think I'm pretty stuck with Shoes.
Thanks.
Update
I've tried running ruby-debug on the code when I make the call to #manager.set_exec_list filename, and stepping through it shows that this call is made, but the code never actually (from what I can tell) jumps into that method, and acts like it's the last line of code in the block. Do I need to include these classes inside the Shoes.app block?
Update Nope. That does nothing different.
update fullsource code follows:
#!/usr/bin/env shoes
require 'rubygems'
require 'ruby-debug'
class MemSegment
attr_accessor :filled, :pid, :seg, :seg_id
def initialize(filled=false, pid=nil, seg=nil, seg_id=0)
#filled = filled
#pid = pid.to_i
#seg = seg.to_s
#seg_id = seg_id.to_i
self
end
def fill(pid, seg, seg_id)
#filled = true; #pid = pid; #seg = seg; #seg_id = seg_id;
self
end
def clear
self.filled = false; self.pid = nil; self.seg = nil;
self
end
def filled?
#filled
end
def to_s
filled? ? "#{seg} #{seg_id} for pid #{pid}" : "Free"
end
end
class SimProc
include Enumerable
attr_accessor :pid, :code, :data
def initialize(pid, code, data)
#pid = pid.to_i
#code = code.to_i
#data = data.to_i
end
def each
yield :code, code
yield :data, data
end
def to_s
"[SimProc :pid => #{pid}, :code => #{code}, :data => #{data}]"
end
def to_a
[#pid, #code, #data]
end
end
class Manager
attr_reader :segments, :processes, :exec_list, :exec_object
def initialize
#exec_list = [[1, 2], [3, 4], [5, 6]]
#processes = {}
#segments = Array.new(8) { MemSegment.new }
end
def print_activity
#segments.each_with_index {|s, index| puts "Seg #{index} => #{s}" }
#processes.each_value {|s| puts s }
end
def load_process(pcb, exec_index)
if pcb.size == 3
p = SimProc.new(*pcb)
bad_load = false
#processes.store p.pid, p
#processes[p.pid].each do |proc_seg, bsize|
(bsize / 512.0).ceil.times do |seg_id|
#segments.each_with_index do |s, index|
if !s.filled
#find the first empty memory segment
s.fill p.pid, proc_seg, seg_id
break
# if all slots are filled and we couldn't place a proc block
elsif index == #segments.size - 1
bad_load = true
puts "Cannot find a place for #{proc_seg} segment of size #{bsize}. Requeueing..."
break;
end
end
break if bad_load
end
end
# recover pages and queue the process for later
if bad_load
#segments.each_with_index do |seg, seg_index|
# clear any segments that didn't get loaded properly
if seg.pid == p.pid
seg.clear
puts "Seg #{seg_index} => segment cleared: #{seg}"
end
end
# reinsert this process after the next in the execution list
# it will attempt to load and run after the next process is performed
#exec_list.insert(exec_index + 2, p.to_a)
end
print_activity
elsif pcb.size == 2 and pcb[1] == -1
# a process is exiting
puts "removing pid #{pcb[0]}"
#segments.each { |s| s.clear if s.pid == pcb[0] }
#processes.delete pcb[0]
print_activity
end
end
def set_exec_list(filename)
file = File.open filename
file.each { |pcb| #exec_list << pcb.split.map(&:to_i) } unless file.nil?
filename
end
def main
exseq = File.open('exseq2.txt')
set_exec_list exseq
# this is the object that will be used to run each process with .next
#exec_object = #exec_list.each_with_index
# #exec_list.each_with_index { |pcb, exec_index| load_process(pcb, exec_index) }
(#exec_list.size + 1).times do
load_process(*#exec_object.next)
end
end
end
=begin
manager = Manager.new
manager.main
=end
#=begin
Shoes.app(:title => "Paging Simulator", :width => 800, :height => 450) do
#manager = Manager.new
stack(:width => 200) do
#exec_list = stack {
title "Execution Queue", :size => 14
#exec_lines = para "click button to load", :size => 9
#file_button = button "Load Process List"
debugger
#file_button.click {
filename = ask_open_file
#manager.set_exec_list filename
# alert "this makes no sense"
# #exec_lines.text = #manager.exec_list
# #exec_lines.text = File.read filename
#exec_lines.text = #manager.exec_list.join "\n"
}
}
end
end
#=end
So, a few things:
#1, I don't have the implementation of Manager, so I can't tell you why it breaks. Did you try checking the Shoes console for any errors? Hit control-/ to bring that up. If 'nothing runs after it hits that line,' that's probably the issue.
#2, this does work for me, as long as you change exec_lines to #exec_lines on the last line. Here's what I tried:
class Manager;end
Shoes.app(:title => "Paging Simulator", :width => 800, :height => 450) do
#manager = Manager.new
stack(:width => 200) do
#exec_list = stack {
title "Execution Queue", :size => 14
#exec_lines = para "click button to load", :size => 9
#file_button = button "Load Process List"
#file_button.click {
filename = ask_open_file
#alert "this makes no sense"
#exec_lines.text = File.read filename
}
}
end
end
Hope that helps!

Resources