How to properly bind context menus to canvas in Ruby/Tk?

J

Josef Wolf

Hello!

I am trying to bind context-menus to a Tk canvas. The canvas itself should
have a context menu, giving the ability to create new objects. In addition,
every object should have its own context menu so that the object can be
modified/deleted. This is what I'm doing:

#! /usr/bin/ruby
require 'tk'
$-w = 1

root = TkRoot.new
canvas = TkCanvas.new.pack

# This is the context menu for the canvas to create new objects
#
menu=TkMenu.new
menu.add('command', 'label'=>'New Foo', 'command'=>proc{p "new foo"})
menu.add('command', 'label'=>'New Bar', 'command'=>proc{p "new bar"})

# canvas binding
canvas.bind("Button-3") { |e| evt=e; menu.popup(e.x_root, e.y_root) }

# Now create a new object and bind to it a context menu to modify/delete
# the object
#
rect = TkcRectangle.new(canvas, 0, 0, 50, 50, "fill"=>"white")
menu=TkMenu.new
menu.add('command', 'label'=>'Edit', 'command'=>proc{p "edit"})
menu.add('command', 'label'=>'Delete', 'command'=>proc{p "del"})

# object binding
canvas.itembind(rect, "Button-3") { |e| menu.popup(e.x_root, e.y_root) }

Tk.mainloop

Unfortunately, this don't work as desired. As soon as the binding to the
object is done, the canvas binding seems to be overridden. Even when
right-klicking on empty space in the canvas, the object's menu is invoked.

Any ideas what I am doing wrong here?
 
J

Josef Wolf

On Mon, Sep 04, 2006 at 12:45:34AM +0900, Paul Lutus wrote:

Thanks for your reply, Paul!
First, rename the second menu something other than "menu" You have already
used that name, and this multiple use is causing you a lot of confusion.

I admit that this is bad style :) I've made copy/paste. In the real
program, the creation/binding of the menus is done in different methods,
thus they are actually two different variables (beacuse they are local
to their method)
Second, after a bit of experimentation, I offer this educated guess. The
rectangle object cannot process mouse events, so it cannot invoke a context
menu different from that bound to the canvas.

AFAIK, the rectangle is no real object in its own right. The canvas
handles everything for it. That's the reason why its binding is done
with canvas.itembind() instead of rect.bind(). IMHO, it is very
important to understand this difference.
It is apparent that the canvas is receiving all mouse events, not the
rectangle,
Exactly

and in order for the rectangle to launch its own context menu,
it would have to process mouse events independently. AFAICS it can't.

The canvas handles those events. But for some reason the canvas fails
to differentiate.
I want to emphasize this is just a guess, and I don't use the 'Tk' library
because it is too poorly documented.

