Your IP : 216.73.217.87
#!/usr/bin/perl -w
# $Id$
# FvwmTabs
# Copyright (C) 2002-2006 Scott Smedley ss@aao.gov.au
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
# see the FvwmTabs(1) man page for how to use this module.
BEGIN {
use vars qw($prefix $datarootdir $datadir);
$prefix = "/usr";
$datarootdir = "${prefix}/share";
$datadir = "${datarootdir}";
}
use strict;
use lib "${datadir}/fvwm/perllib";
use encoding ':locale';
use FVWM::Module::Tk;
use FVWM::Module::Toolkit qw(FileHandle IO::Select X11::Protocol>=0.52 Tk>=804.025 Tk::Balloon Tk::DragDrop Tk::DropSite Tk::LabFrame Tk::BrowseEntry Tk::PNG);
use vars qw($TOP $fvwm $xServer %tabWin @autoSwallow $repeatId $balloon %global);
$TOP = new MainWindow();
$TOP->withdraw();
$fvwm = new FVWM::Module::Tk($TOP,
Name => "FvwmTabs",
Mask => (M_STRING | M_ADD_WINDOW),
Debug => 0);
$fvwm->debug("Debug level is: " . $fvwm->{debug});
$fvwm->add_default_error_handler();
$fvwm->send("Read ConfigFvwmTabs");
sub ver ($)
{
my ($v) = @_;
$v =~ /^(\d+)\.(\d+)\.(\d+)$/;
return $1 + ($2 + ($3 / 1000.0)) / 1000.0;
}
if (ver($fvwm->version()) < ver('2.5.11'))
{
print(STDERR $fvwm->name() . " requires fvwm >= 2.5.11 - exiting.\n");
exit;
}
my $configTracker = $fvwm->track('ModuleConfig',
DefaultConfig => {activeBG => 'MidnightBlue',
activeFG => 'yellow',
inactiveBG => 'royalblue',
inactiveFG => 'antiquewhite',
titleFG => 'black',
titleBG => 'antiquewhite',
activeRelief => 'sunken',
inactiveRelief => 'raised',
buttonYPadding => 3,
pollRate => 250, # in milliseconds.
autoSwallowClass => '',
autoSwallowResource => '',
autoSwallowName => '',
fontSelector => 'gfontsel --print -f "%f"',
buttonFont => 'Helvetica -12 bold',
titleFont => 'Helvetica -12 bold',
menuFont => 'Helvetica -12',
balloonFont => 'Helvetica -12',
balloonWait => 350, # in milliseconds
balloonBG => '#C0C080',
balloonMsg => 'Tab %tabNo:\n%iconText\n%title',
autoResize => 'false',
stateFile => $ENV{FVWM_USERDIR} . '/.fvwmtabs.state',
fixedSizeTabs => 'false',
showTitlebar => 'true',
useTMTitlebar => 'true',
dragDropIcon => 'none',
bBuggyFocus => 'false',
enableSwallowDND => 'true',
swallowDNDTolerance => 10, # pixels
useIconsOnTabs => 'true',
killIcon => 'none',
addIcon => 'none',
releaseIcon => 'none',
menuIcon => 'none',
swallowIcon => 'none'});
my $pConfig = $configTracker->data;
sub setButtonAttr ($$$)
{
my ($b, $attr, $val) = @_;
eval { $b->configure(-lc($attr) => $pConfig->{$val}); };
if ($@)
{
print(STDERR $fvwm->name() . ": invalid value \"" .
$pConfig->{$val} . "\" for \"-" . lc($attr) . "\"\n");
$pConfig->{$val} = $b->cget(-lc($attr));
}
}
sub isTrue ($) { return ($_[0] =~ /true/i ? 1 : 0); }
$configTracker->observe("config line added", sub {
# 1st arg is FVWM::Module::Tk hash
# 2nd arg is FVWM::Tracker::ModuleConfig hash
# 3rd arg is hash of config values
# 4th arg is name of config value that has changed.
my ($self, $h, $v, $p) = @_;
$self->debug("Module Config event - \"$p\" changed, new val: \"$v->{$p}\"");
foreach my $tId (keys(%tabWin))
{
if ($p =~ /^active(FG|BG|Relief)$/)
{
if (defined $tabWin{$tId}{currentTab})
{
setButtonAttr($tabWin{$tId}{tab}[$tabWin{$tId}{currentTab}]{button}, $1, $p);
if ($1 ne 'Relief')
{
my $active = 'active' . ($1 eq 'BG' ? 'back' : 'fore') . 'ground';
setButtonAttr($tabWin{$tId}{tab}[$tabWin{$tId}{currentTab}]{button}, $active, $p);
}
}
}
elsif ($p =~ /^inactive(FG|BG|Relief)$/)
{
# currentTab is always defined if $nTabs > 0
if (defined $tabWin{$tId}{currentTab})
{
my $active = 'active' . ($1 eq 'BG' ? 'back' : 'fore') . 'ground';
for (my $i = 0; $i < $tabWin{$tId}{nTabs}; $i++)
{
next if ($i == $tabWin{$tId}{currentTab});
setButtonAttr($tabWin{$tId}{tab}[$i]{button}, $1, $p);
if ($1 ne 'Relief')
{
setButtonAttr($tabWin{$tId}{tab}[$i]{button}, $active, $p);
}
}
}
}
elsif ($p eq 'buttonYPadding')
{
# currentTab is always defined if $nTabs > 0
if (defined $tabWin{$tId}{currentTab})
{
for (my $i = 0; $i < $tabWin{$tId}{nTabs}; $i++)
{
setButtonAttr($tabWin{$tId}{tab}[$i]{button}, 'pady', $p);
}
}
}
elsif ($p =~ /^title(FG|BG)$/)
{
$tabWin{$tId}{titleFrame}->configure(-lc($1) => $pConfig->{$p});
}
elsif ($p eq 'pollRate')
{
$TOP->afterCancel($repeatId);
$repeatId = $TOP->repeat($pConfig->{pollRate}, \&callback);
return; # not a per-tabber option.
}
elsif ($p =~ /^(button|title|menu)Font$/)
{
setFont($tId, $1, $pConfig->{$p});
}
elsif ($p =~ /^autoSwallow(Class|Resource|Name)$/)
{
parseAutoSwallow($1);
return; # not a per-tabber option.
}
elsif ($p eq 'showTitlebar')
{
$tabWin{$tId}{showTitlebar} = isTrue($pConfig->{showTitlebar});
showTitlebar($tId);
}
elsif ($p =~ /enableSwallowDND|autoResize/)
{
$tabWin{$tId}{$p} = isTrue($pConfig->{$p});
}
elsif ($p eq 'useTMTitlebar')
{
my $bOn = isTrue($pConfig->{useTMTitlebar});
$tabWin{$tId}{useTMTitlebar} = $bOn;
setupMainTitlebar($tId, $bOn);
}
elsif ($p =~ /^(fontSelector|fixedSizeTabs|balloonMsg|stateFile|bBuggyFocus|swallowDNDTolerance|swallowIcon)$/)
{
# handled implicitly.
return;
}
elsif ($p =~ /^(balloon(Font|Wait|BG)|dragDropIcon|useIconsOnTabs|(kill|add|release|menu)Icon)$/)
{
print(STDERR $fvwm->name() . ": \"$p\" option will not take effect until FvwmTabs restarts.\n");
}
else
{
$self->showError("Unknown option: $p");
return;
}
}
});
$pConfig->{global} = $fvwm->track("GlobalConfig")->data;
# ignoreWin() - called when we're contemplating swallowing a window.
sub ignoreWin ($)
{
my ($winId) = @_;
# handle situations in which we release window from tabber.
# don't want to swallow it again straight away.
if (defined $global{ignoreSwallow}{$winId})
{
my $ts = $global{ignoreSwallow}{$winId};
# delete $global{ignoreSwallow}{$winId};
# ignore window for 1 'window move' event or at least 1 second.
# Wish we had millisecond-resolution time ...
return ((time() - $ts) < 2);
}
return 0;
}
sub toPixels ($$)
{
my ($tId, $tol) = @_;
if ($tol =~ /(\d+)%/)
{
return ($1 / 100.0) * min($tabWin{$tId}{winFrame}->height(),
$tabWin{$tId}{winFrame}->width());
}
return $tol;
}
my $winTracker = $fvwm->track("WindowList", "!stack icons names winfo");
$winTracker->observe("window moved", sub {
my ($fvwm, $tracker, $z, $winId, $old) = @_;
return if (ignoreWin($winId));
$fvwm->debug("swallow win move: $winId");
my $p = $tracker->data($winId);
return if (defined winIdToTId($winId)); # ignore tabbers moving.
foreach my $tId (sort(keys(%tabWin)))
{
# window must overlap tabber by at least $tol pixels to be
# swallowed.
my $tol = toPixels($tId, $pConfig->{swallowDNDTolerance});
my $t = $tracker->data($tabWin{$tId}{selfId});
# At this point, $p is the window that moved & $t is the tabber we
# are checking for an overlap with.
next if (!$tabWin{$tId}{enableSwallowDND});
next if ($p->{desk} != $t->{desk} ||
$p->{X} > ($t->{X} + $t->{width} - $tol) ||
($p->{X} + $p->{width}) < ($t->{X} + $tol) ||
$p->{Y} > ($t->{Y} + $t->{height} - $tol) ||
($p->{Y} + $p->{height}) < ($t->{Y} + $tol));
# If OpaqueMoveSize is in use, we need to ensure the window
# isn't swallowed already.
return if (defined getTabNo($tId, $winId));
# ok, we've got a match - this tabber should swallow the window.
addTab($tId, $winId);
return;
}
});
$winTracker->observe("window icon updated", sub {
my ($fvwm, $tracker, $pc, $winId, $old) = @_;
return if (!isTrue($pConfig->{useIconsOnTabs}));
my ($tId, $tabNo) = findWin($winId);
return if (!defined $tId);
my $p = $pc->{$winId};
return if (!defined $p->{mini_icon_name});
if (!defined $old->{mini_icon_name} ||
$p->{mini_icon_name} ne $old->{mini_icon_name})
{
my $pButton = $tabWin{$tId}{tab}[$tabNo]{button};
$pButton->configure(-image => createMiniIcon($p->{mini_icon_name}, $winId));
}
});
sub autoSwallow (%)
{
my (%args) = @_;
$fvwm->debug("AutoSwallow: " . join(", ", map("$_=" . $args{$_},
sort(keys(%args)))));
push(@autoSwallow, \%args);
}
sub parseAutoSwallow ($)
{
my ($type) = @_;
foreach (split(/,/, $pConfig->{'autoSwallow' . $type}))
{
if (!/^\s*(.+?)(\s+(\S+))?\s*$/i)
{
print(STDERR $fvwm->name() . ": unparseable autoSwallow$type.\n");
next;
}
autoSwallow(lc($type) => $1, type => (!defined $3 ? 'any' : $3));
}
}
foreach ('Class', 'Resource', 'Name')
{
parseAutoSwallow($_);
}
$xServer = X11::Protocol->new($TOP->screen());
$xServer->event_handler('queue');
$xServer->error_handler(\&errorHandler);
sub errorHandler ($$)
{
my($self, $data) = @_;
print(STDERR $fvwm->name() . ': ' . $self->format_error_msg($data));
}
# Check for registered X events every $pollRate milliseconds.
$repeatId = $TOP->repeat($pConfig->{pollRate}, \&callback);
$balloon = $TOP->Balloon(-bg => $pConfig->{balloonBG},
-font => $pConfig->{balloonFont},
-initwait => $pConfig->{balloonWait});
sub showMenu ($$)
{
my ($button, $tId) = @_;
my $menu = $tabWin{$tId}{menu};
$menu->delete(0, 'end');
$menu->add('command',
-label => 'Release All',
-command => [\&releaseAll, undef, $tId, 0]);
$menu->add('command',
-label => 'Release All (Iconify)',
-command => [\&releaseAll, undef, $tId, 1]);
$menu->add('command',
-label => 'Add',
-command => [\&pickAndAddClick, undef, $tId, '']);
$menu->add('command',
-label => 'Add Next',
-command => [\&swallowNext, $tId, '']);
$menu->add('command',
-label => 'Multi Add',
-command => [\&pickAndAddClick, undef, $tId, 'multi']);
$menu->add('command',
-label => 'Resize All To Current',
-command => [\&doResize, $tId]);
if ($pConfig->{fontSelector} ne 'none')
{
my $fontMenu = $menu->Menu(-tearoff => 0, -font => $pConfig->{menuFont});
$fontMenu->add('command',
-label => 'Button Font',
-command => [\&selectFont, $tId, 'button']);
$fontMenu->add('command',
-label => 'Title Font',
-command => [\&selectFont, $tId, 'title']);
$fontMenu->add('command',
-label => 'Menu Font',
-command => [\&selectFont, $tId, 'menu']);
$menu->add('cascade', -label => 'Font', -menu => $fontMenu);
}
if (!defined $tabWin{$tId}{optionsMenu})
{
my $oMenu = $menu->Menu(-tearoff => 0, -font => $pConfig->{menuFont});
$oMenu->add('checkbutton',
-label => 'Show Internal Titlebar',
-variable => \$tabWin{$tId}{showTitlebar},
-command => [\&showTitlebar, $tId]);
$oMenu->add('checkbutton',
-label => 'Use Main Titlebar',
-variable => \$tabWin{$tId}{useTMTitlebar},
-command => [\&toggleMainTitlebar, $tId]);
$oMenu->add('checkbutton',
-label => 'Auto Resize',
-variable => \$tabWin{$tId}{autoResize},
-command => [\&toggleAutoResize, $tId]);
$oMenu->add('checkbutton',
-label => 'Swallow Overlapping (D&D)',
-variable => \$tabWin{$tId}{enableSwallowDND});
$tabWin{$tId}{optionsMenu} = $oMenu;
}
$menu->add('cascade',
-label => 'Options',
-menu => $tabWin{$tId}{optionsMenu});
$menu->add('command',
-label => 'Window Tabizer Dialog',
-command => [\&tabizeWindows, $tId]);
$menu->add('separator');
for (my $tabNo = 0; $tabNo < $tabWin{$tId}{nTabs}; $tabNo++)
{
$menu->add('command', -label => $tabWin{$tId}{tab}[$tabNo]{title},
-command => [\&showTab, $tId, $tabNo]);
}
$menu->add('separator') if ($tabWin{$tId}{nTabs} > 0);
$menu->add('command', -label => 'About', -command => [\&about, $tId]);
$menu->add('command', -label => 'Close', -command => [\&closeTabber, $tId]);
$menu->Popup(-popanchor => 'ne', -popover => 'cursor');
}
sub createNewTabber
{
my ($tId, $geom);
foreach (@_)
{
$geom = $2, next if (/^--?g(eometry)?=(.*)/);
$tId = $_;
}
if (!defined $tId)
{
for ($tId = 0; ; $tId++)
{
last if (!defined($tabWin{$tId}));
}
}
elsif (exists $tabWin{$tId})
{
$fvwm->show_error("Tabber \"$tId\" already exists!");
return;
}
elsif ($tId =~ /^(any|lastFocus)$/i)
{
$fvwm->show_error("Tabber name \"$tId\" is illegal!");
return;
}
my $title = $fvwm->name() . " [$tId]";
my $tl = $TOP->Toplevel(Name => $fvwm->name(),
-class => $fvwm->name(),
-title => $title);
$tl->geometry($geom) if (defined $geom);
$tl->iconname($title);
$tl->focusmodel("active");
my $topFrame = $tl->Frame();
my $tabFrame = $topFrame->Frame();
$tabFrame->pack(-side => 'top', -expand => 0, -fill => 'x');
$topFrame->pack(-expand => 1, -fill => 'both', -anchor => 'nw');
# titlebar
my $wrapFrame = $topFrame->Frame();
$wrapFrame->pack(-expand => 0, -fill => 'x');
my $titleFrame = $wrapFrame->Label(-text => "No title",
-font => $pConfig->{titleFont},
-bg => $pConfig->{titleBG},
-fg => $pConfig->{titleFG});
$tabWin{$tId}{showTitlebar} = isTrue($pConfig->{showTitlebar});
$tabWin{$tId}{enableSwallowDND} = isTrue($pConfig->{enableSwallowDND});
$tabWin{$tId}{useTMTitlebar} = isTrue($pConfig->{useTMTitlebar});
$tabWin{$tId}{autoResize} = isTrue($pConfig->{autoResize});
$tabWin{$tId}{titleFrame} = $titleFrame;
$tabWin{$tId}{wrapFrame} = $wrapFrame;
$tabWin{$tId}{nTabs} = 0;
showTitlebar($tId);
my $winFrame = $topFrame->Frame();
$winFrame->pack(-expand => 1, -fill => 'both', -side => 'top');
# ===
my $af = $tabFrame->Frame()->pack(-side => 'right', -fill => 'y');
my @po = qw/-side right -fill y/;
my $bKill = $af->Button(-text => 'K',
-image => createImage($pConfig->{killIcon}),
-padx => 0,
-borderwidth => 0,
-relief => 'flat')->pack(@po);
$bKill->Tk::bind('<ButtonRelease-1>', [\&closeTab, $tId, 'Close']);
$bKill->Tk::bind('<ButtonRelease-2>', [\&killTab, $tId]);
$bKill->Tk::bind('<ButtonRelease-3>', [\&closeTab, $tId, 'Destroy']);
my $bAdd = $af->Button(-text => 'A',
-image => createImage($pConfig->{addIcon}),
-padx => 0,
-borderwidth => 0,
-relief => 'flat')->pack(@po);
$bAdd->Tk::bind('<ButtonRelease-1>', [\&pickAndAddClick, $tId, '']);
$bAdd->Tk::bind('<ButtonRelease-2>', [\&swallowNextClick, $tId]);
$bAdd->Tk::bind('<ButtonRelease-3>', [\&pickAndAddClick, $tId, 'multi']);
$tabWin{$tId}{addButton} = $bAdd;
my $bRelease = $af->Button(-text => 'R',
-image => createImage($pConfig->{releaseIcon}),
-padx => 0,
-borderwidth => 0,
-relief => 'flat')->pack(@po);
$bRelease->Tk::bind('<ButtonRelease-1>', [\&releaseCurrent, $tId]);
$bRelease->Tk::bind('<ButtonRelease-2>', [\&releaseAll, $tId, 1]);
$bRelease->Tk::bind('<ButtonRelease-3>', [\&releaseAll, $tId, 0]);
my $bMenu = $af->Button(-text => 'M',
-image => createImage($pConfig->{menuIcon}),
-padx => 0,
-borderwidth => 0,
-relief => 'flat')->pack(@po);
$bMenu->Tk::bind('<ButtonRelease-1>', [\&showMenu, $tId]);
$bMenu->Tk::bind('<ButtonRelease-2>', [\&tabInfo, $tId]);
$tabWin{$tId}{menu} = $tl->Menu(-tearoff => 0,
-font => $pConfig->{menuFont});
$tabWin{$tId}{toplevel} = $tl;
$tabWin{$tId}{toplevelId} = hex($tabWin{$tId}{toplevel}->id());
$tabWin{$tId}{tabFrame} = $tabFrame;
$tabWin{$tId}{winFrame} = $winFrame;
$tabWin{$tId}{currentTab} = undef;
$tabWin{$tId}{lastId} = undef;
$tabWin{$tId}{balloonMsg} = '?';
$tabWin{$tId}{parent} = hex($winFrame->id);
# ===
$tabFrame->waitVisibility();
# $tabFrame->packPropagate(0);
# $tabFrame->configure(-height => 28, -width => $af->reqwidth());
$tabFrame->configure(-width => $af->reqwidth());
$winFrame->waitVisibility();
my ($root, $parent, @kids) = $xServer->QueryTree($tabWin{$tId}{toplevelId});
$tabWin{$tId}{selfId} = $parent;
$tl->protocol('WM_DELETE_WINDOW', [\&closeTabber, $tId]);
$tl->protocol('WM_TAKE_FOCUS', [\&takeFocus, $tId]);
$xServer->ChangeWindowAttributes($tabWin{$tId}{parent},
event_mask => $xServer->pack_event_mask('ResizeRedirect'));
buggyFocusWorkaround($tId);
}
sub getWrapperWinId ($)
{
my ($tId) = @_;
return hex($tabWin{$tId}{toplevel}->frame());
}
sub buggyFocusWorkaround ($)
{
my ($tId) = @_;
if (exists $tabWin{$tId}{_wrapperWinId})
{
# don't want events on old window id anymore.
$xServer->ChangeWindowAttributes($tabWin{$tId}{_wrapperWinId},
event_mask => $xServer->pack_event_mask());
}
$tabWin{$tId}{_wrapperWinId} = getWrapperWinId($tId);
# request events on new window id.
$xServer->ChangeWindowAttributes($tabWin{$tId}{_wrapperWinId},
event_mask => $xServer->pack_event_mask('EnterWindow'));
$tabWin{$tId}{focusTimestamp} = 'CurrentTime';
}
sub takeFocus ($)
{
my ($tId) = @_;
$global{lastFocus} = $tId;
my $winId;
if ($tabWin{$tId}{toplevel}->state() eq 'iconic')
{
# TODO: who do we give the focus to?
$winId = $tabWin{$tId}{toplevelId};
return;
}
elsif (!defined $tabWin{$tId}{currentTab})
{
$winId = $tabWin{$tId}{toplevelId};
}
else
{
$winId = $tabWin{$tId}{tab}[$tabWin{$tId}{currentTab}]{winId};
}
my $ts = (isTrue($pConfig->{bBuggyFocus}) ?
'CurrentTime' :
$tabWin{$tId}{focusTimestamp});
# "revert-to" (2nd) arg can be 'Parent', 'PointerRoot' or 'None'.
$xServer->SetInputFocus($winId, 'Parent', $ts);
}
# Don't you just *love* Prince!?
sub letItGo ($$$$)
{
my ($button, $tId, $tabNo, $bIconify) = @_;
if ($fvwm->{debug} && $tabNo >= $tabWin{$tId}{nTabs})
{
$fvwm->debug("BUG: invalid tabNo.");
return;
}
my $winId = $tabWin{$tId}{tab}[$tabNo]{winId};
$global{ignoreSwallow}{$winId} = time();
$xServer->ReparentWindow($winId, $xServer->root(), (0, 0));
# position the window at the location we found it. (ie. before the
# window was added to this tabber.)
$xServer->ConfigureWindow($winId,
x => $tabWin{$tId}{tab}[$tabNo]{initialXPos},
y => $tabWin{$tId}{tab}[$tabNo]{initialYPos});
$xServer->MapWindow($winId);
XSync(); # wait for window to popup.
# if the window we're releasing is a tabber, we need to recompute
# the wrapper window id as it can change dynamically.
my $otherTId = winIdToTId($winId);
buggyFocusWorkaround($otherTId) if (defined $otherTId);
$fvwm->send("Iconify", $winId) if ($bIconify);
removeTab($tId, $tabNo, 'letgo');
}
sub letItGoClick ($$$$)
{
my ($button, $tId, $winId, $bIconify) = @_;
letItGo($button, $tId, getTabNo($tId, $winId), $bIconify);
}
# release all windows from <$tId> tabber.
sub releaseAll ($$$)
{
my ($b, $tId, $bIconify) = @_;
for (my $tabNo = $tabWin{$tId}{nTabs} - 1; $tabNo >= 0; $tabNo--)
{
letItGo(undef, $tId, $tabNo, $bIconify);
}
$tabWin{$tId}{tab} = undef;
$tabWin{$tId}{nTabs} = 0;
$tabWin{$tId}{currentTab} = undef;
}
sub releaseCurrent ($$)
{
my ($b, $tId) = @_;
return if (!defined $tabWin{$tId}{currentTab});
letItGo($b, $tId, $tabWin{$tId}{currentTab}, 0);
}
sub closeTab ($$$)
{
my ($b, $tId, $cmd) = @_;
return if (!defined $tabWin{$tId}{currentTab});
my $tabNo = $tabWin{$tId}{currentTab};
# Ensure the window is mapped off-screen.
$tabWin{$tId}{tab}[$tabNo]{initialXPos} = 2000;
$tabWin{$tId}{tab}[$tabNo]{initialYPos} = 2000;
my $winId = $tabWin{$tId}{tab}[$tabNo]{winId};
releaseTab($tId, $tabNo, 0);
# Give the window 1 second to be mapped, before closing it.
$fvwm->send("Schedule 1000 WindowId $winId $cmd");
}
sub killTab ($$)
{
my ($b, $tId) = @_;
return if (!defined $tabWin{$tId}{currentTab});
my $winId = $tabWin{$tId}{tab}[$tabWin{$tId}{currentTab}]{winId};
$xServer->KillClient($winId);
}
sub XSync ()
{
# atom() is cached by X11::Protocol so we need to issue
# the actual X request.
# $xServer->atom("WM_NAME");
$xServer->req('InternAtom', "WM_NAME", 0);
}
sub closeTabber ($)
{
my ($tId) = @_;
$fvwm->debug("closeTabber($tId)");
$xServer->ChangeWindowAttributes($tabWin{$tId}{parent},
event_mask => $xServer->pack_event_mask());
releaseAll(undef, $tId, 0);
$tabWin{$tId}{toplevel}->withdraw();
# We need to wait for the X server to do the reparenting before
# we can destroy the window.
XSync();
# TODO: destroy() call intermittently causes problems.
# $tabWin{$tId}{toplevel}->destroy();
delete $tabWin{$tId};
}
sub winIdToTId ($)
{
my ($winId) = @_;
foreach my $tId (keys(%tabWin))
{
return $tId if ($winId == $tabWin{$tId}{selfId});
}
return undef;
}
# SendToModule commands arrive as M_STRING events.
$fvwm->add_handler(M_STRING, sub {
my ($self, $event) = @_;
$fvwm->debug("M_STRING: " . join(", ", map("$_ = " . $event->args->{$_}, keys(%{$event->args}))));
# Older versions of Perl don't support 'xdigit'.
# if ($event->args->{text} =~ /fn (\S+) (0x[[:xdigit:]]+)\s*(.*)/)
if ($event->args->{text} =~ /fn (\S+) (0x[0-9a-fA-F]+)\s*(.*)/)
{
# fn <fnName> <winId> <optArgs>
my $tId = winIdToTId(hex($2));
print(STDERR $self->name() . ": focus not in tabber. ($2)\n"), return if (!defined $tId);
no strict;
&$1($tId, split(/\s+/, $3));
}
elsif ($event->args->{text} =~ /addme (\S+)(.*)?/)
{
my $bMulti = ($2 eq ' multi');
my $tId = $1;
$tId = winIdToTId(hex($tId)) if (!exists $tabWin{$tId});
if (!defined $tId || !exists $tabWin{$tId})
{
print(STDERR $self->name() . ": Unknown Tabber \"$2\"\n");
return;
}
addTab($tId, $event->args->{win_id});
pickAndAdd($tId, 'multi') if ($bMulti);
}
else
{
# handle createNewTabber, saveState, enableDND & swallowNext.
my @a = split(/\s+/, $event->args->{text});
my $fn = shift(@a);
if (!defined main->can($fn))
{
print(STDERR $self->name() . ": Unknown command: \"$fn(@a)\"\n");
return;
}
no strict;
&$fn(@a);
$fvwm->debug("M_STRING: invoked command \"$fn\"");
}
});
# showNext() - invoked from key binding.
sub showNext ($$)
{
my ($tId, $inc) = @_;
return if ($tabWin{$tId}{nTabs} <= 0);
my $nextTabNo = ($tabWin{$tId}{currentTab} + $inc) % $tabWin{$tId}{nTabs};
showTab($tId, $nextTabNo) if ($nextTabNo != $tabWin{$tId}{currentTab});
}
sub showLast ($)
{
my ($tId) = @_;
return if ($tabWin{$tId}{nTabs} <= 0 || !defined $tabWin{$tId}{lastId});
my $tabNo = getTabNo($tId, $tabWin{$tId}{lastId});
return if (!defined $tabNo);
showTab($tId, $tabNo);
}
# releaseTab() - invoked from key binding.
sub releaseTab ($$$)
{
my ($tId, $tabNo, $bIconify) = @_;
$tabNo = $tabWin{$tId}{currentTab} if ($tabNo eq 'current');
letItGo(undef, $tId, $tabNo, $bIconify);
}
sub emptiestTabber ()
{
my $tId = undef; # return undef if no tabbers running.
my $n = 99;
foreach (keys(%tabWin))
{
if ($tabWin{$_}{nTabs} < $n)
{
$tId = $_;
$n = $tabWin{$_}{nTabs};
}
}
return $tId;
}
sub whichTabber ($)
{
my ($tId) = @_;
if ($tId =~ /any/i)
{
# choose the emptiest tabber.
return emptiestTabber();
}
elsif ($tId =~ /lastFocus/i)
{
return (defined $global{lastFocus} ?
$global{lastFocus} :
emptiestTabber());
}
return $tId;
}
# swallowCheck() - check if a window should be swallowed/tabized by a tabber.
sub swallowCheck ($$)
{
my ($winId, $pArray) = @_;
my $tId = $global{swallowNext};
if (!defined $tId)
{
return if (ignoreWin($winId));
my $s = chr(0);
my $name = getProperty($winId, 'WM_NAME');
return if (!defined $name);
my ($resource, $class) = split(/$s/, getProperty($winId, 'WM_CLASS'));
$resource = '' if (!defined $resource);
$class = '' if (!defined $class);
my $bFound = 0;
foreach (@{$pArray})
{
if ((exists $_->{resource} && $resource =~ /$_->{resource}/) ||
(exists $_->{class} && $class =~ /$_->{class}/) ||
(exists $_->{name} && $name =~ /$_->{name}/))
{
$tId = $_->{type};
$bFound = 1;
last;
}
}
return if (!$bFound);
}
elsif (exists $tabWin{$tId})
{
$tabWin{$tId}{addButton}->configure(-text => "A",
-image => createImage($pConfig->{addIcon}));
}
$tId = whichTabber($tId);
addTab($tId, $winId) if (defined $tId && exists $tabWin{$tId});
delete $global{swallowNext};
}
sub swallowNext
{
my ($tId, @fvwmCommand) = @_;
$tId = $global{swallowNext} = whichTabber($tId);
$fvwm->debug("Got swallowNext: $global{swallowNext}");
$fvwm->debug("fvwmCommand is @fvwmCommand");
if (exists $tabWin{$tId})
{
$tabWin{$tId}{addButton}->configure(-text => "",
-image => createImage($pConfig->{swallowIcon}));
}
$fvwm->send("@fvwmCommand");
}
sub swallowNextClick ($$) { swallowNext($_[1], ''); }
$fvwm->add_handler(M_ADD_WINDOW, sub {
my ($self, $event) = @_;
$fvwm->debug("Add Window: " . join(", ", map("$_ = " . $event->args->{$_}, keys(%{$event->args}))));
my $winId = $event->args->{win_id};
swallowCheck($winId, \@autoSwallow);
});
# ON_EXIT handler is invoked just before this module terminates.
$fvwm->add_handler(ON_EXIT, sub {
my ($self, $event) = @_;
print($fvwm->name() . ": Got ON_EXIT event.\n");
cleanup();
});
$SIG{PIPE} = sub {
print($fvwm->name() . ": SIGPIPE!\n");
cleanup();
exit;
};
sub cleanup ()
{
foreach (keys(%tabWin))
{
closeTabber($_);
}
}
my $fds = IO::Select->new($xServer->connection->fh);
sub callback
{
$xServer->flush();
$xServer->handle_input() if ($fds->can_read(0));
while (my %e = $xServer->dequeue_event())
{
eventHandler(%e)
}
}
sub eventHandler
{
my (%event) = @_;
$fvwm->debug("Got a $event{name} event: " . join(", ", map("$_ = $event{$_}", keys(%event))));
if ($event{name} eq 'ResizeRequest')
{
my $tId;
for (keys(%tabWin))
{
$tId = $_, last if ($tabWin{$_}{parent} == $event{window});
}
return if (!defined $tId);
my $h = $tabWin{$tId}{winFrame}->height();
# resize the $winFrame widget. Can't use $w->configure(-height ...)
# cos we're requesting resize events on this widget.
$xServer->ConfigureWindow($tabWin{$tId}{parent},
"height" => $h, #$event{height},
"width" => $event{width});
if ($tabWin{$tId}{autoResize})
{
resizeAll($tId, $event{width}, $h); # $event{height}
}
else
{
# If the tabber is resized, resize the currently viewed app/tab
# (if any) to fit into the new space.
return if (!defined $tabWin{$tId}{currentTab});
my $winId = $tabWin{$tId}{tab}[$tabWin{$tId}{currentTab}]{winId};
$xServer->ConfigureWindow($winId,
"height" => $h, # $event{height},
"width" => $event{width});
}
XSync();
return;
}
elsif ($event{name} eq 'EnterNotify')
{
my $tId;
for (keys(%tabWin))
{
$tId = $_, last if (getWrapperWinId($_) == $event{event});
}
return if (!defined $tId);
$tabWin{$tId}{focusTimestamp} = $event{time};
$fvwm->debug("focusTimestamp set to $tabWin{$tId}{focusTimestamp}");
takeFocus($tId);
return;
}
my ($tId, $tabNo) = findWin($event{window});
if (!defined $tId)
{
$fvwm->debug("Ignoring event for invalid/unknown window.");
return;
}
if ($event{name} eq 'PropertyNotify')
{
my $prop = $xServer->atom_name($event{atom});
return if ($prop ne 'WM_NAME' && $prop ne 'WM_ICON_NAME');
my $val = getProperty($event{window}, $prop);
return if (!defined $val); # window no longer exists!
if ($prop eq 'WM_NAME')
{
setTabTitle($tId, $tabNo, $val);
}
elsif ($prop eq 'WM_ICON_NAME')
{
setTabName($tId, $tabNo, $val);
}
}
elsif ($event{name} eq 'DestroyNotify')
{
removeTab($tId, $tabNo, 'destroyed');
}
elsif ($event{name} eq 'MapNotify')
{
$xServer->SetInputFocus($event{window}, 'Parent', 'CurrentTime');
}
else
{
$fvwm->debug("Got a strange event: $event{name}");
}
}
sub getXYPos ($)
{
my ($winId) = @_;
my ($na1, $na2, $x, $y) = $xServer->TranslateCoordinates($winId,
$xServer->root(),
0,
0);
return ($x, $y);
}
sub createImage ($)
{
my ($filename) = @_;
if (defined $global{imageCache}{$filename})
{
return $global{imageCache}{$filename};
}
my $orig = $filename;
if (substr($filename, 0, 1) ne '/')
{
my $bFound = 0;
foreach (split(/:/, $pConfig->{global}->{ImagePath}))
{
my $tmp = "$_/$filename";
$filename = $tmp, $bFound = 1, last if (-f $tmp);
}
return '' if (!$bFound);
}
print(STDERR $fvwm->name() . ": Creating icon from $filename\n");
my $img = eval { $TOP->Photo(-file => $filename); };
return '' if (!defined $img);
$global{imageCache}{$orig} = $img;
return $img;
}
sub createMiniIcon ($$)
{
my ($iconName, $winId) = @_;
return createImage($iconName) if ($iconName ne 'ewmh_mini_icon');
my ($val, $type, $format, $bytes_after) =
$xServer->GetProperty($winId,
$xServer->atom("_NET_WM_ICON"),
'AnyPropertyType',
0,
8192,
0);
my $w = unpack("L", substr($val, 0, 4));
my $h = unpack("L", substr($val, 4, 4));
my $img = $TOP->Photo(-width => $w, -height => $h);
my $index = 8;
for my $y (0 .. $h - 1)
{
for my $x (0 .. $w - 1)
{
my $pixel = unpack("L", substr($val, $index, 4));
$index += 4;
my $alpha = ($pixel >> 24) & 0xff;
next if ($alpha == 0);
my $blue = $pixel & 0xff;
my $green = ($pixel >> 8) & 0xff;
my $red = ($pixel >> 16) & 0xff;
my $color = sprintf("#%02x%02x%02x", $red, $green, $blue);
$img->put($color, -to => ($x, $y, $x+1, $y+1));
}
}
# TODO: image resizing quality is crap, but at least it works for now.
my $img2 = $TOP->Photo();
my ($xFactor, $yFactor) = ($w / 16, $h / 16);
$img2->copy($img, -subsample => ($xFactor, $yFactor));
return $img2;
}
my $dndIcon;
if ($pConfig->{dragDropIcon} ne 'none')
{
$dndIcon = createImage($pConfig->{dragDropIcon});
if ($dndIcon eq '')
{
print(STDERR $fvwm->name() . ": Couldn't create D&D icon from " .
$pConfig->{dragDropIcon} . "\n");
}
}
sub addTab ($$)
{
my ($tId, $winId) = @_;
if (defined $tabWin{$tId}{selfId} && $winId == $tabWin{$tId}{selfId})
{
# can't make tabber a child of itself
$fvwm->send("Beep");
$fvwm->show_error("Can't add self to tabber!");
return;
}
# check if the window actually exists before we try to swallow it.
if (!defined getProperty($winId, 'WM_NAME'))
{
print(STDERR $fvwm->name() . ": Window ($winId) doesn't exist.\n");
return;
}
my $tabNo = $tabWin{$tId}{nTabs}++;
$tabWin{$tId}{tab}[$tabNo]{winId} = $winId;
# save position of window for later.
($tabWin{$tId}{tab}[$tabNo]{initialXPos}, $tabWin{$tId}{tab}[$tabNo]{initialYPos}) = getXYPos($winId);
# if the auto-resize option is specified & the new window to add is
# smaller than the tabber, then we need to enlarge the new window. If
# the new window is larger than the tabber, then the tabber will
# expand appropriately (with all existing windows in the tabber) on
# the resize event.
if ($tabWin{$tId}{autoResize})
{
my %g = $xServer->GetGeometry($winId);
my $h = $tabWin{$tId}{winFrame}->height();
my $w = $tabWin{$tId}{winFrame}->width();
if ($g{height} < $h || $g{width} < $w)
{
$xServer->ConfigureWindow($winId,
"height" => max($h, $g{height}),
"width" => max($w, $g{width}));
}
}
$xServer->ReparentWindow($winId, $tabWin{$tId}{parent}, (0, 0));
my $name = getProperty($winId, "WM_NAME");
$tabWin{$tId}{tab}[$tabNo]{title} = $name;
my $iconName = getProperty($winId, "WM_ICON_NAME");
$iconName = $name if ($iconName eq '');
# -borderwidth => 0 also gets rid of 2x2=4 pixels in height, but
# will not be able to display relief of button.
$tabWin{$tId}{tab}[$tabNo]{button} =
$tabWin{$tId}{tabFrame}->Button(-text => $iconName,
-padx => 2,
-pady => $pConfig->{buttonYPadding},
-width => 1,
-anchor => 'w',
-compound => 'left',
-command => [\&showTabClick, $tId, $winId],
-font => $pConfig->{buttonFont});
my $pButton = $tabWin{$tId}{tab}[$tabNo]{button};
if (defined $winTracker &&
defined $winTracker->data($winId) &&
isTrue($pConfig->{useIconsOnTabs}) &&
defined $winTracker->data($winId)->{mini_icon_name})
{
my $icon = createMiniIcon($winTracker->data($winId)->{mini_icon_name}, $winId);
$pButton->configure(-image => $icon);
}
# Raise the FvwmTabs window whenever a tab is selected.
$pButton->Tk::bind('<ButtonPress-1>', sub { $fvwm->send("Current Raise"); });
$pButton->pack(-side => 'left', -expand => 1, -fill => 'x');
$pButton->Tk::bind('<ButtonRelease-2>', [\&letItGoClick, $tId, $winId, 0]);
$pButton->Tk::bind('<ButtonRelease-3>', [\&letItGoClick, $tId, $winId, 1]);
# drag-&-drop to reorder tabs.
my $dd = $pButton->DragDrop(-event => '<Control-B2-Motion>',
-sitetypes => [qw(Local)],
-selection => $winId,
-cursor => 'sb_h_double_arrow',
-text => '*',
-handlers => []);
my $site =
$pButton->DropSite(-droptypes => [qw(Local)],
-dropcommand => [\&reorderTabClick, [$tId, $winId]]);
if ($pConfig->{dragDropIcon} ne 'none' && defined $dndIcon)
{
$dd->configure(-image => $dndIcon);
}
$balloon->attach($pButton,
-balloonmsg => \$tabWin{$tId}{balloonMsg},
-postcommand => [\&setBalloonMsg, $tId, $winId]);
# Ensure we are notified of any attribute changes in the new window.
# PropertyChange will tell us when WM_NAME or WM_ICON_NAME changes.
# StructureNotify will tell us when a program running in a tab
# terminates/is-destroyed.
$xServer->ChangeWindowAttributes($winId,
event_mask => $xServer->pack_event_mask('PropertyChange',
'StructureNotify'));
showTab($tId, $tabNo);
}
sub pickAndAdd ($$)
{
my ($tId, $arg) = @_;
$fvwm->send("Pick SendToModule " . $fvwm->name() . " addme $tId $arg");
}
# sub pickAndAddClick ($$$) { pickAndAdd(@_[1..$#_]); }
sub pickAndAddClick ($$$)
{
my ($b, $tId, $arg) = @_;
pickAndAdd($tId, $arg);
}
sub tabInfo ($$)
{
my ($button, $tId) = @_;
print("\nnTabs=$tabWin{$tId}{nTabs}\n");
printf("parent=0x%x (%d)\n", $tabWin{$tId}{parent}, $tabWin{$tId}{parent});
printf("self=0x%x (%d)\n", $tabWin{$tId}{selfId}, $tabWin{$tId}{selfId});
for (my $tabNo = 0; $tabNo < $tabWin{$tId}{nTabs}; $tabNo++)
{
printf("Tab #$tabNo winId = 0x%x (%d)\n",
$tabWin{$tId}{tab}[$tabNo]{winId},
$tabWin{$tId}{tab}[$tabNo]{winId});
}
print("AutoSwallow:\n");
foreach my $a (@autoSwallow)
{
print("\t" . join(", ", map("$_=" . $a->{$_}, sort(keys(%{$a})))) . "\n");
}
XSync();
}
sub getProperty ($$)
{
my ($winId, $property) = @_;
my @s = $xServer->robust_req('GetProperty',
$winId,
$xServer->atom($property),
'AnyPropertyType', 0, 200, 0);
my $p = $s[0];
return undef if ($p eq 'Window');
# Remove font encoding details (if any).
my ($startM, $endM) = (chr(27), chr(2));
$p->[0] =~ s/$startM.*?$endM//g;
return $p->[0];
}
# find which tabber/tab a window is in (if any).
sub findWin ($)
{
my ($winId) = @_;
foreach (keys(%tabWin))
{
my $tabNo = getTabNo($_, $winId);
return ($_, $tabNo) if (defined $tabNo);
}
return undef;
}
# return the tabNo for the specified window ID or undef if the window is
# not part of the tabber.
# TODO: rename this function to "winIdToTabNo"?
sub getTabNo ($$)
{
my ($tId, $winId) = @_;
for (my $tabNo = 0; $tabNo < $tabWin{$tId}{nTabs}; $tabNo++)
{
return $tabNo if ($tabWin{$tId}{tab}[$tabNo]{winId} == $winId);
}
return undef;
}
# the window for <$tabNo> has been destroyed/letgo - cleanup appropriately.
sub removeTab ($$$)
{
my ($tId, $tabNo, $state) = @_;
$fvwm->debug("removeTab($tId, $tabNo, $state)");
if ($state eq 'letgo')
{
# We are no longer interested in events for this window.
$xServer->ChangeWindowAttributes($tabWin{$tId}{tab}[$tabNo]{winId},
event_mask => $xServer->pack_event_mask());
}
$balloon->detach($tabWin{$tId}{tab}[$tabNo]{button});
$tabWin{$tId}{tab}[$tabNo]{button}->destroy();
$tabWin{$tId}{tab}[$tabNo] = undef;
for (my $t = $tabNo; $t < $tabWin{$tId}{nTabs} - 1; $t++)
{
$tabWin{$tId}{tab}[$t] = $tabWin{$tId}{tab}[$t + 1];
}
$tabWin{$tId}{nTabs}--;
$tabWin{$tId}{tab}[$tabWin{$tId}{nTabs}] = undef;
if ($tabWin{$tId}{currentTab} == $tabNo)
{
$tabWin{$tId}{currentTab} = undef;
if ($tabWin{$tId}{nTabs} > 0)
{
showTab($tId, ($tabNo < $tabWin{$tId}{nTabs} ? $tabNo : $tabNo - 1))
}
else
{
$tabWin{$tId}{toplevel}->wm('grid', '', '', '', '');
$tabWin{$tId}{titleFrame}->configure(-text => 'No title');
$tabWin{$tId}{winFrame}->configure(-height => 1, -width => 0);
}
}
else
{
$tabWin{$tId}{currentTab}-- if ($tabWin{$tId}{currentTab} > $tabNo);
if ($tabWin{$tId}{nTabs} == 1)
{
# allow the selected tab button to expand as much as possible.
$tabWin{$tId}{tab}[$tabWin{$tId}{currentTab}]{button}->pack(-expand => 1);
}
}
}
sub showTab ($$)
{
my ($tId, $tabNo) = @_;
$fvwm->send("Beep"), return if ($tabNo >= $tabWin{$tId}{nTabs});
# Unmap the current tab.
if (defined($tabWin{$tId}{currentTab}))
{
my $currentTab = $tabWin{$tId}{currentTab};
return if ($tabNo == $currentTab);
my $winId = $tabWin{$tId}{tab}[$currentTab]{winId};
$xServer->UnmapWindow($winId);
my $b = $tabWin{$tId}{tab}[$currentTab]{button};
setButtonAttr($b, 'bg', 'inactiveBG');
setButtonAttr($b, 'activebackground', 'inactiveBG');
setButtonAttr($b, 'fg', 'inactiveFG');
setButtonAttr($b, 'activeforeground', 'inactiveFG');
$tabWin{$tId}{tab}[$currentTab]{button}->configure(
-width => 1,
-relief => $pConfig->{inactiveRelief});
$tabWin{$tId}{tab}[$currentTab]{button}->pack(-expand => 1);
$tabWin{$tId}{lastId} = $winId;
}
# Map the new tab.
my $winId = $tabWin{$tId}{tab}[$tabNo]{winId};
$xServer->MapWindow($winId);
$tabWin{$tId}{currentTab} = $tabNo;
my $bEq = isTrue($pConfig->{fixedSizeTabs});
my $b = $tabWin{$tId}{tab}[$tabNo]{button};
setButtonAttr($b, 'bg', 'activeBG');
setButtonAttr($b, 'activebackground', 'activeBG');
setButtonAttr($b, 'fg', 'activeFG');
setButtonAttr($b, 'activeforeground', 'activeFG');
$tabWin{$tId}{tab}[$tabNo]{button}->configure(
-width => $bEq,
-relief => $pConfig->{activeRelief});
my $bExpand = ($bEq || ($tabWin{$tId}{nTabs} == 1));
$tabWin{$tId}{tab}[$tabNo]{button}->pack(-expand => $bExpand);
my %g = $xServer->GetGeometry($winId);
$tabWin{$tId}{winFrame}->configure(-height => $g{height}, -width => $g{width});
$tabWin{$tId}{toplevel}->geometry("");
showTitle($tId, $tabNo, $g{width});
setResizing ($tId, $winId);
# If we're changing the active tab really quickly we have to handle/flush
# all the X events in the queue - this prevents us from getting a
# 'Protocol Error'.
callback();
}
sub showTabClick ($$)
{
my ($tId, $winId) = @_;
showTab($tId, getTabNo($tId, $winId));
}
sub setMainTitlebar ($$)
{
my ($tId, $titleStr) = @_;
my $title = $fvwm->name() . " [$tId]";
$title .= " : $titleStr" if ($titleStr ne '');
$tabWin{$tId}{toplevel}->configure(-title => $title);
}
sub setupMainTitlebar ($$)
{
my ($tId, $bOn) = @_;
my $titleStr = '';
if ($bOn && defined $tabWin{$tId}{currentTab})
{
$titleStr = $tabWin{$tId}{tab}[$tabWin{$tId}{currentTab}]{title};
}
setMainTitlebar($tId, $titleStr);
}
sub toggleMainTitlebar ($)
{
setupMainTitlebar($_[0], $tabWin{$_[0]}{useTMTitlebar});
}
sub showTitle ($$$)
{
my ($tId, $tabNo, $w) = @_;
$fvwm->debug("BUG: showTitle()") if ($tabNo != $tabWin{$tId}{currentTab});
# This is a nice feature - Wrap the title text across multiple lines.
# The following command in an xterm window illustrates the usefulness:
# echo "to be?\nor\nnot to be?" && sleep 3
$w -= 30;
my $titleStr = $tabWin{$tId}{tab}[$tabNo]{title};
# make sure the title text does not exceed this length.
$tabWin{$tId}{titleFrame}->configure(-text => $titleStr,
-wraplength => $w);
setMainTitlebar($tId, $titleStr) if ($tabWin{$tId}{useTMTitlebar});
}
sub setTabTitle($$$)
{
my ($tId, $tabNo, $titleStr) = @_;
$titleStr =~ s/\n//g;
$tabWin{$tId}{tab}[$tabNo]{title} = $titleStr;
if ($tabNo == $tabWin{$tId}{currentTab})
{
# get the width of the title label in pixels.
my $w = $tabWin{$tId}{titleFrame}->width();
showTitle($tId, $tabNo, $w);
}
}
sub showTitlebar ($)
{
my ($tId) = @_;
if ($tabWin{$tId}{showTitlebar})
{
$tabWin{$tId}{titleFrame}->pack(-expand => 0, -fill => 'x', -side => 'top');
}
else
{
$tabWin{$tId}{titleFrame}->pack('forget');
$tabWin{$tId}{wrapFrame}->configure(-width => 0, -height => 1);
}
}
sub setTabName ($$$)
{
my ($tId, $tabNo, $name) = @_;
$tabWin{$tId}{tab}[$tabNo]{button}->configure(-text => $name);
}
sub selectFont ($$)
{
my ($tId, $type) = @_;
my $cmd = $pConfig->{fontSelector};
my $currentFont = $pConfig->{$type . 'Font'};
$cmd =~ s/%f/$currentFont/g;
$SIG{CHLD}= sub { wait; };
my $pipe = new FileHandle();
if (!$pipe->open("$cmd|"))
{
$fvwm->show_error("Select font: command $cmd failed.");
return
}
sub fontCallback ($$$)
{
my ($pipe, $tId, $type) = @_;
my $line = $pipe->getline();
setFont($tId, $type, $line) if (defined($line));
$pipe->close() if ($pipe->eof());
}
$TOP->fileevent($pipe, "readable" => [\&fontCallback, $pipe, $tId, $type]);
}
sub setFont ($$$)
{
my ($tId, $type, $font) = @_;
$fvwm->debug("new $type font is: $font");
$pConfig->{$type . 'Font'} = $font;
if ($type eq 'button')
{
for (my $tabNo = 0; $tabNo < $tabWin{$tId}{nTabs}; $tabNo++)
{
$tabWin{$tId}{tab}[$tabNo]{button}->configure(-font => $font);
}
}
elsif ($type eq 'title')
{
$tabWin{$tId}{titleFrame}->configure(-font => $font);
}
elsif ($type eq 'menu')
{
$tabWin{$tId}{menu}->configure(-font => $font);
}
}
sub about ($)
{
my ($tId) = @_;
my $info = $fvwm->name() . "\n\nby Scott Smedley\nss\@aao.gov.au";
$fvwm->show_message($info, "About " . $fvwm->name());
}
sub setBalloonMsg ($$)
{
my ($tId, $winId) = @_;
my $tabNo = getTabNo($tId, $winId);
my $msg = $pConfig->{balloonMsg};
$msg =~ s/\\n/\n/g;
$msg =~ s/%tabNo/$tabNo/gi;
my $iconText = $tabWin{$tId}{tab}[$tabNo]{button}->cget(-text);
$msg =~ s/%iconText/$iconText/gi;
$msg =~ s/%title/$tabWin{$tId}{tab}[$tabNo]{title}/gi;
$tabWin{$tId}{balloonMsg} = $msg;
}
sub reorderTab ($$$)
{
my ($tId, $insertionPoint, $tabNo) = @_;
return if ($insertionPoint == $tabNo || $insertionPoint == $tabNo + 1);
my ($s, $e) = ($insertionPoint > $tabNo ?
($tabNo, $insertionPoint) :
($insertionPoint, $tabNo));
for (my $i = $s; $i < $tabWin{$tId}{nTabs}; $i++)
{
$tabWin{$tId}{tab}[$i]{button}->pack('forget');
}
my $currentWinId = $tabWin{$tId}{tab}[$tabWin{$tId}{currentTab}]{winId};
my $mover = $tabWin{$tId}{tab}[$tabNo];
if ($insertionPoint > $tabNo)
{
for (my $i = $s; $i < $e - 1; $i++)
{
$tabWin{$tId}{tab}[$i] = $tabWin{$tId}{tab}[$i+1];
}
$tabWin{$tId}{tab}[$e-1] = $mover;
}
else
{
for (my $i = $e; $i > $s; $i--)
{
$tabWin{$tId}{tab}[$i] = $tabWin{$tId}{tab}[$i-1];
}
$tabWin{$tId}{tab}[$insertionPoint] = $mover;
}
$tabWin{$tId}{currentTab} = getTabNo($tId, $currentWinId);
for (my $i = $s; $i < $tabWin{$tId}{nTabs}; $i++)
{
my $bExpand = 1;
if ($i == $tabWin{$tId}{currentTab})
{
my $bEq = isTrue($pConfig->{fixedSizeTabs});
$bExpand = (isTrue($pConfig->{fixedSizeTabs}) ||
($tabWin{$tId}{nTabs} == 1) ? 1 : 0);
}
$tabWin{$tId}{tab}[$i]{button}->pack(-expand => $bExpand, -fill => 'x', -side => 'left');
}
}
sub reorderTabClick ($$$)
{
my ($pa, $winId, $notUsed) = @_;
my ($tId, $insertionPoint) = @$pa;
reorderTab($tId, getTabNo($tId, $insertionPoint), getTabNo($tId, $winId));
}
sub swapRight ($)
{
my ($tId) = @_;
return if ($tabWin{$tId}{nTabs} <= 0);
if ($tabWin{$tId}{currentTab} == $tabWin{$tId}{nTabs} - 2)
{
reorderTab($tId, $tabWin{$tId}{currentTab}, $tabWin{$tId}{currentTab}+1);
}
else
{
my $inc = ($tabWin{$tId}{currentTab} == ($tabWin{$tId}{nTabs} - 1) ? 1 : 2);
reorderTab($tId,
($tabWin{$tId}{currentTab} + $inc) % $tabWin{$tId}{nTabs},
$tabWin{$tId}{currentTab});
}
}
sub swapLeft ($)
{
my ($tId) = @_;
return if ($tabWin{$tId}{nTabs} <= 0);
my $b = ($tabWin{$tId}{currentTab} == 0 ? 1 : 0);
reorderTab($tId,
($tabWin{$tId}{currentTab} - 1) % $tabWin{$tId}{nTabs},
$tabWin{$tId}{currentTab});
swapRight($tId) if ($b);
}
sub saveState ()
{
my $file = $pConfig->{stateFile};
if (!open(OUT, ">$file"))
{
print(STDERR $fvwm->name() . ": Couldn't save state to $file.\n");
return;
}
chmod(0600, $file);
print(STDERR $fvwm->name() . ": Saving state to $file\n");
foreach my $tId (sort(keys(%tabWin)))
{
print(OUT "createNewTabber($tId);\n");
my ($x, $y) = getXYPos(getWrapperWinId($tId));
print(OUT "\$fvwm->send(\"Move ${x}p ${y}p\", \$tabWin{$tId}{selfId});\n");
my $desk = $winTracker->data($tabWin{$tId}{selfId})->{desk};
print(OUT "\$fvwm->send(\"MoveToDesk 0 $desk\", \$tabWin{$tId}{selfId});\n");
for (my $tabNo = 0; $tabNo < $tabWin{$tId}{nTabs}; $tabNo++)
{
print(OUT "addTab($tId, $tabWin{$tId}{tab}[$tabNo]{winId});\n");
}
if ($tabWin{$tId}{nTabs} > 0)
{
print(OUT "showTab($tId, $tabWin{$tId}{currentTab});\n");
}
if ($tabWin{$tId}{toplevel}->state() eq 'iconic')
{
print(OUT "\$fvwm->send(\"Iconify\", \$tabWin{$tId}{selfId});\n");
}
}
close(OUT);
}
sub loadState ()
{
my $file = $pConfig->{stateFile};
if (-r $file)
{
print(STDERR $fvwm->name() . ": Reading state from $file\n");
eval `cat $file`;
print(STDERR $fvwm->name() . ": Error parsing $file\n") if ($@);
unlink($file);
}
}
# ======= ======= =======
sub createThingy ($$$;@)
{
my ($w, $label, $var, @po) = @_;
my $f = $w->Frame()->pack(@po, -expand => 0, -fill => 'x');
$f->Label(-text => $label)->pack(-side => 'left', -anchor => 'w');
my $lb = $f->BrowseEntry(-listwidth => 20,
-state => 'readonly',
-variable => $var,
-width => 3);
$lb->Subwidget('entry')->Subwidget('entry')->configure(-bg => 'white');
$lb->Subwidget('slistbox')->configure(-bg => 'white', -height => 4);
$lb->Subwidget('choices')->configure(-bg => 'yellow');
return $lb;
}
sub createSwallowDialog ($)
{
my ($main) = @_;
$global{tabberId} = $global{deskNo} = $global{pageNo} = 'Any';
my @pad = qw/-padx 3 -pady 3/;
my $name = $fvwm->name . ": Tabizer";
my $tl = $main->Toplevel(-title => $name);
my $top = $tl->Frame()->pack(-expand => 1, -fill => 'both', @pad);
$tl->iconname($name);
$tl->protocol('WM_DELETE_WINDOW', [$tl, 'destroy']);
$global{eTabberId} = createThingy($top,
'Add to Tab-Manager no.:',
\$global{tabberId},
@pad);
$global{eTabberId}->pack(-side => 'left', -anchor => 'w', @pad);
my $lf = $top->LabFrame(-label => 'Swallow Matching Windows',
-labelside => 'acrosstop');
$lf->pack(-expand => 0, -fill => 'both', @pad);
$global{regex} = $lf->LabEntry(-label => 'regex:',
-bg => 'white',
-labelPack => [qw/-side left/]);
$global{regex}->pack(-fill => 'x', @pad);
$global{regexType} = 'Name';
my $f = $lf->Frame()->pack();
foreach ('Name', 'Class', 'Resource')
{
$f->Radiobutton(-text => $_,
-value => $_,
-variable => \$global{regexType})->pack(-side => 'left');
}
$f = $lf->Frame()->pack(-expand => 0, -fill => 'x');
$global{eDeskNo} = createThingy($f, 'Desk:',
\$global{deskNo},
-side => 'left', @pad)->pack(@pad);
$global{ePageNo} = createThingy($f, 'Page:',
\$global{pageNo},
-side => 'left', @pad)->pack(@pad);
$lf->Button(-text => 'Add Matching',
-activeforeground => 'LimeGreen',
-command => \&doRegexSwallow)->pack(-fill => 'x', @pad);
$lf = $top->LabFrame(-label => 'Swallow Individual Windows',
-labelside => 'acrosstop');
$lf->pack(-expand => 1, -fill => 'both', @pad);
$global{list} = $lf->Scrolled('Listbox',
-width => 40,
-height => 10,
-setgrid => 1,
-bg => 'white',
-selectmode => 'extended',
-selectbackground => 'darkblue',
-selectforeground => 'white',
-scrollbars => 'osoe');
$global{list}->pack(-expand => 1, -fill => 'both', -padx => 2, -pady => 2);
my @po = qw/-side left -expand 1 -fill x/;
$lf->Button(-text => 'Add Selected',
-activeforeground => 'LimeGreen',
-command => \&doSwallow)->pack(@po, @pad);
$lf->Button(-text => 'Refresh List',
-activeforeground => 'orange',
-command => \&fillDialog)->pack(@po, @pad);
$top->Button(-text => 'Close',
-activeforeground => 'red',
-command => [$tl, 'destroy'])->pack(@po, @pad);
# $tl->resizable(0, 0);
}
sub doSwallow ()
{
my $tId = $global{tabberId};
$tId = emptiestTabber() if ($global{tabberId} eq 'Any');
return if (!defined $tId);
foreach ($global{list}->curselection())
{
addTab($tId, $global{windows}[$_]);
}
fillDialog();
}
sub doRegexSwallow ()
{
my $regex = $global{regex}->get();
if ($regex ne '')
{
$regex =~ s|/|\/|g;
my $deskNo = $global{deskNo};
my $pageNo = $global{pageNo};
my %p = (lc($global{regexType}) => $regex, type => $global{tabberId});
my @a;
push(@a, \%p);
foreach my $window ($winTracker->windows)
{
next if ($deskNo ne 'Any' &&
$window->{desk} != $deskNo);
next if ($pageNo ne 'Any' &&
$window->{page_nx} != $pageNo);
swallowCheck($window->{win_id}, \@a);
}
}
fillDialog();
}
sub tabizeWindows ($)
{
my ($tId) = @_;
createSwallowDialog($TOP);
$global{eTabberId}->configure(-choices => ['Any', sort(keys(%tabWin))]);
$global{tabberId} = $tId;
fillDialog();
}
sub max ($$) { return ($_[0] > $_[1] ? $_[0] : $_[1]); }
sub min ($$) { return ($_[0] < $_[1] ? $_[0] : $_[1]); }
sub fillDialog ()
{
$global{pageWidth} = $winTracker->pageInfo('vp_width');
$global{pageHeight} = $winTracker->pageInfo('vp_height');
$global{nPagesPerDesk} = $winTracker->pageInfo('desk_pages_x');
$global{maxDesk} = 3; # TODO
my $nPages = $global{nPagesPerDesk} - 1;
$global{eDeskNo}->configure(-choices => ['Any', 0 .. $global{maxDesk}]);
$global{ePageNo}->configure(-choices => ['Any', 0 .. $nPages]);
$global{list}->delete('0.0', 'end');
@{$global{windows}} = ();
foreach my $window ($winTracker->windows)
{
$global{list}->insert('end', $window->{name});
push(@{$global{windows}}, $window->{win_id});
}
}
# ======= ======= =======
sub setResizing ($$)
{
my ($tId, $winId) = @_;
my @s = $xServer->GetProperty($winId,
$xServer->atom('WM_NORMAL_HINTS'),
'AnyPropertyType', 0, 200, 0);
my ($flags) = unpack("L", $s[0]);
my ($wInc, $hInc, $baseW, $baseH) = (1, 1, 0, 0);
if ($flags & (1 << 6)) # PResizeInc
{
# $wInc & $hInc specify multiples of pixels to increment by.
($wInc, $hInc) = unpack("II", substr($s[0], 36, 8));
# $baseW & $baseH are number of pixels in window that aren't
# part of the resizable area. But note that this is completely
# different to the $baseW & $baseH we need to input to wmGrid().
($baseW, $baseH) = unpack("II", substr($s[0], 60, 8));
}
my %g = $xServer->GetGeometry($winId);
# $baseW & $baseH are number of grid units of *resizable* part of window.
$baseW = ($g{width} - $baseW) / $wInc;
$baseH = ($g{height} - $baseH) / $hInc;
# In pre-804.025 versions of Tk there is a bug in wmGrid().
# $tabWin{$tId}{toplevel}->wmGrid($baseW, $baseH, $wInc, $hInc);
$tabWin{$tId}{toplevel}->wm('grid', $baseW, $baseH, $wInc, $hInc);
}
sub enableDND ($)
{
foreach my $tId (keys(%tabWin))
{
$tabWin{$tId}{enableSwallowDND} = $_[0];
}
}
sub resizeAll ($$$)
{
my ($tId, $w, $h) = @_;
for (my $tabNo = 0; $tabNo < $tabWin{$tId}{nTabs}; $tabNo++)
{
$xServer->ConfigureWindow($tabWin{$tId}{tab}[$tabNo]{winId},
"height" => $h,
"width" => $w);
}
}
sub doResize ($)
{
my ($tId) = @_;
return if (!defined $tabWin{$tId}{currentTab});
my $winId = $tabWin{$tId}{tab}[$tabWin{$tId}{currentTab}]{winId};
my %g = $xServer->GetGeometry($winId);
resizeAll($tId, $g{width}, $g{height});
}
sub toggleAutoResize ($)
{
my ($tId) = @_;
return if (!$tabWin{$tId}{autoResize});
doResize($tId);
}
loadState();
print($fvwm->name() . " started with: Perl " . join('.', map(ord($_), split(//, $^V))) . ", X11::Protocol " . $X11::Protocol::VERSION . ", Tk $Tk::VERSION.\n");
$fvwm->send("NOP FINISHED STARTUP");
$fvwm->event_loop();