Chapter 18. Gtk2::UIManager

Objective

In a large application, certain menus may become available depending on the outcome of other actions. A word processor program is a typical example. When you select a graphic, you will have different menus available than when you select text. A word processor usually feature toolbars and menus. This leaves you with two places to access certain menus, and they must be kept in sync.

Gtk+ features the Gtk2::UIManager. This class makes managing the above scenario easy. This lesson will introduce the reader to Gtk2::UIManager, and the Gtk+ classes involved when using it.

18.1. Overview

The Gtk2::UIManager provides a way to create menus and toolbars from an XML-like description. The Gtk2::UIManager uses Gtk2::ActionGroup objects to manage the Gtk2::Action objects providing the common substructure for the menu and toolbar items.

Using the Gtk2::UIManager you can dynamically merge and demerge multiple UI descriptions and actions. This allows you to modify the menus and toolbars when the mode changes in the application (for example, changing from text editing to image editing), or when new plug-in features are added or removed from your application.

A UIManager can be used to create the menus and toolbars for an application user interface as follows:

18.1.1. Background on Gtk2::ActionGroup and Gtk2::Action

The Gtk2::Action and Gtk2::ActionGroup objects work together to provide the images, text, callbacks and accelerators for your application menus and toolbars. The Gtk2::UIManager uses Gtk2::Actions and Gtk2::ActionGroups to build the menubars and toolbars automatically based on a XML specification.

18.1.1.1. Gtk2::Actions

An Gtk2::Action object represents an action that the user can take using an application user interface. It contains information used by proxy UI elements (for example, Gtk2::MenuItems or Gtk2::Toolbar items) to present the action to the user.There are two subclasses of Gtk2::Action:

Table 18-1. Available Gtk2::Action subclasses

Gtk2 Class NameUse
Gtk2::ToggleActionAn Gtk2::Action that can be toggled between two states.
Gtk2::RadioActionAn Gtk2::Action that can be grouped so that only one can be active.

For example, the standard File->Quit menu item can be represented with an icon, mnemonic text and accelerator. When activated, the menu item triggers a callback that could exit the application. Likewise a Gtk2::Toolbar Quit button could share the icon, mnemonic text and callback. Both of these UI elements could be proxies of the same Gtk2::Action.

Note

Although it is possible to create an instance of Gtk2::Action, or its subclasses, we use the available methods of Gtk2::ActionGroup to create and add the Gtk2::Action instances to a Gtk2::ActionGroup instance.

This is because we will use the Gtk2::Action instances mostly as part of a Gtk2::ActionGroup. We also have the ability to extract required Gtk2::Action's from a Gtk2::ActionGroup.

18.1.1.2. Gtk2::ActionGroups

Gtk2::Action objects should be part of an Gtk2::ActionGroup to provide common control over their visibility and sensitivity. For example, in a text processing application the menu items and toolbar buttons for specifying the text justification could be contained in an Gtk2::ActionGroup. A user interface is expected to have multiple Gtk2::ActionGroup objects that cover various aspects of the application. For example, global actions like creating new documents, opening and saving a document and quitting the application likely form one Gtk2::ActionGroup while actions such as modifying the view of the document would form another.

18.1.1.3. Adding Actions to an Actiongroup

The following sections will deal with ways to add the three action types to an Gtk2::ActionGroup.

18.1.1.3.1. Plain Jane actions

When we want to add actions, we need to declare an array first. This array will contain list elements. The data in those list elements will be used to create the actions in the Gtk2::ActionGroup. In the following snippet of code we supply the data for two action items. Please see the comments on what each field of the list elements in the array represent.

my @actions_plain = (
	# name,      stock id,  label,    accelerator,  tooltip, callback
	["FileMenu",	 	#name of the action. Must be specified.
	
	undef,			#stock id for the action. Optional, default 
				#value of undef if a label is specified.
				
	"_File",		#label for the action.Optional, default 
				#value of undef if a stock id is specified.
				
	undef,			#accelerator for the action
	undef,			#tooltip for the action. Optional, default value of undef.
	
	undef,			#callback invoked when the action is activated.
				#Optional, default value of undef.
	],
	
	#same as above, just in one line :)
	[ "New",    'gtk-new',  "_New",   "<control>N", "Create a new file", \&activate_action ],
);