It's poorly documented, but it works great once you figure out how to
use it :-(

Especially the canvas widget is pure black magic. The concept of
attaching arbitrary tags to items combined to the ability to attach
bindings to the tags is very powerfull.
Have you considered using something other than 'Tk'?

I'm not aware of any usable alternatives. What would you suggest?
 
M

Morton Goldberg

Hello!

I am trying to bind context-menus to a Tk canvas. The canvas
itself should
have a context menu, giving the ability to create new objects. In
addition,
every object should have its own context menu so that the object
can be
modified/deleted. This is what I'm doing:

#! /usr/bin/ruby
require 'tk'
$-w = 1

root = TkRoot.new
canvas = TkCanvas.new.pack

# This is the context menu for the canvas to create new objects
#
menu=TkMenu.new
menu.add('command', 'label'=>'New Foo', 'command'=>proc{p "new
foo"})
menu.add('command', 'label'=>'New Bar', 'command'=>proc{p "new
bar"})

# canvas binding
canvas.bind("Button-3") { |e| evt=e; menu.popup(e.x_root,
e.y_root) }

# Now create a new object and bind to it a context menu to modify/
delete
# the object
#
rect = TkcRectangle.new(canvas, 0, 0, 50, 50, "fill"=>"white")
menu=TkMenu.new
menu.add('command', 'label'=>'Edit', 'command'=>proc{p "edit"})
menu.add('command', 'label'=>'Delete', 'command'=>proc{p "del"})

# object binding
canvas.itembind(rect, "Button-3") { |e| menu.popup(e.x_root,
e.y_root) }

Tk.mainloop

Unfortunately, this don't work as desired. As soon as the binding
to the
object is done, the canvas binding seems to be overridden. Even when
right-klicking on empty space in the canvas, the object's menu is
invoked.

Any ideas what I am doing wrong here?

I tried running your code on my system which is OS X 10.4.7 running
Ruby 1.8.2, and I don't see what you report. What I see is following:

* Right-clicking away from the the rectangle, the canvas' contextual
menu pops-up.
* Right-clicking on the rectangle.
** The rectangle's contextual menu pops-up.
** When the rectangle's menu is dismissed, the canvas'
contextual menu pops-up.

With a small tweak, I was able to keep the canvas' contextual menu
from popping up when the the rectangle is right-clicked. So what I
have now running on my system works as you expected.

Regards, Morton

P.S. Just in case you want to compare what I am running with your own
code.
<code>
#! /usr/bin/ruby -w
# Date: September 5, 2006
#
# Contextual menus on a canvas

require 'tk'

DEBUG = []
RIGHT_BUTTON = "Button-2" # On OS X right button is Button-2.

begin
root = TkRoot.new {title 'Ruby Tk'}
canvas = TkCanvas.new.pack
handled_by_rect = false

# This is the context menu for the canvas to create new objects
menu1=TkMenu.new
menu1.add('command', 'label'=>'New Foo', 'command'=>proc{p "new
foo"})
menu1.add('command', 'label'=>'New Bar', 'command'=>proc{p "new
bar"})

# Canvas binding
canvas.bind(RIGHT_BUTTON) do |e|
if handled_by_rect
handled_by_rect = false
else
menu1.popup(e.x_root, e.y_root)
end
end

# Now create a new object and bind to it a context menu to modify/
delete
# the object
rect = TkcRectangle.new(canvas, 50, 50, 100, 100, "fill"=>"white")
menu2=TkMenu.new
menu2.add('command', 'label'=>'Edit', 'command'=>proc{p "edit"})
menu2.add('command', 'label'=>'Delete', 'command'=>proc{p "del"})

# Rectangle binding
canvas.itembind(rect, RIGHT_BUTTON) do |e|
menu2.popup(e.x_root, e.y_root)
handled_by_rect = true
end

# Set initial window geometry; i.e., size and placement.
win_w, win_h = 300, 200
# root.minsize(win_w, win_h)
win_lf = (root.winfo_screenwidth - win_w) / 2
root.geometry("#{win_w}x#{win_h}+#{win_lf}+50")

# Make Cmnd+Q work as expected on OS X.
root.bind('Command-q') {Tk.root.destroy}

Tk.mainloop
ensure
puts DEBUG unless DEBUG.empty?
end
</code>
 
J

Josef Wolf

On Tue, Sep 05, 2006 at 08:43:01PM +0900, Morton Goldberg wrote:

Thanks for the reply, Morton!
I tried running your code on my system which is OS X 10.4.7 running
Ruby 1.8.2, and I don't see what you report. What I see is following:

* Right-clicking away from the the rectangle, the canvas' contextual
menu pops-up.
* Right-clicking on the rectangle.
** The rectangle's contextual menu pops-up.
** When the rectangle's menu is dismissed, the canvas'
contextual menu pops-up.

How do you "dismiss" the menu? Whatever I do, I never get the second menu.
With a small tweak, I was able to keep the canvas' contextual menu
from popping up when the the rectangle is right-clicked. So what I
have now running on my system works as you expected.

Your handled_by_rect flag seems to do the trick. But this would be a
global flag in my case since the menus are created from different
classes. :-( I would like to avoid such a global flag.

The Tk::Bind manpage mentions that processing of matching callbacks can
be controlled by "return" or calling "Tk->break" inside the callback.
That is, the rectangle's callback should call Tk->break. But none of
the involved classes seem to have a break method. And returning
false/true/nil/1/0 don't have any effect, too. Any ideas?
 
M

Morton Goldberg

On Tue, Sep 05, 2006 at 08:43:01PM +0900, Morton Goldberg wrote:

Thanks for the reply, Morton!


How do you "dismiss" the menu? Whatever I do, I never get the
second menu.

By left clicking -- either on or off the menu. Which menu don't you
get, the canvas' or the rect's?
Your handled_by_rect flag seems to do the trick. But this would be a
global flag in my case since the menus are created from different
classes. :-( I would like to avoid such a global flag.

WeLL, I try to avoid globals, too, but sometimes a global is the
simplest solution. On the other hand you could create a class, say
CanvasFlag, to manage the flag -- creating a class will solve most
Ruby design problems ;)
The Tk::Bind manpage mentions that processing of matching callbacks
can
be controlled by "return" or calling "Tk->break" inside the callback.
That is, the rectangle's callback should call Tk->break. But none of
the involved classes seem to have a break method. And returning
false/true/nil/1/0 don't have any effect, too. Any ideas?

I believe the Ruby/Tk equivalent is Tk.callback_break, but I don't
think it will help here where we're dealing with an interaction
between a bind and an itembind.

Regards, Morton
 
J

Josef Wolf

On Sep 5, 2006, at 2:00 PM, Josef Wolf wrote:
By left clicking -- either on or off the menu.

Don't work here.
Which menu don't you get, the canvas' or the rect's?

I always get the canvas menu and never see the rectangle's menu.
I believe the Ruby/Tk equivalent is Tk.callback_break, but I don't
think it will help here where we're dealing with an interaction
between a bind and an itembind.

I tried Tk.callback_break too, and it didn't work. I just forgot to
mention this in my last mail.
 
M

Morton Goldberg

Don't work here.


I always get the canvas menu and never see the rectangle's menu.

I think the reason my code works differently from yours is because I
use separate variable names for the two contextual menus while you
use the same one for both. It really does make a difference.

Regards, Morton
 
J

Josef Wolf

I think the reason my code works differently from yours is because I
use separate variable names for the two contextual menus while you
use the same one for both. It really does make a difference.

It _does_ make a difference, but in another way:

- If I use the same variable, always the last allocated menu wins.
Thus with the version I posted, always rectangle's menu wins.

- If I use different variables (as in my original code because they
are in different methods), always the canvas menu wins.

I _never_ see two different menus unless I introduce the handled_by_root
flag.
 
M

Morton Goldberg

It _does_ make a difference, but in another way:

- If I use the same variable, always the last allocated menu wins.
Thus with the version I posted, always rectangle's menu wins.

- If I use different variables (as in my original code because they
are in different methods), always the canvas menu wins.

I _never_ see two different menus unless I introduce the
handled_by_root
flag.

You _can_ see different menus if you change the posted version of
your code to assign each menu to a different variable. I can't
comment on your original code -- I've never seen it. But your posted
code will work much better if you use different variables for each
menu. Please try it yourself and see what happens.

Regards, Morton
 
J

Josef Wolf

You _can_ see different menus if you change the posted version of
your code to assign each menu to a different variable. I can't
comment on your original code -- I've never seen it. But your posted
code will work much better if you use different variables for each
menu. Please try it yourself and see what happens.

Well, I tried and I see different menus only when I use different
variables _and_ modify the code to use the handled_by_root flag. This
is probably because "dismissing a menu" has different effects in OS-X
than on KDE.
 
M

Morton Goldberg

Well, I tried and I see different menus only when I use different
variables _and_ modify the code to use the handled_by_root flag. This
is probably because "dismissing a menu" has different effects in OS-X
than on KDE.

I find the behavior of your code as I see it under OS X to be what I
expect, but the behavior of it under KDE, as you describe it,
mystifies me. I'm afraid I'm out of ideas on this matter. I'm truly
sorry that I haven't been able to help you.

Regards, Morton
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,744
Messages
2,569,484
Members
44,903
Latest member
orderPeak8CBDGummies

Latest Threads

Top