# Converted from menu.tcl --
# This file defines the default bindings for Tk menus and menubuttons.
# It also implements keyboard traversal of menus and implements a few
# other utility procedures related to menus.
# @(#) menu.tcl 1.34 94/12/19 17:09:09
# Copyright (c) 1992-1994 The Regents of the University of California.
# Copyright (c) 1994 Sun Microsystems, Inc.
# See the file "license.terms" for information on usage and redistribution
# of this file, and for a DISCLAIMER OF ALL WARRANTIES.
$VERSION = '3.045'; # $Id: //depot/Tk8/Tk/Menu.pm#45 $
use base qw(Tk::Wm Tk::Derived Tk::Widget);
Construct Tk
::Widget
'Menu';
sub Tk_cmd
{ \
&Tk
::_menu
}
Tk
::Methods
('activate','add','clone','delete','entrycget','entryconfigure',
'index','insert','invoke','post','postcascade','type',
my ($package,$parent,$args) = @_;
# Remove from hash %$args any configure-like
# options which only apply at create time (e.g. -class for Frame)
# return these as a list of -key => value pairs
foreach $opt (qw(-type -screen -visual -colormap))
my $val = delete $args->{$opt};
push(@result, $opt => $val) if (defined $val);
my $menuitems = delete $args->{-menuitems
};
$menu->SUPER::InitObject
($args);
# If any other args do configure now
$menu->configure(%$args);
$menu->AddItems(@
$menuitems)
$menu->separator; # A separator
my ($kind,$name,%minfo) = ( @
$item );
my $invoke = delete $minfo{'-invoke'};
$minfo{-label
} = $name unless defined($minfo{-label
});
$menu->BackTrace("Don't recognize " . join(' ',@
$item));
#-------------------------------------------------------------------------
# Elements of tkPriv that are used in this file:
# cursor - Saves the -cursor option for the posted menubutton.
# focus - Saves the focus during a menu selection operation.
# Focus gets restored here when the menu is unposted.
# inMenubutton - The name of the menubutton widget containing
# the mouse, or an empty string if the mouse is
# not over any menubutton.
# popup - If a menu has been popped up via tk_popup, this
# gives the name of the menu. Otherwise this
# postedMb - Name of the menubutton whose menu is currently
# posted, or an empty string if nothing is posted
# A grab is set on this widget.
# relief - Used to save the original relief of the current
# window - When the mouse is over a menu, this holds the
# name of the menu; it's cleared when the mouse
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
# This file is tricky because there are four different ways that menus
# 1. As a pulldown from a menubutton. This is the most common usage.
# In this style, the variable tkPriv(postedMb) identifies the posted
# 2. As a torn-off menu copied from some other menu. In this style
# tkPriv(postedMb) is empty, and the top-level menu is no
# 3. As an option menu, triggered from an option menubutton. In thi
# style tkPriv(postedMb) identifies the posted menubutton.
# 4. As a popup menu. In this style tkPriv(postedMb) is empty and
# the top-level menu is override-redirect.
# The various binding procedures use the state described above to
# distinguish the various cases and take different actions in each
#-------------------------------------------------------------------------
# This procedure is invoked the first time the mouse enters a menubutton
# widget or a menubutton widget receives the input focus. It creates
# all of the class bindings for both menubuttons and menus.
# w - The widget that was just entered or just received
# event - Indicates which event caused the procedure to be invoked
# (Enter or FocusIn). It is used so that we can carry out
# the functions of that event in addition to setting up
# Must set focus when mouse enters a menu, in order to allow
# mixed-mode processing using both the mouse and the keyboard.
$mw->bind($class,'<FocusIn>', 'NoOp');
$mw->bind($class,'<Enter>', 'Enter');
$mw->bind($class,'<Leave>', ['Leave',Ev
('X'),Ev
('Y'),Ev
('s')]);
$mw->bind($class,'<Motion>', ['Motion',Ev
('x'),Ev
('y'),Ev
('s')]);
$mw->bind($class,'<ButtonPress>','ButtonDown');
$mw->bind($class,'<ButtonRelease>',['Invoke',1]);
$mw->bind($class,'<space>',['Invoke',0]);
$mw->bind($class,'<Return>',['Invoke',0]);
$mw->bind($class,'<Escape>','Escape');
$mw->bind($class,'<Left>','LeftArrow');
$mw->bind($class,'<Right>','RightArrow');
$mw->bind($class,'<Up>','UpArrow');
$mw->bind($class,'<Down>','DownArrow');
$mw->bind($class,'<KeyPress>', ['TraverseWithinMenu',Ev
('K')]);
$mw->bind($class,'<Alt-KeyPress>', ['TraverseWithinMenu',Ev
('K')]);
if ($menu->cget('-type') eq 'menubar')
if ($menu->cget('-type') eq 'menubar')
$menu->NextMenu('right');
if ($menu->cget('-type') eq 'menubar')
if ($menu->cget('-type') eq 'menubar')
$menu->NextMenu('right');
# This procedure unposts a given menu, plus all of its ancestors up
# to (and including) a menubutton, if any. It also restores various
# values to what they were before the menu was posted, and releases
# a grab if there's a menubutton involved. Special notes:
# 1. It's important to unpost all menus before releasing the grab, so
# that any Enter-Leave events (e.g. from menu back to main
# application) have mode NotifyGrab.
# 2. Be sure to enclose various groups of commands in "catch" so that
# the procedure will complete even if the menubutton or the menu
# or the grab window has been deleted.
# menu - Name of a menu to unpost. Ignored if there
# is a posted menubutton.
# Restore focus right away (otherwise X will take focus away when
# the menu is unmapped and under some window managers (e.g. olvwm)
# we'll lose the focus completely).
eval {local $SIG{__DIE__
}; $Tk::focus
->focus() } if (defined $Tk::focus
);
# Unpost menu(s) and restore some stuff that's dependent on
eval {local $SIG{__DIE__
};
$menu = $mb->cget('-menu');
$mb->configure('-cursor',$Tk::cursor
);
$mb->configure('-relief',$Tk::relief
)
elsif (defined $Tk::popup
)
my $grab = $Tk::popup
->grabCurrent;
$grab->grabRelease if (defined $grab);
elsif (defined $menu && ref $menu &&
$menu->cget('-type') ne 'menubar' &&
$menu->cget('-type') ne 'tearoff'
# We're in a cascaded sub-menu from a torn-off menu or popup.
# Unpost all the menus up to the toplevel one (but not
# including the top-level torn-off one) and deactivate the
# top-level torn off menu if there is one.
my $parent = $menu->parent;
last if (!$parent->IsMenu || !$parent->ismapped);
$parent->postcascade('none');
$parent->GenerateMenuSelect;
$parent->activate('none');
my $type = $parent->cget('-type');
last if ($type eq 'menubar' || $type eq 'tearoff');
$menu->unpost() if ($menu->cget('-type') ne 'menubar');
if ($Tk::tearoff
|| $Tk::menubar
)
if (defined $menu && ref $menu)
my $grab = $menu->grabCurrent;
$grab->grabRelease if (defined $grab);
$Tk::menubar
->configure(-cursor
=> $Tk::cursor
);
if ($Tk::platform
ne 'unix')
if (defined $Tk::oldGrab
)
if ($Tk::grabStatus
eq 'global')
$Tk::oldGrab
->grabGlobal;
my $type = $w->type(shift);
return defined $type && $type eq shift;
# This procedure is called to handle mouse motion events for menus.
# It does two things. First, it resets the active element in the
# menu, if the mouse is over the menu. Second, if a mouse button
# is down, it posts and unposts cascade entries to match the mouse
# menu - The menu window.
# y - The y position of the mouse.
# state - Modifier state (tells whether buttons are down).
my $t = $menu->cget('-type');
if ($menu->IS($Tk::window
))
if ($menu->cget('-type') eq 'menubar')
# if (defined($Tk::focus) && $Tk::focus != $menu)
$menu->activate("\@$x,$y");
$menu->GenerateMenuSelect;
$menu->activate("\@$x,$y");
$menu->GenerateMenuSelect;
if (($state & 0x1f00) != 0)
$menu->postcascade('active')
# Handles button presses in menus. There are a couple of tricky things
# 1. Change the posted cascade entry (if any) to match the mouse position.
# 2. If there is a posted menubutton, must grab to the menubutton so
# that it can track mouse motions over other menubuttons and change
# 3. If there's no posted menubutton (e.g. because we're a torn-off menu
# or one of its descendants) must grab to the top-level menu so that
# we can track mouse motions across the entire menu hierarchy.
# menu - The menu window.
$menu->postcascade('active');
if (defined $Tk::postedMb
)
$Tk::postedMb
->grabGlobal
while ($menu->cget('-type') eq 'normal'
&& $menu->parent->ismapped
if (!defined $Tk::menuBar
)
$Tk::cursor
= $menu->cget('-cursor');
$menu->configure(-cursor
=> 'arrow');
# Don't update grab information if the grab window isn't changing.
# Otherwise, we'll get an error when we unpost the menus and
# restore the grab, since the old grab window will not be viewable
$menu->SaveGrabInfo unless ($menu->IS($menu->grabCurrent));
# Must re-grab even if the grab window hasn't changed, in order
# to release the implicit grab from the button press.
$menu->grabGlobal if ($Tk::platform
eq 'unix');
if ($w->cget('-type') eq 'tearoff')
if ($ev->m ne 'NotifyUngrab')
$w->SetFocus if ($Tk::platform
eq 'unix');
$w->Motion($ev->x, $ev->y, $ev->s);
# This procedure is invoked to handle Leave events for a menu. It
# deactivates everything unless the active element is a cascade element
# and the mouse is now over the submenu.
# menu - The menu window.
# rootx, rooty - Root coordinates of mouse.
# state - Modifier state.
return if ($menu->index('active') eq 'none');
if ($menu->typeIS('active','cascade'))
my $c = $menu->Containing($rootx,$rooty);
return if (defined $c && $menu->entrycget('active','-menu')->IS($c));
$menu->GenerateMenuSelect;
# This procedure is invoked when button 1 is released over a menu.
# It invokes the appropriate menu action and unposts the menu if
# it came from a menubutton.
# w - Name of the menu widget.
if ($release && !defined($Tk::window
))
# Mouse was pressed over a menu without a menu button, then
# dragged off the menu (possibly with a cascade posted) and
# released. Unpost everything and quit.
$w->eventGenerate('<<MenuSelect>>');
my $type = $w->type('active');
if ($w->typeIS('active','cascade'))
$w->postcascade('active');
my $menu = $w->entrycget('active','-menu');
$menu->FirstEntry() if (defined $menu);
elsif ($w->typeIS('active','tearoff'))
elsif ($w->typeIS('active','menubar'))
$w->eventGenerate('<<MenuSelect>>');
# This procedure is invoked for the Cancel (or Escape) key. It unposts
# the given menu and, if it is the top-level menu for a menu button,
# unposts the menu button as well.
# menu - Name of the menu window.
my $parent = $menu->parent;
elsif ($parent->cget('-type') eq 'menubar')
# This procedure is invoked to handle "left" and "right" traversal
# motions in menus. It traverses to the next menu in a menu bar,
# or into or out of a cascaded menu.
# menu - The menu that received the keyboard
# direction - Direction in which to move: "left" or "right"
# First handle traversals into and out of cascaded menus.
if ($direction eq 'right')
if ($menu->typeIS('active','cascade'))
$menu->postcascade('active');
my $m2 = $menu->entrycget('active','-menu');
$m2->FirstEntry if (defined $m2);
my $parent = $menu->parent;
while ($parent->PathName ne '.')
if ($parent->IsMenu && $parent->cget('-type') eq 'menubar')
$parent = $parent->parent;
if ($m2->cget('-type') ne 'menubar')
$menu->GenerateMenuSelect;
# This code unposts any posted submenu in the parent.
my $tmp = $m2->index('active');
# Can't traverse into or out of a cascaded menu. Go to the next
# or previous menubutton, if that makes sense.
if ($m2->cget('-type') eq 'menubar')
return unless defined $w;
my @buttons = $w->parent->children;
my $i = Tk
::lsearch
(\
@buttons,$w)+$count;
last if ($mb->IsMenubutton && $mb->cget('-state') ne 'disabled'
&& defined($mb->cget('-menu'))
&& $mb->cget('-menu')->index('last') ne 'none'
# Activate the next higher or lower entry in the posted menu,
# wrapping around at the ends. Disabled entries are skipped.
# menu - Menu window that received the keystroke.
# count - 1 means go to the next lower entry,
# -1 means go to the next higher entry.
if ($menu->index('last') eq 'none')
my $length = $menu->index('last')+1;
my $active = $menu->index('active');
my $i = ($active eq 'none') ?
0 : $active+$count;
return if ($quitAfter <= 0);
my $state = eval {local $SIG{__DIE__
}; $menu->entrycget($i,'-state') };
last if (defined($state) && $state ne 'disabled');
return if ($i == $active);
$menu->GenerateMenuSelect;
if ($menu->type($i) eq 'cascade')
my $cascade = $menu->entrycget($i, '-menu');
$cascade->FirstEntry if (defined $cascade);
# This procedure implements keyboard traversal within a menu. It
# searches for an entry in the menu that has "char" underlined. If
# such an entry is found, it is invoked and the menu is unposted.
# w - The name of the menu widget.
# char - The character to look for; case is
# ignored. If the string is empty then
return unless (defined $char);
my $last = $w->index('last');
return if ($last eq 'none');
for (my $i = 0;$i <= $last;$i += 1)
my $label = eval {local $SIG{__DIE__
}; $w->entrycget($i,'-label') };
next unless defined($label);
my $ul = $w->entrycget($i,'-underline');
if (defined $ul && $ul >= 0)
$label = substr("\L$label",$ul,1);
if (defined($label) && $label eq $char)
if ($w->type($i) eq 'cascade')
my $m2 = $w->entrycget($i,'-menu');
$m2->FirstEntry if (defined $m2);
if ($menu->cget('-type') eq 'menubar')
if (!defined($char) || $char eq '')
$menu->TraverseWithinMenu($char);
# Given a menu, this procedure finds the first entry that isn't
# disabled or a tear-off or separator, and activates that entry.
# However, if there is already an active entry in the menu (e.g.,
# because of a previous call to tkPostOverPoint) then the active
# entry isn't changed. This procedure also sets the input focus
# menu - Name of the menu window (possibly empty).
return if (!defined($menu) || $menu eq '' || !ref($menu));
return if ($menu->index('active') ne 'none');
my $last = $menu->index('last');
return if ($last eq 'none');
for (my $i = 0;$i <= $last;$i += 1)
my $state = eval {local $SIG{__DIE__
}; $menu->entrycget($i,'-state') };
if (defined $state && $state ne 'disabled' && !$menu->typeIS($i,'tearoff'))
$menu->GenerateMenuSelect;
if ($menu->type($i) eq 'cascade')
my $cascade = $menu->entrycget($i,'-menu');
# Given a menu and a text string, return the index of the menu entry
# that displays the string as its label. If there is no such entry,
# return an empty string. This procedure is tricky because some names
# like "active" have a special meaning in menu commands, so we can't
# always use the "index" widget command.
# menu - Name of the menu widget.
# s - String to look for.
if ($s !~ /^active$|^last$|^none$|^[0-9]|^@/)
$i = eval {local $SIG{__DIE__
}; $menu->index($s) };
my $last = $menu->index('last');
return if ($last eq 'none');
for ($i = 0;$i <= $last;$i += 1)
my $label = eval {local $SIG{__DIE__
}; $menu->entrycget($i,'-label') };
return $i if (defined $label && $label eq $s);
# This procedure posts a given menu such that a given entry in the
# menu is centered over a given point in the root window. It also
# activates the given entry.
# x, y - Root coordinates of point.
# entry - Index of entry within menu to center over (x,y).
# If omitted or specified as {}, then the menu's
# upper-left corner goes at (x,y).
if ($entry == $menu->index('last'))
$y -= ($menu->yposition($entry)+$menu->height)/2;
$y -= ($menu->yposition($entry)+$menu->yposition($entry+1))/2;
if (defined($entry) && $menu->entrycget($entry,'-state') ne 'disabled')
$menu->GenerateMenuSelect;
# This procedure pops up a menu and sets things up for traversing
# the menu and its submenus.
# menu - Name of the menu to be popped up.
# x, y - Root coordinates at which to pop up the
# entry - Index of a menu entry to center over (x,y).
# If omitted or specified as {}, then menu's
# upper-left corner goes at (x,y).
return unless (defined $menu);
Unpost
(undef) if (defined($Tk::popup
) || defined($Tk::postedMb
));
$menu->PostOverPoint($x,$y,$entry);
$Tk::focus
= $menu->focusCurrent;
$Tk::focus
= $menu->focusCurrent if (!defined($Tk::focus
));
$Tk::activeItem
= $menu->index('active');
$menu->eventGenerate('<<MenuSelect>>'); # FIXME
# Converted from tearoff.tcl --
# This file contains procedures that implement tear-off menus.
# @(#) tearoff.tcl 1.3 94/12/17 16:05:25
# Copyright (c) 1994 The Regents of the University of California.
# Copyright (c) 1994 Sun Microsystems, Inc.
# See the file "license.terms" for information on usage and redistribution
# of this file, and for a DISCLAIMER OF ALL WARRANTIES.
# Given the name of a menu, this procedure creates a torn-off menu
# that is identical to the given menu (including nested submenus).
# The new torn-off menu exists as a toplevel window managed by the
# window manager. The return value is the name of the new menu.
# w - The menu to be torn-off (duplicated).
my $x = (@_) ?
shift : 0;
my $y = (@_) ?
shift : 0;
$x = $w->rootx if $x == 0;
$y = $w->rooty if $y == 0;
# Find a unique name to use for the torn-off menu. Find the first
# ancestor of w that is a toplevel but not a menu, and use this as
# the parent of the new menu. This guarantees that the torn off
# menu will be on the same screen as the original menu. By making
# it a child of the ancestor, rather than a child of the menu, it
# can continue to live even if the menu is deleted; it will go
# away when the toplevel goes away.
while ($parent->toplevel != $parent || $parent->IsMenu)
$parent = $parent->parent;
my $menu = $w->clone($parent,'tearoff');
# Pick a title for the new menu by looking at the parent of the
# original: if the parent is a menu, then use the text of the active
# entry. If it's a menubutton then use its text.
my $title = $menu->cget('-title');
unless (defined $title && length($title))
if ($parent->IsMenubutton)
$title = $parent->cget('-text');
$title = $parent->entrycget('active','-label');
$menu->title($title) if (defined $title && length($title));
# Set tkPriv(focus) on entry: otherwise the focus will get lost
# after keyboard invocation of a sub-menu (it will stay on the
$menu->bind('<Enter>','EnterFocus');
$menu->Callback('-tearoffcommand');
# Given a menu (hierarchy), create a duplicate menu (hierarchy)
# src - Source window. Must be a menu. It and its
# menu descendants will be duplicated at dst.
# dst - Name to use for topmost menu in duplicate
my $type = (@_) ?
shift : 'normal';
my %args = (-type
=> $type) ;
foreach my $option ($src->configure())
$args{$$option[0]} = $$option[4] unless exists $args{$$option[0]};
my $dst = ref($src)->new($parent,%args);
$dst->transient($parent->MainWindow);
my $last = $src->index('last');
for (my $i = $src->cget('-tearoff'); $i <= $last; $i++)
my $type = $src->type($i);
foreach my $option ($src->entryconfigure($i))
push(@args,$$option[0],$$option[4]) if (defined $$option[4]);
# Duplicate the binding tags and bindings from the source menu.
my @bindtags = $src->bindtags;
my $path = $src->PathName;
$_ = $dst if ($_ eq $path);
$dst->bindtags([@bindtags]);
foreach my $event ($src->bind)
my $cb = $src->bind($event);
$dst->bind($event,$cb->Substitute($src,$dst));
# Some convenience methods
sub separator
{ require Tk
::Menu
::Item
; shift->Separator(@_); }
sub cascade
{ require Tk
::Menu
::Item
; shift->Cascade(@_); }
sub checkbutton
{ require Tk
::Menu
::Item
; shift->Checkbutton(@_); }
sub radiobutton
{ require Tk
::Menu
::Item
; shift->Radiobutton(@_); }
if (exists $args{-button
})
# Backward compatible stuff from 'Menubar'
my $button = delete $args{-button
};
$button = ['Misc', -underline
=> 0 ] unless (defined $button);
($button,@bargs) = @
$button if (ref($button) && ref $button eq 'ARRAY');
$menu = $menu->Menubutton(-label
=> $button, @bargs);
my $name = delete($args{'-text'}) || $args{'-label'};;
$args{'-label'} = $name if (defined $name);
my $items = delete $args{'-menuitems'};
foreach my $opt (qw(-pack -after -before -side -padx -ipadx -pady -ipady -fill))
if (defined($name) && !defined($args{-underline
}))
my $underline = ($name =~ s/^(.*)~/$1/) ?
length($1): undef;
if (defined($underline) && ($underline >= 0))
$args{-underline
} = $underline;
my $hash = $menu->TkHash('MenuButtons');
delete $args{'-tearoff'}; # too late!
$mb->configure(%args) if %args;
$mb = $menu->cascade(%args);
$mb->menu->AddItems(@
$items) if defined($items) && @
$items;
my ($menu,$balloon,$X,$Y,@opt) = @_;
my $i = $menu->index('active');
my $y = $Y - $menu->rooty;
$i = $menu->index("\@$y");
my $info = $balloon->GetOption($opt,$menu);
if ($opt =~ /^-(statusmsg|balloonmsg)$/ && UNIVERSAL
::isa
($info,'ARRAY'))
return '' if $i eq 'none';
return ${$info}[$i] || '';