Create  Edit  FrontPage  Index  Search  Changes  History  RSS  Login

tut-gtk2-mnstbs-popup

Pop-up Menus

In this chapter we will learn how to create pop-up menus, menu bars, and toolbars. We will begin by creating each of these manually to see how these widgets are constructed, and what are the principles and concepts on which the toolbars and menus are built. After that we will introduce the Gtk::UIManager, which allows us to dynamically construct a user interface (menus and toolbars) from UI definitions stored in XML files. Each UI file is loaded into the GTK system, and each element applied to a corresponding actionobject, which is responsible for item's presentation on the display and for its behaviour (actions).

You will begin this chapter by learning how to create a pop-up menu. A pop-up menu is a Gtk::Menu widget that is displayed to the user when the right mouse button is clicked when hovering above certain widgets, or when 'window accelerator keys <Shift+F10>' are pressed (provided, the application is programmed to catch this eventuality).

The popup menu itself is created by the Gtk::Menu#popup(parent_menu_shell, parent_menu_item, button, activate_time){|menu, x, y, push_in| ... } method. We see a number of parameters including an optional user supplied block used to position the menu. Applications can use this method to display context-sensitive menus, and will usually supply nil for the 'parent_menu_shell' and 'parent_menu_item' parameters. When the user uses accelerator keys rather than mouse to open the context menu, the button parameter should be set to 0. Also if the action is invoked by accelerator keys, you would not have an event object which you would normally use to provide 'event.time'. In that case you would need to provide the time value by passing it Gdk::Event::CURRENT_TIME as 'activate_time' parameter.

Lets start now by creating a bare bones "Hello World" program, with the simplest possible context menu:

bare-bones-context-menu.rb

#!/usr/bin/env ruby

require 'gtk2'

menu = Gtk::Menu.new
menu.append(mitem1 = Gtk::MenuItem.new("Test1"))
menu.append(mitem2 = Gtk::MenuItem.new("Test2"))
menu.show_all

mitem1.signal_connect('activate') { |w| puts "#{w.class} - Test2" }
mitem2.signal_connect('activate') { |w| puts "#{w.class} - Test2" }

window = Gtk::Window.new("Bare Bones Context Menu")
# Make window sensitive to Right-mouse-click, to open the pop-up menu.
window.add_events(Gdk::Event::BUTTON_PRESS_MASK)
window.signal_connect("button_press_event") do |widget, event|
  menu.popup(nil, nil, event.button, event.time) if (event.button == 3)
end
# Make window sensitive to <Shift+F10> accelerator keys. These
# accelerator keys generate the 'popup-menu' signal for window,
# which opens the popup-menu.
window.signal_connect("popup_menu") do |w|
  menu.popup(nil, nil, 0, Gdk::Event::CURRENT_TIME)
end

window.set_default_size(300, 100).show_all
window.signal_connect('destroy') { Gtk.main_quit }
window.add(Gtk::Label.new("Hello World\n" +
                          "You may 'right-click' me\n\n" +
                          "or use <Shift+F10>"))
window.show_all
Gtk.main

This short example shod give you a good idea, about creating a pop-up menu. We started by creating the menu, adding to it two menu items. Note, from Gtk::Widget inherited 'show_all' method which you have to run after you set up your menu. Though, at this point not yet essential, but eventually required is the code to connect item-menu's'activate'signal to callbacks. The last is a rather important step directly related to pop-up menu implementation, which depending on the capabilities of the widget to which the menu is going to be attached, can be accomplished in more ways. Simpler ones are the two we use here in our bare-bones pop-up menu example, since window provides or inherits all necessary behaviours. Here we need to to mention at least window's'button-press-event'signal which it inherits from Gtk::Widget. This signal is emitted when a user clicks 'right-mouse-button', and is used in windows' signal handler to open the pop-up menu.