Note

The order of items in the array does not matter, since the structure of where which item will eventually be displayed will be determined by the UI XML descriptions added to the Gtk2::UIManager. It makes sense though to keep them more or less logically grouped.

The name field must be unique, and will be used in the XML descriptions to indicate which Gtk2::Action we want to tie to the XML element.

To create the Gtk2::Action instances, and add it to the Gtk2::ActionGroup we use the 'add_actions' method referencing our array of items. Optionally we can supply a second argument with user data.
$action_group->add_actions(\@actions_plain, undef);

18.1.1.3.2. Gtk2::ToggleAction actions

Adding a Gtk2::ToggleAction instance is much the same as adding a Gtk2::Action instance. There is just an extra field to be declared, which specify whether the toggle is active or not.

my @toggle_actions = (
	 
	["Show_hidden", 	#name of the action. Must be specified.
	
	undef,			#stock id for the action. Optional, default 
				#value of undef if a label is specified.
				
	"_Show hidden files",	#label for the action.Optional, default 
				#value of undef if a stock id is specified.
				
	undef,			#accelerator for the action
	undef,			#tooltip for the action. Optional, default value of undef.
	
	undef,			#callback invoked when the action is activated.
				#Optional, default value of undef.
				
	TRUE,			#is active. Optional, default value is FALSE
	],
	
);
To create the Gtk2::ToggleAction instances, and add it to the Gtk2::ActionGroup we use the 'add_toggle_actions' method referencing our array of items. Optionally we can supply a second argument with user data.
$action_group->add_toggle_actions(\@toggle_actions, undef);

18.1.1.3.3. Gtk2::RadioAction actions

The fields in the array used for adding Gtk2::RadioAction instances is a bit different. We do not specify a "callback" field, but rather a "value" field. It is good practice to declare constants representing each "value", since it must be of type integer.

use constant COLOR_RED   => 0;
use constant COLOR_GREEN => 1;
use constant COLOR_BLUE  => 2;

my @color_entries = (
  # name,    stock id, label,    accelerator,  tooltip, value 
  [ "Red",   undef,    "_Red",   "<control>R", "Blood", COLOR_RED   ],
  [ "Green", undef,    "_Green", "<control>G", "Grass", COLOR_GREEN ],
  [ "Blue",  undef,    "_Blue",  "<control>B", "Sky",   COLOR_BLUE  ],
);
To create the Gtk2::RadioAction instances, and add it to the Gtk2::ActionGroup we use the 'add_radio_actions' method referencing our array of items. We also have to specify which Gtk2::RadioAction must be currently active (giving one of the "value" entries from the declared array ), and can specify a callback sub with userdata.
$actions->add_radio_actions (\@color_entries, COLOR_RED,
                                   \&activate_radio_action);
When the sub is called, it will be given the following values, which can then be used:
my ($action, $current) = @_;
Both are instances of the selected Gtk2::RadioAction. To discover which one it is, use the "get_name" method of Gtk2::Action.

18.1.1.4. Managing actions.

The Gtk2::ActionGroup class has a methods to control the sensitivity and visibility of all the Gtk2::Actions in it. They are the 'set_sensitive' and 'set_visible' methods respectively.

The above methods are also available on a "per action" instance. You can get actions from a Gtk2::ActionGroup by using the 'get_action' method. You may also want to remove actions by using the 'remove_action' method. Consult the man page for more detail.

18.1.2. Background on UI Descriptions

The UI descriptions accepted by Gtk2::UIManager are simple XML definitions with the following elements:

Table 18-2. Available XML elements for the UI definition

