Permalink
Fetching contributors…
Cannot retrieve contributors at this time
5993 lines (5662 sloc) 198 KB
# Copyright (C) 2005-2015 Quentin Sculo <squentin@free.fr>
#
# This file is part of Gmusicbrowser.
# Gmusicbrowser is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3, as
# published by the Free Software Foundation.
use strict;
use warnings;
package Layout;
use constant
{
TRUE => 1,
FALSE => 0,
SIZE_BUTTONS => 'large-toolbar',
SIZE_FLAGS => 'menu',
};
our @MenuQueue=
( {label => _"Queue album", code => sub { ::EnqueueSame('album',$_[0]{ID}); }, istrue=>'ID', },
{label => _"Queue artist", code => sub { ::EnqueueSame('artist',$_[0]{ID});}, istrue=>'ID', }, # or use field 'artists' or 'first_artist' ?
{ include => sub
{ my $menu=$_[1];
my @modes= map { $_=>$::QActions{$_}{long} } ::List_QueueActions(0);
::BuildChoiceMenu( \@modes, menu=>$menu, ordered_hash=>1, 'reverse'=>1,
check=> sub {$::QueueAction}, code=> sub { ::EnqueueAction($_[1]); }, );
},
},
{label => _"Clear queue", code => \&::ClearQueue, test => sub{@$::Queue}},
{label => _"Shuffle queue", code => sub {$::Queue->Shuffle}, test => sub{@$::Queue}},
{label => _"Auto fill up to", code => sub { $::Options{MaxAutoFill}=$_[1]; ::HasChanged('QueueAction','maxautofill'); },
submenu => sub { my $m= ::max(1,$::Options{MaxAutoFill}-5); return [$m..$m+10]; },
check => sub {$::Options{MaxAutoFill};},
},
{label => _"Edit...", code => \&::EditQueue, test => sub { !$_[0]{mode} || $_[0]{mode} ne 'Q' }, },
{ separator=>1},
{ include => sub
{ my $menu=$_[1];
my @modes= map { $_=>$::QActions{$_}{long_next} } grep $_ ne '', ::List_QueueActions(1);
::BuildChoiceMenu( \@modes, menu=>$menu, ordered_hash=>1, 'reverse'=>1, radio_as_checks=>1,
check=> sub {$::NextAction},
code=> sub { my $m=$_[1]; $m='' if $m eq $::NextAction; ::SetNextAction($m); }, );
},
},
);
our @MainMenu=
( {label => _"Add files or folders",code => sub {::ChooseAddPath(0,1)}, stockicon => 'gtk-add' },
{label => _"Settings", code => 'OpenPref', stockicon => 'gtk-preferences' },
{label => _"Open Browser", code => \&::OpenBrowser,stockicon => 'gmb-playlist' },
{label => _"Open Context window",code => \&::ContextWindow, stockicon => 'gtk-info'},
{label => _"Switch to fullscreen mode",code => \&::ToggleFullscreenLayout, stockicon => 'gtk-fullscreen'},
{label => _"About", code => \&::AboutDialog,stockicon => 'gtk-about' },
{label => _"Quit", code => \&::Quit, stockicon => 'gtk-quit' },
);
our %Widgets=
( Prev =>
{ class => 'Layout::Button',
#size => SIZE_BUTTONS,
stock => 'gtk-media-previous',
tip => _"Recently played songs",
text => _"Previous",
group => 'Recent',
activate=> \&::PrevSong,
options => 'nbsongs',
nbsongs => 10,
click3 => sub { ::ChooseSongs([::GetPrevSongs($_[0]{nbsongs})]); },
},
Stop =>
{ class => 'Layout::Button',
stock => 'gtk-media-stop',
tip => _"Stop",
activate=> \&::Stop,
click2 => 'EnqueueAction(stop)',
click3 => 'SetNextAction(stop)',
},
Play =>
{ class => 'Layout::Button',
state => sub {$::TogPlay? 'pause' : 'play'},
stock => {pause => 'gtk-media-pause', play => 'gtk-media-play' },
tip => sub {$::TogPlay? _"Pause" : _"Play"},
activate=> \&::PlayPause,
click3 => 'Stop',
event => 'Playing',
},
Next =>
{ class => 'Layout::Button',
stock => 'gtk-media-next',
tip => _"Next Song",
text => _"Next",
group => 'Next',
activate=> \&::NextSong,
options => 'nbsongs',
nbsongs => 10,
click3 => sub { ::ChooseSongs([::GetNextSongs($_[0]{nbsongs})]); },
},
OpenBrowser =>
{ class => 'Layout::Button',
oldopt1 => 'toggle',
options => 'toggle',
stock => 'gmb-playlist',
tip => _"Open Browser window",
activate=> sub { ::OpenSpecialWindow('Browser',$_[0]{toggle}); },
click3 => sub { ::OpenSpecialWindow('Browser'); },
},
OpenContext =>
{ class => 'Layout::Button',
oldopt1 => 'toggle',
options => 'toggle',
stock => 'gtk-info',
tip => _"Open Context window",
activate=> sub { ::OpenSpecialWindow('Context',$_[0]{toggle}); },
click3 => sub { ::OpenSpecialWindow('Context'); },
},
OpenQueue =>
{ class => 'Layout::Button',
stock => 'gmb-queue-window',
tip => _"Open Queue window",
options => 'toggle',
activate=> sub { ::OpenSpecialWindow('Queue',$_[0]{toggle}); },
},
Pref =>
{ class => 'Layout::Button',
stock => 'gtk-preferences',
tip => _"Edit Settings",
text => _"Settings",
activate=> 'OpenPref',
click3 => sub {Layout::Window->new($::Options{Layout});}, #mostly for debugging purpose
click2 => \&::AboutDialog,
},
Quit =>
{ class => 'Layout::Button',
stock => 'gtk-quit',
tip => _"Quit",
activate=> \&::Quit,
click2 => 'EnqueueAction(quit)',
click3 => 'SetNextAction(quit)',
},
Lock =>
{ class => 'Layout::Button',
button => 0,
size => SIZE_FLAGS,
options => 'field',
field => 'fullfilename', #default field to make sure it's defined
state => sub { ($::TogLock && $::TogLock eq $_[0]{field})? 'on' : 'off' },
stock => { on => 'gmb-lock', off => '. gmb-locklight' },
tip => sub { ::__x(_"Lock on {field}", field=> Songs::FieldName($_[0]{field})) },
click1 => sub {::ToggleLock($_[0]{field});},
event => 'Lock',
},
LockSong =>
{ parent => 'Lock',
field => 'fullfilename',
tip => _"Lock on song",
},
LockArtist =>
{ parent => 'Lock',
field => 'first_artist',
tip => _"Lock on Artist",
click2 => 'EnqueueArtist',
},
LockAlbum =>
{ parent => 'Lock',
field => 'album',
tip => _"Lock on Album",
click2 => 'EnqueueAlbum',
},
Sort =>
{ class => 'Layout::Button',
button => 0,
size => SIZE_FLAGS,
state => sub { my $s=$::Options{'Sort'};($s=~m/^random:/)? 'random' : ($s eq 'shuffle')? 'shuffle' : 'sorted'; },
stock => { random => 'gmb-random', shuffle => 'gmb-shuffle', sorted => 'gtk-sort-ascending' },
tip => sub { _("Play order") ." :\n". ::ExplainSort($::Options{Sort}); },
text => sub { ::ExplainSort($::Options{Sort},1); },
click1 => 'MenuPlayOrder',
click3 => 'ToggleRandom',
event => 'Sort SavedWRandoms SavedSorts',
},
Filter =>
{ class => 'Layout::Button',
button => 0,
size => SIZE_FLAGS,
state => sub { defined $::ListMode ? 'list'
: $::SelectedFilter->is_empty ? 'library' : 'filter'; },
stock => { list => 'gmb-list', library => 'gmb-library', filter => 'gmb-filter' },
tip => sub
{ defined $::ListMode ? _"static list"
: _("Playlist filter :\n").$::SelectedFilter->explain;
},
text => sub { $::ListMode ? _"static list" : $::SelectedFilter->name; },
click1 => 'MenuPlayFilter',
click3 => 'ClearPlayFilter',
event => 'Filter SavedFilters',
},
Queue =>
{ class => 'Layout::Button',
button => 0,
size => SIZE_FLAGS,
state => sub { $::NextAction? $::NextAction :
@$::Queue? 'queue' :
$::QueueAction? $::QueueAction :
'noqueue'
},
stock => sub {$_[0] eq 'queue' ? 'gmb-queue' :
$_[0] eq 'noqueue'? '. gmb-queue' :
$::QActions{$_[0]}{icon} ;
},
tip => sub { if ($::NextAction) { return $::QActions{$::NextAction}{long_next} }
::CalcListLength($::Queue,'queue')
.($::QueueAction? "\n". ::__x( _"then {action}", action => $::QActions{$::QueueAction}{short} ) : '');
},
text => _"Queue",
click1 => 'MenuQueue',
click3 => sub { ::EnqueueAction(''); ::SetNextAction(''); ::ClearQueue(); }, #FIXME replace with 3 gmb commands once new command system is done
event => 'Queue QueueAction',
dragdest=> [::DRAG_ID,sub {shift;shift;::Enqueue(@_);}],
},
VolumeIcon =>
{ class => 'Layout::Button',
button => 0,
state => sub { ::GetMute() ? 'm' : ::GetVol() },
stock => sub { 'gmb-vol'.( $_[0] eq 'm' ? 'm' : int(($_[0]-1)/100*$::NBVolIcons) ); },
tip => sub { _("Volume : ").::GetVol().'%' },
click1 => sub { ::PopupLayout('Volume',$_[0]); },
click3 => sub { ::ChangeVol('mute') },
event => 'Vol',
},
Button =>
{ class => 'Layout::Button',
},
EventBox =>
{ class => 'Layout::Button',
button => 0,
},
Text =>
{ class => 'Layout::Label',
oldopt1 => sub { 'text',$_[0] },
group => 'Play',
},
Pos =>
{ class => 'Layout::Label',
group => 'Play',
initsize=> ::__n("%d song in queue","%d songs in queue",99999), #longest string that will be displayed
click1 => sub { ::ChooseSongs([::GetNeighbourSongs(5)]) unless $::RandomMode || @$::Queue; },
update => sub { my $t=(@$::ListPlay==0) ? '':
@$::Queue ? ::__n("%d song in queue","%d songs in queue", scalar @$::Queue):
!defined $::Position ? ::__n("%d song","%d songs",scalar @$::ListPlay):
($::Position+1).'/'.@$::ListPlay;
$_[0]->set_markup_with_format( '<small>%s</small>', $t );
},
event => 'Pos Queue Filter',
},
Title =>
{ class => 'Layout::Label',
group => 'Play',
minsize => 20,
markup => '<b><big>%S</big></b>%V',
markup_empty => '<b><big>&lt;'._("Playlist Empty").'&gt;</big></b>',
click1 => \&PopupSongsFromAlbum,
click3 => sub { my $ID=::GetSelID($_[0]); ::PopupContextMenu(\@::SongCMenu,{mode=> 'P', self=> $_[0], IDs => [$ID]}) if defined $ID;},
dragsrc => [::DRAG_ID,\&DragCurrentSong],
dragdest=> [::DRAG_ID,sub {::Select(song => $_[2]);}],
cursor => 'hand2',
},
Title_by =>
{ class => 'Layout::Label',
parent => 'Title',
markup => ::__x(_"{song} by {artist}",song => "<b><big>%S</big></b>%V", artist => "<b>%a</b>"),
},
Artist =>
{ class => 'Layout::Label',
group => 'Play',
minsize => 20,
markup => '<b>%a</b>',
click1 => sub { ::PopupAA('artists'); },
click3 => sub { my $ID=::GetSelID($_[0]); ::ArtistContextMenu( Songs::Get_gid($ID,'artists'),{self =>$_[0], ID=>$ID, mode => 'P'}) if defined $ID; },
dragsrc => [::DRAG_ARTIST,\&DragCurrentArtist],
cursor => 'hand2',
},
Album =>
{ class => 'Layout::Label',
group => 'Play',
minsize => 20,
markup => '<b>%l</b>',
click1 => sub { my $ID=::GetSelID($_[0]); ::PopupAA( 'album', from=> Songs::Get_gid($ID,'artists')) if defined $ID; },
click3 => sub { my $ID=::GetSelID($_[0]); ::PopupAAContextMenu({self =>$_[0], field=>'album', ID=>$ID, gid=>Songs::Get_gid($ID,'album'), mode => 'P'}) if defined $ID; },
dragsrc => [::DRAG_ALBUM,\&DragCurrentAlbum],
cursor => 'hand2',
},
Year =>
{ class => 'Layout::Label',
group => 'Play',
markup => ' %y',
markup_empty=> '',
},
Comment =>
{ class => 'Layout::Label',
group => 'Play',
markup => '%C',
},
Length =>
{ class => 'Layout::Label',
group => 'Play',
initsize=> ::__x( _" of {length}", 'length' => "XX:XX"),
markup => ::__x( _" of {length}", 'length' => "%m" ),
markup_empty=> ::__x( _" of {length}", 'length' => "0:00" ),
# font => 'Monospace',
},
PlayingTime =>
{ class => 'Layout::Label::Time',
group => 'Play',
markup => '%s',
xalign => 1,
options => 'remaining markup_stopped',
saveoptions => 'remaining',
markup_stopped=> '--:--',
initsize=> '-XX:XX',
# font => 'Monospace',
event => 'Time',
click1 => sub { $_[0]{remaining}=!$_[0]{remaining}; $_[0]->update_time; },
update => sub { $_[0]->update_time unless $_[0]{busy}; },
},
Time =>
{ parent => 'PlayingTime',
xalign => .5,
markup => '%s' . ::__x( _" of {length}", 'length' => "%m" ),
markup_empty=> '%s' . ::__x( _" of {length}", 'length' => "0:00" ),
initsize=> '-XX:XX' . ::__x( _" of {length}", 'length' => "XX:XX"),
},
TimeBar =>
{ class => 'Layout::Bar',
group => 'Play',
event => 'Time',
update => sub { $_[0]->set_val($::PlayTime); },
fields => 'length',
schange => sub { $_[0]->set_max( defined $_[1] ? Songs::Get($_[1],'length') : 0); },
set => sub { ::SkipTo($_[1]) },
scroll => sub { $_[1] ? ::Forward(undef,10) : ::Rewind (undef,10) },
set_preview => \&Layout::Bar::update_preview_Time,
cursor => 'hand2',
text_empty=> '',
},
TimeSlider =>
{ class => 'Layout::Bar::Scale',
parent => 'TimeBar',
cursor => undef,
},
VolumeBar =>
{ class => 'Layout::Bar',
orientation => 'left-to-right',
event => 'Vol',
update => sub { $_[0]->set_val( ::GetVol() ); },
set => sub { ::UpdateVol($_[1]) },
scroll => sub { ::ChangeVol($_[1] ? 'up' : 'down') },
max => 100,
cursor => 'hand2',
},
VolumeSlider =>
{ class => 'Layout::Bar::Scale',
orientation => 'bottom-to-top',
parent => 'VolumeBar',
cursor => undef,
},
Volume =>
{ class => 'Layout::Label',
initsize=> '000',
event => 'Vol',
update => sub { $_[0]->set_label(sprintf("%d",::GetVol())); },
},
Stars =>
{ New => \&Stars::new_layout_widget,
group => 'Play',
field => 'rating',
event => 'Icons',
schange => \&Stars::update_layout_widget,
update => sub { $_[0]->update_layout_widget( ::GetSelID($_[0]) ); },
cursor => 'hand2',
},
Cover =>
{ class => 'Layout::AAPicture',
group => 'Play',
aa => 'album',
oldopt1 => 'maxsize',
schange => sub { my $key=(defined $_[1])? Songs::Get_gid($_[1],'album') : undef ; $_[0]->set($key); },
click1 => \&PopupSongsFromAlbum,
click3 => sub { my $ID=::GetSelID($_[0]); ::PopupAAContextMenu({self =>$_[0], field=>'album', ID=>$ID, gid=>Songs::Get_gid($ID,'album'), mode => 'P'}) if defined $ID; },
event => 'Picture_album',
update => \&Layout::AAPicture::Changed,
noinit => 1,
dragsrc => [::DRAG_ALBUM,\&DragCurrentAlbum],
fields => 'album',
},
ArtistPic =>
{ class => 'Layout::AAPicture',
group => 'Play',
aa => 'artist',
oldopt1 => 'maxsize',
schange => sub { my $key=(defined $_[1])? Songs::Get_gid($_[1],'artists') : undef ;$_[0]->set($key); },
click1 => sub { ::PopupAA('artist'); },
event => 'Picture_artist',
update => \&Layout::AAPicture::Changed,
noinit => 1,
dragsrc => [::DRAG_ARTIST,\&DragCurrentArtist],
fields => 'artist',
},
LabelsIcons =>
{ New => sub { Gtk2::Table->new(1,1); },
group => 'Play',
field => 'label',
options => 'field',
schange => \&UpdateLabelsIcon,
update => \&UpdateLabelsIcon,
event => 'Icons',
tip => '%L',
},
Filler =>
{ New => sub { Gtk2::HBox->new; },
},
QueueList =>
{ New => sub { $_[0]{type}='Q'; SongList::Common->new($_[0]); },
tabtitle=> _"Queue",
tabicon => 'gmb-queue',
issonglist=>1,
},
PlayList =>
{ New => sub { $_[0]{type}='A'; SongList::Common->new($_[0]); },
tabtitle=> _"Playlist",
tabicon => 'gtk-media-play',
issonglist=>1,
},
SongList =>
{ New => sub { SongList->new($_[0]); },
oldopt1 => 'mode',
issonglist=>1,
},
SongTree =>
{ New => sub { SongTree->new($_[0]); },
issonglist=>1,
},
EditList =>
{ New => sub { $_[0]{type}='L'; SongList::Common->new($_[0]); },
tabtitle=> \&SongList::Common::MakeTitleLabel,
tabrename=>\&SongList::Common::RenameTitleLabel,
tabicon => 'gmb-list',
issonglist=>1,
},
TabbedLists =>
{ class => 'Layout::NoteBook',
EndInit => \&Layout::NoteBook::EndInit,
default_child => 'PlayList',
#these options will be passed to songlist/songtree children :
options_for_list => 'songlist songtree sort songxpad songypad no_typeahead cols grouping',
},
Context =>
{ class => 'Layout::NoteBook',
EndInit => \&Layout::NoteBook::EndInit,
group => 'Play',
typesubmenu=> 'C',
match => 'context page',
#these options will be passed to context children :
options_for_context => 'group',
},
SongInfo=>
{ class => 'Layout::SongInfo',
group => 'Play',
expander=> 1,
hide_empty => 1,
tabicon => 'gtk-info',
tabtitle=> _"Song informations",
},
PictureBrowser=>
{ class => 'Layout::PictureBrowser',
group => 'Play',
field => 'album',
options => 'field',
xalign => .5,
yalign => .5,
follow => 1,
scroll_zoom => 1,
show_list => 0,
show_folders => 1,
show_toolbar => 0,
pdf_mode => 1,
embedded_mode=>0,
hpos => 140,
vpos => 80,
reset_zoom_on=>'folder', #can be group, folder, file or never
nowrap => 0,
schange => \&Layout::PictureBrowser::queue_song_changed,
autoadd_type => 'context page pictures',
tabicon => 'gmb-picture',
tabtitle => _"Album pictures",
},
AABox =>
{ class => 'GMB::AABox',
oldopt1 => sub { 'aa='.( $_[0] ? 'artist' : 'album' ) },
},
ArtistBox =>
{ class => 'GMB::AABox',
aa => 'artists',
},
AlbumBox =>
{ class => 'GMB::AABox',
aa => 'album',
},
FilterPane =>
{ class => 'FilterPane',
oldopt1 => sub
{ my ($nb,$hide,@pages)=split ',',$_[0];
return (nb => ++$nb,hide => $hide,pages=>join('|',@pages));
},
},
Total =>
{ class => 'LabelTotal',
oldopt1 => 'mode',
saveoptions=> 'mode',
},
FilterBox =>
{ New => \&Browser::makeFilterBox,
dragdest => [::DRAG_FILTER,sub { ::SetFilter($_[0],$_[2]);}],
},
FilterLock=> { New => \&Browser::makeLockToggle,
relief => 'none',
},
HistItem => { New => \&Layout::MenuItem::new,
text => _"Recent Filters",
updatemenu => \&Browser::fill_history_menu,
},
PlayItem => { New => \&Layout::MenuItem::new,
text => _"Playing",
updatemenu => sub { my $sl=::GetSonglist($_[0]); unless ($sl) {warn "Error : no associated songlist with $_[0]{name}\n"; return} ::BuildMenu(\@Browser::MenuPlaying, { self => $_[0], songlist => $sl }, $_[0]->get_submenu); },
},
LSortItem => { New => \&Layout::MenuItem::new,
text => _"Sort",
updatemenu => \&Browser::make_sort_menu,
},
PSortItem => { New => \&Layout::MenuItem::new,
text => _"Play order",
updatemenu => sub { SortMenu($_[0]->get_submenu); },
},
PFilterItem => { New => \&Layout::MenuItem::new,
text => _"Playlist filter",
updatemenu => sub { FilterMenu($_[0]->get_submenu); },
},
QueueItem => { New => \&Layout::MenuItem::new,
text => _"Queue",
updatemenu => sub{ ::BuildMenu(\@MenuQueue,{ID=>$::SongID}, $_[0]->get_submenu); },
},
LayoutItem => { New => \&Layout::MenuItem::new,
text => _"Layout",
updatemenu => sub{ ::BuildChoiceMenu( Layout::get_layout_list(qr/G.*\+/),
tree=>1,
check=> sub {$::Options{Layout}},
code => sub { $::Options{Layout}=$_[1]; ::IdleDo('2_ChangeLayout',500, \&::CreateMainWindow ); },
menu => $_[0]->get_submenu, # re-use menu
);
},
},
MainMenuItem => { New => \&Layout::MenuItem::new,
text => _"Main",
updatemenu => sub{ ::BuildMenu(\@MainMenu,undef, $_[0]->get_submenu); },
},
MenuItem => { New => \&Layout::MenuItem::new,
},
SeparatorMenuItem=>
{ New => sub { Gtk2::SeparatorMenuItem->new },
},
Refresh =>
{ class => 'Layout::Button',
size => 'menu',
stock => 'gtk-refresh',
tip => _"Refresh list",
activate=> sub { ::RefreshFilters($_[0]); },
},
PlayFilter =>
{ class => 'Layout::Button',
size => 'menu',
stock => 'gtk-media-play',
tip => _"Play filter",
activate=> sub { ::Select( filter => ::GetFilter($_[0]), song=> 'trykeep', play =>1 ); },
click2 => sub { ::EnqueueFilter( ::GetFilter($_[0]) ); },
},
QueueFilter =>
{ class => 'Layout::Button',
size => 'menu',
stock => 'gmb-queue',
tip => _"Enqueue filter",
activate=> sub { ::EnqueueFilter( ::GetFilter($_[0]) ); },
},
ResetFilter =>
{ class => 'Layout::Button',
size => 'menu',
stock => 'gtk-clear',
tip => _"Reset filter",
activate=> sub { ::SetFilter($_[0],undef); },
},
ToggleButton =>
{ class => 'Layout::TogButton',
size => 'menu',
},
HSeparator =>
{ New => sub {Gtk2::HSeparator->new},
},
VSeparator =>
{ New => sub {Gtk2::VSeparator->new},
},
Choose =>
{ class => 'Layout::Button',
stock => 'gtk-add',
tip => _"Choose Artist/Album/Song",
activate=> sub { Layout::Window->new('Search'); },
},
ChooseRandAlbum =>
{ class => 'Layout::Button',
stock => 'gmb-random-album',
tip => _"Choose Random Album",
options => 'action',
activate=> sub { my $al=AA::GetAAList('album'); my $r=int rand(@$al); my $key=$al->[$r]; my $list=AA::GetIDs('album',$key); if (my $ac=$_[0]{action}) { ::DoActionForList($ac,$list); } else { my $ID=::FindFirstInListPlay($list); ::Select( song => $ID)}; },
click3 => sub { my @list; my $al=AA::GetAAList('album'); my $nb=5; while ($nb--) { my $r=int rand(@$al); push @list, splice(@$al,$r,1); last unless @$al; } ::PopupAA('album', list=>\@list, format=> ::__x( _"{album}\n<small>by</small> {artist}", album => "%a", artist => "%b")); },
},
AASearch =>
{ class => 'AASearch',
},
ArtistSearch =>
{ class => 'AASearch',
aa => 'artists',
},
AlbumSearch =>
{ class => 'AASearch',
aa => 'album',
},
SongSearch =>
{ class => 'SongSearch',
},
SimpleSearch =>
{ class => 'SimpleSearch',
dragdest=> [::DRAG_FILTER,sub { ::SetFilter($_[0],$_[2]);}],
},
Visuals =>
{ New => sub {my $darea=Gtk2::DrawingArea->new; return $darea unless $::Play_package->{visuals}; $::Play_package->add_visuals($darea); my $eb=Gtk2::EventBox->new; $eb->add($darea); return $eb},
click1 => sub {$::Play_package->set_visual('+') if $::Play_package->{visuals};}, #select next visual
click2 => \&ToggleFullscreen, #FIXME use a fullscreen layout instead,
click3 => \&VisualsMenu,
minheight=>50,
minwidth=>200,
},
Connections => #FIXME could be better
{ class => 'Layout::Label',
update => sub { unless ($::Play_package->can('get_connections')) { $_[0]->hide; $_[0]->set_no_show_all(1); return }; $_[0]->show; $_[0]->child->show_all; my @c= $::Play_package->get_connections; my $t= @c? _("Connections from :")."\n".join("\n",@c) : _("No connections"); $_[0]->child->set_text($t); },
event => 'connections',
},
ShuffleList =>
{ class => 'Layout::Button',
stock => 'gmb-shuffle',
size => SIZE_FLAGS,
tip => _"Shuffle list",
activate=> sub { my $songarray= ::GetSongArray($_[0]) || return; $songarray->Shuffle; },
event => 'SongArray',
update => \&SensitiveIfMoreOneSong,
PostInit=> \&SensitiveIfMoreOneSong,
},
EmptyList =>
{ class => 'Layout::Button',
stock => 'gtk-clear',
size => SIZE_FLAGS,
tip => _"Empty list",
activate=> sub { my $songarray= ::GetSongArray($_[0]) || return; $songarray->Replace(); },
event => 'SongArray',
update => \&SensitiveIfMoreZeroSong,
PostInit=> \&SensitiveIfMoreZeroSong,
},
EditListButtons =>
{ class => 'EditListButtons',
},
QueueActions =>
{ class => 'QueueActions',
},
Fullscreen =>
{ class => 'Layout::Button',
stock => 'gtk-fullscreen',
tip => _"Toggle fullscreen mode",
text => _"Fullscreen",
activate=> \&::ToggleFullscreenLayout,
click3 => \&ToggleFullscreen,
autoadd_type => 'button main',
autoadd_option => 'AddFullscreenButton',
},
Repeat =>
{ New => sub { my $w=Gtk2::CheckButton->new(_"Repeat"); $w->signal_connect(clicked => sub { ::SetRepeat($_[0]->get_active); }); return $w; },
event => 'Repeat Sort',
update => sub { if ($_[0]->get_active xor $::Options{Repeat}) { $_[0]->set_active($::Options{Repeat});} $_[0]->set_sensitive(!$::RandomMode); },
},
AddLabelEntry =>
{ New => \&AddLabelEntry,
group => 'Play',
},
LabelToggleButtons =>
{ class => 'Layout::LabelToggleButtons',
group => 'Play',
field => 'label',
},
PlayOrderCombo =>
{ New => \&PlayOrderComboNew,
event => 'Sort SavedWRandoms SavedSorts',
update => \&PlayOrderComboUpdate,
minwidth=> 100,
},
Progress =>
{ class => 'Layout::Progress',
compact=>1,
},
VProgress =>
{ class => 'Layout::Progress',
vertical=>1,
},
Equalizer =>
{ New => \&Layout::Equalizer::new,
event => 'Equalizer',
update => \&Layout::Equalizer::update,
preamp => 1,
labels => 'x-small',
},
EqualizerPresets =>
{ class => 'Layout::EqualizerPresets',
event => 'Equalizer',
update => \&Layout::EqualizerPresets::update,
onoff => 1,
},
EqualizerPresetsSimple =>
{ parent => 'EqualizerPresets',
open =>1,
notoggle=>1,
}
# RadioList =>
# { class => 'GMB::RadioList',
# },
);
# aliases for previous widget names
{ my %aliases=
( Playlist => 'OpenBrowser',
BContext => 'OpenContext',
Date => 'Year',
Label => 'Text',
Vol => 'VolumeIcon',
LabelVol => 'Volume',
FLock => 'FilterLock',
TogButton => 'ToggleButton',
ProgressV => 'VProgress',
FBox => 'FilterBox',
Scale => 'TimeSlider',
VolSlider => 'VolumeSlider',
VolBar => 'VolumeBar',
FPane => 'FilterPane',
LabelTime => 'PlayingTime',
#Pos => 'PlaylistPosition', 'Position', ?
#SimpleSearch => 'Search', ?
);
while ( my($alias,$real)= each %aliases )
{ $Widgets{$alias}||=$Widgets{$real};
}
}
our %Layouts;
sub get_layout_list
{ my $type=$_[0];
my @list=keys %Layouts;
@list=grep defined $Layouts{$_}{Type} && $Layouts{$_}{Type}=~m/$type/, @list if $type;
#return { map { $_ => _ ($Layouts{$_}{Name} || $_) } @list }; #use name instead of id if it exists, and translate
my %cat;
my @tree;
for my $id (@list)
{ my $name2=$id;
my $cat= $Layouts{$id}{Category};
my $name= $Layouts{$id}{Name} || _( $name2 );
my $array= $cat ? ($cat{$cat}||=[]) : \@tree;
push @$array, $id, $name;
}
push @tree, $cat{$_},$_ for keys %cat;
return \@tree;
}
sub get_layout_name
{ my $layout=shift;
my $def= $Layouts{$layout};
return sprintf(_"Unknown layout '%s'",$layout) unless $def;
my $name= $def->{Name} || _( $layout );
return $name;
}
sub InitLayouts
{ undef %Layouts;
my @files= ::FileList( qr/\.layout$|(?:$::QSLASH)layouts$/o, $::DATADIR.::SLASH.'layouts',
$::HomeDir.'layouts',
$::CmdLine{searchpath} );
ReadLayoutFile($_) for @files;
die "No layouts file found.\n" unless keys %Layouts;
if ($::CmdLine{layoutlist})
{ print "Available layouts : ((type) id\t: name)\n";
my ($max)= sort {$b<=>$a} map length, keys %Layouts;
for my $id (sort keys %Layouts)
{ my $name= get_layout_name($id);
my $type= $Layouts{$id}{Type} || '';
$type="($type)" if $type;
printf "%-4s %-${max}s : %s\n",$type,$id,$name;
}
exit;
}
::QHasChanged('Layouts');
}
sub ReadLayoutFile
{ my $file=shift;
return unless -f $file;
warn "Reading layouts in $file\n" if $::debug;
open my$fh,"<:utf8",$file or do { warn $!; return };
my $first;
my $linecount=0; my ($linefirst,$linenext);
while (1)
{ my ($next,$longline);
my @lines=($first);
while (local $_=<$fh>)
{ $linecount++;
s#^\s+##;
next if m/^#/;
s#\s*[\n\r]+$##;
if (s#\\$##) {$longline.=$_;next}
next if $_ eq '';
if ($longline) {$_=$longline.$_;undef $longline;}
if (m#^[{[]#) { $next=$_; $linenext=$linecount; last}
push @lines,$_;
}
if ($first)
{ if ($first=~m#^\[#) {ParseLayout(\@lines,$file,$linefirst)}
else {ParseSongTreeSkin(\@lines)}
}
$first=$next; $linefirst=$linenext;
last unless $first;
}
close $fh;
}
sub ParseLayout
{ my ($lines,$file,$line)=@_;
my $first=shift @$lines;
my $name;
if ($first=~m/^\[([^]=]+)\](?:\s*based on (.+))?$/)
{ if (defined $2 && !exists $Layouts{$2})
{ warn "Ignoring layout '$1' because it is based on unknown layout '$2'\n";
return;
}
$name=$1;
if (defined $2) { %{$Layouts{$name}}=%{$Layouts{$2}}; delete $Layouts{$name}{Name}; }
else { delete $Layouts{$name}; }
}
else {return}
my $currentkey;
for (@$lines)
{ s#_\"([^"]+)"#my $tr=_( $1 ); $tr=~y/"/'/; qq/"$tr"/#ge; #translation, escaping the " so it is not picked up as a translatable string. Replace any " in translations because they would cause trouble
unless (m/^(\w+)\s*=\s*(.*)$/) { $Layouts{$name}{$currentkey} .= ' '.$1 if m/\s*(.*)$/; next } #continuation of previous line if doesn't begin with "word="
$currentkey=$1;
if ($2 eq '') {delete $Layouts{$name}{$currentkey};next}
$Layouts{$name}{$currentkey}= $2;
}
for my $key (qw/Name Category Title/)
{ $Layouts{$name}{$key}=~s/^"(.*)"$/$1/ if $Layouts{$name}{$key}; #remove quotes from layout name and category
}
my $path=$file; $path=~s#([^/]+)$##; $file=$1;
$Layouts{$name}{PATH}=$path;
$Layouts{$name}{FILE}=$file;
$Layouts{$name}{LINE}=$line;
}
sub ParseSongTreeSkin
{ my $lines=$_[0];
my $first=shift @$lines;
my $ref;
my $name;
if ($first=~m#{(Column|Group) (.*)}#)
{ $ref= $1 eq 'Column' ? \%SongTree::STC : \%SongTree::GroupSkin;
$name=$2;
$ref=$ref->{$name}={};
}
else {return}
for (@$lines)
{ my ($key,$e,$string)= m#^(\w+)\s*([=:])\s*(.*)$#;
next unless defined $key;
if ($e eq '=')
{ if ($key eq 'elems' || $key eq 'options') { warn "Can't use reserved keyword $key in SongTreee column $name\n"; next }
$string= _( $1 ) if $string=~m/_\"([^"]+)"/; #translation, escaping the " so it is not picked up as a translatable string
$ref->{$key}=$string;
}
elsif ($string=~m#^Option(\w*)\((.+)\)$#)
{ my $type=$1;
my $opt=::ParseOptions($2);
$opt->{type}=$type;
$ref->{options}{$key}=$opt;
}
else { push @{$ref->{elems}}, $key.'='.$string; }
}
}
sub GetDefaultLayoutOptions
{ my $layout=$_[0];
my %default;
my $options= $Layout::Layouts{$layout}{Default} || '';
if ($options=~m/^\w+\(/) #new format (v1.1.2)
{ for my $nameopt (::ExtractNameAndOptions($options))
{ $default{$1}=$2 if $nameopt=~m/^(\w+)\((.+)\)$/;
}
}
else # old format (version <1.1.2)
{ #warn "Old options format not supported for layout $layout => ignored\n";
#$opt2={};
my @optlist=split /\s+/,$options;
unshift @optlist,'Window' if @optlist%2; #very old format (v<0.9573)
%default= @optlist;
}
$_=::ParseOptions($_) for values %default;
$default{DEFAULT_OPTIONS}=1;
$default{Window}{DEFAULT_OPTIONS}=1;
return \%default;
}
sub SaveWidgetOptions #Save options for this layout by collecting options of its widgets
{ my @widgets=@_;
my %states;
for my $widget (@widgets)
{ my $key=$widget->{name};
unless ($key) { warn "Error: no name for widget $widget\n"; next }
my $opt;
if (my $sub=$widget->{SaveOptions})
{ my @opt=$sub->($widget);
$opt= @opt>1 ? {@opt} : $opt[0];
}
if (my $keys=$widget->{options_to_save})
{ $opt->{$_}=$widget->{$_} for grep defined $widget->{$_}, split / /,$keys;
}
next unless $opt;
if (!ref $opt) { warn "invalid options returned from $key\n";next }
$opt=+{@$opt} if ref $opt eq 'ARRAY';
next unless keys %$opt;
$states{$key}=$opt;
}
if ($::debug)
{ warn "Saving widget options :' :\n";
for my $key (sort keys %states)
{ warn " $key:\n";
warn " $_ = $states{$key}{$_}\n" for sort keys %{$states{$key}};
}
}
return \%states;
}
sub InitLayout
{ my ($self,$layout,$opt2)=@_;
$self->{layout}=$layout;
$self->set_name($layout);
my $boxes=$Layouts{$layout};
$self->{KeyBindings}=::make_keybindingshash($boxes->{KeyBindings}) if $boxes->{KeyBindings};
$self->{widgets}={};
$self->{global_options}{default_group}=$self->{group};
for (qw/PATH SkinPath SkinFile DefaultFont DefaultFontColor/)
{ my $val= $self->{options}{$_} || $boxes->{$_};
$self->{global_options}{$_}=$val if defined $val;
}
my $mainwidget= $self->CreateWidgets($boxes,$opt2);
$mainwidget ||= do { my $l=Gtk2::Label->new("Error : empty layout"); my $hbox=Gtk2::HBox->new; $hbox->add($l); $hbox; };
$self->add($mainwidget);
if (my $name=$boxes->{DefaultFocus})
{ $self->SetFocusOn($name);
}
}
sub CreateWidgets
{ my ($self,$boxes,$opt2)=@_;
if ($self->{layoutdepth} && $self->{layoutdepth}>10) { warn "Too many imbricated layouts\n"; return }
$self->{layoutdepth}++;
my $widgets=$self->{widgets};
# create boxes
my @boxlist;
my $defaultgroup= $self->{global_options}{default_group};
for my $key (keys %$boxes)
{ my $fullname=$key;
my $type=substr $key,0,2;
$type=$Layout::Boxes::Boxes{$type};
next unless $type;
my $line=$boxes->{$key};
my $opt1={};
if ($line=~m#^\(#)
{ $opt1=::ExtractNameAndOptions($line);
$line=~s#^\s+##;
$opt1=~s#^\(##; $opt1=~s/\)$//;
$opt1= ::ParseOptions($opt1);
}
my $opt2=$opt2->{$key} || {};
%$opt1= (group=>'',%$opt1,%$opt2);
my $group=$opt1->{group};
$opt1->{group}= $defaultgroup.(length $group ? "-$group" : '') unless $group=~m/^[A-Z]/;
my $box=$widgets->{$key}= $type->{New}( $opt1 );
$box->{$_}=$opt1->{$_} for grep exists $opt1->{$_}, qw/group tabicon tabtitle maxwidth maxheight expand_weight/;
ApplyCommonOptions($box,$opt1);
$box->{name}=$fullname;
$box->set_border_width($opt1->{border}) if $opt1 && exists $opt1->{border} && $box->isa('Gtk2::Container');
$box->set_name($key);
push @boxlist,$key,$line;
}
#pack boxes
while (@boxlist)
{ my $key=shift @boxlist;
my $line=shift @boxlist;
my $type=substr $key,0,2;
$type=$Layout::Boxes::Boxes{$type};
my $box=$widgets->{$key};
my @names= ::ExtractNameAndOptions($line,$type->{Prefix});
for my $name (@names)
{ my $packoptions;
($name,$packoptions)=@$name if ref $name;
my $opt1;
$opt1=$1 if $name=~s/\((.*)\)$//; #remove (...) and put it in $opt1
my $widget= $widgets->{$name};
my $placeholder;
if (!$widget) #create widget if doesn't exist yet (only boxes have already been created)
{ $widget= NewWidget($name,$opt1,$opt2->{$name},$self->{global_options});
if ($widget) { $self->{widgets}{$name}=$widget; }
else
{ $placeholder={name => $name, opt2=>$opt2->{$name}, };
}
};
if ($widget)
{ if ($widget->parent) {warn "layout error: $name already has a parent -> can't put it in $key\n"; next;}
$type->{Pack}( $box,$widget,$packoptions );
}
elsif ($placeholder)
{ $placeholder->{opt1}=$opt1;
$placeholder->{defaultgroup}=$defaultgroup;
$placeholder=Layout::PlaceHolder->new( $type,$box,$placeholder,$packoptions);
$self->{PlaceHolders}{$name}=$placeholder if $placeholder;
}
}
$type->{EndInit}($box) if $type->{EndInit};
}
for my $key (grep m/^[HV]Size/, keys %$boxes)
{ my $mode= ($key=~m/^V/)? 'vertical' : 'horizontal';
my @names=split /\s+/,$boxes->{$key};
if ( $names[0]=~m/^\d+$/ )
{ my $s=shift @names;
my @req=($mode eq 'vertical')? (-1,$s) : ($s,-1);
$_->set_size_request(@req) for grep defined, map $widgets->{$_}, @names;
next if @names==1;
}
my $sizegroup=Gtk2::SizeGroup->new($mode);
for my $n (@names)
{ if (my $w=$widgets->{$n}) { $sizegroup->add_widget($w); }
else { warn "Can't add unknown widget '$n' to sizegroup\n" }
}
}
if (my $l=$boxes->{VolumeScroll})
{ $widgets->{$_}->signal_connect(scroll_event => \&::ChangeVol)
for grep $widgets->{$_}, split /\s+/,$l;
}
$self->signal_connect(key_press_event => \&KeyPressed,0);
$self->signal_connect_after(key_press_event => \&KeyPressed,1);
for my $widget (values %$widgets) { my $postinit= delete $widget->{PostInit}; $postinit->($widget) if $postinit; }
$self->{layoutdepth}--;
my @noparentboxes=grep m/^(?:[HV][BP]|[AMETNFSW]B|FR)/ && !$widgets->{$_}->parent, keys %$boxes;
if (@noparentboxes==0) {warn "layout empty ('$self->{layout}')\n"; return;}
elsif (@noparentboxes!=1) {warn "layout error: (@noparentboxes) have no parent -> can't find toplevel box\n"}
return $widgets->{ $noparentboxes[0] };
}
sub Parse_opt1
{ my ($opt,$oldopt)=@_;
my %opt;
if (defined $opt)
{ if ($oldopt && $opt!~m/=/)
{ if (ref $oldopt) { %opt= $oldopt->($opt); }
else { @opt{split / /,$oldopt}=split ',',$opt; }
}
else
{ #%opt= $opt=~m/(\w+)=([^,]*)(?:,|$)/g;
return Hash_to_HoH( ::ParseOptions($opt) );
}
}
return \%opt;
}
sub Hash_to_HoH # turn { 'key1/key2' => value } into { key1 => { key2 => value } }
{ my $hash=shift;
for my $key (grep m#/#, keys %$hash)
{ my $val=delete $hash->{$key};
my @keys=split '/',$key;
$key= pop @keys;
my $h=$hash;
for (@keys)
{ $h= $h->{$_}||={};
last if !ref $h;
}
$h->{$key}=$val;
}
return $hash; # the hash ref hasn't changed, but can be handy to return it anyway
}
sub NewWidget
{ my ($name,$opt1,$opt2,$global_opt)=@_;
my $namefull=$name;
$name=~s/\d+$//;
my $ref;
$global_opt ||={};
if ($name=~m/^@(.+)$/)
{ $ref= { class => 'Layout::Embedded', };
$global_opt={ %$global_opt, layout=>$1 };
}
else { $ref=$Widgets{$name} }
unless ($ref) { return undef; }
while (my $p=$ref->{parent}) #inherit from parent
{ my $pref=$Widgets{$p};
$ref= { %$pref, %$ref };
delete $ref->{parent} if $ref->{parent} eq $p;
}
$opt1=Parse_opt1($opt1,$ref->{oldopt1}) unless ref $opt1;
$opt2||={};
my %options= (group=>'', %$ref, %$opt1, %$opt2, name=>$namefull, %$global_opt);
$options{font} ||= $global_opt->{DefaultFont} if $global_opt->{DefaultFont};
my $group= $options{group}; #FIXME make undef group means parent's group ?
my $defaultgroup= $options{default_group} || 'default_group';
$options{group}= $defaultgroup.($group=~m/^\w/ ? '-' : '').$group unless $group=~m/^[A-Z]/; #group local to window unless it begins with uppercase
my $widget= $ref->{class}
? $ref->{class}->new(\%options,$ref)
: $ref->{New}(\%options);
return unless $widget;
$widget->{$_}= $options{$_} for 'group',split / /, ($ref->{options} || '');
$widget->{$_}=$options{$_} for grep exists $options{$_}, qw/tabtitle tabicon tabrename maxwidth maxheight expand_weight/;
$widget->{options_to_save}=$ref->{saveoptions} if $ref->{saveoptions};
$widget->{name}=$namefull;
$widget->set_name($name);
ApplyCommonOptions($widget,\%options);
$widget->{actions}{$_}=$options{$_} for grep m/^click\d*/, keys %options;
$widget->signal_connect(button_press_event => \&Button_press_cb) if $widget->{actions};
if (my $cursor=$options{cursor})
{ $widget->signal_connect(realize => sub {
my ($widget,$cursor)=@_;
my $gdkwin= $widget->window;
if ($widget->isa('Gtk2::EventBox') && !$widget->get_visible_window)
{ # for eventbox using an input-only gdkwindow, $widget->window is actually the parent's gdkwin,
# the only way to get to the input-only gdkwin is looking at all the children of its parent :(
for my $child ($gdkwin->get_children)
{ my $w= Glib::Object->new_from_pointer($child->get_user_data);
if ($w && $w==$widget) { $gdkwin=$child; last }
}
}
$gdkwin->set_cursor(Gtk2::Gdk::Cursor->new($cursor));
},$cursor);
}
my $tip= $options{tip};
if ( defined $tip)
{ if (!ref $tip)
{ my @fields=::UsedFields($tip);
if (@fields)
{ $widget->{song_tip}=$tip;
::WatchSelID($widget,\&UpdateSongTip,\@fields);
UpdateSongTip($widget,::GetSelID($widget));
}
else
{ $tip=~s#\\n#\n#g;
$widget->set_tooltip_text($tip);
}
}
else { $widget->{state_tip}=$tip; }
}
if (my $schange=$ref->{schange})
{ my $fields= $options{fields} || $options{field};
$fields= $fields ? [ split / /,$fields ] : undef;
::WatchSelID($widget,$schange, $fields);
$schange->($widget,::GetSelID($widget));
}
if ($ref->{event})
{ my $sub=$ref->{update} || \&UpdateObject;
::Watch($widget,$_,$sub ) for split / /,$ref->{event};
$sub->($widget) unless $ref->{noinit};
}
::set_drag($widget,source => $ref->{dragsrc}, dest => $ref->{dragdest});
my $init= delete $widget->{EndInit} || $ref->{EndInit};
$init->($widget) if $init;
$widget->{PostInit}||= $ref->{PostInit};
return $widget;
}
sub ApplyCommonOptions # apply some options common to both boxes and other widgets
{ my ($widget,$opt)=@_;
if ($opt->{minwidth} or $opt->{minheight})
{ my ($minwidth,$minheight)=$widget->get_size_request;
$minwidth= $opt->{minwidth} || $minwidth;
$minheight= $opt->{minheight} || $minheight;
$widget->set_size_request($minwidth,$minheight);
}
if ($opt->{hover_layout}) # only works with widgets/boxes that have their own gdkwindow (put it into a WB box otherwise)
{ $widget->{$_}=$opt->{$_} for qw/hover_layout hover_delay hover_layout_pos/;
Layout::Window::Popup::set_hover($widget);
}
}
sub RegisterWidget
{ my ($name,$hash)=@_;
my $action;
if ($hash)
{ if ($Widgets{$name} && $Widgets{$name}!=$hash) { warn "Widget $name already registered\n"; return }
$Widgets{$name}=$hash;
::HasChanged(Widgets=>'new',$name);
}
else
{ ::HasChanged(Widgets=>'remove',$name);
delete $Widgets{$name};
}
}
sub WidgetChangedAutoAdd
{ my $name=shift;
::HasChanged(Widgets=>'option',$name) if $Widgets{$name};
}
sub UpdateObject
{ my $widget=$_[0];
if ( my $tip=$widget->{state_tip} )
{ $tip= $tip->($widget) if ref $tip;
$widget->set_tooltip_text($tip);
}
if ($widget->{skin}) {$widget->queue_draw}
elsif ($widget->{stock}) { $widget->UpdateStock }
}
sub Button_press_cb
{ my ($self,$event)=@_;
my $actions=$self->{actions};
my $key='click'.$event->button;
my $sub=$actions->{$key};
return 0 if !$sub && $self->{clicked_cmd};
$sub||= $actions->{click} || $actions->{click1};
return 0 unless $sub;
if (ref $sub) {&$sub}
else { ::run_command($self,$sub) }
1;
}
sub UpdateSongTip
{ my ($widget,$ID)=@_;
if ($widget->{song_tip})
{ my $tip= defined $ID ? ::ReplaceFields($ID,$widget->{song_tip}) : '';
$widget->set_tooltip_text($tip);
}
}
#sub SetSort
#{ my($self,$sort)=@_;
# $self->{songlist}->Sort($sort);
#}
sub ShowHide
{ my ($self,$names,$resize,$show)=@_;
$show= !grep $_ && $_->visible, map $self->{widgets}{$_}, split /\|/,$names unless defined $show;
if ($show) { Show($self,$names,$resize); }
else { Hide($self,$names,$resize); }
}
sub Hide
{ my ($self,$names,$resize)=@_;
my @resize=split //,$resize||'';
my $r;
my ($ww,$wh)=$self->get_size;
for my $name ( split /\|/,$names )
{ my $widget=$self->{widgets}{$name};
$r=shift @resize if @resize;
next unless $widget;# && $widget->visible;
my $alloc=$widget->allocation;
my $w=$alloc->width;
my $h=$alloc->height;
$self->{hidden}{$name}=$w.'x'.$h;
if ($r)
{ if ($r eq 'v') {$wh-=$h}
elsif ($r eq 'h') {$ww-=$w}
}
$widget->hide;
}
$self->resize($ww,$wh) if $resize && $resize ne '_';
::HasChanged('HiddenWidgets');
}
sub Show
{ my ($self,$names,$resize)=@_;
my @resize=split //,$resize||'';
my $r;
my ($ww,$wh)=$self->get_size;
for my $name ( split /\|/,$names )
{ my $widget=$self->{widgets}{$name};
next unless $widget && !$widget->visible;
$widget->show;
my $oldsize=delete $self->{hidden}{$name};
next unless $oldsize && $oldsize=~m/x/;
my ($w,$h)=split 'x',$oldsize;
$r=shift @resize if @resize;
if ($r)
{ if ($r eq 'v') {$wh+=$h}
elsif ($r eq 'h') {$ww+=$w}
}
}
$self->resize($ww,$wh) if $resize && $resize ne '_';
::HasChanged('HiddenWidgets');
}
sub GetShowHideState
{ my ($self,$names)=@_;
my $hidden;
for my $name ( split /\|/,$names )
{ my $widget=$self->{widgets}{$name};
next unless $widget;
$hidden++ unless $widget->visible;
}
return !$hidden;
}
sub ToggleFullscreen
{ return unless $_[0];
my $win= ::get_layout_widget($_[0])->get_toplevel;
if ($win->{fullscreen})
{ if ($::FullscreenWindow && $win==$::FullscreenWindow) { $win->close_window }
else {$win->unfullscreen}
}
else {$win->fullscreen}
}
sub KeyPressed
{ my ($self,$event,$after)=@_;
my $key=Gtk2::Gdk->keyval_name( $event->keyval );
my $focused=$self->get_toplevel->get_focus;
return 0 if !$after && $focused && ($focused->isa('Gtk2::Entry') || $focused->isa('Gtk2::SpinButton'));
my $mod;
$mod.='c' if $event->state >= 'control-mask';
$mod.='a' if $event->state >= 'mod1-mask';
$mod.='w' if $event->state >= 'mod4-mask';
$mod.='s' if $event->state >= 'shift-mask';
$key= ($after? '':'+') . ($mod? "$mod-":'') . lc($key);
my ($cmd,$arg);
if ( exists $::CustomBoundKeys{$key} )
{ $cmd= $::CustomBoundKeys{$key};
}
elsif ($self->{KeyBindings} && exists $self->{KeyBindings}{$key} )
{ $cmd= $self->{KeyBindings}{$key};
}
elsif ( exists $::GlobalBoundKeys{$key} )
{ $cmd= $::GlobalBoundKeys{$key};
}
elsif ($after && $self->{fullscreen} && $key eq 'Escape') { $cmd='ToggleFullscreen' }
return 0 unless $cmd;
if ($self->isa('Gtk2::Window')) #try to find the focused widget (gmb widget, not gtk one), so that the cmd can act on it
{ my $widget=$self->get_focus;
while ($widget) {last if exists $widget->{group}; $widget=$widget->parent}
$self=$widget if $widget;
}
::run_command($self,$cmd);
return 1;
}
sub EnqueueSelected
{ my $self=shift;
return unless $self;
if (my $songlist=::GetSonglist($self))
{ $songlist->EnqueueSelected;
}
}
sub GoToCurrentSong
{ my $self=shift;
return unless $self;
if (my $songlist=::GetSonglist($self))
{ $songlist->FollowSong;
}
}
sub SetFocusOn
{ my ($self,$name)=@_;
while ($name=~s#^([^/]+)/##) # if name contains slashes, divide it into parent and child, where parent can be an Embedded layout or a TabbedLists/Context/NB
{ $self=$self->{widgets}{$1};
return unless $self;
}
my $widget=$self->{widgets}{$name};
if ($widget)
{ $widget=$widget->{DefaultFocus} while $widget->{DefaultFocus};
TurnPagesToWidget($widget);
$widget->grab_focus;
}
}
sub TurnPagesToWidget #change the current page of all parent notebook so that widget is on it
{ my $parent=$_[0];
while (1)
{ my $child=$parent;
$parent=$child->parent;
last unless $parent;
if ($parent->isa('Gtk2::Notebook'))
{ $parent->set_current_page($parent->page_num($child)); }
}
}
sub SensitiveIfMoreOneSong { my $songarray= ::GetSongArray($_[0]); $_[0]->set_sensitive($songarray && @$songarray>1); }
sub SensitiveIfMoreZeroSong { my $songarray= ::GetSongArray($_[0]); $_[0]->set_sensitive($songarray && @$songarray>0); }
#################################################################################
sub PlayOrderComboNew
{ my $opt=$_[0];
my $store=Gtk2::ListStore->new(('Glib::String')x3);
my $combo=Gtk2::ComboBox->new($store);
my $cell=Gtk2::CellRendererPixbuf->new;
$cell->set_fixed_size( Gtk2::IconSize->lookup('menu') );
$combo->pack_start($cell,0);
$combo->add_attribute($cell,stock_id => 2);
$cell=Gtk2::CellRendererText->new;
$combo->pack_start($cell,1);
$combo->add_attribute($cell, text => 0);
$combo->signal_connect( changed => sub
{ my $combo=$_[0];
return if $combo->{busy};
my $store=$combo->get_model;
my $sort=$store->get($combo->get_active_iter,1);
if ($sort=~m/^EDIT (.)$/)
{ PlayOrderComboUpdate($combo); #so that the combo doesn't stay on Edit...
if ($1 eq 'O')
{ ::EditSortOrder(undef,$::Options{Sort},undef, \&::Select_sort);
}
elsif ($1 eq 'R')
{ ::EditWeightedRandom(undef,$::Options{Sort},undef, \&::Select_sort);
}
}
else { ::Select('sort' => $sort); }
});
return $combo;
}
sub PlayOrderComboUpdate
{ my $combo=$_[0];
$combo->{busy}=1;
my $store=$combo->get_model;
$store->clear;
my $check=$::Options{Sort};
my $found; my $iter;
for my $name (sort keys %{$::Options{SavedWRandoms}})
{ my $sort=$::Options{SavedWRandoms}{$name};
$store->set(($iter=$store->append), 0,$name, 1,$sort, 2,'gmb-random');
$found=$iter if $sort eq $check;
}
if (!$found && $check=~m/^random:/)
{ $store->set($iter=$store->append, 0, _"unnamed random mode", 1,$check,2,'gmb-random');
$found=$iter;
}
$store->set($store->append, 0, _"Edit random modes ...", 1,'EDIT R');
$store->set($iter=$store->append, 0, _"Shuffle", 1,'shuffle',2,'gmb-shuffle');
$found=$iter if 'shuffle' eq $check;
if (defined $::ListMode)
{ $store->set($iter=$store->append, 0, _"List order", 1,'',2,'gmb-list');
$found=$iter if '' eq $check;
}
for my $name (sort keys %{$::Options{SavedSorts}})
{ my $sort=$::Options{SavedSorts}{$name};
$store->set($iter=$store->append, 0, $name, 1,$sort,2,'gtk-sort-ascending');
$found=$iter if $sort eq $check;
}
if (!$found)
{ $store->set($iter=$store->append, 0, ::ExplainSort($check), 1,$check,2,'gtk-sort-ascending');
$found=$iter;
}
$store->set($store->append, 0, _"Edit ordered modes ...",1,'EDIT O');
$combo->set_active_iter($found);
$combo->{busy}=undef;
}
sub SortMenu
{ my $nopopup= $_[0];
my $menu = $_[0] || Gtk2::Menu->new;
my $return=0;
$return=1 unless @_;
my $check=$::Options{Sort};
my $found;
my $callback=sub { ::Select('sort' => $_[1]); };
my $append=sub
{ my ($menu,$name,$sort,$true,$cb)=@_;
$cb||=$callback;
$true=($sort eq $check) unless defined $true;
my $item = Gtk2::CheckMenuItem->new_with_label($name);
$item->set_draw_as_radio(1);
$item->set_active($found=1) if $true;
$item->signal_connect (activate => $cb, $sort );
$menu->append($item);
};
my $submenu= Gtk2::Menu->new;
my $sitem = Gtk2::MenuItem->new(_"Weighted Random");
for my $name (sort keys %{$::Options{SavedWRandoms}})
{ $append->($submenu,$name, $::Options{SavedWRandoms}{$name} );
}
my $editcheck=(!$found && $check=~m/^random:/);
$append->($submenu,_"Custom...", undef, $editcheck, sub
{ ::EditWeightedRandom(undef,$::Options{Sort},undef, \&::Select_sort);
});
$sitem->set_submenu($submenu);
$menu->prepend($sitem);
$append->($menu,_"Shuffle",'shuffle') unless $check eq 'shuffle';
if ($check=~m/shuffle/)
{ my $item=Gtk2::MenuItem->new(_"Re-shuffle");
$item->signal_connect(activate => $callback, $check );
$menu->append($item);
}
{ my $item=Gtk2::CheckMenuItem->new(_"Repeat");
$item->set_active($::Options{Repeat});
$item->set_sensitive(0) if $::RandomMode;
$item->signal_connect(activate => sub { ::SetRepeat($_[0]->get_active); } );
$menu->append($item);
}
$menu->append(Gtk2::SeparatorMenuItem->new); #separator between random and non-random modes
$append->($menu,_"List order", '' ) if defined $::ListMode;
for my $name (sort keys %{$::Options{SavedSorts}})
{ $append->($menu,$name, $::Options{SavedSorts}{$name} );
}
$append->($menu,_"Custom...",undef,!$found,sub
{ ::EditSortOrder(undef,$::Options{Sort},undef, \&::Select_sort );
});
return $menu if $nopopup;
::PopupMenu($menu);
}
sub FilterMenu
{ my $nopopup= $_[0];
my $menu = $_[0] || Gtk2::Menu->new;
my ($check,$found);
$check=$::SelectedFilter->{string} if $::SelectedFilter;
my $item_callback=sub { ::Select(filter => $_[1]); };
my $item0= Gtk2::CheckMenuItem->new(_"All songs");
$item0->set_active($found=1) if !$check && !defined $::ListMode;
$item0->set_draw_as_radio(1);
$item0->signal_connect ( activate => $item_callback ,'' );
$menu->append($item0);
for my $list (sort keys %{$::Options{SavedFilters}})
{ my $filt=$::Options{SavedFilters}{$list}->{string};
my $item = Gtk2::CheckMenuItem->new_with_label($list);
$item->set_draw_as_radio(1);
$item->set_active($found=1) if defined $check && $filt eq $check;
$item->signal_connect ( activate => $item_callback ,$filt );
$menu->append($item);
}
my $item=Gtk2::CheckMenuItem->new(_"Custom...");
$item->set_active(1) if defined $check && !$found;
$item->set_draw_as_radio(1);
$item->signal_connect ( activate => sub
{ ::EditFilter(undef,$::SelectedFilter,undef, sub {::Select(filter => $_[0])});
});
$menu->append($item);
if (my @SavedLists=::GetListOfSavedLists())
{ my $submenu=Gtk2::Menu->new;
my $list_cb=sub { ::Select( staticlist => $_[1] ) };
for my $list (@SavedLists)
{ my $item = Gtk2::CheckMenuItem->new_with_label($list);
$item->set_draw_as_radio(1);
$item->set_active(1) if defined $::ListMode && $list eq $::ListMode;
$item->signal_connect( activate => $list_cb, $list );
$submenu->append($item);
}
my $sitem=Gtk2::MenuItem->new(_"Saved Lists");
#my $sitem=Gtk2::CheckMenuItem->new('Saved Lists');
#$item->set_draw_as_radio(1);
$sitem->set_submenu($submenu);
$menu->prepend($sitem);
}
return $menu if $nopopup;
::PopupMenu($menu);
}
sub VisualsMenu
{ my $menu=Gtk2::Menu->new;
my $cb=sub { $::Play_package->set_visual($_[1]) if $::Play_package->{visuals}; };
return unless $::Play_package->{visuals};
my @l= $::Play_package->list_visuals;
my $current= $::Options{gst_visual}||$l[0];
for my $v (@l)
{ my $item=Gtk2::CheckMenuItem->new_with_label($v);
$item->set_draw_as_radio(1);
$item->set_active(1) if $current eq $v;
$item->signal_connect (activate => $cb,$v);
$menu->append($item);
}
::PopupMenu($menu);
}
sub UpdateLabelsIcon
{ my $table=$_[0];
$table->remove($_) for $table->get_children;
return unless defined $::SongID;
my $row=0; my $col=0;
my $count=0;
for my $stock ( Songs::Get_icon_list($table->{field},$::SongID) )
{ my $img=Gtk2::Image->new_from_stock($stock,'menu');
$count++;
$table->attach($img,$col,$col+1,$row,$row+1,'shrink','shrink',1,1);
if (++$row>=1) {$row=0; $col++}
}
$table->show_all;
}
sub AddLabelEntry #create entry to add a label to the current song
{ my $entry=Gtk2::Entry->new;
$entry->set_tooltip_text(_"Adds labels to the current song");
$entry->signal_connect(activate => sub
{ my $entry=shift;
my $label= $entry->get_text;
my $ID= ::GetSelID($entry);
return unless defined $ID & defined $label;
$entry->set_text('');
Songs::Set($ID,"+label",$label);
});
GMB::ListStore::Field::setcompletion($entry,'label');
return $entry;
}
sub DragCurrentSong
{ ::DRAG_ID,$::SongID;
}
sub DragCurrentArtist
{ ::DRAG_ARTIST,@{Songs::Get_gid($::SongID,'artists')};
}
sub DragCurrentAlbum
{ ::DRAG_ALBUM,Songs::Get_gid($::SongID,'album');
}
sub PopupSongsFromAlbum
{ my $ID=::GetSelID($_[0]);
return unless defined $ID;
my $aid=Songs::Get_gid($ID,'album');
::ChooseSongsFromA($aid,nocover=>0);
}
####################################
package Layout::Window;
our @ISA;
BEGIN {push @ISA,'Layout';}
use base 'Gtk2::Window';
sub new
{ my ($class,$layout,%options)=@_;
my @original_args=@_;
my $fallback=delete $options{fallback} || 'Lists, Library & Context';
my $opt0={};
if (my $opt= $layout=~m/^[^(]+\(.*=/)
{ ($layout,$opt0)= $layout=~m/^([^(]+)\((.*)\)$/; #separate layout id and options
$opt0= ::ParseOptions($opt0);
}
unless (exists $Layout::Layouts{$layout})
{ if ($fallback eq 'NONE') { warn "Layout '$layout' not found\n"; return undef; }
warn "Layout '$layout' not found, using '$fallback' instead\n";
$layout=$fallback; #FIXME if not a player window
$Layout::Layouts{$layout} ||= { VBmain=>'Label(text="Error : fallback layout not found")' }; #create an error layout if fallback not found
}
my $opt2=$::Options{Layouts}{$layout};
$opt2||= Layout::GetDefaultLayoutOptions($layout);
for my $child_key (grep m#./.#, keys %options)
{ my ($child,$key)=split "/",$child_key,2;
$opt2->{$child}{$key}= delete $options{$child_key};
}
my $opt1=::ParseOptions( $Layout::Layouts{$layout}{Window}||'' );
%options= ( borderwidth=>0, %$opt1, %{$opt2->{Window}||{}}, %options, %$opt0 );
#warn "window options (layout=$layout) :\n";warn " $_ => $options{$_}\n" for sort keys %options;
my $uniqueid= $options{uniqueid} || 'layout='.$layout;
# ifexist=toggle => if a window with same uniqueid exist it will be closed
# ifexist=present => if a window with same uniqueid exist it presented
if (my $mode=$options{ifexist})
{ my ($window)=grep $_->isa('Layout::Window') && $_->{uniqueid} eq $uniqueid, Gtk2::Window->list_toplevels;
if ($window)
{ if ($mode eq 'toggle' && !$window->{quitonclose}) { $window->close_window; return }
elsif ($mode eq 'replace' && !$window->{quitonclose}) { $window->close_window; return Layout::Window::new(@original_args,ifexists=>0); } # destroying previous window make it save its settings, then restart new() from the start with new $opt2 but the same original arguments, add ifexists=>0 to make sure it doesn't loop
elsif ($mode eq 'present') { $window->force_present; return }
}
}
my $wintype= delete $options{wintype} || 'toplevel';
my $self=bless Gtk2::Window->new($wintype), $class;
$self->{uniqueid}= $uniqueid;
$self->set_role($layout);
$self->set_type_hint(delete $options{typehint}) if $options{typehint};
$self->{options}=\%options;
$self->{name}='Window';
$self->{SaveOptions}=\&SaveWindowOptions;
$self->{group}= 'Global('.::refaddr($self).')';
::Watch($self,Save=>\&SaveOptions);
$self->set_title(::PROGRAM_NAME);
if ($options{dragtomove})
{ $self->add_events(['button-press-mask']);
$self->signal_connect_after(button_press_event => sub { my $event=$_[1]; $_[0]->begin_move_drag($event->button, $event->x_root, $event->y_root, $event->time); 1; });
}
#$self->signal_connect (show => \&show_cb);
$self->signal_connect (window_state_event => sub
{ my $self=$_[0];
my $wstate=$_[1]->new_window_state();
warn "window $self is $wstate\n" if $::debug;
$self->{sticky}=($wstate >= 'sticky'); #save sticky state
$self->{fullscreen}=($wstate >= 'fullscreen');
$self->{ontop}=($wstate >= 'above');
$self->{below}=($wstate >= 'below');
$self->{withdrawn}=($wstate >= 'withdrawn');
$self->{iconified}=($wstate >= 'iconified');
0;
});
$self->signal_connect(focus_in_event=> sub { $_[0]{last_focused}=time;0; });
$self->signal_connect(delete_event => \&close_window);
# ::set_drag($self, dest => [::DRAG_FILE,sub
# { my ($self,$type,@values)=@_;
# warn "@values";
# }],
# motion => sub
# { my ($self,$context,$x,$y,$time)=@_;
# my $target=$self->drag_dest_find_target($context, $self->drag_dest_get_target_list);
# $context->{get_data}=1;
# $self->drag_get_data($context, $target, $time);
# ::TRUE;
# }
# );
$self->InitLayout($layout,$opt2);
$self->SetWindowOptions(\%options);
if (my $skin=$Layout::Layouts{$layout}{Skin}) { $self->set_background_skin($skin) }
$self->init;
::HasChanged('HiddenWidgets');
$self->set_opacity($self->{opacity}) if exists $self->{opacity} && $self->{opacity}!=1;
::QHasChanged('Windows');
return $self;
}
sub init
{ my $self=$_[0];
if ($self->{options}{transparent})
{ if ($::CairoOK)
{ make_transparent($self);
}
else { warn "no Cairo perl module => can't make the window transparent\n" }
}
$self->child->show_all; #needed to get the true size of the window
$self->realize;
$self->Resize if $self->{size};
{ my @hidden;
# widgets that were saved as hidden
@hidden=keys %{ $self->{hidden} } if $self->{hidden};
my $widgets=$self->{widgets};
# look for widgets asking for other widgets to be hidden at init
for my $w (values %$widgets)
{ my $names= delete $w->{need_hide};
next unless $names;
push @hidden, split /\|/, $names;
}
# hide them
$_->hide for grep defined, map $widgets->{$_}, @hidden;
}
#$self->set_position();#doesn't work before show, at least with sawfish
my ($x,$y)= $self->Position;
$self->move($x,$y) if defined $x;
$self->show;
$self->move($x,$y) if defined $x;
$self->parse_geometry( delete $::CmdLine{geometry} ) if $::CmdLine{geometry};
$self->set_workspace( delete $::CmdLine{workspace} ) if exists $::CmdLine{workspace};
if ($self->{options}{insensitive})
{ my $mask=Gtk2::Gdk::Bitmap->create_from_data(undef,'',1,1);
$self->input_shape_combine_mask($mask,0,0);
}
}
sub layout_name
{ my $self=shift;
my $id=$self->{layout};
return Layout::get_layout_name($id);
}
sub close_window
{ my $self=shift;
$self->SaveOptions;
unless ($self->{quitonclose}) { $_->destroy for values %{$self->{widgets}}; $self->destroy; return }
if ($::Options{CloseToTray}) { ::ShowHide(0); return 1}
else { &::Quit }
}
sub SaveOptions
{ my $self=shift;
my $opt=Layout::SaveWidgetOptions($self,values %{ $self->{widgets} }, values %{ $self->{PlaceHolders} });
$::Options{Layouts}{$self->{layout}} = $opt;
}
sub SaveWindowOptions
{ my $self=$_[0];
my %wstate;
$wstate{size}=join 'x',$self->get_size;
#unless ($self->{options}{DoNotSaveState})
{ $wstate{sticky}=1 if $self->{sticky};
$wstate{fullscreen}=1 if $self->{fullscreen};
$wstate{ontop}=1 if $self->{ontop};
$wstate{below}=1 if $self->{below};
$wstate{nodecoration}=1 unless $self->get_decorated;
$wstate{skippager}=1 if $self->get_skip_pager_hint;
if ($self->{saved_position})
{ $wstate{pos}=$self->{saved_position};
$wstate{skiptaskbar}=1 if $self->{skip_taskbar_hint};
}
else
{ $wstate{pos}=join 'x',$self->get_position;
$wstate{skiptaskbar}=1 if $self->get_skip_taskbar_hint;
}
}
my $hidden=$self->{hidden};
if ($hidden && keys %$hidden)
{ $wstate{hidden}= join '|', map { my $dim=$hidden->{$_}; $_.($dim ? ":$dim" : '') } sort keys %$hidden;
}
return \%wstate;
}
sub SetWindowOptions
{ my ($self,$opt)=@_;
my $layouthash= $Layout::Layouts{ $self->{layout} };
if ($opt->{fullscreen}) { $self->fullscreen; }
else
{ $self->{size}=$opt->{size};
#window position in format numberxnumber number can be a % of screen size
$self->{pos}=$opt->{pos};
}
$self->stick if $opt->{sticky};
$self->set_keep_above(1) if $opt->{ontop};
$self->set_keep_below(1) if $opt->{below};
$self->set_decorated(0) if $opt->{nodecoration};
$self->set_skip_pager_hint(1) if $opt->{skippager};
$self->set_skip_taskbar_hint(1) if $opt->{skiptaskbar};
$self->{opacity}=$opt->{opacity} if defined $opt->{opacity};
$self->{hidden}={ $opt->{hidden}=~m/(\w+)(?::?(\d+x\d+))?/g } if $opt->{hidden};
$self->{size}= $self->{fixedsize}= $opt->{fixedsize} if $opt->{fixedsize};
$self->set_border_width($self->{options}{borderwidth});
$self->set_gravity($opt->{gravity}) if $opt->{gravity};
my $title= $layouthash->{Title} || $opt->{title} || _"%S by %a";
$title=~s/^"(.*)"$/$1/;
if (my @l=::UsedFields($title))
{ $self->{TitleString}=$title;
my %fields; $fields{$_}=undef for @l;
::Watch($self,'CurSong',\&UpdateWindowTitle,\%fields);
$self->UpdateWindowTitle();
}
else { $self->set_title($title) }
}
sub UpdateWindowTitle
{ my $self=shift;
my $ID=$::SongID;
if (my $title=$self->{TitleString})
{ $title= defined $ID ? ::ReplaceFields($ID,$title)
: '<'._("Playlist Empty").'>';
$self->set_title($title);
}
}
sub Resize
{ my $self=shift;
my ($w,$h)= split 'x',delete $self->{size};
return unless defined $h;
my $screen=$self->get_screen;
my $monitor=$screen->get_monitor_at_window($self->window);
my (undef,undef,$monitorwidth,$monitorheight)=$screen->get_monitor_geometry($monitor)->values;
$w= $1*$monitorwidth/100 if $w=~m/(\d+)%/;
$h= $1*$monitorheight/100 if $h=~m/(\d+)%/;
if ($self->{options}{DEFAULT_OPTIONS}) { $monitorwidth-=40; $monitorheight-=80; } # if using default layout size, reserve some space for potential panels and decorations #FIXME use gdk_screen_get_monitor_workarea once ported to gtk3
$w=$monitorwidth if $w>$monitorwidth;
$h=$monitorheight if $h>$monitorheight;
if ($self->{fixedsize})
{ $w=-1 if $w<1; # -1 => do not override default minimum size
$h=-1 if $h<1;
$self->set_size_request($w,$h);
$self->set_resizable(0);
}
else
{ $w=1 if $w<1; # 1 => resize to minimum size
$h=1 if $h<1;
$self->resize($w,$h);
}
}
sub Position
{ my $self=shift;
my $pos=delete $self->{pos};
return unless $pos; #format : 100x100 50%x100% -100x-100 500-100% x 500-50% 1@50%x100%
my ($monitor,$x,$xalign,$y,$yalign)= $pos=~m/(?:(\d+)@)?\s*([+-]?\d+%?)(?:([+-]\d+)%)?\s*x\s*([+-]?\d+%?)(?:([+-]\d+)%)?/;
my ($w,$h)=$self->get_size; # size of window to position
my $screen=$self->get_screen;
my $absolute_coords;
if (defined $monitor) { $monitor=undef if $monitor>=$screen->get_n_monitors; }
if (!defined($monitor) && $x!~m/[-%]/ && $y!~m/[-%]/)
{ $monitor=$screen->get_monitor_at_point($x,$y);
$absolute_coords=1;
}
if (!defined $monitor)
{ $monitor=$screen->get_monitor_at_window($self->window);
}
my ($xmin,$ymin,$monitorwidth,$monitorheight)=$screen->get_monitor_geometry($monitor)->values;
$xalign= $x=~m/%/ ? 50 : 0 unless defined $xalign;
$yalign= $y=~m/%/ ? 50 : 0 unless defined $yalign;
$x= $monitorwidth*$1/100 if $x=~m/(-?\d+)%/;
$y= $monitorheight*$1/100 if $y=~m/(-?\d+)%/;
$x= $monitorwidth-$x if $x<0;
$y= $monitorheight-$y if $y<0;
$x-= $xalign*$w/100;
$y-= $yalign*$h/100;
if ($absolute_coords)
{ $x-=$xmin if $x>$xmin;
$y-=$ymin if $y>$ymin;
}
$x=0 if $x<0; $x=$monitorwidth -$w if $x+$w>$monitorwidth;
$y=0 if $y<0; $y=$monitorheight-$h if $y+$h>$monitorheight;
$x+=$xmin;
$y+=$ymin;
return $x,$y;
}
sub set_workspace #only works with Gnome2::Wnck
{ my ($self,$workspace)=@_;
eval {require Gnome2::Wnck};
if ($@) { warn "Setting workspace : error loading Gnome2::Wnck : $@\n"; return }
my $screen= Gnome2::Wnck::Screen->get_default;
$screen->force_update;
$workspace= $screen->get_workspace($workspace);
return unless $workspace;
my $xid= $self->window->get_xid;
my $w=Gnome2::Wnck::Window->get($xid);
return unless $w;
$w->move_to_workspace($workspace);
}
sub make_transparent
{ my @children=($_[0]);
my $colormap=$children[0]->get_screen->get_rgba_colormap;
return unless $colormap;
while (my $widget=shift @children)
{ push @children, $widget->get_children if $widget->isa('Gtk2::Container');
unless ($widget->no_window)
{ $widget->set_colormap($colormap);
$widget->set_app_paintable(1);
$widget->signal_connect(expose_event => \&transparent_expose_cb);
}
if ($widget->isa('Gtk2::container'))
{ $widget->signal_connect(add => sub { make_transparent($_[1]); } );
}
}
}
sub transparent_expose_cb #use Cairo
{ my ($w,$event)=@_;
my $cr=Gtk2::Gdk::Cairo::Context->create($event->window);
$cr->set_operator('source');
$cr->set_source_rgba(0, 0, 0, 0);
$cr->rectangle($event->area);
$cr->fill;
if (my $pixbuf=$w->{skinpb}) { $cr->set_source_pixbuf($pixbuf,0,0); $cr->paint; }
return 0; #send expose to children
}
sub set_background_skin
{ my ($self,$skin)=@_;
my ($file,$crop,$resize)=split /:/,$skin;
$self->{pixbuf}=Skin::_load_skinfile($file,$crop,$self->{global_options});
return unless $self->{pixbuf};
$self->{resizeparam}=$resize;
$self->{skinsize}='0x0';
$self->signal_connect(size_allocate => \&resize_skin_cb);
return if $self->{options}{transparent};
# following not needed when using transparency
$self->signal_connect(style_set => sub {warn "style set : @_" if $::debug;$_[0]->set_style($_[2]);} ,$self->get_style); #FIXME find the cause of these signals, seems related to stock icons
my $rc_style= Gtk2::RcStyle->new;
#$rc_style->bg_pixmap_name($_,'<parent>') for qw/normal selected prelight insensitive active/;
$rc_style->bg_pixmap_name('normal','<parent>');
my @children=($self->child);
while (my $widget=shift @children)
{ push @children, $widget->get_children if $widget->isa('Gtk2::Container');
$widget->modify_style($rc_style) unless $widget->no_window;
}
$self->set_app_paintable(1);
}
sub resize_skin_cb #FIXME needs to add a delay to better deal with a burst of resize events
{ my ($self,$alloc)=@_;
my ($w,$h)=($alloc->width,$alloc->height);
return unless $self->realized;
return if $w.'x'.$h eq $self->{skinsize};
my $pb=Skin::_resize($self->{pixbuf},$self->{resizeparam},$w,$h);
return unless $pb;
if ($self->{options}{transparent})
{ $self->{skinpb}=$pb; #will be used by transparent_expose_cb()
if (my $shape= $self->{options}{shape})
{ my $mask=Gtk2::Gdk::Pixmap->new(undef,$w,$h,1);
$pb->render_threshold_alpha($mask,0,0,0,0,-1,-1, $shape);
$self->input_shape_combine_mask($mask,0,0);
}
}
else
{ #my ($pixmap,$mask)=$pb->render_pixmap_and_mask(1); #leaks X memory for Gtk2 <1.146 or <1.153
# create shape mask
my $mask=Gtk2::Gdk::Pixmap->new(undef,$w,$h,1);
$pb->render_threshold_alpha($mask,0,0,0,0,-1,-1,1);
$self->shape_combine_mask($mask,0,0);
# create pixmap background
my $pixmap=Gtk2::Gdk::Pixmap->new($self->window,$w,$h,-1);
$pb->render_to_drawable($pixmap, Gtk2::Gdk::GC->new($self->window), 0,0,0,0,-1,-1,'none',0,0);
$self->window->set_back_pixmap($pixmap,0);
}
$self->{skinsize}=$w.'x'.$h;
$self->queue_draw;
}
package Layout::Window::Popup;
our @ISA;
BEGIN {push @ISA,'Layout','Layout::Window';}
sub new
{ my ($class,$layout,$widget)=@_;
$layout||=$::Options{LayoutT};
my $self=Layout::Window::new($class,$layout, wintype=>'popup', 'pos'=>undef, size=>undef, fallback=>'full with buttons', popped_from=>$widget);
if ($widget) #warning : widget can be a Gtk2::StatusIcon
{ ::weaken( $widget->{PoppedUpWindow}=$self );
$self->set_screen($widget->get_screen);
#$self->set_transient_for($widget->get_toplevel);
#$self->move( ::windowpos($self,$widget) );
$self->signal_connect(enter_notify_event => \&CancelDestroy);
}
else { $self->set_position('mouse'); }
$self->show;
return $self;
}
sub init
{ my $self=$_[0];
#add a frame
my $child=$self->child;
$self->remove($self->child);
my $frame=Gtk2::Frame->new;
$self->add($frame);
$frame->add($child);
my $shadow= $self->{options}{transparent} ? 'none' : 'out';
$frame->set_shadow_type($shadow);
$child->set_border_width($self->get_border_width);
$self->set_border_width(0);
##$self->set_type_hint('tooltip'); #TEST
##$self->set_type_hint('notification'); #TEST
#$self->set_focus_on_map(0);
#$self->set_accept_focus(0); #?
$self->signal_connect(leave_notify_event => sub { $_[0]->CheckCursor if $_[1]->detail ne 'inferior'; 0; });
$self->SUPER::init;
}
sub CheckCursor # StartDestroy if popup is not ancestor of widget under cursor and cursor isn't grabbed (menu)
{ my $self=shift;
$self->{check_timeout} ||= Glib::Timeout->add(800, \&CheckCursor, $self);
return 1 if $self->get_display->pointer_is_grabbed; # to prevent destroying while a menu is open
if (my $sicon=$self->{popped_from})
{ return 1 if $sicon->isa('Gtk2::StatusIcon') && OnStatusIcon($sicon); #check if pointer above statusicon
}
my ($gdkwin)=Gtk2::Gdk::Window->at_pointer;
my $widget= $gdkwin ? Glib::Object->new_from_pointer($gdkwin->get_user_data) : undef;
while ($widget)
{ last if $widget->isa('Gtk2::StatusIcon');
$widget= ::find_ancestor($widget,'Layout::Window::Popup');
last unless $widget;
return 1 if $widget==$self; # don't destroy if cursor is over child of self
$widget= $widget->{popped_from};# parent popup
}
$self->StartDestroy;
return 1
}
sub OnStatusIcon #return true if pointer is above sicon
{ my $sicon=shift;
my ($screen,$area)= $sicon->get_geometry;
my ($x,$y,$w,$h)= $area->values;
my ($pscreen,$px,$py)= $screen->get_display->get_pointer;
return $pscreen==$screen && $px>=$x && $px<=$x+$w && $py>=$y && $py<=$y+$h;
}
sub Position
{ my $self=shift;
if ( my $widget= delete $self->{options}{popped_from})
{ ::weaken( $self->{popped_from}=$widget );
if (my $pos=$widget->{hover_layout_pos})
{ my ($x0,$y0)= split /\s*x\s*/,$pos;
my ($width,$height)=$self->get_size;
my ($x,$y)= $widget->window->get_origin;
my ($ww,$wh)=$widget->window->get_size;
if ($widget->no_window)
{ (my$wx,my$wy,$ww,$wh)=$widget->allocation->values;
$x+=$wx;$y+=$wy;
}
$x=$y=0 if $x0=~s/abs:\s*//;
my $screen=$widget->get_screen;
$x+=_compute_pos($x0,$width, $ww,$screen->get_width);
$y+=_compute_pos($y0,$height,$wh,$screen->get_height);
return $x,$y;
}
return ::windowpos($self,$widget);
}
$self->SUPER::Position;
}
sub _compute_pos
{ my ($def,$wp,$ww,$ws)=@_;
my %h;
$def="+$def" unless $def=~m/^[-+]/;
::setlocale(::LC_NUMERIC, 'C'); # so that decimal separator is the dot
# can parse strings such as : +3s/2-w-p/2+20
for my $v ($def=~m/([-+][^-+]+)/g)
{ if ($v=~m#([-+]\d*\.?\d*)([pws])(?:/([0-9]+))?#)
{ $h{$2}= ($1 eq '+' ? 1 : $1 eq '-' ? -1 : $1) / ($3||1);
}
elsif ($v=~m/^[-+]\d+$/) { $h{n}=$v }
}
::setlocale(::LC_NUMERIC, '');
# smart alignment if alignment not specified and only widget or screen relative
if (!defined $h{p} && (defined $h{w} xor defined $h{s}) && !$h{n})
{ my $ws= $h{w} || $h{s} || 0;
$h{p}= $ws==0 ? 0 : $ws==1 ? -1 : -.5;
}
$h{$_}||=0 for qw/n p w s/;
my $x= $h{n} + $h{p}*$wp + $h{w}*$ww + $h{s}*$ws;
return $x;
}
sub HoverPopup
{ my $widget=shift;
delete $widget->{hover_timeout};
return 0 if $widget->isa('Gtk2::StatusIcon') && !OnStatusIcon($widget); # for statusicon, don't popup if no longer above icon
return 0 if $widget->{block_popup};
Popup($widget);
0;
}
sub Popup
{ my ($widget,$addtimeout)=@_;
my $self= $widget->{PoppedUpWindow};
$addtimeout=0 if $self && !$self->{destroy_timeout}; #don't add timeout if there wasn't already one
$self ||= Layout::Window::Popup->new($widget->{hover_layout},$widget);
return 0 unless $self;
$self->CancelDestroy;
$self->{destroy_timeout}=Glib::Timeout->add( $addtimeout,\&DestroyNow,$self) if $addtimeout;
$self->{check_timeout} ||= Glib::Timeout->add(400, \&CheckCursor, $self) if $widget->isa('Gtk2::StatusIcon') && !$addtimeout;
0;
}
sub set_hover
{ my $widget=$_[0];
if ($widget->isa('Gtk2::StatusIcon'))
{ $widget->set_has_tooltip(1);
$widget->signal_connect(query_tooltip => sub { return if $_[0]{hover_timeout}; &PreparePopup });
}
else
{ $widget->signal_connect(enter_notify_event => \&PreparePopup);
$widget->signal_connect(leave_notify_event => \&CancelPopup );
}
}
sub PreparePopup
{ my $widget=shift; #widget can be a statusicon
return 0 if $widget->{block_popup};
if (!$widget->{PoppedUpWindow})
{ my $delay=$widget->{hover_delay}||1000;
if (my $t=delete $widget->{hover_timeout}) { Glib::Source->remove($t); }
$widget->{hover_timeout}= Glib::Timeout->add($delay,\&HoverPopup, $widget);
}
else {Popup($widget)}
0;
}
sub CancelPopup
{ my $widget=shift;
if (my $t=delete $widget->{hover_timeout}) { Glib::Source->remove($t); }
if (my $self=$widget->{PoppedUpWindow})
{ $self->StartDestroy;
$self->{check_timeout} ||= Glib::Timeout->add(1000, \&CheckCursor, $self);
}
}
sub CancelDestroy
{ my $self=shift;
if (my $t=delete $self->{destroy_timeout}) { Glib::Source->remove($t); }
if (my $t=delete $self->{check_timeout}) { Glib::Source->remove($t); }
}
sub StartDestroy
{ my $self=shift;
$self->{destroy_timeout} ||= Glib::Timeout->add(300,\&DestroyNow,$self);
0;
}
sub DestroyNow
{ my $self=shift;
$self->CancelDestroy;
$self->close_window;
0;
}
package Layout::Embedded;
use base 'Gtk2::Container';
our @ISA;
push @ISA,'Layout';
sub new
{ my ($class,$opt)=@_;
my $layout=$opt->{layout};
my $def= $Layout::Layouts{$layout};
return undef unless $def;
my $self=bless Gtk2::VBox->new(0,0), $class;
$self->{SaveOptions}=\&SaveEmbeddedOptions;
$self->{group}=$opt->{group};
my %children_opt;
for my $child_key (grep m#./.#, keys %$opt)
{ my ($child,$key)=split "/",$child_key,2;
$children_opt{$child}{$key}= $opt->{$child_key};
}
%children_opt=( %children_opt, %{$opt->{children_opt}} ) if $opt->{children_opt};
$self->InitLayout($layout,\%children_opt);
$self->{tabicon}= $self->{tabicon} || $def->{Icon};
$self->{tabtitle}= $self->{tabtitle} || $def->{Title} || $def->{Name} || $layout;
$self->show_all;
return $self;
}
sub SaveEmbeddedOptions
{ my $self=shift;
my $opt=Layout::SaveWidgetOptions(values %{ $self->{widgets} }, values %{ $self->{PlaceHolders} });
return children_opt => $opt;
}
package Layout::Boxes;
our %Boxes=
( HB =>
{ New => sub { SHBox->new; },
#New => sub { Gtk2::HBox->new(::FALSE,0); },
Prefix => qr/([-_.0-9]*)/,
Pack => \&SBoxPack,
},
VB =>
{ New => sub { SVBox->new; },
#New => sub { Gtk2::VBox->new(::FALSE,0); },
Prefix => qr/([-_.0-9]*)/,
Pack => \&SBoxPack,
},
HP =>
{ New => sub { PanedNew('Gtk2::HPaned',$_[0]); },
Prefix => qr/([_+]*)/,
Pack => \&PanedPack,
},
VP =>
{ New => sub { PanedNew('Gtk2::VPaned',$_[0]); },
Prefix => qr/([_+]*)/,
Pack => \&PanedPack,
},
TB => #tabbed #deprecated
{ New => \&NewTB,
Prefix => qr/((?:"[^"]*[^\\]")|[^ ]*)\s+/,
Pack => \&PackTB,
},
NB => #tabbed 2
{ New => sub { Layout::NoteBook->new(@_); },
Pack => \&Layout::NoteBook::Pack,
EndInit => \&Layout::NoteBook::EndInit,
},
MB =>
{ New => sub { Gtk2::MenuBar->new },
Pack => sub { $_[0]->append($_[1]); },
},
SM => #submenu
{ New => sub { my $item=Gtk2::MenuItem->new($_[0]{label}); my $menu=Gtk2::Menu->new; $item->set_submenu($menu); return $item; },
Pack => sub { $_[0]->get_submenu->append($_[1]); },
},
BM => #button menu
{ New => sub { Layout::ButtonMenu->new(@_); },
Pack => sub { $_[0]->append($_[1]); },
},
EB =>
{ New => sub { my $self=Gtk2::Expander->new($_[0]{label}); $self->set_expanded($_[0]{expand}); $self->{SaveOptions}=sub { expand=>$_[0]->get_expanded; }; return $self; },
Pack => \&SimpleAdd,
},
FB =>
{ New => sub { SFixed->new; },
Prefix => qr/^(-?\.?\d+,-?\.?\d+(?:,\.?\d+,\.?\d+)?),?\s+/, # "5,4 " or "-5,.4,5,.2 "
Pack => \&Fixed_pack,
},
FR =>
{ New => sub { my $f=Gtk2::Frame->new($_[0]{label}); $f->set_shadow_type($_[0]{shadow}) if $_[0]{shadow};return $f; },
Pack => \&SimpleAdd,
},
SB =>
{ New => sub { my $sw=Gtk2::ScrolledWindow->new; },
Pack => sub { $_[0]->add_with_viewport($_[1]); },
},
AB =>
{ New => sub { my %opt=(xalign=>.5, yalign=>.5, xscale=>1, yscale=>1, %{$_[0]}); Gtk2::Alignment->new(@opt{qw/xalign yalign xscale yscale/});},
Pack => \&SimpleAdd,
},
WB =>
{ New => sub { Gtk2::EventBox->new; },
Pack => \&SimpleAdd,
},
);
sub SimpleAdd
{ $_[0]->add($_[1]);
}
sub NewTB
{ my ($opt)=@_;
my $nb=Gtk2::Notebook->new;
$nb->set_scrollable(::TRUE);
$nb->popup_enable;
#$nb->signal_connect( button_press_event => sub {return !::IsEventInNotebookTabs(@_);});
if (my $p=$opt->{tabpos}) { $nb->set_tab_pos($p); }
if (my $p=$opt->{page}) { $nb->{SetPage}=$p; }
$nb->{SaveOptions}=sub { page => $_[0]->get_current_page };
return $nb;
}
sub PackTB
{ my ($nb,$wg,$title)=@_;
$title=~s/^"// && $title=~s/"$//;
$nb->append_page($wg, Gtk2::Label->new($title) );
$nb->set_tab_reorderable($wg,::TRUE);
my $n=$nb->{SetPage}||0;
if ($n==($nb->get_n_pages-1)) {$wg->show; $nb->set_current_page($n); $nb->{DefaultFocus}=$wg; }
}
sub SBoxPack
{ my ($box,$wg,$opt)=@_;
my $pad= $opt=~m/([0-9]+)/ ? $1 : 0;
my $exp= $opt=~m/_/;
my $end= $opt=~m/-/;
my $fill=$opt!~m/\./;
if ($end) { $box->pack_end( $wg,$exp,$fill,$pad ); }
else { $box->pack_start( $wg,$exp,$fill,$pad ); }
if ($Gtk2::VERSION<1.163 || $Gtk2::VERSION==1.170) { $wg->{SBOX_packoptions}=[$exp,$fill,$pad, ($end ? 'end' : 'start')]; } #to work around memory leak (gnome bug #498334)
}
sub PanedPack
{ my ($paned,$wg,$opt)=@_;
my $expand= $opt=~m/_/;
my $shrink= $opt!~m/\+/;
if (!$paned->child1) {$paned->pack1($wg,$expand,$shrink);}
elsif (!$paned->child2) {$paned->pack2($wg,$expand,$shrink);}
else {warn "layout error : trying to pack more than 2 widgets in a paned container\n"}
}
sub PanedNew
{ my ($class,$opt)=@_;
my $self=$class->new;
::setlocale(::LC_NUMERIC, 'C');
($self->{size1},$self->{size2})= map $_+0, split /-|_/, $opt->{size} if defined $opt->{size}; # +0 to make the conversion to numeric while LC_NUMERIC is set to C
::setlocale(::LC_NUMERIC, '');
if (defined $self->{size1})
{ $self->set_position($self->{size1});
$self->set('position-set',1); # in case $self->{size1}==0 'position-set' is not set to true if child1's size is 0 (which is the case here as child1 doesn't exist yet)
}
$self->{SaveOptions}=sub { ::setlocale(::LC_NUMERIC, 'C'); my $s=$_[0]{size1}; $s.='-'. $_[0]{size2} if $_[0]{size2}; ::setlocale(::LC_NUMERIC, ''); return size => $s };
$self->signal_connect(size_allocate => \&Paned_size_cb ); #needed to correctly save/restore the handle position
return $self;
}
sub Paned_size_cb
{ my $self=shift;
my $max=$self->get('max-position');
return unless $max;
my $size1=$self->{size1};
my $size2=$self->{size2};
if (defined $size1 && defined $size2 && abs($max-$size1-$size2)>5 || $self->{need_resize})
{ my $not_enough;
if ($self->child1_resize && !$self->child2_resize) { $size1= ::max($max-$size2,0); $not_enough= $size2>$max; }
elsif ($self->child2_resize && !$self->child1_resize) { $size1= $max if $not_enough= $size1>$max; }
else { $size1= $max*$size1/($size1+$size2); }
if ($not_enough) #don't change the saved value if couldn't restore the size properly
{ $self->{need_resize}=1; # => will retry in a later size_allocate event unless the position is set manually
}
else
{ $self->set_position( $size1 );
$self->{size1}= $size1;
$self->{size2}= $max-$size1;
delete $self->{need_resize};
}
}
else { my $size1=$self->get_position; $self->{size1}=$size1; $self->{size2}=$max-$size1; delete $self->{need_resize}; }
}
sub Fixed_pack
{ my ($self,$wg,$opt)=@_;
if (my ($x,$y,$w,$h)= $opt=~m/^(-?\.?\d+),(-?\.?\d+)(?:,(\.?\d+),(\.?\d+))?/)
{ if ($1=~m/[-.]/ || $2=~m/[-.]/) { $wg->{SFixed_dynamic_pos}=[$x,$y]; $self->put($wg,0,0);}
else {$self->put($wg,$x,$y); }
if ($w||$h)
{ if ($w=~m/\./ || $h=~m/\./) { $wg->{SFixed_dynamic_size}=[$w,$h]; }
else
{ my ($w2,$h2)=$wg->get_size_request;
$wg->set_size_request($w||$w2,$h||$h2);
}
}
}
else { warn "Invalid position '$opt' for widget $wg\n" }
}
package SFixed;
use Glib::Object::Subclass
Gtk2::Fixed::,
signals =>
{ size_allocate => \&size_allocate,
};
sub size_allocate
{ my ($self,$alloc)=@_;
my ($ox,$oy,$w,$h)=$alloc->values;
my $border=$self->get_border_width;
$ox+=$border; $w-=$border*2;
$oy+=$border; $h-=$border*2;
for my $child ($self->get_children)
{ my ($x,$y)=$self->child_get_property($child,qw/x y/);
if (my $ref=$child->{SFixed_dynamic_pos})
{ my ($x2,$y2)=@$ref;
$x=~m/\./ and $x*=$w;
$x2=~m/\./ and $x2=int($x2*$w);
$y2=~m/\./ and $y2=int($y2*$h);
$x2=~m/^-/ and $x2+=$w;
$y2=~m/^-/ and $y2+=$h;
if ($x2!=$x || $y2!=$y) { $self->move($child,$x=$x2,$y=$y2); }
}
my ($ww,$wh);
if (my $ref=$child->{SFixed_dynamic_size})
{ ($ww,$wh)=@$ref;
$ww=~m/\./ and $ww*=$w;
$wh=~m/\./ and $wh*=$h;
}
$ww||=$child->size_request->width;
$wh||=$child->size_request->height;
$child->size_allocate(Gtk2::Gdk::Rectangle->new($ox+$x, $oy+$y, $ww,$wh));
}
}
package Layout::NoteBook;
use base 'Gtk2::Notebook';
our @contextmenu=
( { label => _"New list", code => sub { $_[0]{self}->newtab('EditList',1,{songarray=>''}); }, type=> 'L', stockicon => 'gtk-add', },
{ label => _"Open Queue", code => sub { $_[0]{self}->newtab('QueueList',1); }, type=> 'L', stockicon => 'gmb-queue',
test => sub { !grep $_->{name} eq 'QueueList', $_[0]{self}->get_children } },
{ label => _"Open Playlist", code => sub { $_[0]{self}->newtab('PlayList',1); }, type=> 'L', stockicon => 'gtk-media-play',
test => sub { !grep $_->{name} eq 'PlayList', $_[0]{self}->get_children } },
{ label => _"Open existing list", code => sub { $_[0]{self}->newtab('EditList',1, {songarray=>$_[1]}); }, type=> 'L',
submenu => sub { my %h; $h{ $_->{array}->GetName }=1 for grep $_->{name}=~m/^EditList\d*$/, $_[0]{self}->get_children; return [grep !$h{$_}, ::GetListOfSavedLists()]; } },
{ label => _"Open page layout", code => sub { $_[0]{self}->newtab('@'.$_[1],1); }, type=> 'P',
submenu => sub { Layout::get_layout_list('P') }, submenu_tree=>1, },
{ label => _"Open context page", type=> 'C',
submenu => sub { $_[0]{self}->make_widget_list('context page'); }, submenu_reverse=>1,
code => sub { $_[0]{self}->newtab($_[1],1); },
},
{ label => _"Delete list", code => sub { $_[0]{page}->DeleteList; }, type=> 'L', istrue=>'page', test => sub { $_[0]{page}{name}=~m/^EditList\d*$/; } },
{ label => _"Rename", code => \&pagerename_cb, istrue => 'rename',},
{ label => _"Close", code => sub { $_[0]{self}->close_tab($_[0]{page},1); }, istrue => 'close', stockicon=> 'gtk-close',},
);
our @DefaultOptions=
( closebuttons => 1,
tablist => 1,
newbutton => 'end',
);
sub new
{ my ($class,$opt)=@_;
my $self= bless Gtk2::Notebook->new, $class;
%$opt=( @DefaultOptions, %$opt );
$self->set_scrollable(1);
$self->set_tab_hborder(0);
$self->set_tab_vborder(0);
if (my $tabpos=$opt->{tabpos})
{ ($tabpos,$self->{angle})= $tabpos=~m/^(left|right|top|bottom)?(90|180|270)?/;
$self->set_tab_pos($tabpos) if $tabpos;
}
$self->set_show_tabs(0) if $opt->{hidetabs};
$opt->{typesubmenu}='LPC' unless exists $opt->{typesubmenu};
$self->{$_}=$opt->{$_} for qw/group default_child match pages page typesubmenu closebuttons tablist/;
for my $class (qw/list context layout/) # option begining with list_ / context_ / layout_ will be passed to children of this class
{ my @opt1;
if (my $optkeys=$opt->{'options_for_'.$class}) #no need for a prefix for these options
{ push @opt1, $_=> $opt->{$_} for grep exists $opt->{$_}, split / /,$optkeys;
}
push @opt1, $_=> $opt->{$class.'_'.$_} for map m/^${class}_(.+)/, keys %$opt;
$self->{children_opt1}{$class}={ @opt1 };
}
$self->signal_connect(switch_page => \&SwitchedPage);
$self->signal_connect(button_press_event => \&button_press_event_cb);
::Watch($self, SavedLists=> \&SavedLists_changed);
::Watch($self, Widgets => \&Widgets_changed_cb);
$self->{groupcount}=0;
$self->{SaveOptions}=\&SaveOptions;
$self->{widgets}={};
$self->{widgets_opt}= $opt->{page_opt} ||={};
if (my $bl=$opt->{blacklist})
{ $self->{blacklist}{$_}=undef for split / +/, $bl;
}
$opt->{newbutton}=0 unless *Gtk2::Notebook::set_action_widget{CODE}; # Gtk2::Notebook::set_action_widget requires gtk+ >= 2.20 and perl-Gtk2 >= 1.23
if ($opt->{typesubmenu} && $opt->{newbutton} && $opt->{newbutton} ne 'none') # add a button next to the tabs to show new-tab menu
{ my $button= ::NewIconButton('gtk-add');
$button->signal_connect(button_press_event => \&newbutton_cb);
$button->signal_connect(clicked => \&newbutton_cb);
$button->show_all;
my $pos= $opt->{newbutton} eq 'start' ? 'start' : 'end';
$self->set_action_widget($button,$pos);
}
return $self;
}
sub SaveOptions
{ my $self=shift;
my $i= $self->get_current_page;
my @children= $self->get_children;
my @dyn_widgets=values %{ $self->{widgets} };
my @pages;
for my $child (@children)
{ my $name=$child->{name};
$name='+'.$name if grep $_==$child, @dyn_widgets;
push @pages,$name;
}
my @opt=
( page => $pages[$i],
pages => join(' ',@pages),
page_opt=> Layout::SaveWidgetOptions( @dyn_widgets ),
);
if (my $bl=$self->{blacklist})
{ push @opt, blacklist=>join (' ',sort keys %$bl) if keys %$bl;
}
return @opt;
}
sub EndInit
{ my $self=shift;
my %pagewidget;
$pagewidget{ $_->{name} }=$_ for $self->get_children;
if (my $pages=delete $self->{pages})
{ my @pagelist=split / +/,$pages;
$pagewidget{"+$_"}=$self->newtab($_) for map m/^\+(.+)$/, @pagelist; #recreate dynamic pages (page name begin with +)
my $i=0;
$self->reorder_child($_,$i++) for grep $_, map $pagewidget{$_}, @pagelist; #reorder pages to the saved order
}
if (my $name=delete $self->{page}) #restore selected tab
{ if (my $page= $pagewidget{$name})
{ $page->show; #needed to set as current page
$self->set_current_page( $self->page_num($page) );
}
}
$self->Widgets_changed_cb('init') if $self->{match};
$self->insert_default_page unless $self->get_children;
}
sub newtab
{ my ($self,$name,$setpage,$opt2)=@_;
$self->SaveOptions if $setpage; #used to save default options of SongTree/SongList before creating a new one
my $wtype= $name; $wtype=~s/\d+$//;
$wtype= $Layout::Widgets{$wtype} || {};
my $wclass= $wtype->{issonglist} ? 'list' : $name=~m/^@/ ? 'layout' : 'context';
my $group=$self->{group};
$group= 'Global('.::refaddr($self).'-'.$self->{groupcount}++.')' if $wclass eq 'list'; # give songlist/songtree their own group
if ($opt2) #new widget => use a new name not already used
{ my $n=0;
$n++ while $self->{widgets}{$name.$n} || $self->{widgets_opt}{$name.$n};
$name.=$n;
}
else { $opt2= $self->{widgets_opt}{$name}; }
return if $self->{widgets}{$name};
my $opt1= $self->{children_opt1}{$wclass} || {};
my $widget= Layout::NewWidget($name,$opt1,$opt2, {default_group=>$group});
return unless $widget;
$self->{widgets}{$name}=$widget;
$widget->{tabcanclose}=1;
delete $self->{blacklist}{$name};
$self->Pack($widget);
$widget->show_all;
$self->set_current_page( $self->get_n_pages-1 ) if $setpage; #set current page to the new page
return $widget;
}
sub Pack
{ my ($self,$wg)=@_;
if (delete $self->{chooser_mode}) { $self->remove($_) for $self->get_children; }
my $angle= $self->{angle} || 0;
my $label= $wg->{tabtitle};
if (!defined $label) { $label= $wg->{name} } #FIXME ? what to do if no tabtitle given
elsif (ref $label eq 'CODE') { $label= $label->($wg); }
elsif ($wg->can('DynamicTitle')) { $label= $wg->DynamicTitle($label); }
$label=Gtk2::Label->new($label) unless ref $label;
$label->set_angle($angle) if $angle;
::weaken( $wg->{tab_page_label}=$label ) if $wg->{tabrename};
# set base gravity to auto so that rotated tabs handle vertical scripts (asian languages) better
$label->get_pango_context->set_base_gravity('auto');
$label->signal_connect(hierarchy_changed=> sub { $_[0]->get_pango_context->set_base_gravity('auto'); }); # for some reason (gtk bug ?) the setting is reverted when the tab is dragged, so this re-set it
my $icon= $wg->{tabicon};
$icon=Gtk2::Image->new_from_stock($icon,'menu') if defined $icon;
my $close;
if ($wg->{tabcanclose} && $self->{closebuttons})
{ $close=Gtk2::Button->new;
$close->set_relief('none');
$close->can_focus(0);
::weaken( $close->{page}=$wg );
$close->signal_connect(clicked => sub {my $page=$_[0]{page}; my $self=$page->parent; $self->close_tab($page,1);});
$close->add(Gtk2::Image->new_from_file(::PIXPATH.'smallclosetab.png'));
$close->set_size_request(Gtk2::IconSize->lookup('menu'));
$close->set_border_width(0);
}
my $tab= $angle%180 ? Gtk2::VBox->new(0,0) : Gtk2::HBox->new(0,0);
my @icons= $angle%180 ? ($close,0,$icon,4) : ($icon,4,$close,0);
my ($i,$pad)=splice @icons,0,2;
$tab->pack_start($i,0,0,$pad) if $i;
$tab->pack_start($label,1,1,2);
($i,$pad)=splice @icons,0,2;
$tab->pack_start($i,0,0,$pad) if $i;
$self->append_page($wg,$tab);
$self->set_tab_reorderable($wg,1);
$tab->show_all;
}
sub insert_default_page
{ my $self=shift;
return if $self->get_children;
$self->newtab( $self->{default_child} ) if $self->{default_child};
::IdleDo('5_create_chooser_page',500, \&create_chooser_page, $self) if !$self->get_children && $self->{match};
}
sub close_tab
{ my ($self,$page,$manual)=@_;
my $name=$page->{name};
delete $self->{widgets}{$name};
if ($manual && $self->{match} && $Layout::Widgets{$name} && $Layout::Widgets{$name}{autoadd_type}) { $self->{blacklist}{$name}=undef }
my $opt=$self->{widgets_opt};
my $pageopt= Layout::SaveWidgetOptions($page);
%$opt= ( %$opt, %$pageopt );
$self->remove($page);
delete $self->{DefaultFocus} if $self->{DefaultFocus} && $self->{DefaultFocus}==$page;
$self->insert_default_page unless $self->get_children;
}
sub SavedLists_changed #remove EditList tab if corresponding list has been deleted
{ my ($self,$name,$action)=@_;
return unless $action && $action eq 'remove';
my @remove=grep $_->{name}=~m/^EditList\d*$/ && !defined $_->{array}->GetName, $self->get_children;
$self->close_tab($_) for @remove;
}
sub newbutton_cb
{ my $self= ::find_ancestor($_[0],__PACKAGE__);
::PopupContextMenu(\@contextmenu, { self=>$self, type=>$self->{typesubmenu}, usemenupos=>1 } );
1;
}
sub button_press_event_cb
{ my ($self,$event)=@_;
return 0 if $event->button != 3;
return 0 unless ::IsEventInNotebookTabs($self,$event); #to make right-click on tab arrows work
my $pagenb=$self->get_current_page;
my $page=$self->get_nth_page($pagenb);
#my $listname= $page? $page->{tabbed_listname} : undef;
my @menu;
my @opt=
( self=> $self, page=> $page, type => $self->{typesubmenu},
'close'=> $page->{tabcanclose}, 'rename' => $page->{tabrename},
);
push @menu, @contextmenu;
if ($self->{tablist} && !$self->{chooser_mode})
{ push @menu, { separator=>1 };
for my $page ($self->get_children) #append page list to menu
{ my $label= $page->{tab_page_label} ? $page->{tab_page_label}->get_text : $page->{tabtitle};
my $icon= $page->{tabicon};
my $i= $self->page_num($page);
my $cb= sub { $_[0]{self}->set_current_page($i); };
push @menu, {label=>$label, stockicon=>$icon, code=> $cb, };
}
}
::PopupContextMenu(\@menu, { @opt } );
return 1;
}
sub pagerename_cb
{ my $page=$_[0]{page};
my $tab=$_[0]{self}->get_tab_label($page);
my $renamesub=$_[0]{'rename'};
my $label=$page->{tab_page_label};
my $entry=Gtk2::Entry->new;
$entry->set_has_frame(0);
$entry->set_inner_border(undef) if *Gtk2::Entry::set_inner_border{CODE}; #Gtk2->CHECK_VERSION(2,10,0);
$entry->set_text( $label->get_text );
$entry->set_size_request( 20+$label->allocation->width ,-1);
$_->hide for grep !$_->isa('Gtk2::Image'), $tab->get_children;
$tab->pack_start($entry,::FALSE,::FALSE,2);
$entry->grab_focus;
$entry->show_all;
$entry->signal_connect(key_press_event => sub #abort if escape
{ my ($entry,$event)=@_;
return 0 unless Gtk2::Gdk->keyval_name( $event->keyval ) eq 'Escape';
$entry->set_text('');
$entry->set_sensitive(0); #trigger the focus-out event
1;
});
$entry->signal_connect(activate => sub {$_[0]->set_sensitive(0)}); #trigger the focus-out event
$entry->signal_connect(populate_popup => sub { ::weaken($_[0]{popupmenu}=$_[1]); });
$entry->signal_connect(focus_out_event => sub
{ my $entry=$_[0];
my $popupmenu= delete $entry->{popupmenu};
return 0 if $entry->get_display->pointer_is_grabbed && $popupmenu && $popupmenu->mapped; # prevent error when context menu of the entry pops-up
my $new=$entry->get_text;
$tab->remove($entry);
$_->show for $tab->get_children;
if ($new ne '') #user has entered new name -> do the renaming
{ $renamesub->($label,$new);
}
0;
});
}
sub create_chooser_page
{ my $self=shift;
return if $self->get_children && !$self->{chooser_mode};
$self->remove($_) for $self->get_children; #remove a previous version of this page
my $list= $self->make_widget_list;
return unless keys %$list;
$self->{chooser_mode}=1;
my $cb=sub { my $self=::find_ancestor($_[0],__PACKAGE__); $self->newtab($_[1]); };
my $bbox=Gtk2::VButtonBox->new;
$bbox->set_layout('start');
for my $name (sort { $list->{$a} cmp $list->{$b} } keys %$list)
{ my $button=Gtk2::Button->new($list->{$name});
$button->signal_connect(clicked=> $cb,$name);
$bbox->add($button);
}
$bbox->show_all;
$bbox->{name}='';
$self->append_page($bbox,_"Choose page to open");
}
sub make_widget_list
{ my ($self,$match,@names)=@_;
$match ||= $self->{match};
return unless $match;
my @match= $match=~m/(?<!-)\b(\w+)\b/g; #words not preceded by -
my @matchnot= $match=~m/-(\w+)\b/g; #words preceded by -
my $wdef=\%Layout::Widgets;
@names= @names? grep $wdef->{$_}, @names : keys %$wdef;
@names=grep $wdef->{$_}{autoadd_type}, @names;
my %ok;
for my $name (@names)
{ next if grep $name eq $_->{name}, $self->get_children; #or use $self->{widgets}{$name} ?
my $autoadd=$wdef->{$name}{autoadd_type};
my %h; $h{$_}=1 for split / +/,$autoadd;
next if grep !$h{$_}, @match;
next if grep $h{$_}, @matchnot;
$ok{$name}=$wdef->{$name}{tabtitle};
}
return \%ok;
}
sub Widgets_changed_cb #new or removed widgets => check if a widget should be added or removed
{ my ($self,$changetype,@widgets)=@_;
if ($changetype eq 'remove')
{ for my $name (@widgets)
{ $self->close_tab($_) for grep $name eq $_->{name}, $self->get_children;
}
return
}
my $match=$self->{match};
return unless $match;
@widgets=keys %Layout::Widgets unless @widgets;
@widgets=sort grep $Layout::Widgets{$_}{autoadd_type}, @widgets;
for my $name (@widgets)
{ my $ref=$Layout::Widgets{$name};
my $add;
if (my $autoadd= $ref->{autoadd_type})
{ #every words in $match must be in $autoadd, except for words starting with - that must not
my %h; $h{$_}=1 for split / +/,$autoadd;
next if grep !$h{$_}, $match=~m/(?<!-)\b(\w+)\b/g;
next if grep $h{$_}, $match=~m/-(\w+)\b/g;
$add=1;
}
if (my $opt=$ref->{autoadd_option}) { $add=$::Options{$opt} }
my @already= grep $name eq $_->{name}, $self->get_children;
if ($add)
{ next if exists $self->{blacklist}{$name};
$self->newtab($name,0,undef,1) unless @already;
}
else
{ $self->close_tab($_) for @already;
}
}
::IdleDo('5_create_chooser_page',500, \&create_chooser_page, $self) if !$self->get_children || $self->{chooser_mode};
}
sub SwitchedPage
{ my ($self,undef,$pagenb)=@_;
delete $self->{DefaultFocus};
if (defined(my $group=delete $self->{active_group}))
{ ::UnWatch($self,'SelectedID_'.$group);
::UnWatch($self,'Selection_'.$group);
#::UnWatchFilter($self,$group);
}
my $page=$self->get_nth_page($pagenb);
::weaken( $self->{DefaultFocus}=$page );
my $metagroup= $self->{group};
return if !$page->{group} || $page->{group} eq $metagroup;
my $group= $self->{active_group}= $page->{group};
my $ID= ::GetSelID($group);
::HasChangedSelID($metagroup,$ID) if defined $ID;
if (my $songlist=$SongList::Common::Register{$group})
{ $songlist->RegisterGroup($self->{group});
::HasChanged('Selection_'.$self->{group});
::Watch($self,'Selection_'.$group, sub { ::HasChanged('Selection_'.$_[0]->{group}); });
::HasChanged('SongArray',$songlist->{array},'proxychange');
}
# FIXME can't use WatchSelID, should special-case special groups : Play Recent\d *Next\d* ...
::Watch($self,'SelectedID_'.$group, sub { my ($self,$ID)=@_; ::HasChangedSelID($self->{group},$ID) if defined $ID; });
#::WatchFilter($self,$group, sub { });
}
package Layout::PlaceHolder;
our %PlaceHolders=
( #ContextPages =>
#{ event_Widgets => \&Widgets_changed_cb,
# match => 'context page',
# init => sub { $_[0]->Widgets_changed_cb('init'); },
#},
ExtraButtons =>
{ event_Widgets => \&Widgets_changed_cb,
match => 'button main',
init => sub { $_[0]->Widgets_changed_cb('init'); },
},
);
sub new
{ my ($class,$boxfunc,$box,$ph,$packoptions)=@_;
my $name= $ph->{name};
$name=~s/\d+$//;
my $type= $PlaceHolders{$name};
unless ($type)
{ return Layout::PlaceHolder::Single->new($boxfunc,$box,$ph,$packoptions);
}
bless $ph,$class;
::weaken( $ph->{boxwidget}=$box );
$ph->{widgets}={};
$ph->{$_}||=$type->{$_} for qw/match/;
$ph->{SaveOptions}=\&SaveOptions;
$ph->{widgets_opt}=delete $ph->{opt2}{widgets_opt};
for my $event (grep m/^event_/, keys %$type)
{ my $cb= $type->{$event};
$event=~s/^event_//;
::Watch($ph, $event, $cb);
}
$ph->{packsub}= $boxfunc->{Pack};
$ph->{packoptions}= $packoptions;
if (my $init=$type->{init}) { $init->($ph) } #used to create children at creation time for some placeholders
return $ph;
}
sub DESTROY
{ ::UnWatch_all($_[0]);
}
sub SaveOptions
{ my $self=shift;
my $opt= Layout::SaveWidgetOptions(values %{$self->{widgets}});
return unless keys %$opt;
return widgets_opt => $opt;
}
sub AddWidget
{ my ($ph,$name)=@_;
return if $ph->{widgets}{$name};
my $widget= Layout::NewWidget($name,$ph->{opt1},$ph->{widgets_opt}{$name}, { default_group => $ph->{group} });
return unless $widget;
$ph->{widgets}{$name}= $widget;
$ph->{packsub}->($ph->{boxwidget},$widget, $ph->{packoptions});
$widget->show_all;
return $widget;
}
sub RemoveWidget
{ my ($ph,$name)=@_;
$name=$name->{name} if ref $name;
my $widget= delete $ph->{widgets}{$name};
return unless $widget;
my $opt=$ph->{widgets_opt}||={};
%$opt= ( %$opt, %{ Layout::SaveWidgetOptions($widget) } );
$ph->{boxwidget}->remove($widget);
}
sub Widgets_changed_cb #new or removed widgets => check if a widget should be added or removed
{ my ($ph,$changetype,@widgets)=@_;
@widgets=keys %Layout::Widgets unless @widgets;
@widgets=sort grep $Layout::Widgets{$_}{autoadd_type}, @widgets;
my $match=$ph->{match};
for my $name (@widgets)
{ my $ref=$Layout::Widgets{$name};
my $add= $changetype ne 'remove' ? 1 : 0;
if (my $autoadd= $ref->{autoadd_type})
{ #every words in $match must be in $autoadd, except for words starting with - that must not
my %h; $h{$_}=1 for split / +/,$autoadd;
next if grep !$h{$_}, $match=~m/(?<!-)\b(\w+)\b/g;
next if grep $h{$_}, $match=~m/-(\w+)\b/g;
}
if (my $opt=$ref->{autoadd_option}) { $add=$::Options{$opt} }
if ($add)
{ my $widget= $ph->AddWidget($name);
}
else
{ $ph->RemoveWidget($name);
}
}
}
sub Options_changed
{ my ($ph,$option)=@_;
return unless exists $ph->{watchoptions}{$option};
$ph->Widgets_changed_cb('optchanged');
}
package Layout::PlaceHolder::Single;
sub new
{ my ($class,$boxfunc,$box,$ph,$packoptions)=@_;
bless $ph,$class;
::weaken( $ph->{boxwidget}=$box );
::Watch($ph, Widgets => \&Widgets_changed_cb);
$ph->{packsub}= $boxfunc->{Pack};
$ph->{packoptions}= $packoptions;
return $ph;
}
sub DESTROY
{ ::UnWatch_all(shift);
}
sub Widgets_changed_cb
{ my ($ph,$changetype,@widgets)=@_;
my $name=$ph->{name};
$name=~s/\d+$//;
return unless grep $name eq $_, @widgets;
if ($changetype eq 'new' && !$ph->{widget})
{ my $widget= Layout::NewWidget($ph->{name},$ph->{opt1},$ph->{opt2}, { default_group => $ph->{group} });
return unless $widget;
$ph->{widget}= $widget;
$ph->{SaveOptions}=\&SaveOptions;
$ph->{packsub}->($ph->{boxwidget},$widget, $ph->{packoptions});
$widget->show_all;
}
elsif ($changetype eq 'remove' && $ph->{widget})
{ my $widget= delete $ph->{widget};
$ph->{opt2}= Layout::SaveWidgetOptions($widget);
$ph->{boxwidget}->remove($widget);
delete $ph->{SaveOptions};
}
}
sub SaveOptions
{ my $ph=shift;
Layout::SaveWidgetOptions($ph->{widget});
}
package Layout::Button;
use base 'Gtk2::Bin';
our @default_options= (button=>1, relief=>'none', size=> Layout::SIZE_BUTTONS, ellipsize=> 'none', );
sub new
{ my ($class,$opt,$ref)=@_;
%$opt=( @default_options, %$opt );
my $isbutton= $opt->{button};
my $self;
my $activate= $opt->{activate};
if ($isbutton)
{ $self=Gtk2::Button->new;
$self->set_relief($opt->{relief});
$self->{clicked_cmd}= $activate;
$self->signal_connect(clicked => \&clicked_cb);
}
else
{ $self=Gtk2::EventBox->new;
$self->set_visible_window(0);
$opt->{click} ||= $activate;
}
bless $self, $class;
my $text= $opt->{text} || $opt->{label};
my $stock= $opt->{stock};
if (!ref $stock && $ref->{'state'})
{ my $default= $ref->{stock};
my %hash;
%hash = %$default if ref $default eq 'HASH'; #make a copy of the default setting if it is a hash
# extract icon(s) for each state using format : "state1: icon1 facultative_icon2 state2: icon3"
$hash{$1}=$2 while $stock=~s/(\w+) *: *([^:]+?) *$//;
$stock=\%hash;
#if default setting is a function, use a function that look in the hash, and fallback to the default function (this is the case for Queue and VolumeIcon widgets)
$stock= sub { $hash{$_[0]} || &$default } if ref $default eq 'CODE';
}
$self->{state}=$ref->{state} if $ref->{state};
if ($opt->{skin})
{ my $skin=Skin->new($opt->{skin},$self,$opt);
$self->signal_connect(expose_event => \&Skin::draw,$skin);
$self->{skin}=1; # will force a repaint on stock state change
$self->set_app_paintable(1); #needed ?
if (0 && !$isbutton && $opt->{shape}) #mess up button-press cb TESTME
{ $self->{shape}=1;
}
}
elsif ($stock)
{ $self->{stock}=$stock;
$self->{size}= $opt->{size};
my $img= $self->{img}= Gtk2::Image->new;
$img->set_size_request(Gtk2::IconSize->lookup($self->{size})); #so that it always request the same size, even when no icon
if ($opt->{with_text})
{ my $hbox=Gtk2::HBox->new(0,2);
my $label= $self->{label}= Gtk2::Label->new;
my $ellip= $opt->{ellipsize};
$ellip='end' if $ellip eq '1';
$label->set_ellipsize($ellip);
$self->{string}= $text || $opt->{tip};
$self->{markup}= $opt->{markup} || ($opt->{size} eq 'menu' ? "<small>%s</small>" : "%s");
$hbox->pack_start($img,0,0,0);
$hbox->pack_start($label,1,1,0);
$self->add($hbox);
}
else { $self->add($img); }
$self->{EndInit}=\&UpdateStock;
}
elsif (defined $text) { $self->add( Gtk2::Label->new($text) ); }
return $self;
}
sub clicked_cb
{ my $self=$_[0];
my $sub=$self->{clicked_cmd};
return 0 unless $sub;
if (ref $sub) {&$sub}
else { ::run_command($self,$sub) }
1;
}
sub UpdateStock
{ my ($self,undef,$index)=@_;
my $stock=$self->{stock};
if (my $state=$self->{state})
{ $state=&$state;
$stock = (ref $stock eq 'CODE')? $stock->($state) : $stock->{$state};
}
if ($stock=~m/ /)
{ $stock= (split /\s+/,$stock)[ $index || 0 ];
$stock='' if $stock eq '.'; #needed ? the result is the same : no icon
unless (exists $self->{hasenterleavecb})
{ $self->{hasenterleavecb}=undef;
$self->signal_connect(enter_notify_event => \&UpdateStock,1);
$self->signal_connect(leave_notify_event => \&UpdateStock);
}
}
$self->{img}->set_from_stock($stock,$self->{size});
if (my $l=$self->{label})
{ my $string=$self->{string};
$string= $string->() if ref $string eq 'CODE';
$l->set_markup_with_format($self->{markup},$string);
}
0;
}
package Layout::Label;
use base 'Gtk2::EventBox';
use constant INCR => 1; #scroll increment in pixels
our @default_options= ( xalign=>0, yalign=>.5, );
sub new
{ my ($class,$opt)=@_;
%$opt=( @default_options, %$opt );
my $self = bless Gtk2::EventBox->new, $class;
my $label=Gtk2::Label->new;
$label->set_alignment($opt->{xalign},$opt->{yalign});
$self->set_visible_window(0);
for (qw/markup markup_empty autoscroll interval/)
{ $self->{$_}=$opt->{$_} if exists $opt->{$_};
}
my $font= $opt->{font} && Gtk2::Pango::FontDescription->from_string($opt->{font});
$label->modify_font($font) if $font;
if (my $color= $opt->{color} || $opt->{DefaultFontColor})
{ $label->modify_fg('normal', Gtk2::Gdk::Color->parse($color) );
}
$self->add($label);
#$self->signal_connect(enter_notify_event => sub {$_[0]->set_markup('<u>'.$_[0]->child->get_label.'</u>')});
#$self->signal_connect(leave_notify_event => sub {my $m=$_[0]->child->get_label; $m=~s#^<u>##;$m=~s#</u>$##; $_[0]->set_markup($m)});
$self->{expand_max}= $opt->{maxwidth} || -1 if $opt->{expand_max};
my $minsize= $opt->{minsize};
if (my $el=$opt->{ellipsize})
{ $label->set_ellipsize($el);
$minsize=undef;
}
if ($minsize && $minsize=~m/^\d+p?$/)
{ unless ($minsize=~s/p$//)
{ my $lay=$label->create_pango_layout( 'X' x $minsize );
$lay->set_font_description($font) if $font;
($minsize)=$lay->get_pixel_size;
}
$self->set_size_request($minsize,-1);
$label->signal_connect(expose_event => \&expose_cb);
if ($self->{autoscroll})
{ $self->{interval} ||=50; # default to a scroll every 50ms
$self->signal_connect(size_allocate => \&restart_scrollcheck);
}
else # scroll when mouse is over it
{ $self->{interval} ||=20; # default to a scroll every 20ms
$self->signal_connect(enter_notify_event => \&enter_leave_cb, INCR());
$self->signal_connect(leave_notify_event => \&enter_leave_cb,-INCR());
}
}
elsif (defined $opt->{initsize})
{ #$label->set_size_request($label->create_pango_layout( $opt->{initsize} )->get_pixel_size);
my $lay=$label->create_pango_layout( $opt->{initsize} );
$lay->set_font_description($font) if $font;
$label->set_size_request($lay->get_pixel_size);
$self->{resize}=1;
}
if (exists $opt->{markup})
{ my $m=$opt->{markup};
if (my @fields=::UsedFields($m))
{ $self->{EndInit}=\&init; # needs $self->{group} set before this can be done
}
else { $self->set_markup($m) }
}
elsif (exists $opt->{text})
{ $label->set_text($opt->{text});
}
return $self;
}
sub init
{ my $self=shift;
::WatchSelID($self,\&update_text);
update_text($self,::GetSelID($self));
}
sub update_text
{ my ($self,$ID)=@_;
if ($self->{markup})
{ my $markup= defined $ID ? ::ReplaceFieldsAndEsc( $ID,$self->{markup} ) :
defined $self->{markup_empty} ? $self->{markup_empty} :
'';
$self->set_markup($markup);
}
}
sub set_label
{ my $label=$_[0]->child; $label->set_label($_[1]); $label->{dx}=0;
$_[0]->checksize;
}