# Make window sensitive to Right-mouse-click, to open the pop-up menu.
window.add_events(Gdk::Event::BUTTON_PRESS_MASK)
window.signal_connect("button_press_event") do |widget, event|
  menu.popup(nil, nil, event.button, event.time) if (event.button == 3)
end
# Make window sensitive to <Shift+F10> accelerator keys. These
# accelerator keys generate the 'popup-menu' signal for window,
# which opens the popup-menu.
window.signal_connect("popup_menu") do |w|
  menu.popup(nil, nil, 0, Gdk::Event::CURRENT_TIME)
end

Do not worry, if you do not understand all the features above, and if some are not covered in full, we are going to review them shortly.



At this point we should also mention that some widgets such as Gtk::Entry and Gtk::TextView, already have a pop-up menus built into the widget by default. If you want to change the pop-up menu of a widget that offers one by default, you should edit the supplied Gtk::Menu widget in the pop-up callback procedure, i.e. block or proc. For example, both Gtk::Entry and Gtk::TextView have apopulate-popoup signal, which receives the Gtk::Menu that is going to be displayed. You can edit this menu in any way you see fit before displaying it to the user.

Creating a Pop-up Menu

mnstbs-popup-01.png

For most widgets you will need to create your own pop-up menu. In this section you are going to learn how to supply a pop-up menu to a Gtk::ProgressBar widget. You can see the pop-up menu we are going to implement in the picture on the right.

In the example three pop-up menu items are used to pulse the progress bar, set it as 100 percent complete, and to clear it. You will notice that the progress bar is contained in an event box. This is because the Gtk::ProgressBar widget, just like the Gtk::Label, which in chapter 3 in section called Event Boxes we made click-able, is not capable of intercepting GDK events by itself.

dialog-warning.png

Surprise - Progress Bar in Ruby, can be made event sensitive?!

API documentation for Gtk::Widget#event= tells us that for the widgets which do not have Gtk::Widget::NO_WINDOW flag set, you should be able to specify which events you wish the widget would respond to by using either Gtk::Widget#event= or Gtk::Widget#add_events(events) instance methods. The following code segment:

# Gtk::Widget#flags reveals Gtk::Widget#GtkWidgetFlags
if progress.flags & Gtk::Widget::NO_WINDOW
  puts "#{progress.class} does not contain Gdk::Window"
end

