Time to integrate our GTK plot_icon script. Some big changes here and, also some new ideas. So this time I’m going to go through them in some detail - we may be in for a long haul here.
Item one on the agenda is a Module object that understands about GTK. The problem is that both GTK and FVWM::Module expect to be in charge of the main loop. Luckily there’s a GTK aware subclass of FVWM::Module which does the hard work for us. In fact all we need to do is change the use statements for FvwmRingmenu::Ring.pm
#! /usr/bin/perl -w
package FvwmRingMenu::Ring;
use strict;
use lib `fvwm-perllib dir`;
use FVWM::Module::Gtk;
use Gtk -init;
use Class::Struct;
use FvwmRingMenu::Menu;
The FvwmRingMenu file itself shoudn’t need any changes at all.
Next, let’s think about images. By relying on FvwmButtons, we saved ourselves a good deal of work in this department. In particular, before we can display an icon we need to find it (using ImagePath) read it into a Pixbuf, composite it and render it. Then we need to store the image and the mask ready for plotting. All this suggests that we could do with an new perl class: FvwmRingMenu::Image.pm
#! /usr/bin/perl -w
package FvwmRingMenu::Image;
use strict;
use Class::Struct;
use Gtk;
use Gtk::Gdk::Pixbuf;
struct (
module => '$', # reference to Fvwm::Module object
file => '$', # image file
pixbuf => '$', # this is the GDK pixbuf
pixmap => '$', # this is the GDK pixmap
widget => '$', # this is the GTK pixmap widget
mask => '$', # this is the mask that goes with it
#
# These should be in a subclass, but that's awkward using Class::Struct
#
comp_x => '$', # x-offset for composition
comp_y => '$', # y-offset for composition
);
1;
__END__
Basically, we’re storing most of the stuff from last time’s plot icon script. Most of the pixmap and pixbuf processing ends up in here too. Before I get carried away however, let’s get back to the problem of finding an icon file.
Fvwm uses the ImagePath command to manage its search path for image files. We can get at this with a global configuration tracker - there’s one supplied as part of perllib. I’m going to make the tracker global, so I can set it once and then refer to it as required.
#
# these are a global configuration tracker and a directory list from
# ImagePath
#
my $globals = undef;
my @dir_list = ();
The @dir_list variable is going to hold the list of directories. We set the variables with a func like this one.
sub set_globals
{
my $self = shift;
#
# first set the global variable that holds the global tracker
# (why does FVWM namespace always seem to get so congested?)
#
$globals = $self->module->track("GlobalConfig");
#
# now we extract the image path from the tracker
#
my $image_path = $globals->data->{'ImagePath'};
#
# split the path into component directories and store the list
#
@dir_list = split /:/, $image_path;
#
# should never happen
#
die "no image path" unless scalar( @dir_list );
}
We call that from the Image init subroutine to set the variables. Once that’s been done, we can call a search for image files like this:
sub find_image
{
my $proto = shift; # may be called as a class method
my $file = shift;
#
# return the file name if it starts with a slash
#
return $file if $file =~ m[^/];
#
# search image path for file
#
for my $dir ( @dir_list ) {
next unless -f "$dir/$file";
return "$dir/$file";
}
die "$file not found in @dir_list\n";
return undef;
}
The init code for the image class starts out with the usual arg processing:
sub init
{
my $proto = shift;
my $self = $proto->new(@_);
#
# make sure we got a referenec to FVWM::Module or suitable subclass
#
$self->module or die "FvwmRingMenu::Image: a module must be specified";
$self->file or die "FvwmRingMenu::Image: an icon file is required";
Then invokes the sub to set the ImagePath globals and expands the image file to a full path:
#
# if need be, init the package variables for the global config tracker
# and the image path
#
unless( $globals ) {
$self->set_globals();
}
#
# make sure the file is stored as a fully qualified path
#
my $full_path = $self->find_image($self->file);
$full_path or do {
$self->module->debug(
"FvwmRingMenu::Image: can't find " .
$self->file
);
};
$self->file($full_path);
And lastly, it constructs a GDK Pixbuf from the file:
#
# now - read the file into a GDK pixbuf
#
$self->pixbuf( Gtk::Gdk::Pixbuf->new_from_file($full_path) );
die "undefined pixbuf for $full_path" unless defined $self->pixbuf;
#
# And that'll do for now
#
return $self;
}
You may be wondering why stop there? Why not go on and construct all the bits and pieces needed right up to the windows? Well there are a few reasons. one of them is that I want to keep that for a second initialisation pass after the config file processing is complete. Another is that I want to use this class for the setting images as well, and I’ll never need to take them past pixbuf level.
All of which becomes apparent in the second pass function, prepare_pixbuf:
sub prepare_pixbuf
{
my $self = shift;
my $setting = shift;
#
# make sure we already have a pixmap initialised
#
die "undefined pixbuf" unless defined $self->pixbuf;
#
# if we have been supplied with a setting image
# compose the final pixuf
#
if($setting) {
my $composite = do_composition(
$self->pixbuf, $setting->pixbuf->copy()
);
#
# overwrite our pixbuf with the composition
#
$self->pixbuf( $composite );
}
die "undefined pixbuf" unless defined $self->pixbuf;
The $setting parameter refers to the optional background image that may be passed as a parameter to the subroutine. If it is set, we compisite the two images and replace the pixbuf with the composited one.
The rest of the sub is pretty much straight out of plot_icon
#
# get a GDK pixmap and mask from the pixbuf, and store them
#
my ($map,$mask) = $self->pixbuf->render_pixmap_and_mask(127);
$self->pixmap($map);
$self->mask($mask);
#
# make a Gtk::Pixmap ready for display
#
$self->widget(
new Gtk::Pixmap( $map, $mask )
);
}
The do_composite sub is also lifted from plot_icon. The only real change is that I’m calculating the x and y offsets from the sizes of the two images involved. I’ll not go into the details - you can get them from the linked source. There’s nothing new in there that isn’t really self-explanatory.
There is one thing in the Image.pm file that needs a little explanation:
sub DESTROY
{
my $self = shift;
$self->module ( undef );
}
I added this to try and stop the “new instance of dead object” errors I was getting whenever I shut down the module. Perl has a very good garbage collector that recycles an object once it has no more references. Normally it just works, but if you get loops in reference tree then it can’t free objects because something else is pointing at them. So the idea here is to set the module ref to undefined when the image object is garbage collected so as to allow the module to be freed up properly. I’m not at all sure that makes any sort of sense, but it seems to be working at the mo’ so it can stay for now.
In the Icon.pm file, the class def changes:
struct(
module => '$',
name => '$',
menu_name => '$',
file => '$',
image => 'FvwmRingMenu::Image',
actions => '%',
offset_x => '$',
offset_y => '$',
window => '$',
);
That’s storing the icon position as offsets from the circle origin (which will now be supplied by parameter) an Image object for the icon, and a Gtk window object to display the icon itself.
The setup sub in this file imports the rest of the code from plot_icon. It creates a Fixed widget:
#
# we need to compose the final image
#
$self->image->prepare_pixbuf($setting);
my $fixed = new Gtk::Fixed();
$fixed->set_usize( 100, 100 );
$fixed->put( $self->image->widget, 0, 0 );
And then adds a click handler to the window. This is a GTK handler, rather than a FVWM::Module one, although they work in similar ways.
#
# Add button presses to the event mask, and then catch the event
#
$fixed->add_events( [ 'button_press_mask' ] );
$fixed->signal_connect(
"button_press_event" => sub {
$self->resolve(@_);
return 1
},
);
That resolve call starts the process of resolving mouse click. We’ll get to that in a minute.
Lastly, the setup subroutine creates the window and and applies the mask.
All we need do now is postion it and show() it.
my $window = new Gtk::Window( "popup" );
$self->window( $window );
$window->signal_connect( "delete_event", sub { Gtk->exit( 0 ); } );
$window->add( $fixed );
$window->shape_combine_mask( $self->image->mask, 0, 0 );
The resolve function does some Gtk stuff to get the button number, checks to see if there is a action defined that can resolve it, and passes the resolution to the action if found. Otherwise the click is ignored.
The subroutine opens with a bit of standard Gtk-Perl hoodoo:
sub resolve
{
my $self = shift;
my ( $widget, @data ) = @_;
my $event = pop( @data );
That gets GTK objects for the widget that generated the event and the event itself. The @data array means that if we decide to specify arguents in the handler callback, we will be able to find then in that array. As it is we don’t specify any, and the array probably only contains the event object. All the same, it’s good practice with Gtk.
The $event object has a field called “type” which should tell us the kind of event that caused the sub to be invoked. Let’s do some checking, just to be safe:
#
# make sure type is defined - it should be.
#
unless( defined( $event->{'type'} ) ) {
$self->module->showError("can't tell event type");
return;
}
#
# make sure it was a mouse click event - it should be!
#
if ( $event->{'type'} ne 'button_press' ) {
$self->module->showError("can't cope with event type " .
$event->{'type'}
);
return;
}
Once we’re sure of our ground, we can get the button number:
#
# get the button number
#
my $button = $event->{'button'};
Check for an action for the button that was clicked:
#
# if there is an action for this button number, resolve the action
#
if($self->actions($button)) {
return $self->actions($button)->resolve();
}
And, failing that, check for a generic action for the icon:
#
# otherwise, check for a button zero action and use that
#
if($self->actions(0)) {
return $self->actions(0)->resolve();
}
#
# if no action is specified, ignore the event
#
return;
}
We also have a withdraw method that calls hide() to make the icon go away when the menu closes, and a DESTROY method like the one in Image.pm.
Next, let’s look at Action.pm, which is the source of some wierdness. The problem is that to resolve some actions, we need access to the Ring object and its list of menus. We could pass a reference to the ring down to the action level, but I’ve been trying to avoid this. For one thing, it’s another circular reference, and for another it’s a proper pain to add in.
So what we’re going to do is declare a variable as a reference to a subroutine.
Perl allows you to takes references to almost anything, subroutines included. The problem is that the ring class needs to supply the subroutine. I’ll explain why when we get to the ring class. Meanwhile we’ll do this
my $process_click_func;
sub set_process_click_func
{
my $class = shift;
my $func = shift;
$process_click_func = $func;
}
And rely on Ring.pm calling us to set the $process_click_func variable so we can pass commands back up the chain.
Once that’s set, we can call it to send commands back to the Ring to be resolved:
sub resolve
{
my $self = shift;
my $command = $self->command;
$process_click_func->($command);
}
Over in the Ring class, the set_process_click_func sub gets called in the init func:
FvwmRingMenu::Action->set_process_click_func(
sub {
my $command = shift;
#
# withdraw any old menu
#
$self->withdraw();
#
# if it starts with a star it's for us
# otherwise it gets sent to FVWM proper.
#
if($command =~ /^\*/) {
$self->star_command($command);
}
else {
$self->module->send($command);
}
}
);
This is what the perl man pages refer to as a closure. Closures are one of the areas where perl starts getting medium weird[1], but the priciple is simple enough. Any variable that can be accessed when an anonymous subroutine is defined, is still in scope when that sub gets called. Which in this case means that the sub we’re passing back to Action.pm can access the local $self variable from the init subroutine, even though the subroutine itself has exited and the variable would normally have been garbage collected.
Overall, it’s more elegant than passing a reference to the Ring object right down the tree and it saves us from cyclic reference chains.
Anyway, since we’re in Ring.pm, let’s look at the rest of it. First of all the structure itself has seen some changes:
struct(
#
# FVWM interface stuff
#
module => '$',
page => 'FVWM::Tracker::PageInfo',
config => 'FVWM::Tracker::ModuleConfig',
#
# Configuration data
#
offset_x => '$', # offset from ring origin
offset_y => '$', # ditto
radius => '$', # radius of ring
origin_policy => '$', # "Pointer" or "Screen"
icon_size => '$', # advisory for off screen plots
debug => '$', # set debug level: 0 is off
menus => '%', # hash of defined menus
current => '$', # current displayed menu
#
# for icon composition
#
setting_file => '$', # image file
setting_x => '$', # offset within the image
setting_y => '$', # say it again, brother!
setting => 'FvwmRingMenu::Image',
);
the first new function is simple enough. get_image_path returns the ImagePath setting so Image.pm can call it and get the directory list.
sub get_image_path
{
my $class = shift;
return $globals->data( "ImagePath" );
}
What else? There are subs to parse and set the origin policy (do you want the ring centered on the mouse pounter or the middle of the screen?) and the Setting option. Nothing new there - even the setting option just stores the filename. After the parse loop in read_config - that’s where the setting file gets initialised:
#
# we may as well initialise the menus while we're here
#
my $setting;
if($self->setting_file) {
$setting = FvwmRingMenu::Image->init(
module => $self->module,
file => $self->setting_file,
comp_x => $self->setting_x,
comp_y => $self->setting_y,
);
$self->setting( $setting );
}
Which is straightforward enough, really.
The main change to the popup sub involves where to put the origin:
#
# where to center the ring? mid screen or on the mouse pointer.
# fixed user co-ords should be an option too
#
my ($x, $y);
if($self->origin_policy eq "Pointer") {
#
# use an origin based upon the position of the mouse pointer
#
($x, $y) = $self->pointer_origin();
}
else {
#
# use the screen centre as the origin of the ring
#
$x = $self->page->data->{ vp_height } / 2;
$y = $self->page->data->{ vp_width } / 2;
}
$menu->popup($x, $y);
The interesting bit there is farmet out to the pointer_origin func. Basically we need to get the screen co-ords of the mouse pointer
sub pointer_origin
{
my $self = shift;
my $irad = $self->icon_size;
#
# this morsel of occult data is the way to get the mouse pointer co-ords
# using GTK and perl. Use this knowledge wisely.
#
my ($x,$y) = Gtk::Gdk->ROOT_PARENT()->get_pointer();
Then we need to check and make sure half the menu isn’t printing off-screen. If that happens, there’s this weird reflection effect that kicks in and spoils the effect. So we need to check the x co-ord and adjust if necessary:
#
# if the current x co-ordinate would result in the icons being plotted
# of the desktop, adjust the origin to fit them in
#
if($x - ($self->radius + $irad) < 0) {
$x = $self->radius + $irad;
}
elsif($x + $self->radius + $irad > $self->page->data->{ vp_width } ) {
$x = $self->page->data->{ vp_width };
$x -= $self->radius;
$x -= $irad * 2;
}
And exactly the same for the y co-ord. I’ll not print that bit here.
The rest of Ring.pm should be familiar enough by now. So on to our final file - Menu.pm. Again, most of it shoudl be starting to feel familar. The icon plotting code changes since we’re not using FvwmButtons any more:
for my $icon ( @{ $self->icons } ) {
$icon->plot(
$origin_x + $icon->offset_x,
$origin_y + $icon->offset_y,
);
}
With a similar if(){} for the center icon. There’s a withdraw sub that does more or less the same thing, passing the job on down to the icons:
sub withdraw
{
my $self = shift;
my $menu_name = $self->name;
#
# chatter on a bit
#
$self->module->debug("withdraw: $menu_name");
#
# do centre first
#
if($self->center) {
$self->center->withdraw();
}
for my $icon ( @{ $self->icons } ) {
$icon->withdraw();
}
}
And that’s really about it. The setup subroutine is changed to calculate offsets to the menu rather than absolute co-ords, but that’s simple enough.
for(my $i = 0; $i < $n_icons; $i++) {
my $icon = $self->icons($i);
#
# to get the co-ords of the icon relative to the origin
# we use the sin and cos funcs. These however take radians
# where 360 degrees = 2 * PI;
#
my $rads = $i * 2 * PI / $n_icons;
my $x = $ring->offset_x + sin($rads) * $ring->radius;
my $y = $ring->offset_y - cos($rads) * $ring->radius;
$icon->setup(
$self->name,
$self->setting || $setting,
$x, $y
);
}
There’s nothing else we haven’t already convered elsewhere.
A quick look at the config file is in order:
DestroyModuleConfig FvwmRingMenu: *
KillModule FvwmRingMenu
KillModule FvwmButtons FvwmRingMenu-FvwmButtons-*
#
# IconSize is an advisory value when calculating how much margin to allow
# when popping menus at the edge of the screen
#
*FvwmRingMenu: Debug 0
*FvwmRingMenu: IconSize 90
#
# offset lets you nudge the center of the ring to compensate for
# icon width. Shouldn't need it with IconSize, really.
#
*FvwmRingMenu: Offset -22,-22
*FvwmRingMenu: Radius 60
*FvwmRingMenu: Setting menu_setting.png, Geometry +0-2
#
# Where to center the ring: Two possible arguments - "Pointer" and "Screen"
# should really allow a geometry argument as well...
#
*FvwmRingMenu: OriginPolicy Pointer
#
# actions can invoke fvwm functions
#
*FvwmRingMenu: Menu TopLevel, Icon l33t_TER_term.png, \
Mouse1 "InvokeAterm" \
Mouse3 "InvokeConsole"
*FvwmRingMenu: Menu TopLevel, Icon l33t_BRO_firefox.png, \
Mouse1 "Exec firefox"
*FvwmRingMenu: Menu TopLevel, Icon l33t_IMS_gaim2.xpm, \
Mouse1 "Exec gaim"
*FvwmRingMenu: Menu TopLevel, Icon l33t_OFF_openoffice.xpm, \
Mouse1 "Exec ooffice"
*FvwmRingMenu: Menu TopLevel, Icon l33t_GRA_gimp.xpm, \
Mouse1 "Exec gimp"
*FvwmRingMenu: Menu TopLevel, Icon l33t_UNK_rox.xpm, \
Mouse1 "Exec rox"
*FvwmRingMenu: Menu TopLevel, Icon l33t_UNK_ssh.png, \
Mouse1 "*SubMenu SSH"
*FvwmRingMenu: Menu TopLevel, Icon l33t_UNK_package_settings.png, \
Mouse1 "*SubMenu Fvwm"
*FvwmRingMenu: Menu TopLevel, Center l33t_DES_desktop.png, \
Mouse0 *Withdraw
*FvwmRingMenu: Menu SSH, Center l33t_UNK_ssh.png, \
Mouse0 "*PopUp TopLevel"
*FvwmRingMenu: Menu SSH, Icon l33t_UNK_freddie.png, \
Mouse0 "InvokeSSH $[USER] fred"
*FvwmRingMenu: Menu SSH, Icon l33t_UNK_victor.png, \
Mouse0 "InvokeSSH $[USER] victor"
*FvwmRingMenu: Menu SSH, Icon l33t_UNK_frankie.png, \
Mouse0 "Exec xterm -e ssh -X frankie"
*FvwmRingMenu: Menu SSH, Icon l33t_UNK_igor.png, \
Mouse0 "Exec xterm -e ssh -X igor"
#
# A left mouse button click launches a normal menu,
# a right mouse click launches a ring menu
#
Mouse 3 R A SendToModule FvwmRingMenu PopUp TopLevel
That’s still not a complete config since I want to think about my layout a bit more. It does to illustrate how stuff is used for now.
And after all that, and lots of debugging, it works. I think I could have made that setting a bit smaller though
There’s a little bit more work to do. I need to write the man page for the module, and I have a cool idea for implementing hangons for ring menus. Then I fancy trying some animation options for menu launch time - but there are a free other projects I need to attend to before that, so it may be sometime before I get there.
Meanwhile, I think this baby is about ready to be released on an unsuspecting world. See you next time.
[1] For seriously weird, checkout some of the ACME packages on cpan.org. Especially the ones written by Damian Conway.