XML elementDescription
uiThe root element of a UI description. It can be omitted. Can contain menubar, popup, toolbar and accelerator elements.
menubarA top level element describing a Gtk2::MenuBar structure that can contain menuitem, separator, placeholder and menu elements. It has an optional name attribute. If name is not specified, "menubar" is used as the name.
popupA top level element describing a popup Gtk2::Menu structure that can contain menuitem, separator, placeholder, and menu elements. It has an optional name attribute. If name is not specified, "popup" is used as the name.
toolbarA top level element describing a Gtk2::Toolbar structure that can contain toolitem, separator and placeholder elements. It has an optional name attribute. If name is not specified, "toolbar" is used as the name.
placeholderAn element identifying a position in a menubar, toolbar, popup or menu. A placeholder can contain menuitem, separator, placeholder, and menu elements. placeholder elements are used when merging UI descriptions to allow, for example, a menu to be built up from UI descriptions using common placeholder names. It has an optional name attribute. If name is not specified, "placeholder" is used as the name.
menuAn element describing a Gtk2::Menu structure that can contain menuitem, separator, placeholder, and menu elements. A menu element has a required attribute action that names an Gtk2::Action object to be used to create the Gtk2::Menu. It also has optional name and position attributes. If name is not specified, the action name is used as the name. The position attribute can have either the value "top" or "bottom" with "bottom" the default if position is not specified.
menuitemAn element describing a Gtk2::MenuItem. A menuitem element has a required attribute action that names an Gtk2::Action object to be used to create the Gtk2::MenuItem. It also has optional name and position attributes. If name is not specified, the action name is used as the name. The position attribute can have either the value "top" or "bottom" with "bottom" the default if position is not specified.
toolitemAn element describing a toolbar Gtk2::ToolItem. A toolitem element has a required attribute action that names an Gtk2::Action object to be used to create the Gtk2::ToolItem. It also has optional name and position attributes. If name is not specified, the action name is used as the name. The position attribute can have either the value "top" or "bottom" with "bottom" the default if position is not specified.
separatorAn element describing a Gtk2::SeparatorMenuItem or a Gtk2::SeparatorToolItem as appropriate.
acceleratorAn element describing a keyboard accelerator. An accelerator element has a required attribute action that names an Gtk2::Action object that defines the accelerator key combination and is activated by the accelerator. It also has an optional name attribute. If name is not specified, the action name is used as the name.

18.1.3. Practical sample

Enough theory, lets see Gtk2::UIManager in action. The sample program will imagine we have acquired a DVD writer, and our user interface needs to change the list of available optical media.

Table 18-3. Gtk2 object classes used

Gtk2 Class Name
Gtk2::Window
Gtk2::VBox
Gtk2::CheckButton
Gtk2::HButtonBox
Gtk2::UIManager
Gtk2::ActionGroup
Gtk2::Action
Gtk2::MenuToolButton
Gtk2::SeparatorToolItem

18.1.3.1. The Screenshot.

Figure 18-1. UIManager

18.1.3.2. The Code.