will confirm that Gtg::ProgressBar is a widget which does not provide its own Gdk::Window (see: Gtk::Widget#GtkWidgetFlags). Nevertheless, as of Ruby 1.9.3 (October 2012), the following still works:

progress.add_events(Gdk::Event::BUTTON_PRESS_MASK)
progress.signal_connect("button_press_event") do |widget, event|
  if (event.button == 3)
    menu.popup(nil, nil, event.button, event.time)
  end	
end

In our example program we chose the traditional approach reflecting the fact that for widgets incapable of responding to desired events we supply the Even Box. Let's look at the program:


popupmenus.rb

#!/usr/bin/env ruby
require 'gtk2'

# Create the poup menu with three items and a separator.
# Then, attach it to the progress bar and show it to the
# user.
def create_popup_menu(menu, progb)
  pulse  = Gtk::MenuItem.new("Pulse Progress")
  separator = Gtk::MenuItem.new
  fill   = Gtk::MenuItem.new("Set as Complete")
  clear  = Gtk::MenuItem.new("_Clear Progress")

  menu.append(pulse)
  menu.append(separator)
  menu.append(fill)
  menu.append(clear)

  pulse.signal_connect('activate') { |w| pulse_activated(w, progb) }
  fill.signal_connect('activate')  { |w| fill_activated(w,  progb) }
  clear.signal_connect('activate') { |w| clear_activated(w, progb) }
  menu.show_all
end

def pulse_activated(menuitem, progbar)
  progbar.pulse
  progbar.text = "Pulse!"
end

def fill_activated(menuitem, progbar)
  progbar.fraction = 1.0
  progbar.text = "One Hundred Percent"
end

def clear_activated(menuitem, progbar)
  progbar.fraction = 0.0
  progbar.text = "Reset to Zero"
end

window = Gtk::Window.new(Gtk::Window::TOPLEVEL)
window.resizable = true
window.title = "Popup Menus"
window.border_width = 10
window.signal_connect('delete_event') { Gtk.main_quit }
window.set_size_request(250, -1)

# Create all of the necessary widgets and initialize the popup menu.
menu = Gtk::Menu.new
eventbox = Gtk::EventBox.new
progress = Gtk::ProgressBar.new
progress.text = "Nothing yet happened"
progress.pulse
progress.pulse_step = 0.05

create_popup_menu(menu, progress)

eventbox = Gtk::EventBox.new
eventbox.events = Gdk::Event::BUTTON_PRESS_MASK
eventbox.signal_connect('button_press_event') do |w, event|
  if event.event_type == Gdk::Event::BUTTON_PRESS
    if event.button == 3   # right mouse button
      menu.popup(nil, nil, event.button, event.time)
    end
  end
end
eventbox.add(progress)
window.add(eventbox)
eventbox.realize
window.show_all
Gtk.main

In most cases you will want to use Gtk:Widget'sbutton-press-eventsignal to detect when the user wants the pop-up menu to be shown. At this point you may also check whether the right mouse button (event.button == 3) was clicked.

Initially an empty pop-up menu is created. We use the helper'create_popup_menu'method to create and add (append) individual menu items to the menu. You could also use Gtk::MenuShell#prepend(child) or Gtk::MenuShell#insert(child, position). Finally we bind the menu items to the appropriate callback methods and Gtk::MenuItem'sactivatesignal.

In this example it is not at all necessary to call Gtk::Menu#attach_to_widget(attach_widget){|attach_widgt, menu| ... }, which sets the callback procedure (block) to be invoked when the menu calls Gtk::Menu#detach during its destruction.

For Separators in menus use MenuItem with no label

Separators are extremely important when designing a menu structure, because they organize menu items into groups so the user can easily find the appropriate item. Preferably you should use the Gtk::SeparatorMenuItem class to create the separator objects, though Gtk::MenuItem(s) with no label work as well. I experimented with both since currently there seems to be a bug in this area. (see: the warning below).

Pop-up Menu Callbacks

After creating the necessary widgets, you need to handle the button-press-event signal. In our example the pop-up menu is displayed every time the right mouse button is clicked while hovering over the progress bar. We use Gtk::Menu#popup(parent_menu_shell, parent_menu_item, button, activate_time){|menu, x, y, push_in| ... } to display the menu on the screen.

menu.popup(nil, nil, event.button, event.time)

We had all the parameters except the button and activate_time set to nil. If the pop-up menu was activated by something other than a mouse button click, you should supply 0 to the button parameter.

Note:

If the action was invoked by apopup-menusignal emitted by the<Shift+F10>accelerator keys, the 'event.time' would not be available. You would need to provide the time value yourself (Gdk::Event::CURRENT_TIME):

# Accelerator keys <Shift+F10> generate 'popup-menu' signal
# for window, but neither for progress bar nor for event box!
window.signal_connect("popup_menu") do |w|
  menu.popup(nil, nil, 0, Gdk::Event::CURRENT_TIME)
end

Keyboard Accelerators

accelerator-keys.png

When creating a menu, one of the most important things to do is to set up keyboard accelerators. A keyboard accelerator is a key combination created from one accelerator key and one or more modifiers like Ctrl, Alt and Shift. When a user presses a particular key combination, the appropriate signal is emitted. However, you can also use accelerator keys in programs that do not utilize menus of any kind. We will talk about this shortly, and since we are discussing the popup menus here, let's look first at the example in the context of menus:

accelerators-in-menus.rb


#!/usr/bin/env ruby
require 'gtk2'

# Create the poup menu with three items and a separator.
# Then, attach it to the progress bar and show it to the
# user.
def create_popup_menu(menu, progb, window)
  pulse  = Gtk::MenuItem.new("Pulse Progress")
  separator = Gtk::MenuItem.new
  fill   = Gtk::MenuItem.new("Set as Complete")
  clear  = Gtk::MenuItem.new("_Clear Progress")

  menu.append(pulse)
  menu.append(separator)
  menu.append(fill)
  menu.append(clear)

  # Create a keyboard accelerator group for the application.
  group = Gtk::AccelGroup.new
  window.add_accel_group(group)
## menu.accel_group=(group)     ## Despite what is said in API, this line is not really required here

  # Add the necessary keyboard accelerators, to menu items.
  # Widget#add_accelerator('activate', group, accel_key, accel_mods, accel_flags)
  pulse.add_accelerator('activate', group, Gdk::Keyval::GDK_P,
        Gdk::Window::CONTROL_MASK, Gtk::ACCEL_VISIBLE)
  fill.add_accelerator('activate', group, Gdk::Keyval::GDK_F,
        Gdk::Window::CONTROL_MASK, Gtk::ACCEL_VISIBLE)
  clear.add_accelerator('activate', group, Gdk::Keyval::GDK_C,
        Gdk::Window::CONTROL_MASK|Gdk::Window::SHIFT_MASK, Gtk::ACCEL_VISIBLE)

  pulse.signal_connect('activate') { |w| pulse_activated(w, progb) }
  fill.signal_connect('activate')  { |w| fill_activated(w, progb) }
  clear.signal_connect('activate') { |w| clear_activated(w, progb) }

  # We do not need this method since there is no detach in our program.
  # Also the API documentation is rather vague about its usage
  menu.attach_to_widget(progb) {|attach_widgt, mnu| puts "detaching" }
  menu.show_all
end

def pulse_activated(menuitem, progbar)
  puts "pulse_activated"
  progbar.pulse
  progbar.text = "Pulse!"
end

def fill_activated(menuitem, progbar)
  puts "fill_activated"
  progbar.fraction = 1.0
  progbar.text = "One Hundred Percent"
end

def clear_activated(menuitem, progbar)
  puts "clear_activated"
  progbar.fraction = 0.0
  progbar.text = "Reset to Zero"
end

window = Gtk::Window.new("Accelerators")
window.resizable = true
window.border_width = 10
window.signal_connect('destroy') { Gtk.main_quit }
window.set_size_request(250, -1)

# Create all of the necessary widgets and initialize the popup menu.
menu = Gtk::Menu.new
eventbox = Gtk::EventBox.new
progress = Gtk::ProgressBar.new
progress.text = "Nothing yet happened"
progress.pulse
progress.pulse_step = 0.05

create_popup_menu(menu, progress, window)

eventbox = Gtk::EventBox.new
eventbox.events = Gdk::Event::BUTTON_PRESS_MASK
eventbox.signal_connect('button_press_event') do |w, event|
  if event.event_type == Gdk::Event::BUTTON_PRESS
    if event.button == 3   # left mouse button
      menu.popup(nil, nil, event.button, event.time)
    end
  end
end
eventbox.add(progress)
window.add(eventbox)
eventbox.realize
window.show_all
Gtk.main

To create this example program we have used the previous "popupmenus.rb" program. Beside a trivial change in it's name label, the only important difference can be found in the create_popup_menu method. There are quite a few things there that require our attention. First is the creation of the accelerator group Gtk::AccelGroup object. That is the object in which keyboard accelerators are stored. In order to implement accelerators in your application you need to create a new accelerator group. This group must be added to the Gtk::Window, where the menu will appear for it to be able to respond to the accelerator key presses when the window widget has the focus. Accelerator group must also be associated with any menus that will take advantage of its accelerators, this is accomplished with Gtk::Menu#accel_group=(accel_group) and Gtk::Widget#add_accelerator.

# Create a keyboard accelerator group for the application.
group = Gtk::AccelGroup.new
window.add_accel_group(group)
## menu.accel_group=(group)     ## Despite what is said in API, this line is not really required here

# Add the necessary keyboard accelerators.
# Widget#add_accelerator(accel_signal='activate', group, accel_key, accel_mods, accel_flags)
pulse.add_accelerator('activate', group, Gdk::Keyval::GDK_P,
      Gdk::Window::CONTROL_MASK, Gtk::ACCEL_VISIBLE)
fill.add_accelerator('activate', group, Gdk::Keyval::GDK_F,
      Gdk::Window::CONTROL_MASK, Gtk::ACCEL_VISIBLE)
clear.add_accelerator('activate', group, Gdk::Keyval::GDK_C,
      Gdk::Window::CONTROL_MASK|Gdk::Window::SHIFT_MASK, Gtk::ACCEL_VISIBLE)

To add an accelerator to a widget, you can use Gtk::Widget#add_accelerator method, which will make sure the signal specified by the "accel_signal='activate'"argument will be emitted, when the user presses the key combination on that widget (in our case this widget is the clicked menu item). Indeed for this to work, the accelerator group must be associated with the window and the menu items as mentioned earlier. The modifiers are specified in (GdkModifierType). Most often used modifiers are Gdk::Window::SHIFT_MASK, Gdk::Window::CONTROL_MASK, and Gdk::Window::MOD1_MASK, which correspond to Shift, Ctrl, and Alt keys respectively. The last parameter accel_flags (Gtk::ACCEL_VISIBLE) will make the accelerator visible in the label, Gtk::ACCEL_LOCKED will prevent the user from modifying the accelerator, Gtk::ACCEL_MASK will set both flags for the widget accelerator.

For your convenience following is the Gtk::Widget#add_accelerator API documentation. You can better understand its many parameters by reading the API segment that describes it:

add_accelerator(accel_signal, accel_group, accel_key, accel_mods, accel_flags)
Installs an accelerator for this widget in accel_group that causes accel_signal to be emitted if the accelerator is activated. The accel_group needs to be added to the widget's toplevel via Gtk::Window#add_accel_group. Accelerators added through this function are not user changeable during run-time. If you want to support accelerators that can be changed by the user, use Gtk::AccelMap#add_entry and Gtk::Widget#set_accel_path or Gtk::MenuItem#accel_path= instead.

Accelerator Path

It is possible to manually create keyboard accelerators with Gtk::AccelMap, but in most cases the Gtk::Widget#add_accelerator will provide all the necessary functionality, nevertheless, we will see the AccelMap feature in the following example'custom-accelerators.rb'.

The accelerator path is a string you create. It must consist of "<WINDOWTYPE>/Category1/Category2/.../Action", where <WINDOWTYPE> should be a unique application-specific identifier, that corresponds to the kind of window the accelerator is being used in, e.g. "Gimp-Image", "Abiword-Document" or "Gnumeric-Settings". The Category1/.../Action portion is most appropriately chosen by the action the accelerator triggers, i.e. for accelerators on menu items, choose the item's menu path, e.g. "File/Save As", "Image/View/Zoom" or "Edit/Select All". So a full valid accelerator path may look like: "<Gimp-Toolbox>/File/Dialogs/Tool Options...". Accelerator path is mapped to accelerator keys and added to the global accelerator map with Gtk::AccelMap.add_entry(accel_path, accel_key, accel_mods).

Accelerator Path In Menus
We have not sufficiently acquainted ourselves with the Gtk::Menu widgets to fully understand the significance of implementing accelerator path in a context of a menu. (If you wish to jump ahead, you can check it out by jumping to section 9.5.1 entitled 'Keyboard Accelerators And Their Relationships With Menus And Other Widgets'.) Nevertheless, it is worthwhile mentioning the following conclusion, from that section:
Using Accelerator Path In Menus (from section 9.5.2)
Unfortunately building accelerator path with Gtk::AccelMap.add_entry does not bring anything valuable to the menu designs. Creating 'accel-path' and adding it to the Gtk::AccelGroup seems redundant and incompletely implemented in Gtk. In particular mi.accel_path = "my-acc-p/xyz" has no effect, and fails to display the accelerator key on the menu item! The only thing that works is that callback is called when accelerator key is pressed.
Accelerators can also be used in programs without menus
All accelerators, regardless of whether they are associated with menus or not, use global Gtk::AccelMap object which defines a list entries in this table in the form of mappings between a unique accelerator name (in the Gtk::AccelMap known as the 'accel_path')and the actual key combinations. These accelerators (keys) or 'accel_paths' eventually have to be mapped to actions. There however, is a difference how actions are associated with menu item accelerators in windows with menus and in windows without menus, i.e. for windows themselves. First you will notice that in either case we use Gtk::Window#add_accel_group(group). Note, that this statement is mandatory only in the programs without menus, but should be included in programs with menus only when in addition to triggering actions through menu items you also wish to provide the access to this functionality via accelerator keys. The other more pronounced difference is how actions are associated either with menu items, or merely with the accelerator abstractions, as is the case in programs without the menus. While for menu items their respective actions are triggered by a specified signal (remember the"accel_signal='activate'" parameter to the Gtk::Widget#add_accelerator method above), in programs without menus accelerator key actions are associated directly to the corresponding accelerator key abstractions (e.g.: 'accel_key,...' and 'accel_path'). This is accomplished with the help of 'Gtk::AccelGroup#connect(accel_key, accel_mods, accel_flags, closure)', 'Gtk::AccelGroup#connect(accel_key, accel_mods, accel_flags) {...}', or 'Gtk::AccelGroup#connect(accel_path, closure)', or 'Gtk::AccelGroup#connect(accel_path) {...}'.

accelerator-keys-2.png

Following program example will make these things more clear. In this program's main window we defined only a label reminding the user what key combinations are available to elicit a different action for each of them. For instance, as suggested by the text on the label, pressing merely a single key 'A' or 'B', or a combination '<Ctrl+Shift>+C' will print a particular message on the console (control terminal from which you executed 'ruby custom-accelerators.rb'). Note also, that we are not at all concerned with any kind of signals here (all this his handled for us automatically by GTK).

custom-accelerators.rb

#!/usr/bin/env ruby
require 'gtk2'

ag = Gtk::AccelGroup.new
ag.connect(Gdk::Keyval::GDK_Z, Gdk::Window::CONTROL_MASK, Gtk::ACCEL_VISIBLE) {
  p "You've presswd Ctrl+Z."
}

Gtk::AccelMap.add_entry("<AccelMap Demo>/test_a", Gdk::Keyval::GDK_A, 0)
Gtk::AccelMap.add_entry("<AccelMap Demo>/test_b", Gdk::Keyval::GDK_B, 0)
Gtk::AccelMap.add_entry("<AccelMap Demo>/test ctrl+sh+c", Gdk::Keyval::GDK_C, 
	Gdk::Window::CONTROL_MASK|Gdk::Window::SHIFT_MASK)

ag.connect("<AccelMap Demo>/test_a") { p "Hello with (A)" }
ag.connect("<AccelMap Demo>/test_b") { p "Hello (B)" }
ag.connect("<AccelMap Demo>/test ctrl+sh+c") { p "Hello World, with (Ctrl+Shift+C)" }

window = Gtk::Window.new("Custom Acceleratots")
window.add(Gtk::Label.new("Press keys:\n\tA, B, Ctrl+Shift+C,\n\tor Ctrl+Z")) 
window.resizable = true
window.set_size_request(250, -1)
window.signal_connect("destroy") {Gtk.main_quit}
window.add_accel_group(ag)
window.show_all
Gtk.main
Last modified:2012/11/07 07:58:20
Keyword(s):
References:[tut-gtk2-mnstbs-tbi] [tut-gtk2-mnstbs] [tut-gtk2-mnstbs-mnui] [tut-gtk2-mnstbs-mnub] [tut-gtk2-mnstbs-popup] [tut-gtk]