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

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?

Josef Wolf wrote:

/ ...

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?

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.

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.

It is apparent that the canvas is receiving all mouse events, not the
rectangle, and in order for the rectangle to launch its own context menu,
it would have to process mouse events independently. AFAICS it can't.

I want to emphasize this is just a guess, and I don't use the 'Tk' library
because it is too poorly documented.

Have you considered using something other than 'Tk'?

···

--
Paul Lutus
http://www.arachnoid.com

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

···

On Sep 3, 2006, at 10:40 AM, Josef Wolf wrote:

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?

#
# 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>

Thanks for your reply, Paul!

> 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?

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 :slight_smile: 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 :frowning:

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?

···

On Mon, Sep 04, 2006 at 12:45:34AM +0900, Paul Lutus 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. :frowning: 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?

···

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.

By left clicking -- either on or off the menu. Which menu don't you get, the canvas' or the rect's?

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. :frowning: 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 :wink:

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

···

On Sep 5, 2006, at 2:00 PM, Josef Wolf wrote:

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

Josef Wolf wrote:

/ ...

Have you considered using something other than 'Tk'?

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

I use the Qt library for most of my projects. I don't know what platform you
are working with or any other factors, but Qt seems to be easy to use and
is better-documented.

p.s. sorry for the delayed reply, there has been a Usenet posting problem on
this NG lately.

···

--
Paul Lutus
http://www.arachnoid.com

>How do you "dismiss" the menu? Whatever I do, I never get the
>second menu.
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.

>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.

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

···

On Wed, Sep 06, 2006 at 04:00:45AM +0900, Morton Goldberg wrote:

On Sep 5, 2006, at 2:00 PM, Josef Wolf wrote:

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

···

On Sep 6, 2006, at 12:50 PM, Josef Wolf wrote:

On Wed, Sep 06, 2006 at 04:00:45AM +0900, Morton Goldberg wrote:

On Sep 5, 2006, at 2:00 PM, Josef Wolf wrote:

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

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.

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.

···

On Thu, Sep 07, 2006 at 10:31:00AM +0900, Morton Goldberg wrote:

On Sep 6, 2006, at 12:50 PM, Josef Wolf wrote:
>On Wed, Sep 06, 2006 at 04:00:45AM +0900, Morton Goldberg wrote:

>>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 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.

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

···

On Sep 7, 2006, at 1:40 AM, Josef Wolf wrote:

On Thu, Sep 07, 2006 at 10:31:00AM +0900, Morton Goldberg wrote:

On Sep 6, 2006, at 12:50 PM, Josef Wolf wrote:

On Wed, Sep 06, 2006 at 04:00:45AM +0900, Morton Goldberg wrote:

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 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.

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.

···

On Thu, Sep 07, 2006 at 11:41:55PM +0900, Morton Goldberg wrote:

On Sep 7, 2006, at 1:40 AM, Josef Wolf wrote:
>On Thu, Sep 07, 2006 at 10:31:00AM +0900, Morton Goldberg wrote:
>>On Sep 6, 2006, at 12:50 PM, Josef Wolf wrote:
>>>On Wed, Sep 06, 2006 at 04:00:45AM +0900, Morton Goldberg wrote:

>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.

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

···

On Sep 7, 2006, at 1:30 PM, Josef Wolf wrote:

On Thu, Sep 07, 2006 at 11:41:55PM +0900, Morton Goldberg wrote:

On Sep 7, 2006, at 1:40 AM, Josef Wolf wrote:

On Thu, Sep 07, 2006 at 10:31:00AM +0900, Morton Goldberg wrote:

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.

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.