The program can be found here: 'gtk2_uimanager.pl'

  1 #! /usr/bin/perl -w
  2 
  3 use strict;
  4 use Gtk2 '-init';
  5 use Glib qw/TRUE FALSE/; 
  6 
  7 #===========================================================
  8 #Declare all the data first, to keep it away from the part 
  9 #containing the widgets.
 10 
 11 #--------------------------
 12 my @entries = (
 13   #Fields for each action item:
 14   #[name, stock_id, value, label, accelerator, tooltip, callback]
 15   
 16   #file menus  
 17   [ "FileMenu",undef,"_File"],
 18   [ "New",    'gtk-new',  "_New",   "<control>N", "Create a new file", \&activate_action ],
 19   [ "Open",   'gtk-open', "_Open",  "<control>O", "Open a file",       \&activate_action ],
 20   [ "Quit",   'gtk-quit', "_Quit",  "<control>Q", "Quit",              \&activate_action ],
 21   
 22   #Media menus
 23   [ "Media",undef,"_Media"],
 24   [ "media_optical",'gtk-cdrom', "_Optical"],
 25   
 26 );
 27 #--------------------------
 28 
 29 use constant CD_R_M   	=> 0;
 30 use constant CD_R_P   	=> 1;
 31 use constant CD_RW_M	=> 3;
 32 use constant CD_RW_P	=> 4;
 33 
 34 my @cd_types = (
 35 
 36   #Fields for each radio-action item:
 37   #[name, stock_id, value, label, accelerator, tooltip, value]
 38 
 39 [ "cd_r_m",   undef, "CD-R",  undef, "CD-R media", CD_R_M  ],
 40 [ "cd_r_p",   undef, "CD+R",  undef, "CD+R media", CD_R_P  ],
 41 [ "cd_rw_m",   undef, "CD-RW",  undef, "CD-RW media", CD_RW_M  ],
 42 [ "cd_rw_p",   undef, "CD+RW",  undef, "CD+RW media", CD_RW_P  ],
 43 
 44 );
 45 #--------------------------
 46 
 47 use constant DVD_R_M   	=> 5;
 48 use constant DVD_R_P   	=> 6;
 49 use constant DVD_RW_M	=> 7;
 50 use constant DVD_RW_P	=> 8;
 51 
 52 my @dvd_types = (
 53 
 54 [ "dvd_r_m",   undef, "DVD-R",  undef, "DVD-R media", DVD_R_M  ],
 55 [ "dvd_r_p",   undef, "DVD+R",  undef, "DVD+R media", DVD_R_P  ],
 56 [ "dvd_rw_m",   undef, "DVD-RW",  undef, "DVD-RW media", DVD_RW_M  ],
 57 [ "dvd_rw_p",   undef, "DVD+RW",  undef, "DVD+RW media", DVD_RW_P  ],
 58 
 59 );
 60 #--------------------------
 61 #
 62 #===========================================================
 63 #Declare the two XML structures, one contains the basics
 64 #the other one contains the elements for the DVD
 65 
 66 my $ui_basic = "<ui>
 67   <menubar name='MenuBar'>
 68     <menu action='FileMenu'> 
 69      <menuitem action='New' position='top'/>
 70      <menuitem action='Open' position='top'/>
 71      <separator/>
 72      <menuitem action='Quit'/>
 73     </menu>
 74     <menu action='Media'> 
 75      <menu action='media_optical'>
 76      		<menuitem  action='cd_r_m'/>
 77       		<menuitem action='cd_r_p'/>
 78       		<menuitem  action='cd_rw_m'/>
 79       		<menuitem  action='cd_rw_p'/>
 80       	</menu>
 81     </menu>
 82    </menubar>
 83    <toolbar name='Toolbar'>
 84    	<placeholder name='optical'>
 85 		<separator/>
 86       		<toolitem  action='cd_r_m'/>
 87       		<toolitem action='cd_r_p'/>
 88       		<toolitem  action='cd_rw_m'/>
 89       		<toolitem  action='cd_rw_p'/>
 90       </placeholder>   
 91    </toolbar>
 92 </ui>";
 93 
 94 my $ui_dvd = "<ui>
 95   <menubar name='MenuBar'>
 96     <menu action='Media'> 
 97      <menu action='media_optical'>
 98      		<menuitem  action='dvd_r_m'/>
 99       		<menuitem action='dvd_r_p'/>
100       		<menuitem  action='dvd_rw_m'/>
101       		<menuitem  action='dvd_rw_p'/>
102       </menu>
103     </menu>
104    </menubar>
105    <toolbar name='Toolbar'>
106       <placeholder name='optical'>
107       		<toolitem  action='dvd_r_m'/>
108       		<toolitem action='dvd_r_p'/>
109       		<toolitem  action='dvd_rw_m'/>
110       		<toolitem  action='dvd_rw_p'/>
111       </placeholder>
112    </toolbar>
113 </ui>";
114 #===========================================================
115 
116 #standard window creation, placement, and signal connecting
117 my $window = Gtk2::Window->new('toplevel');
118 $window->signal_connect('delete_event' => sub { Gtk2->main_quit; });
119 $window->set_border_width(5);
120 $window->set_position('center_always');
121 
122 #this vbox will return the bulk of the gui
123 my $vbox = &ret_vbox();
124 
125 #add and show the vbox
126 $window->add($vbox);
127 $window->show();
128 
129 #our main event-loop
130 Gtk2->main();
131 
132 sub ret_vbox {
133 
134 #Flag indicating if the DVD was added or not
135 my $merge_flag = 0;
136 
137 my $vbox = Gtk2::VBox->new(FALSE,5);
138 $vbox->set_size_request (600, 200);
139 
140 #===========================================================
141 #This part has to do with the Gtk2::UIManager:
142 
143 	# Create a Gtk2::UIManager instance	
144 	my $uimanager = Gtk2::UIManager->new;
145 	
146 	#________
147 		#extract the accelgroup and add it to the window
148 		my $accelgroup = $uimanager->get_accel_group;
149   		$window->add_accel_group($accelgroup);
150 		
151 	#________
152 		# Create the basic Gtk2::ActionGroup instance
153 		#and fill it with Gtk2::Action instances
154 		my $actions_basic = Gtk2::ActionGroup->new ("actions_basic");
155 		$actions_basic->add_actions (\@entries, undef);
156 	
157 	# Add the actiongroup to the uimanager	
158 	$uimanager->insert_action_group($actions_basic,0);
159 	
160 	#________	
161 		# Create the CD Gtk2::ActionGroup instance
162 		#and fill it with Gtk2::RadioAction instances
163 		my $actions_media_cd = Gtk2::ActionGroup->new ("media_cd");
164 		$actions_media_cd->add_radio_actions(\@cd_types,CD_R_M,undef);
165 		
166 		#get the group, this will be needed when we add radio actions
167 		my $group = ($actions_media_cd->get_action('cd_r_m'))->get_group;
168 	
169 	# Add the actiongroup to the uimanager		
170 	$uimanager->insert_action_group($actions_media_cd,0);
171 	
172 	#________	
173 		# Create the DVD Gtk2::ActionGroup instance
174 		#and fill it with Gtk2::RadioAction instances
175 		my $actions_media_dvd = Gtk2::ActionGroup->new ("media_dvd");
176 		$actions_media_dvd->add_radio_actions(\@dvd_types,CD_R_M,undef);
177 	
178 		#run through the DVD types list, getting each action,
179 		#and set its group ans state
180 		foreach my $name (@dvd_types){
181 	
182 			my $action = $actions_media_dvd->get_action(${$name}[0]);
183 			#here we use the group :)
184 			$action->set_group($group);
185 			#need to specify, else we will have TWO ACTIVE radio buttons
186 			$action->set_active(FALSE);
187 		}
188 			
189 	# Add the actiongroup to the uimanager
190 	$uimanager->insert_action_group($actions_media_dvd,0);
191 	
192 	#________
193 	#add the basic XML description of the GUI
194 	$uimanager->add_ui_from_string ($ui_basic);
195 	
196 	#________
197 	#extract the menubar
198 	my $menubar = $uimanager->get_widget('/MenuBar');
199 	
200 $vbox->pack_start($menubar,FALSE,FALSE,0);
201 
202 	#________
203 	#extract the toolbar
204 	my $toolbar = $uimanager->get_widget('/Toolbar');
205 	$toolbar->set_show_arrow(FALSE);
206 	
207 	#Create a menu tool button and set its menu to the opticals.
208 		my $t_mbtn_optical = Gtk2::MenuToolButton->new_from_stock('gtk-cdrom');
209 		$t_mbtn_optical->set_label('Optical');
210 			
211 			#the UIManager created an Gtk2::ImageMenuItem, we extract it and
212 			#use its 'get_submenu' to get its attached menu widget :)
213 			my $menu_item = $uimanager->get_widget('/MenuBar/Media/media_optical');
214 		$t_mbtn_optical->set_menu ($menu_item->get_submenu);
215 								
216 	$toolbar->insert($t_mbtn_optical,0);
217 	$toolbar->insert(Gtk2::SeparatorToolItem->new,1);
218 #===========================================================
219 	
220 $vbox->pack_start($toolbar,FALSE,FALSE,0);
221 
222 	my $h_bb = Gtk2::HButtonBox->new;
223 	$h_bb->set_layout('edge');
224 	
225 		my $cb_sensitive = Gtk2::CheckButton->new('CD Sensitive');
226 		$cb_sensitive->set_active(TRUE);
227 		$cb_sensitive->signal_connect('toggled' =>sub{
228 			#sets the sensitivity of the CD menus
229 			$actions_media_cd->set_sensitive($cb_sensitive->get_active);
230 		});
231 		
232 	$h_bb->pack_start_defaults($cb_sensitive);
233 	
234 		my $cb_visible = Gtk2::CheckButton->new('CD Visible');
235 		$cb_visible->set_active(TRUE);
236 		$cb_visible->signal_connect('toggled' =>sub{
237 			#set the visibility of the CD menus
238 			$actions_media_cd->set_visible($cb_visible->get_active);
239 		});
240 		
241 	$h_bb->pack_start_defaults($cb_visible);
242 	
243 		my $cb_dvd = Gtk2::CheckButton->new('Add a DVD Writer');
244 		$cb_dvd->signal_connect('toggled' =>sub {
245 		#this will add or remove the DVD ui descriptions
246 		#it returns a number greater than 0 if successful
247 			if ($merge_flag){
248 				$uimanager->remove_ui($merge_flag);
249 				$merge_flag = 0;
250 			}else{
251 				$merge_flag = $uimanager->add_ui_from_string ($ui_dvd);
252 			}
253 		});
254 		
255 	$h_bb->pack_start_defaults($cb_dvd);
256 
257 $vbox->pack_end($h_bb,FALSE,FALSE,0);	
258 $vbox->show_all();
259 return $vbox;
260 }

18.1.3.3. Points of interest.

  • It is good practice to keep the data separate from the rest of the code. In our program we start of declaring data that will be used to generate the Gtk2::Action instances in various Gtk2::ActionGroups later in the program.

  • We also declare two XML structures which will be used by our program. A basic one and one that will merge with the basic one when we simulate the addition of a DVD Writer.

    When two XML structures merge, the one added last's elements will be added as siblings to the first one's elements. You can force position and grouping of widgets by making use of the position attribute, as well as the placeholder element.

    The position attribute will normally be used to keep certain Gtk2::MenuItems at the top, or buttom. The placeholder element can be used to group together Gtk2::RadioToolButton items in a Gtk2::Toolbar.

  • The widget hierarchy created using a UI description is very similar to the XML element hierarchy except that placeholder elements are merged into their parents.

    A widget in the hierarchy created by a UI description can be accessed using its path which is composed of the name of the widget element and its ancestor elements joined by slash ("/") characters.

    This is used as a argument to the get_widget method of Gtk2::UIManager:

    211 		#the UIManager created an Gtk2::ImageMenuItem, we extract it and
    212 		#use its 'get_submenu' to get its attached menu widget :)
    213 		my $menu_item = $uimanager->get_widget('/MenuBar/Media/media_optical');
    214 	$t_mbtn_optical->set_menu ($menu_item->get_submenu);

    Warning

    Remember to include placeholder elements in the path! We use the XML element hierarchy, and NOT the widget hierarchy as a path for the get_widget method.

  • Remember to extract the group of a Gtk2::RadioAction instance if you later want to append items to that group of Gtk2::RadioAction's.

    166 	#get the group, this will be needed when we add radio actions
    167 	my $group = ($actions_media_cd->get_action('cd_r_m'))->get_group;
    Make sure when you append to set the active attribute of each new addition to FALSE, else you can end up with a set of radio buttons containing two active ones at the same time!
    178 	#run through the DVD types list, getting each action,
    179 	#and set its group ans state
    180 	foreach my $name (@dvd_types){
    181 	
    182 		my $action = $actions_media_dvd->get_action(${$name}[0]);
    183 		#here we use the group :)
    184 		$action->set_group($group);
    185 		#need to specify, else we will have TWO ACTIVE radio buttons
    186 		$action->set_active(FALSE);
    187 	}

  • You can disable the option to the user to select certain items in the gui using various ways. In the sample program we used three different ways. ( Visibility, sensitivity, and the merging / removing of XML UI descriptions.) When to use which method is for the programmer to decide.

  • Methods used to add a XML UI description returns an integer value. This value can be used again when you want to remove the XML UI description.

    247 	if ($merge_flag){
    248 		$uimanager->remove_ui($merge_flag);
    249 		$merge_flag = 0;
    250 	}else{
    251 		$merge_flag = $uimanager->add_ui_from_string ($ui_dvd);
    252 	}

  • The widgets that you extract from the Gtk2::UIManager instance is standard, and can be used in any normal way. In the demo program, we added a Gtk2::MenuToolButton and Gtk2::SeparatorToolItem to it.