1# Copyright (C) 2005-2015 Quentin Sculo <squentin@free.fr> 2# 3# This file is part of Gmusicbrowser. 4# Gmusicbrowser is free software; you can redistribute it and/or modify 5# it under the terms of the GNU General Public License version 3, as 6# published by the Free Software Foundation. 7 8use strict; 9use warnings; 10 11package Browser; 12use constant { TRUE => 1, FALSE => 0, }; 13 14our @MenuPlaying= 15( { label => _"Follow playing song", code => sub { $_[0]{songlist}->FollowSong if $_[0]{songlist}->{follow}; }, toggleoption => 'songlist/follow' }, 16 { label => _"Filter on playing Album", code => sub { ::SetFilter($_[0]{songlist}, Songs::MakeFilterFromID('album',$::SongID) ) if defined $::SongID; }}, 17 { label => _"Filter on playing Artist", code => sub { ::SetFilter($_[0]{songlist}, Songs::MakeFilterFromID('artists',$::SongID) )if defined $::SongID; }}, 18 { label => _"Filter on playing Song", code => sub { ::SetFilter($_[0]{songlist}, Songs::MakeFilterFromID('title',$::SongID) ) if defined $::SongID; }}, 19 { label => _"Use the playing filter", code => sub { ::SetFilter($_[0]{songlist}, $::PlayFilter ); }, test => sub {::GetSonglist($_[0]{songlist})->{mode} ne 'playlist'}}, #FIXME if queue use queue, if $ListMode use list 20 { label => _"Recent albums", submenu => sub { my $sl=$_[0]{songlist};my @gid= ::uniq( Songs::Map_to_gid('album',$::Recent) ); $#gid=19 if $#gid>19; my $m=::PopupAA('album',nosort=>1,nominor=>1,widget => $_[0]{self}, list=>\@gid, cb=>sub { ::SetFilter($sl, $_[0]{filter}); }); return $m; } }, 21 { label => _"Recent artists", submenu => sub { my $sl=$_[0]{songlist};my @gid= ::uniq( Songs::Map_to_gid('artist',$::Recent) ); $#gid=19 if $#gid>19; my $m=::PopupAA('artists',nosort=>1,nominor=>1,widget => $_[0]{self}, list=>\@gid, cb=>sub { ::SetFilter($sl, $_[0]{filter}); }); return $m; } }, 22 { label => _"Recent songs", submenu_use_markup => 1, submenu_ordered_hash => 1, submenu_reverse=>1, 23 submenu => sub { my @ids=@$::Recent; $#ids=19 if $#ids>19; return [map {$_, ::ReplaceFieldsAndEsc($_, ::__x( _"{song} by {artist}", song => "<b>%S</b>%V", artist => "%a"))} @ids]; }, 24 code => sub { ::SetFilter($_[0]{songlist}, Songs::MakeFilterFromID('title',$_[1]) ); }, }, 25); 26 27sub makeFilterBox 28{ my $box=Gtk2::HBox->new; 29 my $FilterWdgt=GMB::FilterBox->new 30 ( sub { my $filt=shift; ::SetFilter($box,$filt); }, 31 undef, 32 'title:si:', 33 _"Edit filter..." => sub 34 { ::EditFilter($box,::GetFilter($box),undef,sub {::SetFilter($box,$_[0]) if defined $_[0]}); 35 }); 36 my $okbutton=::NewIconButton('gtk-apply',undef,sub {$FilterWdgt->activate},'none'); 37 $okbutton->set_tooltip_text(_"apply filter"); 38 $box->pack_start($FilterWdgt, FALSE, FALSE, 0); 39 $box->pack_start($okbutton, FALSE, FALSE, 0); 40 return $box; 41} 42 43sub makeLockToggle 44{ my $opt=$_[0]; 45 my $toggle=Gtk2::ToggleButton->new; 46 $toggle->set_relief( $opt->{relief} ) if $opt->{relief}; 47 $toggle->add(Gtk2::Image->new_from_stock('gmb-lock','menu')); 48 #$toggle->set_active(1) if $self->{Filter0}; 49 $toggle->signal_connect( clicked =>sub 50 { my $self=$_[0]; 51 return if $self->{busy}; 52 my $f=::GetFilter($self,0); 53 my $empty=Filter::is_empty($f); 54 if ($empty) { ::SetFilter($self,::GetFilter($self),0); } 55 else { ::SetFilter($self,undef,0); } 56 }); 57 $toggle->signal_connect (button_press_event => sub 58 { my ($self,$event)=@_; 59 return 0 unless $event->button==3; 60 ::SetFilter($self,::GetFilter($self),0); 61 1; 62 }); 63 ::set_drag($toggle, dest => [::DRAG_FILTER,sub {::SetFilter($_[0],$_[2],0);}]); 64 ::WatchFilter($toggle,$opt->{group},sub 65 { my ($self,undef,undef,$group)=@_; 66 my $filter=$::Filters{$group}[0+1]; #filter for level 0 67 my $empty=Filter::is_empty($filter); 68 $self->{busy}=1; 69 $self->set_active(!$empty); 70 $self->{busy}=0; 71 my $desc=($empty? _("No locked filter") : _("Locked on :\n").$filter->explain); 72 $self->set_tooltip_text($desc); 73 }); 74 return $toggle; 75} 76 77sub make_sort_menu 78{ my $selfitem=$_[0]; 79 my $songlist= $selfitem->isa('SongList::Common') ? $selfitem : ::GetSonglist($selfitem); 80 my $menu= ($selfitem->can('get_submenu') && $selfitem->get_submenu) || Gtk2::Menu->new; 81 my $menusub=sub { $songlist->Sort($_[1]) }; 82 for my $name (sort keys %{$::Options{SavedSorts}}) 83 { my $sort=$::Options{SavedSorts}{$name}; 84 my $item = Gtk2::CheckMenuItem->new_with_label($name); 85 $item->set_draw_as_radio(1); 86 $item->set_active(1) if $songlist->{sort} eq $sort; 87 $item->signal_connect (activate => $menusub,$sort ); 88 $menu->append($item); 89 } 90 my $itemEditSort=Gtk2::ImageMenuItem->new(_"Custom..."); 91 $itemEditSort->set_image( Gtk2::Image->new_from_stock('gtk-preferences','menu') ); 92 $itemEditSort->signal_connect (activate => sub 93 { my $sort=::EditSortOrder($selfitem,$songlist->{sort}); 94 $songlist->Sort($sort) if $sort; 95 }); 96 $menu->append($itemEditSort); 97 return $menu; 98} 99 100sub fill_history_menu 101{ my $selfitem=$_[0]; 102 my $menu= $selfitem->get_submenu || Gtk2::Menu->new; 103 my $mclicksub=sub { $_[0]{middle}=1 if $_[1]->button == 2; return 0; }; 104 my $menusub=sub 105 { my $f=($_[0]{middle})? Filter->newadd(FALSE, ::GetFilter($selfitem,1),$_[1]) : $_[1]; 106 ::SetFilter($selfitem,$f); 107 }; 108 for my $f (@{ $::Options{RecentFilters} }) 109 { my $item = Gtk2::MenuItem->new_with_label( $f->explain ); 110 $item->signal_connect(activate => $menusub,$f); 111 $item->signal_connect(button_release_event => $mclicksub,$f); 112 $menu->append($item); 113 } 114 return $menu; 115} 116 117package LabelTotal; 118use base 'Gtk2::Bin'; 119 120our %Modes= 121( list => {label=> _"Listed songs", setup => \&list_Set, update=>\&list_Update, delay=> 1000, }, 122 filter => {label=> _"Filter", setup => \&filter_Set, update=>\&filter_Update, delay=> 1500, }, 123 library => {label=> _"Library", setup => \&library_Set, update=>\&library_Update, delay=> 4000, }, 124 selected => {label=> _"Selected songs", setup => \&selected_Set,update=>\&selected_Update, delay=> 500, }, 125); 126 127our @default_options= 128( button =>1, format => 'long', relief=> 'none', mode=> 'list', 129); 130 131sub new 132{ my ($class,$opt) = @_; 133 %$opt=( @default_options, %$opt ); 134 my $self; 135 if ($opt->{button}) 136 { $self=Gtk2::Button->new; 137 $self->set_relief($opt->{relief}); 138 } 139 else { $self=Gtk2::EventBox->new; } 140 bless $self,$class; 141 $self->{$_}= $opt->{$_} for qw/size format group noheader/; 142 $self->add(Gtk2::Label->new); 143 $self->signal_connect( destroy => \&Remove); 144 $self->signal_connect( button_press_event => \&button_press_event_cb); 145 ::Watch($self, SongsChanged => \&SongsChanged_cb); 146 $self->Set_mode($opt->{mode}); 147 return $self; 148} 149 150sub Set_mode 151{ my ($self,$mode)=@_; 152 $self->Remove; 153 $self->{mode}=$mode; 154 $Modes{ $self->{mode} }{setup}->($self); 155 $self->QueueUpdateFast; 156} 157 158sub Remove 159{ my $self=shift; 160 delete $::ToDo{'9_Total'.$self}; 161 ::UnWatchFilter($self,$self->{group}); 162 ::UnWatch($self,'Selection_'.$self->{group}); 163 ::UnWatch($self,$_) for qw/SongArray SongsAdded SongsHidden SongsRemoved/; 164} 165 166sub button_press_event_cb 167{ my ($self,$event)=@_; 168 my $menu=Gtk2::Menu->new; 169 for my $mode (sort {$Modes{$a}{label} cmp $Modes{$b}{label}} keys %Modes) 170 { my $item = Gtk2::CheckMenuItem->new( $Modes{$mode}{label} ); 171 $item->set_draw_as_radio(1); 172 $item->set_active($mode eq $self->{mode}); 173 $item->signal_connect( activate => sub { $self->Set_mode($mode) } ); 174 $menu->append($item); 175 } 176 ::PopupMenu($menu); 177} 178 179sub QueueUpdateFast 180{ my $self=shift; 181 $self->{needupdate}=2; 182 ::IdleDo('9_Total'.$self, 10, \&Update, $self); 183} 184sub QueueUpdateSlow 185{ my $self=shift; 186 return if $self->{needupdate}; 187 $self->{needupdate}=1; 188 my $maxdelay= $Modes{ $self->{mode} }{delay}; 189 ::IdleDo('9_Total'.$self, $maxdelay, \&Update, $self); 190} 191sub Update 192{ my $self=shift; 193 delete $::ToDo{'9_Total'.$self}; 194 my ($text,$array,$tip)= $Modes{ $self->{mode} }{update}->($self); 195 $text='' if $self->{noheader}; 196 if (!$array) { $tip=$text=_"error"; } 197 else { $text.= ::CalcListLength($array,$self->{format}); } 198 my $format= $self->{size} ? '<span size="'.$self->{size}.'">%s</span>' : '%s'; 199 $self->child->set_markup_with_format($format,$text); 200 $self->set_tooltip_text($tip); 201 $self->{needupdate}=0; 202} 203 204sub SongsChanged_cb 205{ my ($self,$IDs,$fields)=@_; 206 return if $self->{needupdate}; 207 my $needupdate= $fields && (grep $_ eq 'length' || $_ eq 'size', @$fields); 208 if (!$needupdate && $self->{mode} eq 'filter') 209 { my $filter=::GetFilter($self); 210 $needupdate=$filter->changes_may_affect($IDs,$fields); 211 } 212 #if in list mode, could check : return if $IDs && !$songarray->AreIn($IDs) 213 $self->QueueUpdateSlow if $needupdate; 214} 215 216### filter functions 217sub filter_Set 218{ my $self=shift; 219 ::WatchFilter($self,$self->{group}, \&QueueUpdateFast); 220 ::Watch($self, SongsAdded => \&SongsChanged_cb); 221 ::Watch($self, SongsRemoved => \&SongsChanged_cb); 222 ::Watch($self, SongsHidden => \&SongsChanged_cb); 223} 224sub filter_Update 225{ my $self=shift; 226 my $filter=::GetFilter($self); 227 my $array=$filter->filter; 228 return _("Filter : "), $array, $filter->explain; 229} 230 231### list functions 232sub list_Set 233{ my $self=shift; 234 ::Watch($self, SongArray =>\&list_SongArray_changed); 235} 236sub list_SongArray_changed 237{ my ($self,$array,$action)=@_; 238 return if $self->{needupdate}; 239 my $array0=::GetSongArray($self) || return; 240 return unless $array0==$array; 241 return if grep $action eq $_, qw/mode sort move up down/; 242 $self->QueueUpdateFast; 243} 244sub list_Update 245{ my $self=shift; 246 my $array=::GetSongArray($self) || return; 247 return _("Listed : "), $array, ::__n('%d song','%d songs',scalar@$array); 248} 249 250### selected functions 251sub selected_Set 252{ my $self=shift; 253 ::Watch($self,'Selection_'.$self->{group}, \&QueueUpdateFast); 254} 255sub selected_Update 256{ my $self=shift; 257 my $songlist=::GetSonglist($self); 258 return unless $songlist; 259 my @list=$songlist->GetSelectedIDs; 260 return _('Selected : '), \@list, ::__n('%d song selected','%d songs selected',scalar@list); 261} 262 263### library functions 264sub library_Set 265{ my $self=shift; 266 ::Watch($self, SongsAdded =>\&QueueUpdateSlow); 267 ::Watch($self, SongsRemoved =>\&QueueUpdateSlow); 268 ::Watch($self, SongsHidden =>\&QueueUpdateSlow); 269} 270sub library_Update 271{ my $tip= ::__n('%d song in the library','%d songs in the library',scalar@$::Library); 272 return _('Library : '), $::Library, $tip; 273} 274 275 276package EditListButtons; 277use Glib qw(TRUE FALSE); 278use base 'Gtk2::Box'; 279 280sub new 281{ my ($class,$opt)=@_; 282 my $self= ($opt->{orientation}||'') eq 'vertical' ? Gtk2::VBox->new : Gtk2::HBox->new; 283 bless $self, $class; 284 285 $self->{group}=$opt->{group}; 286 $self->{bshuffle}=::NewIconButton('gmb-shuffle',($opt->{small} ? '' : _"Shuffle"),sub {::GetSongArray($self)->Shuffle}); 287 $self->{brm}= ::NewIconButton('gtk-remove', ($opt->{small} ? '' : _"Remove"),sub {::GetSonglist($self)->RemoveSelected}); 288 $self->{bclear}=::NewIconButton('gtk-clear', ($opt->{small} ? '' : _"Clear"),sub {::GetSonglist($self)->Empty} ); 289 $self->{bup}= ::NewIconButton('gtk-go-up', undef, sub {::GetSonglist($self)->MoveUpDown(1)}); 290 $self->{bdown}= ::NewIconButton('gtk-go-down', undef, sub {::GetSonglist($self)->MoveUpDown(0)}); 291 $self->{btop}= ::NewIconButton('gtk-goto-top', undef, sub {::GetSonglist($self)->MoveUpDown(1,1)}); 292 $self->{bbot}= ::NewIconButton('gtk-goto-bottom', undef, sub {::GetSonglist($self)->MoveUpDown(0,1)}); 293 294 $self->{brm}->set_tooltip_text(_"Remove selected songs"); 295 $self->{bclear}->set_tooltip_text(_"Remove all songs"); 296 297 if (my $r=$opt->{relief}) { $self->{$_}->set_relief($r) for qw/brm bclear bup bdown btop bbot bshuffle/; } 298 $self->pack_start($self->{$_},FALSE,FALSE,2) for qw/btop bup bdown bbot brm bclear bshuffle/; 299 300 ::Watch($self,'Selection_'.$self->{group}, \&SelectionChanged); 301 ::Watch($self,SongArray=> \&ListChanged); 302 $self->{PostInit}= sub { $self->SelectionChanged; $self->ListChanged; }; 303 304 return $self; 305} 306 307sub ListChanged 308{ my ($self,$array)=@_; 309 my $songlist=::GetSonglist($self); 310 my $watchedarray= $songlist && $songlist->{array}; 311 return if !$watchedarray || ($array && $watchedarray!=$array); 312 $self->{bclear}->set_sensitive(@$watchedarray>0); 313 $self->{bshuffle}->set_sensitive(@$watchedarray>1); 314 $self->set_sensitive( !$songlist->{autoupdate} ); 315 $self->set_visible( !$songlist->{autoupdate} ); 316} 317 318sub SelectionChanged 319{ my ($self)=@_; 320 my $rows; 321 my $songlist=::GetSonglist($self); 322 if ($songlist) 323 { $rows=$songlist->GetSelectedRows; 324 } 325 if ($rows && @$rows) 326 { $self->{brm}->set_sensitive(1); 327 my $i=0; 328 $i++ while $i<@$rows && $rows->[$i]==$i; 329 $self->{$_}->set_sensitive($i!=@$rows) for qw/btop bup/; 330 $i=$#$rows; 331 my $array=$songlist->{array}; 332 $i-- while $i>-1 && $rows->[$i]==$#$array-$#$rows+$i; 333 $self->{$_}->set_sensitive($i!=-1) for qw/bbot bdown/; 334 } 335 else 336 { $self->{$_}->set_sensitive(0) for qw/btop bbot brm bup bdown/; 337 } 338} 339 340package QueueActions; 341use Glib qw(TRUE FALSE); 342use base 'Gtk2::Box'; 343 344sub new 345{ my $class=$_[0]; 346 my $self=bless Gtk2::HBox->new, $class; 347 348 my $action_store=Gtk2::ListStore->new(('Glib::String')x3); 349 350 $self->{queuecombo}= 351 my $combo=Gtk2::ComboBox->new($action_store); 352 353 my $renderer=Gtk2::CellRendererPixbuf->new; 354 $combo->pack_start($renderer,FALSE); 355 $combo->add_attribute($renderer, stock_id => 0); 356 $renderer=Gtk2::CellRendererText->new; 357 $combo->pack_start($renderer,TRUE); 358 $combo->add_attribute($renderer, text => 1); 359 360 $combo->signal_connect(changed => sub 361 { return if $self->{busy}; 362 my $iter=$_[0]->get_active_iter; 363 my $action=$_[0]->get_model->get_value($iter,2); 364 ::EnqueueAction($action); 365 }); 366 $self->{eventcombo}=Gtk2::EventBox->new; 367 $self->{eventcombo}->add($combo); 368 $self->{spin}=::NewPrefSpinButton('MaxAutoFill', 1,50, step=>1, page=>5, cb=>sub 369 { return if $self->{busy}; 370 ::HasChanged('QueueAction','maxautofill'); 371 }); 372 $self->{spin}->set_no_show_all(1); 373 374 $self->pack_start($self->{$_},FALSE,FALSE,2) for qw/eventcombo spin/; 375 376 ::Watch($self, QueueAction => \&Update); 377 ::Watch($self, QueueActionList => \&Fill); 378 $self->Fill; 379 return $self; 380} 381 382sub Fill 383{ my $self=shift; 384 my $store= $self->{queuecombo}->get_model; 385 $self->{busy}=1; 386 $store->clear; 387 delete $self->{actionindex}; 388 my $i=0; 389 for my $action (::List_QueueActions(0)) 390 { $store->set($store->append, 0,$::QActions{$action}{icon}, 1,$::QActions{$action}{short} ,2, $action ); 391 $self->{actionindex}{$action}=$i++; 392 } 393 $self->Update; 394} 395 396sub Update 397{ my $self=$_[0]; 398 $self->{busy}=1; 399 my $action=$::QueueAction; 400 $self->{queuecombo}->set_active( $self->{actionindex}{$action} ); 401 $self->{eventcombo}->set_tooltip_text( $::QActions{$action}{long} ); 402 $self->{spin}->set_visible($::QActions{$action}{autofill}); 403 $self->{spin}->set_value($::Options{MaxAutoFill}); 404 delete $self->{busy}; 405} 406 407package SongList::Common; #common functions for SongList and SongTree 408our %Register; 409our $EditList; #list that will be used in 'editlist' mode, used only for editing a list in a separate window 410 411our @DefaultOptions= 412( 'sort' => 'path album:i disc track file', 413 hideif => '', 414 colwidth=> '', 415 autoupdate=>1, 416); 417our %Markup_Empty= 418( Q => _"Queue empty", 419 L => _"List empty", 420 A => _"Playlist empty", 421 B => _"No songs found", 422 S => _"No songs found", 423); 424 425sub new 426{ my $opt=$_[1]; 427 my $package= $opt->{songtree} ? 'SongTree' : $opt->{songlist} ? 'SongList' : 'SongList'; 428 $package->new($opt); 429} 430 431sub CommonInit 432{ my ($self,$opt)=@_; 433 434 %$opt=( @DefaultOptions, %$opt ); 435 $self->{$_}=$opt->{$_} for qw/mode group follow sort hideif hidewidget shrinkonhide markup_empty markup_library_empty autoupdate/,grep(m/^activate\d?$/, keys %$opt); 436 $self->{mode}||=''; 437 my $type= $self->{type}= 438 $self->{mode} eq 'playlist' ? 'A' : 439 $self->{mode} eq 'editlist' ? 'L' : 440 $opt->{type} || 'B'; 441 $self->{mode}='playlist' if $type eq 'A'; 442 #default double-click action : 443 $self->{activate} ||= $type eq 'L' ? 'playlist' : 444 $type eq 'Q' ? 'remove_and_play' : 445 'play'; 446 $self->{activate2}||='queue' unless $type eq 'Q'; #default to 'queue' songs when double middle-click 447 448 $self->{markup_empty}= $Markup_Empty{$type} unless defined $self->{markup_empty}; 449 $self->{markup_library_empty}= _"Library empty.\n\nUse the settings dialog to add music." 450 unless defined $self->{markup_library_empty} or $type=~m/[QL]/; 451 452 ::WatchFilter($self,$self->{group}, \&SetFilter ) if $type!~m/[QL]/; 453 $self->{need_init}=1; 454 $self->signal_connect_after(show => sub 455 { my $self=$_[0]; 456 return unless delete $self->{need_init}; 457 if ($self->{type}=~m/[QLA]/) 458 { $self->SongArray_changed_cb($self->{array},'replace'); 459 } 460 else { ::InitFilter($self); } 461 }); 462 $self->signal_connect_after('map' => sub { $_[0]->FollowSong }) unless $self->{type}=~m/[QL]/; 463 464 $self->{colwidth}= { split / +/, $opt->{colwidth} }; 465 466 my $songarray=$opt->{songarray}; 467 if ($type eq 'A') 468 { #$songarray= SongArray->new_copy($::ListPlay); 469 $self->{array}=$songarray=$::ListPlay; 470 $self->{sort}= $::RandomMode ? $::Options{Sort_LastOrdered} : $::Options{Sort}; 471 $self->UpdatePlayListFilter; 472 ::Watch($self,Filter=> \&UpdatePlayListFilter); 473 $self->{follow}=1 if !defined $self->{follow}; #default to follow current song on new playlists 474 } 475 elsif ($type eq 'L') 476 { if (defined $EditList) { $songarray=$EditList; $EditList=undef; } #special case for editing a list via ::WEditList 477 unless (defined $songarray && $songarray ne '') #create a new list if none specified 478 { $songarray='list000'; 479 $songarray++ while $::Options{SavedLists}{$songarray}; 480 } 481 } 482 elsif ($type eq 'Q') { $songarray=$::Queue; } 483 elsif ($type eq 'B' || $type eq 'S') { $songarray=SongArray::AutoUpdate->new($self->{autoupdate},$self->{sort}); } 484 485 if ($songarray && !ref $songarray) #if not a ref, treat it as the name of a saved list 486 { ::SaveList($songarray,[]) unless $::Options{SavedLists}{$songarray}; #create new list if doesn't exists 487 $songarray=$::Options{SavedLists}{$songarray}; 488 } 489 $self->{follow}=0 if !defined $self->{follow}; 490 491 delete $self->{autoupdate} unless $songarray && $songarray->isa('SongArray::AutoUpdate'); 492 $self->{array}= $songarray || SongArray->new; 493 494 $self->RegisterGroup($self->{group}); 495 $self->{SaveOptions}=\&CommonSave; 496} 497sub RegisterGroup 498{ my ($self,$group)=@_; 499 $Register{ $group }=$self; 500 ::weaken($Register{ $group }); #or use a destroy cb ? 501} 502sub UpdatePlayListFilter 503{ my $self=shift; 504 $self->{ignoreSetFilter}=1; 505 ::SetFilter($self,$::PlayFilter,0); 506 $self->{ignoreSetFilter}=0; 507} 508sub CommonSave 509{ my $self=shift; 510 my $opt= $self->SaveOptions; 511 $opt->{$_}= $self->{$_} for qw/sort rowtip/; 512 $opt->{autoupdate}=$self->{autoupdate} if exists $self->{autoupdate}; 513 $opt->{follow}= ! !$self->{follow}; 514 515 #save options as default for new SongTree/SongList of same type 516 my $name= $self->isa('SongTree') ? 'songtree_' : 'songlist_'; 517 $name= $name.$self->{name}; $name=~s/\d+$//; 518 $::Options{"DefaultOptions_$name"}={%$opt}; 519 520 if ($self->{type} eq 'L' && defined(my $n= $self->{array}->GetName)) { $opt->{type}='L'; $opt->{songarray}=$n; } 521 return $opt; 522} 523 524sub Sort 525{ my ($self,$sort)=@_; 526 $self->{array}->Sort($sort); 527} 528sub SetFilter 529{ my ($self,$filter)=@_;# ::red($self->{type},' ',($self->{filter} || 'no'), ' ',$filter);::callstack(); 530 if ($self->{hideif} eq 'nofilter') 531 { $self->Hide($filter->is_empty); 532 return if $filter->is_empty; 533 } 534 $self->{filter}=$filter; 535 return if $self->{ignoreSetFilter}; 536 $self->{array}->SetSortAndFilter($self->{sort},$filter); 537} 538sub Empty 539{ my $self=shift; 540 $self->{array}->Replace; 541} 542 543sub GetSelectedIDs 544{ my $self=shift; 545 my $rows=$self->GetSelectedRows; 546 my $array=$self->{array}; 547 return map $array->[$_], @$rows; 548} 549sub PlaySelected ## 550{ my $self=$_[0]; 551 my @IDs=$self->GetSelectedIDs; 552 ::Select(song=>'first',play=>1,staticlist => \@IDs ) if @IDs; 553} 554sub EnqueueSelected## 555{ my $self=$_[0]; 556 my @IDs=$self->GetSelectedIDs; 557 ::Enqueue(@IDs) if @IDs; 558} 559sub RemoveSelected 560{ my $self=shift; 561 return if $self->{autoupdate}; #can't remove selection from an always-filtered list 562 my $songarray=$self->{array}; 563 $songarray->Remove($self->GetSelectedRows); 564} 565 566sub PopupContextMenu 567{ my $self=shift; 568 #return unless @{$self->{array}}; #no context menu for empty lists 569 my @IDs=$self->GetSelectedIDs; 570 my %args=(self => $self, mode => $self->{type}, IDs => \@IDs, listIDs => $self->{array}); 571 $args{allowremove}=1 unless $self->{autoupdate}; 572 ::PopupContextMenu(\@::SongCMenu,\%args); 573} 574 575sub MoveUpDown 576{ my ($self,$up,$max)=@_; 577 my $songarray=$self->{array}; 578 my $rows=$self->GetSelectedRows; 579 if ($max) 580 { if ($up){ $songarray->Top($rows); } 581 else { $songarray->Bottom($rows); } 582 $self->Scroll_to_TopEnd(!$up); 583 } 584 else 585 { if ($up){ $songarray->Up($rows) } 586 else { $songarray->Down($rows) } 587 } 588} 589 590sub Hide 591{ my ($self,$hide)=@_; 592 my $name=$self->{hidewidget} || $self->{name}; 593 my $toplevel=::get_layout_widget($self); 594 unless ($toplevel) 595 { $self->{need_hide}=$name if $hide; 596 return; 597 } 598 if ($hide) { $toplevel->Hide($name,$self->{shrinkonhide}) } 599 else { $toplevel->Show($name,$self->{shrinkonhide}) } 600} 601 602sub Activate 603{ my ($self,$button)=@_; 604 my $row= $self->GetCurrentRow; 605 return unless defined $row; 606 my $songarray=$self->{array}; 607 my $ID=$songarray->[$row]; 608 my $activate=$self->{'activate'.$button} || $self->{activate}; 609 my $aftercmd; 610 $aftercmd=$1 if $activate=~s/&(.*)$//; 611 612 if ($activate eq 'playlist') { ::Select( staticlist=>[@$songarray], position=>$row, play=>1); } 613 elsif ($activate eq 'filter_and_play'){ ::Select(filter=>$self->{filter}, song=>$ID, play=>1); } 614 elsif ($activate eq 'filter_sort_and_play'){ ::Select(sort=>$self->{sort}, filter=>$self->{filter}, song=>$ID, play=>1); } 615 elsif ($activate eq 'remove_and_play') 616 { $songarray->Remove([$row]); 617 ::Select(song=>$ID,play=>1); 618 } 619 elsif ($activate eq 'remove') { $songarray->Remove([$row]); } 620 elsif ($activate eq 'properties') { ::DialogSongProp($ID); } 621 elsif ($activate eq 'play') 622 { if ($self->{type} eq 'A') { ::Select(position=>$row,play=>1); } 623 else { ::Select(song=>$ID,play=>1); } 624 } 625 else { ::DoActionForList($activate,[$ID]); } 626 627 ::run_command($self,$aftercmd) if $aftercmd; 628} 629 630# functions for dynamic titles 631sub DynamicTitle 632{ my ($self,$format)=@_; 633 return $format unless $format=~m/%n/; 634 my $label=Gtk2::Label->new; 635 $label->{format}=$format; 636 ::weaken( $label->{songarray}=$self->{array} ); 637 ::Watch($label,SongArray=> \&UpdateDynamicTitle); 638 UpdateDynamicTitle($label); 639 return $label; 640} 641sub UpdateDynamicTitle 642{ my ($label,$array)=@_; 643 return if $array && $array != $label->{songarray}; 644 my $format=$label->{format}; 645 my $nb= @{ $label->{songarray} }; 646 $format=~s/%(.)/$1 eq 'n' ? $nb : $1/eg; 647 $label->set_text($format); 648} 649 650# functions for SavedLists, ie type=L 651sub MakeTitleLabel 652{ my $self=shift; 653 my $name=$self->{array}->GetName; 654 my $label=Gtk2::Label->new($name); 655 ::weaken( $label->{songlist}=$self ); 656 ::Watch($label,SavedLists=> \&UpdateTitleLabel); 657 return $label; 658} 659sub UpdateTitleLabel 660{ my ($label,$list,$action,$newname)=@_; 661 return unless $action && $action eq 'renamedto'; 662 my $self=$label->{songlist}; 663 my $old=$label->get_text; 664 my $new=$self->{array}->GetName; 665 return if $old eq $new; 666 $label->set_text($new); 667} 668sub RenameTitleLabel 669{ my ($label,$newname)=@_; 670 my $self=$label->{songlist}; 671 my $oldname=$self->{array}->GetName; 672 return if $newname eq '' || exists $::Options{SavedLists}{$newname}; 673 ::SaveList($oldname,$self->{array},$newname); 674} 675sub DeleteList 676{ my $self=shift; 677 my $name=$self->{array}->GetName; 678 ::SaveList($name,undef) if defined $name; 679} 680 681sub DrawEmpty 682{ my ($self,$window,$window_size,$offset)=@_; 683 return unless $window; 684 $offset||=0; 685 $window_size||=$window; 686 my $type=$self->{type}; 687 my $markup= scalar @$::Library ? undef : $self->{markup_library_empty}; 688 $markup ||= $self->{markup_empty}; 689 if ($markup) 690 { $markup=~s#(?:\\n|<br>)#\n#g; 691 my ($width,$height)=$window_size->get_size; 692 my $layout= Gtk2::Pango::Layout->new( $self->create_pango_context ); 693 $width-=2*5; 694 $layout->set_width( Gtk2::Pango->scale * $width ); 695 $layout->set_wrap('word-char'); 696 $layout->set_alignment('center'); 697 my $style= $self->style; 698 my $font= $style->font_desc; 699 $font->set_size( 2 * $font->get_size ); 700 $layout->set_font_description($font); 701 $layout->set_markup( "\n".$markup ); 702 my $gc=$style->text_aa_gc($self->state); 703 $window->draw_layout($gc, $offset+5,5, $layout); 704 } 705} 706 707sub SetRowTip 708{ my ($self,$tip)=@_; 709 $tip= "<b><big>%t</big></b>\\nby <b>%a</b>\\nfrom <b>%l</b>" if $tip && $tip eq '1'; #for rowtip=1, deprecated 710 $self->{rowtip}=$tip||''; 711 return unless *Gtk2::Widget::set_has_tooltip{CODE}; # since gtk+ 2.12, Gtk2 1.160 712 $self->set_has_tooltip(!!$tip); 713} 714 715sub EditRowTip 716{ my $self=shift; 717 if ($self->{rowtip_edit}) { $self->{rowtip_edit}->force_present; return; } 718 my $dialog = Gtk2::Dialog->new(_"Edit row tip", $self->get_toplevel, 719 [qw/destroy-with-parent/], 720 'gtk-apply' => 'apply', 721 'gtk-ok' => 'ok', 722 'gtk-cancel' => 'none', 723 ); 724 ::weaken( $self->{rowtip_edit}=$dialog ); 725 ::SetWSize($dialog,'RowTip'); 726 $dialog->set_default_response('ok'); 727 my $combo=Gtk2::ComboBoxEntry->new_text; 728 my $hist= $::Options{RowTip_history} ||=[ _("Play count").' : $playcount\\n'._("Last played").' : $lastplay', 729 '<b>$title</b>\\n'._('<i>by</i> %a\\n<i>from</i> %l'), 730 '$title\\n$album\\n$artist\\n<small>$comment</small>', 731 '$comment', 732 ]; 733 $combo->append_text($_) for @$hist; 734 my $entry=$combo->child; 735 $entry->set_text($self->{rowtip}); 736 $entry->set_activates_default(::TRUE); 737 my $preview= Label::Preview->new(event => 'CurSong', wrap=>1, entry=>$entry, noescape=>1, 738 format=>'<small><i>'._("example :")."\n\n</i></small>%s", 739 preview => sub { defined $::SongID ? ::ReplaceFieldsAndEsc($::SongID,$_[0]) : $_[0]; }, 740 ); 741 $preview->set_alignment(0,.5); 742 $dialog->vbox->pack_start($_,::FALSE,::FALSE,4) for $combo,$preview; 743 $dialog->show_all; 744 $dialog->signal_connect( response => sub 745 { my ($dialog,$response)=@_; 746 my $tip=$entry->get_text; 747 if ($response eq 'ok' || $response eq 'apply') 748 { ::PrefSaveHistory(RowTip_history=>$tip) if $tip; 749 $self->SetRowTip($tip); 750 } 751 $dialog->destroy unless $response eq 'apply'; 752 }); 753} 754 755package SongList; 756use Glib qw(TRUE FALSE); 757use Gtk2::Pango; #for PANGO_WEIGHT_BOLD, PANGO_WEIGHT_NORMAL 758use base 'Gtk2::ScrolledWindow'; 759 760our @ISA; 761our %SLC_Prop; 762INIT 763{ unshift @ISA, 'SongList::Common'; 764 %SLC_Prop= 765 ( #PlaycountBG => #TEST 766# { value => sub { Songs::Get($_[2],'playcount') ? 'grey' : '#ffffff'; }, 767# attrib => 'cell-background', type => 'Glib::String', 768# #can't be updated via a event key, so not updated on its own for now, but will be updated if a playcount row is present 769# }, 770 # italicrow & boldrow are special 'playrow', can't be updated via a event key, a redraw is made when CurSong changed if $self->{playrow} 771 italicrow => 772 { value => sub 773 { defined $::SongID && $_[2]==$::SongID && (!$_[0]{is_playlist} || !defined $::Position || $::Position==$_[1]) ? 774 'italic' : 'normal'; 775 }, 776 attrib => 'style', type => 'Gtk2::Pango::Style', 777 }, 778 boldrow => 779 { value => sub 780 { defined $::SongID && $_[2]==$::SongID && (!$_[0]{is_playlist} || !defined $::Position || $::Position==$_[1]) ? 781 PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL; 782 }, 783 attrib => 'weight', type => 'Glib::Uint', 784 }, 785 786 right_aligned_folder=> 787 { menu => _("Folder (right-aligned)"), title => _("Folder"), 788 value => sub { Songs::Display($_[2],'path'); }, 789 attrib => 'text', type => 'Glib::String', depend => 'path', 790 sort => 'path', width => 200, 791 init => { ellipsize=>'start', }, 792 }, 793 titleaa => 794 { menu => _('Title - Artist - Album'), title => _('Song'), 795 value => sub { ::ReplaceFieldsAndEsc($_[2],"<b>%t</b>%V\n<small><i>%a</i> - %l</small>"); }, 796 attrib => 'markup', type => 'Glib::String', depend => 'title version artist album', 797 sort => 'title:i', noncomp => 'boldrow', width => 200, 798 }, 799 playandqueue => 800 { menu => _('Playing and queue icons'), title => '', width => 20, 801 value => sub { ::Get_PPSQ_Icon($_[2], !(defined $::SongID && $_[2]==$::SongID && (!$_[0]{is_playlist} || !defined $::Position || $::Position==$_[1]))); }, 802 class => 'Gtk2::CellRendererPixbuf', attrib => 'stock-id', 803 type => 'Glib::String', noncomp => 'boldrow italicrow', 804 event => 'Playing Queue CurSong', 805 }, 806 playandqueueandtrack => 807 { menu => _('Play, queue or track'), title => '#', width => 20, 808 value => sub { my $ID=$_[2]; ::Get_PPSQ_Icon($ID, !(defined $::SongID && $ID==$::SongID && (!$_[0]{is_playlist} || !defined $::Position || $::Position==$_[1])),'text') || Songs::Display($ID,'track'); }, 809 type => 'Glib::String', attrib => 'markup', yalign => '0.5', 810 event => 'Playing Queue CurSong', sort => 'track', 811 depend=> 'track', 812 }, 813 icolabel => 814 { menu => _("Labels' icons"), title => '', value => sub { $_[2] }, 815 class => 'CellRendererIconList',attrib => 'ID', type => 'Glib::Uint', 816 depend => 'label', sort => 'label:i', noncomp => 'boldrow italicrow', 817 event => 'Icons', width => 50, 818 init => {field => 'label'}, 819 }, 820 albumpic => 821 { title => _("Album picture"), width => 100, 822 value => sub { CellRendererSongsAA::get_value('album',$_[0]{array},$_[1]); }, 823 class => 'CellRendererSongsAA', attrib => 'ref', type => 'Glib::Scalar', 824 depend => 'album', sort => 'album:i', noncomp => 'boldrow italicrow', 825 init => {aa => 'album'}, 826 event => 'Picture_album', 827 }, 828 artistpic => 829 { title => _("Artist picture"), 830 value => sub { CellRendererSongsAA::get_value('first_artist',$_[0]{array},$_[1]); }, 831 class => 'CellRendererSongsAA', attrib => 'ref', type => 'Glib::Scalar', 832 depend => 'artist', sort => 'artist:i', noncomp => 'boldrow italicrow', 833 init => {aa => 'first_artist', markup => '<b>%a</b>'}, event => 'Picture_artist', 834 }, 835 stars => 836 { title => _("Rating"), menu => _("Rating (picture)"), 837 value => sub { Songs::Stars( Songs::Get($_[2],'rating'),'rating'); }, 838 class => 'Gtk2::CellRendererPixbuf', attrib => 'pixbuf', 839 type => 'Gtk2::Gdk::Pixbuf', noncomp => 'boldrow italicrow', 840 depend => 'rating', sort => 'rating', 841 }, 842 rownumber=> 843 { menu => _("Row number"), title => '#', width => 50, 844 value => sub { $_[1]+1 }, 845 type => 'Glib::String', attrib => 'text', init => { xalign => 1, }, 846 }, 847 ); 848 %{$SLC_Prop{albumpicinfo}}=%{$SLC_Prop{albumpic}}; 849 $SLC_Prop{albumpicinfo}{title}=_"Album picture & info"; 850 $SLC_Prop{albumpicinfo}{init}={aa => 'album', markup => "<b>%a</b>%Y\n<small>%s <small>%l</small></small>"}; 851} 852 853our @ColumnMenu= 854( { label => _"_Sort by", submenu => sub { Browser::make_sort_menu($_[0]{self}) }, }, 855 { label => _"_Insert column", submenu => sub 856 { my %names=map {my $l=$SLC_Prop{$_}{menu} || $SLC_Prop{$_}{title}; defined $l ? ($_,$l) : ()} keys %SLC_Prop; 857 delete $names{$_->{colid}} for $_[0]{self}->child->get_columns; 858 return \%names; 859 }, submenu_reverse =>1, 860 code => sub { $_[0]{self}->ToggleColumn($_[1],$_[0]{pos}); }, stockicon => 'gtk-add' 861 }, 862 { label => sub { _('_Remove this column').' ('. ($SLC_Prop{$_[0]{pos}}{menu} || $SLC_Prop{$_[0]{pos}}{title}).')' }, 863 code => sub { $_[0]{self}->ToggleColumn($_[0]{pos},$_[0]{pos}); }, stockicon => 'gtk-remove' 864 }, 865 { label => _("Edit row tip").'...', code => sub { $_[0]{self}->EditRowTip; }, 866 }, 867 { label => _"Keep list filtered and sorted", code => sub { $_[0]{self}{array}->SetAutoUpdate( $_[0]{self}{autoupdate} ); }, 868 toggleoption => 'self/autoupdate', mode => 'B', 869 }, 870 { label => _"Follow playing song", code => sub { $_[0]{self}->FollowSong if $_[0]{self}{follow}; }, 871 toggleoption => 'self/follow', 872 }, 873 { label => _"Go to playing song", code => sub { $_[0]{self}->FollowSong; }, }, 874); 875 876our @DefaultOptions= 877( cols => 'playandqueue title artist album year length track file lastplay playcount rating', 878 playrow => 'boldrow', 879 headers => 'on', 880 no_typeahead => 0, 881); 882 883sub init_textcolumns #FIXME support calling it multiple times => remove columns for removed fields, update added columns ? 884{ 885 for my $key (Songs::ColumnsKeys()) 886 { $SLC_Prop{$key}= 887 { title => Songs::FieldName($key), value => sub { Songs::Display($_[2],$key)}, 888 type => 'Glib::String', attrib => 'text', 889 sort => Songs::SortField($key), width => Songs::FieldWidth($key), 890 depend => join(' ',Songs::Depends($key)), 891 }; 892 $SLC_Prop{$key}{init}{xalign}=1 if Songs::ColumnAlign($key); 893 } 894} 895 896sub new 897{ my ($class,$opt) = @_; 898 899 my $self = bless Gtk2::ScrolledWindow->new, $class; 900 $self->set_shadow_type('etched-in'); 901 $self->set_policy('automatic','automatic'); 902 ::set_biscrolling($self); 903 904 #use default options for this songlist type 905 my $name= 'songlist_'.$opt->{name}; $name=~s/\d+$//; 906 my $default= $::Options{"DefaultOptions_$name"} || {}; 907 908 %$opt=( @DefaultOptions, %$default, %$opt ); 909 $self->CommonInit($opt); 910 $self->{$_}=$opt->{$_} for qw/songypad playrow/; 911 912 my $store=SongStore->new; $store->{array}=$self->{array}; $store->{size}=@{$self->{array}}; 913 $store->{is_playlist}= $self->{mode} eq 'playlist'; 914 my $tv=Gtk2::TreeView->new($store); 915 $self->add($tv); 916 $self->{store}=$store; 917 918 ::set_drag($tv, 919 source =>[::DRAG_ID,sub { my $tv=$_[0]; return ::DRAG_ID,$tv->parent->GetSelectedIDs; }], 920 dest =>[::DRAG_ID,::DRAG_FILE,\&drag_received_cb], 921 motion => \&drag_motion_cb, 922 ); 923 $tv->signal_connect(drag_data_delete => sub { $_[0]->signal_stop_emission_by_name('drag_data_delete'); }); #ignored 924 925 $tv->set_rules_hint(TRUE); 926 $tv->set_headers_clickable(TRUE); 927 $tv->set_headers_visible(FALSE) if $opt->{headers} eq 'off'; 928 $tv->set('fixed-height-mode' => TRUE); 929 $tv->set_enable_search(!$opt->{no_typeahead}); 930 $tv->set_search_equal_func(\&SongStore::search_equal_func); 931 $tv->signal_connect(key_release_event => sub 932 { my ($tv,$event)=@_; 933 if (Gtk2::Gdk->keyval_name( $event->keyval ) eq 'Delete') 934 { $tv->parent->RemoveSelected; 935 return 1; 936 } 937 return 0; 938 }); 939 MultiTreeView::init($tv,__PACKAGE__); 940 $tv->signal_connect(cursor_changed => \&cursor_changed_cb); 941 $tv->signal_connect(row_activated => \&row_activated_cb); 942 $tv->get_selection->signal_connect(changed => \&sel_changed_cb); 943 $tv->get_selection->set_mode('multiple'); 944 $tv->signal_connect(query_tooltip=> \&query_tooltip_cb) if *Gtk2::Widget::set_has_tooltip{CODE}; # requires gtk+ 2.12, Gtk2 1.160 945 $self->SetRowTip($opt->{rowtip}); 946 947 # used to draw text when treeview empty 948 $tv->signal_connect(expose_event=> \&expose_cb); 949 $tv->get_hadjustment->signal_connect_swapped(changed=> sub { my $tv=shift; $tv->queue_draw unless $tv->get_model->iter_n_children },$tv); 950 951 $self->AddColumn($_) for split / +/,$opt->{cols}; 952 $self->AddColumn('title') unless $tv->get_columns; #make sure there is at least one column 953 954 ::Watch($self, SongArray => \&SongArray_changed_cb); 955 ::Watch($self, SongsChanged => \&SongsChanged_cb); 956 ::Watch($self, CurSongID => \&CurSongChanged); 957 $self->{DefaultFocus}=$tv; 958 959 return $self; 960} 961 962sub SaveOptions 963{ my $self=shift; 964 my %opt; 965 my $tv=$self->child; 966 #save displayed cols 967 $opt{cols}=join ' ',(map $_->{colid},$tv->get_columns); 968 #save their width 969 my %width; 970 $width{$_}=$self->{colwidth}{$_} for keys %{$self->{colwidth}}; 971 $width{ $_->{colid} }=$_->get_width for $tv->get_columns; 972 $opt{colwidth}= join ' ',map "$_ $width{$_}", sort keys %width; 973 return \%opt; 974} 975 976sub AddColumn 977{ my ($self,$colid,$pos)=@_; 978 my $prop=$SLC_Prop{$colid}; 979 unless ($prop) {warn "Ignoring unknown column $colid\n"; return undef} 980 my $renderer= ( $prop->{class} || 'Gtk2::CellRendererText' )->new; 981 if (my $init=$prop->{init}) 982 { $renderer->set(%$init); 983 } 984 $renderer->set(ypad => $self->{songypad}) if defined $self->{songypad}; 985 my $colnb=SongStore::get_column_number($colid); 986 my $attrib=$prop->{attrib}; 987 my @attributes=($prop->{title},$renderer,$attrib,$colnb); 988 if (my $playrow=$self->{playrow}) 989 { if (my $noncomp=$prop->{noncomp}) { $playrow=undef if (grep $_ eq $playrow, split / /,$noncomp); } 990 push @attributes,$SLC_Prop{$playrow}{attrib},SongStore::get_column_number($playrow) if $playrow; 991 #$playrow='PlaycountBG'; #TEST 992 #push @attributes,$SLC_Prop{$playrow}{attrib},SongStore::get_column_number($playrow); #TEST 993 } 994 my $column = Gtk2::TreeViewColumn->new_with_attributes(@attributes); 995 996 #$renderer->set_fixed_height_from_font(1); 997 $column->{colid}=$colid; 998 $column->set_sizing('fixed'); 999 $column->set_resizable(TRUE); 1000 $column->set_min_width(0); 1001 $column->set_fixed_width( $self->{colwidth}{$colid} || $prop->{width} || 100 ); 1002 $column->set_clickable(TRUE); 1003 $column->set_reorderable(TRUE); 1004 1005 $column->signal_connect(clicked => sub 1006 { my $self=::find_ancestor($_[0]->get_widget,__PACKAGE__); 1007 my $s=$_[1]; 1008 $s='-'.$s if $self->{sort} eq $s; 1009 $self->Sort($s); 1010 },$prop->{sort}) if defined $prop->{sort}; 1011 my $tv=$self->child; 1012 if (defined $pos) { $tv->insert_column($column, $pos); } 1013 else { $tv->append_column($column); } 1014 #################################### connect col selection menu to right-click on column 1015 my $label=Gtk2::Label->new($prop->{title}); 1016 $column->set_widget($label); 1017 $label->show; 1018 my $button_press_sub=sub 1019 { my $event=$_[1]; 1020 return 0 unless $event->button == 3; 1021 my $self=::find_ancestor($_[0],__PACKAGE__); 1022 $self->SelectColumns($_[2]); # $_[2]=$colid 1023 1; 1024 }; 1025 if (my $event=$prop->{event}) 1026 { ::Watch($label,$_,sub { my $self=::find_ancestor($_[0],__PACKAGE__); $self->queue_draw if $self; }) for split / /,$event; # could queue_draw only column 1027 } 1028 my $button=$label->get_ancestor('Gtk2::Button'); #column button 1029 $button->signal_connect(button_press_event => $button_press_sub,$colid) if $button; 1030 return $column; 1031} 1032 1033sub UpdateSortIndicator 1034{ my $self=$_[0]; 1035 my $tv=$self->child; 1036 $_->set_sort_indicator(FALSE) for grep $_->get_sort_indicator, $tv->get_columns; 1037 return if $self->{no_sort_indicator}; 1038 if ($self->{sort}=~m/^(-)?([^ ]+)$/) 1039 { my $order=($1)? 'descending' : 'ascending'; 1040 my @cols=grep( ($SLC_Prop{$_->{colid}}{sort}||'') eq $2, $tv->get_columns); 1041 for my $col (@cols) 1042 { $col->set_sort_indicator(TRUE); 1043 $col->set_sort_order($order); 1044 } 1045 } 1046} 1047 1048sub SelectColumns 1049{ my ($self,$pos)=@_; 1050 ::PopupContextMenu( \@ColumnMenu, {self=>$self, 'pos' => $pos, mode=>$self->{type}, } ); 1051} 1052 1053sub ToggleColumn 1054{ my ($self,$colid,$colpos)=@_; 1055 my $tv=$self->child; 1056 my $position; 1057 my $n=0; 1058 for my $column ($tv->get_columns) 1059 { if ($column->{colid} eq $colid) 1060 { $self->{colwidth}{$colid}= $column->get_width; 1061 $tv->remove_column($column); 1062 undef $position; 1063 last; 1064 } 1065 $n++; 1066 $position=$n if $column->{colid} eq $colpos; 1067 } 1068 $self->AddColumn($colid,$position) if defined $position; 1069 $self->AddColumn('title') unless $tv->get_columns; #if removed the last column 1070 $self->{cols_to_watch}=undef; #to force update list of columns to watch 1071} 1072 1073sub set_has_tooltip { $_[0]->child->set_has_tooltip($_[1]) } 1074 1075sub expose_cb 1076{ my ($tv,$event)=@_; 1077 my $self=$tv->parent; 1078 unless ($tv->get_model->iter_n_children && $event->window != $tv->window) 1079 { $tv->get_bin_window->clear; 1080 # draw empty text when no songs 1081 $self->DrawEmpty($tv->get_bin_window,$tv->window, $tv->get_hadjustment->value); 1082 } 1083 return 0; 1084} 1085 1086sub query_tooltip_cb 1087{ my ($tv, $x, $y, $keyb, $tooltip)=@_; 1088 return 0 if $keyb; 1089 my ($path, $column)=$tv->get_path_at_pos($tv->convert_widget_to_bin_window_coords($x,$y)); 1090 return 0 unless $path; 1091 my ($row)=$path->get_indices; 1092 my $self=::find_ancestor($tv,__PACKAGE__); 1093 my $ID=$self->{array}[$row]; 1094 return unless defined $ID; 1095 my $markup= ::ReplaceFieldsAndEsc($ID,$self->{rowtip}); 1096 $tooltip->set_markup($markup); 1097 $tv->set_tooltip_row($tooltip,$path); 1098 1; 1099} 1100 1101sub GetCurrentRow 1102{ my $self=shift; 1103 my $tv=$self->child; 1104 my ($path)= $tv->get_cursor; 1105 return unless $path; 1106 my $row=$path->to_string; 1107 return $row; 1108} 1109 1110sub GetSelectedRows 1111{ my $self=shift; 1112 return [map $_->to_string, $self->child->get_selection->get_selected_rows]; 1113} 1114 1115sub drag_received_cb 1116{ my ($tv,$type,$dest,@IDs)=@_; 1117 $tv->signal_stop_emission_by_name('drag_data_received'); #override the default 'drag_data_received' handler on GtkTreeView 1118 my $self=$tv->parent; 1119 my $songarray=$self->{array}; 1120 my (undef,$path,$pos)=@$dest; 1121 my $row=$path? ($path->get_indices)[0] : scalar@{$self->{array}}; 1122 $row++ if $path && $pos && $pos eq 'after'; 1123 1124 if ($tv->{drag_is_source}) 1125 { $songarray->Move($row,$self->GetSelectedRows); 1126 return; 1127 } 1128 1129 if ($type==::DRAG_FILE) #convert filenames to IDs 1130 { @IDs=::FolderToIDs(1,0,map ::decode_url($_), @IDs); 1131 return unless @IDs; 1132 } 1133 $songarray->Insert($row,\@IDs); 1134} 1135 1136sub drag_motion_cb 1137{ my ($tv,$context,$x,$y,$time)=@_;# warn "drag_motion_cb @_"; 1138 my $self=$tv->parent; 1139 if ($self->{autoupdate}) { $context->status('default',$time); return } # refuse any drop if autoupdate is on 1140 ::drag_checkscrolling($tv,$context,$y); 1141 return if $x<0 || $y<0; 1142 my ($path,$pos)=$tv->get_dest_row_at_pos($x,$y); 1143 if ($path) 1144 { $pos= ($pos=~m/after$/)? 'after' : 'before'; 1145 } 1146 else #cursor is in an empty (no rows) zone #FIXME also happens when above or below treeview 1147 { my $n=$tv->get_model->iter_n_children; 1148 $path=Gtk2::TreePath->new_from_indices($n-1) if $n; #at the end 1149 $pos='after'; 1150 } 1151 $context->{dest}=[$tv,$path,$pos]; 1152 $tv->set_drag_dest_row($path,$pos); 1153 $context->status(($tv->{drag_is_source} ? 'move' : 'copy'),$time); 1154 return 1; 1155} 1156 1157sub sel_changed_cb 1158{ my $treesel=$_[0]; 1159 my $group=$treesel->get_tree_view->parent->{group}; 1160 ::IdleDo('1_Changed'.$group,10, \&::HasChanged, 'Selection_'.$group); #delay it, because it can be called A LOT when, for example, removing 10000 selected rows 1161} 1162sub cursor_changed_cb 1163{ my $tv=$_[0]; 1164 my ($path)= $tv->get_cursor; 1165 return unless $path; 1166 my $self=$tv->parent; 1167 my $ID=$self->{array}[ $path->to_string ]; 1168 ::HasChangedSelID($self->{group},$ID); 1169} 1170 1171sub row_activated_cb 1172{ my ($tv,$path,$column)=@_; 1173 my $self=$tv->parent; 1174 $self->Activate(1); 1175} 1176 1177sub ResetModel 1178{ my $self=$_[0]; 1179 my $tv=$self->child; 1180 $tv->set_model(undef); 1181 $self->{store}{size}=@{$self->{array}}; 1182 $tv->set_model($self->{store}); 1183 $self->UpdateSortIndicator; 1184 1185 my $ID=::GetSelID($self); 1186 my $songarray=$self->{array}; 1187 if (defined $ID && $songarray->IsIn($ID)) #scroll to last selected ID if in the list 1188 { my $row= ::first { $songarray->[$_]==$ID } 0..$#$songarray; 1189 $row=Gtk2::TreePath->new($row); 1190 $tv->get_selection->select_path($row); 1191 $tv->scroll_to_cell($row,undef,::TRUE,0,0); 1192 } 1193 else 1194 { $self->Scroll_to_TopEnd(); 1195 $self->FollowSong if $self->{follow}; 1196 } 1197} 1198 1199sub Scroll_to_TopEnd 1200{ my ($self,$end)=@_; 1201 my $songarray=$self->{array}; 1202 return unless @$songarray; 1203 my $row= $end ? $#$songarray : 0; 1204 $row=Gtk2::TreePath->new($row); 1205 $self->child->scroll_to_cell($row,undef,::TRUE,0,0); 1206} 1207 1208sub CurSongChanged 1209{ my $self=$_[0]; 1210 $self->queue_draw if $self->{playrow}; 1211 $self->FollowSong if $self->{follow}; 1212} 1213 1214sub SongsChanged_cb 1215{ my ($self,$IDs,$fields)=@_; 1216 my $usedfields= $self->{cols_to_watch}||= do 1217 { my $tv=$self->child; 1218 my %h; 1219 for my $col ($tv->get_columns) 1220 { if (my $d= $SLC_Prop{ $col->{colid} }{depend}) 1221 { $h{$_}=undef for split / /,$d; 1222 } 1223 } 1224 [keys %h]; 1225 }; 1226 return unless ::OneInCommon($fields,$usedfields); 1227 if ($IDs) 1228 { my $changed=$self->{array}->AreIn($IDs); 1229 return unless @$changed; 1230 #call UpdateID(@$changed) ? update individual rows or just redraw everything ? 1231 } 1232 $self->child->queue_draw; 1233} 1234 1235sub SongArray_changed_cb 1236{ my ($self,$array,$action,@extra)=@_; 1237 #if ($self->{mode} eq 'playlist' && $array==$::ListPlay) 1238 #{ $self->{array}->Mirror($array,$action,@extra); 1239 #} 1240 return unless $self->{array}==$array; 1241 warn "SongArray_changed $action,@extra\n" if $::debug; 1242 my $tv=$self->child; 1243 my $store=$tv->get_model; 1244 my $treesel=$tv->get_selection; 1245 my @selected=map $_->to_string, $treesel->get_selected_rows; 1246 my $updateselection; 1247 if ($action eq 'sort') 1248 { my ($sort,$oldarray)=@extra; 1249 $self->{'sort'}=$sort; 1250 my @order; 1251 $order[ $array->[$_] ]=$_ for reverse 0..$#$array; #reverse so that in case of duplicates ID, $order[$ID] is the first row with this $ID 1252 my @IDs= map $oldarray->[$_], @selected; 1253 @selected= map $order[$_]++, @IDs; # $order->[$ID]++ so that in case of duplicates ID, the next row (with same $ID) are used 1254 $self->ResetModel; 1255 #$self->UpdateSortIndicator; #not needed : already called by $self->ResetModel 1256 $updateselection=1; 1257 } 1258 elsif ($action eq 'update') #should only happen when in filter mode, so no duplicates IDs 1259 { my $oldarray=$extra[0]; 1260 my @selectedID; 1261 $selectedID[$oldarray->[$_]]=1 for @selected; 1262 @selected=grep $selectedID[$array->[$_]], 0..$#$array; 1263 # lie to the model, just tell it that some rows were removed/inserted and refresh 1264 # if it cause a problem, just use $self->ResetModel; instead 1265 my $diff= @$array - @$oldarray; 1266 if ($diff>0) { $store->rowinsert(scalar @$oldarray,$diff); } 1267 elsif ($diff<0) { $store->rowremove([$#$array+1..$#$oldarray]); } 1268 $self->queue_draw; 1269 $updateselection=1; 1270 } 1271 elsif ($action eq 'insert') 1272 { my ($destrow,$IDs)=@extra; 1273 #$_>=$destrow and $_+=@$IDs for @selected; #not needed as the treemodel will update the selection 1274 $store->rowinsert($destrow,scalar @$IDs); 1275 } 1276 elsif ($action eq 'move') 1277 { my (undef,$rows,$destrow)=@extra; 1278 my $i= my $j= my $delta=0; 1279 if (@selected) 1280 { for my $row (0..$selected[-1]) 1281 { if ($row==$destrow+$delta) {$delta-=@$rows} 1282 if ($i<=$#$rows && $row==$rows->[$i]) #row moved 1283 { if ($selected[$j]==$rows->[$i]) { $selected[$j]=$destrow+$i; $j++; } #row moved and selected 1284 $delta++; $i++; 1285 } 1286 elsif ($row==$selected[$j]) #row selected 1287 { $selected[$j]-=$delta; $j++; } 1288 } 1289 $updateselection=1; 1290 } 1291 $self->queue_draw; 1292 #$store->rowremove($rows); 1293 #$store->rowinsert($destrow,scalar @$rows); 1294 } 1295 elsif ($action eq 'up') 1296 { my $rows=$extra[0]; 1297 my $i=0; 1298 for my $row (@$rows) 1299 { $i++ while $i<=$#selected && $selected[$i]<$row-1; 1300 last if $i>$#selected; 1301 if ($selected[$i]==$row-1) { $selected[$i]++ unless $i<=$#selected && $selected[$i+1]==$row;$updateselection=1; } 1302 elsif ($selected[$i]==$row) { $selected[$i]--;$updateselection=1; $i++ } 1303 } 1304 $self->queue_draw; 1305 } 1306 elsif ($action eq 'down') 1307 { my $rows=$extra[0]; 1308 my $i=$#selected; 1309 for my $row (reverse @$rows) 1310 { $i-- while $i>=0 && $selected[$i]>$row+1; 1311 last if $i<0; 1312 if ($selected[$i]==$row+1) { $selected[$i]-- unless $i>=0 && $selected[$i-1]==$row;$updateselection=1; } 1313 elsif ($selected[$i]==$row) { $selected[$i]++;$updateselection=1; $i-- } 1314 } 1315 $self->queue_draw; 1316 } 1317 elsif ($action eq 'remove') 1318 { my $rows=$extra[0]; 1319 $store->rowremove($rows); 1320 $self->ResetModel if @$array==0; #don't know why, but when the list is not empty and adding/removing columns that result in a different row height; after removing all the rows, and then inserting a row, the row height is reset to the previous height. Doing a reset model when the list is empty solves this. 1321 } 1322 elsif ($action eq 'mode' || $action eq 'proxychange') {return} #the list itself hasn't changed 1323 else #'replace' or unknown action 1324 { $self->ResetModel; #FIXME if replace : check if a filter is in $extra[0] 1325 #$treesel->unselect_all; 1326 } 1327 $self->SetSelection(\@selected) if $updateselection; 1328 $self->Hide(!scalar @$array) if $self->{hideif} eq 'empty'; 1329} 1330 1331sub FollowSong 1332{ my $self=$_[0]; 1333 my $tv=$self->child; 1334 #$tv->get_selection->unselect_all; 1335 my $songarray=$self->{array}; 1336 return unless defined $::SongID; 1337 my $rowplaying; 1338 if ($self->{mode} eq 'playlist') { $rowplaying=$::Position; } #$::Position may be undef even if song is in list (random mode), in that case fallback to the usual case below 1339 $rowplaying= ::first { $songarray->[$_]==$::SongID } 0..$#$songarray unless defined $rowplaying && $rowplaying>=0; 1340 if (defined $rowplaying) 1341 { my $path=Gtk2::TreePath->new($rowplaying); 1342 my $visible; 1343 my $win = $tv->get_bin_window; 1344 if ($win) #check if row is visible -> no need to scroll_to_cell 1345 { #maybe should use gtk_tree_view_get_visible_range (requires gtk 2.8) 1346 my $first=$tv->get_path_at_pos(0,0); 1347 my $last=$tv->get_path_at_pos(0,($win->get_size)[1] - 1); 1348 if ((!$first || $first->to_string < $rowplaying) && (!$last || $rowplaying < $last->to_string)) 1349 { 1350 $visible=1; 1351 } 1352 } 1353 $tv->scroll_to_cell($path,undef,TRUE,.5,.5) unless $visible; 1354 $tv->set_cursor($path); 1355 } 1356 elsif (defined $::SongID) #Set the song ID even if the song isn't in the list 1357 { ::HasChangedSelID($self->{group},$::SongID); } 1358} 1359 1360sub SetSelection 1361{ my ($self,$select)=@_; 1362 my $treesel=$self->child->get_selection; 1363 $treesel->unselect_all; 1364 $treesel->select_path( Gtk2::TreePath->new($_) ) for @$select; 1365} 1366 1367#sub UpdateID #DELME ? update individual rows or just redraw everything ? 1368#{ my $self=$_[0]; 1369# my $array=$self->{array}; 1370# my $store=$self->child->get_model; 1371# my %updated; 1372# warn "update ID @_\n" if $::debug; 1373# $updated{$_}=undef for @_; 1374# my $row=@$array; 1375# while ($row-->0) #FIXME maybe only check displayed rows 1376# { my $ID=$$array[$row]; 1377# next unless exists $updated{$ID}; 1378# $store->rowchanged($row); 1379# #delete $updated{$ID}; 1380# #last unless (keys %updated); 1381# } 1382#} 1383 1384################################################################################ 1385package SongStore; 1386use Glib qw(TRUE FALSE); 1387 1388my (%Columns,@Value,@Type); 1389 1390use Glib::Object::Subclass 1391 Glib::Object::, 1392 interfaces => [Gtk2::TreeModel::], 1393 ; 1394 1395sub get_column_number 1396{ my $colid=$_[0]; 1397 my $colnb=$Columns{$colid}; 1398 unless (defined $colnb) 1399 { push @Value, $SongList::SLC_Prop{$colid}{value}; 1400 push @Type, $SongList::SLC_Prop{$colid}{type}; 1401 $colnb= $Columns{$colid}= $#Value; 1402 } 1403 return $colnb; 1404} 1405 1406sub INIT_INSTANCE { 1407 my $self = $_[0]; 1408 # int to check whether an iter belongs to our model 1409 $self->{stamp} = $self+0;#sprintf '%d', rand (1<<31); 1410} 1411#sub FINALIZE_INSTANCE 1412#{ #my $self = $_[0]; 1413# # free all records and free all memory used by the list 1414#} 1415sub GET_FLAGS { [qw/list-only iters-persist/] } 1416sub GET_N_COLUMNS { $#Value } 1417sub GET_COLUMN_TYPE { $Type[ $_[1] ]; } 1418sub GET_ITER 1419{ my $self=$_[0]; my $path=$_[1]; 1420 die "no path" unless $path; 1421 1422 # we do not allow children 1423 # depth 1 = top level; a list only has top level nodes and no children 1424# my $depth = $path->get_depth; 1425# die "depth != 1" unless $depth == 1; 1426 1427 my $n=$path->get_indices; #return only one value because it's a list 1428 #warn "GET_ITER $n\n"; 1429 return undef if $n >= $self->{size} || $n < 0; 1430 1431 #my $ID = $self->{array}[$n]; 1432 #die "no ID" unless defined $ID; 1433 #return iter : 1434 return [ $self->{stamp}, $n, $self->{array} , undef ]; 1435} 1436 1437sub GET_PATH 1438{ my ($self, $iter) = @_; #warn "GET_PATH\n"; 1439 die "no iter" unless $iter; 1440 1441 my $path = Gtk2::TreePath->new; 1442 $path->append_index ($iter->[1]); 1443 return $path; 1444} 1445 1446sub GET_VALUE 1447{ my $row=$_[1][1]; #warn "GET_VALUE\n"; 1448 $Value[$_[2]]( $_[0], $row, $_[1][2][$row] ); #args : self, row, ID 1449} 1450 1451sub ITER_NEXT 1452{ #my ($self, $iter) = @_; 1453 my $self=$_[0]; 1454# return undef unless $_[1]; 1455 my $n=$_[1]->[1]; #$iter->[1] 1456 #warn "GET_NEXT $n\n"; 1457 return undef unless ++$n < $self->{size}; 1458 return [ $self->{stamp}, $n, $self->{array}, undef ]; 1459} 1460 1461sub ITER_CHILDREN 1462{ my ($self, $parent) = @_; #warn "GET_CHILDREN\n"; 1463 # this is a list, nodes have no children 1464 return undef if $parent; 1465 # parent == NULL is a special case; we need to return the first top-level row 1466 # No rows => no first row 1467 return undef unless $self->{size}; 1468 # Set iter to first item in list 1469 return [ $self->{stamp}, 0, $self->{array}, undef ]; 1470} 1471sub ITER_HAS_CHILD { FALSE } 1472sub ITER_N_CHILDREN 1473{ my ($self, $iter) = @_; #warn "ITER_N_CHILDREN\n"; 1474 # special case: if iter == NULL, return number of top-level rows 1475 return ( $iter? 0 : $self->{size} ); 1476} 1477sub ITER_NTH_CHILD 1478{ #my ($self, $parent, $n) = @_; #warn "ITER_NTH_CHILD\n"; 1479 # a list has only top-level rows 1480 return undef if $_[1]; #$parent; 1481 my $self=$_[0]; my $n=$_[2]; 1482 # special case: if parent == NULL, set iter to n-th top-level row 1483 return undef if $n >= $self->{size}; 1484 1485 return [ $self->{stamp}, $n, $self->{array}, undef ]; 1486} 1487sub ITER_PARENT { FALSE } 1488 1489sub search_equal_func 1490{ #my ($self,$col,$string,$iter)=@_; 1491 my $iter= $_[3]->to_arrayref($_[0]{stamp}); 1492 my $ID= $iter->[2][ $iter->[1] ]; 1493 my $string=uc $_[2]; 1494 #my $r; for (qw/title album artist/) { $r=index uc(Songs::Display($ID,$_)), $string; last if $r==0 } return $r; 1495 index uc(Songs::Display($ID,'title')), $string; 1496} 1497 1498sub rowremove 1499{ my ($self,$rows)=@_; 1500 for my $row (reverse @$rows) 1501 { $self->row_deleted( Gtk2::TreePath->new($row) ); 1502 $self->{size}--; 1503 } 1504} 1505sub rowinsert 1506{ my ($self,$row,$number)=@_; 1507 for (1..$number) 1508 { $self->{size}++; 1509 $self->row_inserted( Gtk2::TreePath->new($row), $self->get_iter_from_string($row) ); 1510 $row++; 1511 } 1512} 1513#sub rowchanged #not used anymore 1514#{ my $self=$_[0]; my $row=$_[1]; 1515# my $iter=$self->get_iter_from_string($row); 1516# return unless $iter; 1517# $self->row_changed( $self->get_path($iter), $iter); 1518#} 1519 1520package MultiTreeView; 1521#for common functions needed to support correct multi-rows drag and drop in treeviews 1522 1523sub init 1524{ my ($tv,$selfpkg)=@_; 1525 $tv->{selfpkg}=$selfpkg; 1526 $tv->{drag_begin_cb}=\&drag_begin_cb; 1527 $tv->signal_connect(button_press_event=> \&button_press_cb); 1528 $tv->signal_connect(button_release_event=> \&button_release_cb); 1529} 1530 1531sub drag_begin_cb 1532{ my ($tv,$context)=@_;# warn "drag_begin @_"; 1533 $tv->{pressed}=undef; 1534} 1535 1536sub button_press_cb 1537{ my ($tv,$event)=@_; 1538 return 0 if $event->window!=$tv->get_bin_window; #ignore click outside the bin_window (for example the column headers) 1539 my $self=::find_ancestor($tv, $tv->{selfpkg} ); 1540 my $but=$event->button; 1541 my $sel=$tv->get_selection; 1542 if ($but!=1 && $event->type eq '2button-press') 1543 { $self->Activate($but); 1544 return 1; 1545 } 1546 my $ctrl_shift= $event->get_state * ['shift-mask', 'control-mask']; 1547 if ($but==1) # do not clear multi-row selection if button press on a selected row (to allow dragging selected rows) 1548 {{ last if $ctrl_shift; #don't interfere with default if control or shift is pressed 1549 last unless $sel->count_selected_rows > 1; 1550 my $path=$tv->get_path_at_pos($event->get_coords); 1551 last unless $path && $sel->path_is_selected($path); 1552 $tv->{pressed}=1; 1553 return 1; 1554 }} 1555 if ($but==3) 1556 { my $path=$tv->get_path_at_pos($event->get_coords); 1557 if ($path && !$sel->path_is_selected($path)) 1558 { $sel->unselect_all unless $ctrl_shift; 1559 #$sel->select_path($path); 1560 $tv->set_cursor($path); 1561 } 1562 $self->PopupContextMenu; 1563 return 1; 1564 } 1565 return 0; #let the event propagate 1566} 1567 1568sub button_release_cb #clear selection and select current row only if the press event was on a selected row and there was no dragging 1569{ my ($tv,$event)=@_; 1570 return 0 unless $event->button==1 && $tv->{pressed}; 1571 $tv->{pressed}=undef; 1572 my $path=$tv->get_path_at_pos($event->get_coords); 1573 return 0 unless $path; 1574 my $sel=$tv->get_selection; 1575 $sel->unselect_all; 1576 $sel->select_path($path); 1577 return 1; 1578} 1579 1580package FilterPane; 1581use base 'Gtk2::Box'; 1582 1583use constant { TRUE => 1, FALSE => 0, }; 1584 1585our %Pages= 1586( filter => [SavedTree => 'F', 'i', _"Filter" ], 1587 list => [SavedTree => 'L', 'i', _"List" ], 1588 savedtree=>[SavedTree => 'FL', 'i', _"Saved" ], 1589 folder => [FolderList => 'path', 'n', _"Folder" ], 1590 filesys => [Filesystem => '', '',_"Filesystem"], 1591); 1592 1593our @MenuMarkupOptions= 1594( "%a", 1595 "<b>%a</b>%Y\n<small>%s <small>%l</small></small>", 1596 "<b>%a</b>%Y\n<small>%b</small>", 1597 "<b>%a</b>%Y\n<small>%b</small>\n<small>%s <small>%l</small></small>", 1598 "<b>%y %a</b>", 1599); 1600my @picsize_menu= 1601( _("no pictures") => 0, 1602 _("automatic size") => -1, 1603 _("small size") => 16, 1604 _("medium size") => 32, 1605 _("big size") => 64, 1606); 1607my @mpicsize_menu= 1608( _("small size") => 32, 1609 _("medium size") => 64, 1610 _("big size") => 96, 1611 _("huge size") => 128, 1612); 1613my @cloudstats_menu= 1614( _("number of songs") => 'count', 1615 _("rating average") => 'rating:average', 1616 _("play count average") => 'playcount:average', 1617 _("skip count average") => 'skipcount:average', 1618), 1619 1620my %sort_menu= 1621( year => _("year"), 1622 year2=> _("year (highest)"), 1623 alpha=> _("alphabetical"), 1624 songs=> _("number of songs in filter"), 1625 'length'=> _("length of songs"), 1626); 1627my %sort_menu_album= 1628( %sort_menu, 1629 artist => _("artist") 1630); 1631my @sort_menu_append= 1632( {separator=>1}, 1633 { label=> _"reverse order", check=> sub { $_[0]{self}{'sort'}[$_[0]{depth}]=~m/^-/ }, 1634 code=> sub { my $self=$_[0]{self}; $self->{'sort'}[$_[0]{depth}]=~s/^(-)?/$1 ? "" : "-"/e; $self->SetOption; } 1635 }, 1636); 1637 1638our @MenuPageOptions; 1639my @MenuSubGroup= 1640( { label => sub {_("Set subgroup").' '.$_[0]{depth}}, submenu => sub { return {0 => _"None",map {$_=>Songs::FieldName($_)} Songs::FilterListFields()}; }, 1641 first_key=> "0", submenu_reverse => 1, 1642 code => sub { $_[0]{self}->SetField($_[1],$_[0]{depth}) }, 1643 check => sub { $_[0]{self}{field}[$_[0]{depth}] ||0 }, 1644 }, 1645 { label => sub {_("Options for subgroup").' '.$_[0]{depth}}, submenu => \@MenuPageOptions, 1646 test => sub { $_[0]{depth} <= $_[0]{self}{depth} }, 1647 }, 1648); 1649 1650@MenuPageOptions= 1651( { label => _"show pictures", code => sub { my $self=$_[0]{self}; $self->{lpicsize}[$_[0]{depth}]=$_[1]; $self->SetOption; }, mode => 'LS', 1652 submenu => \@picsize_menu, submenu_ordered_hash => 1, check => sub {$_[0]{self}{lpicsize}[$_[0]{depth}]}, 1653 test => sub { Songs::FilterListProp($_[0]{subfield},'picture'); }, }, 1654 { label => _"text format", code => sub { my $self=$_[0]{self}; $self->{lmarkup}[$_[0]{depth}]= $_[1]; $self->SetOption; }, 1655 submenu => sub{ my $field= $_[0]{self}{type}[ $_[0]{depth} ]; 1656 my $gid= Songs::Get_gid($::SongID,$field); $gid=$gid->[0] if ref $gid; 1657 return unless $gid; # option not shown if no current song, FIXME could try to find a song in the library 1658 return [ map { AA::ReplaceFields( $gid,$_,$field,::TRUE ), ($_ eq "%a" ? 0 : $_) } @MenuMarkupOptions ]; 1659 }, submenu_ordered_hash => 1, submenu_use_markup => 1, 1660 check => sub { $_[0]{self}{lmarkup}[$_[0]{depth}]}, istrue => 'aa', mode => 'LS', }, 1661 { label => _"text mode", code => sub { $_[0]{self}->SetOption(mmarkup=>$_[1]); }, 1662 submenu => [ 0 => _"None", below => _"Below", right => _"Right side", ], submenu_ordered_hash => 1, submenu_reverse => 1, 1663 check => sub { $_[0]{self}{mmarkup} }, mode => 'M', }, 1664 { label => _"picture size", code => sub { $_[0]{self}->SetOption(mpicsize=>$_[1]); }, 1665 mode => 'M', 1666 submenu => \@mpicsize_menu, submenu_ordered_hash => 1, check => sub {$_[0]{self}{mpicsize}}, istrue => 'aa' }, 1667 1668 { label => _"font size depends on", code => sub { $_[0]{self}->SetOption(cloud_stat=>$_[1]); }, 1669 mode => 'C', 1670 submenu => \@cloudstats_menu, submenu_ordered_hash => 1, check => sub {$_[0]{self}{cloud_stat}}, }, 1671 { label => _"minimum font size", code => sub { $_[0]{self}->SetOption(cloud_min=>$_[1]); }, 1672 mode => 'C', 1673 submenu => sub { [2..::min(20,$_[0]{self}{cloud_max}-1)] }, check => sub {$_[0]{self}{cloud_min}}, }, 1674 { label => _"maximum font size", code => sub { $_[0]{self}->SetOption(cloud_max=>$_[1]); }, 1675 mode => 'C', 1676 submenu => sub { [::max(10,$_[0]{self}{cloud_min}+1)..40] }, check => sub {$_[0]{self}{cloud_max}}, }, 1677 1678 { label => _"sort by", code => sub { my $self=$_[0]{self}; $self->{'sort'}[$_[0]{depth}]=$_[1]; $self->SetOption; }, 1679 check => sub {$_[0]{self}{sort}[$_[0]{depth}]}, submenu => sub { $_[0]{field} eq 'album' ? \%sort_menu_album : \%sort_menu; }, 1680 submenu_reverse => 1, append => \@sort_menu_append, 1681 }, 1682 { label => _"group by", 1683 code => sub { my $self=$_[0]{self}; my $d=$_[0]{depth}; $self->{type}[$d]=$self->{field}[$d].'.'.$_[1]; $self->Fill('rehash'); }, 1684 check => sub { my $n=$_[0]{self}{type}[$_[0]{depth}]; $n=~s#^[^.]+\.##; $n }, 1685 submenu=>sub { Songs::LookupCode( $_[0]{self}{field}[$_[0]{depth}], 'subtypes_menu' ); }, submenu_reverse => 1, 1686 #test => sub { $FilterList::Field{ $_[0]{self}{field}[$_[0]{depth}] }{types}; }, 1687 }, 1688 { repeat => sub { map [\@MenuSubGroup, depth=>$_, mode => 'S', subfield => $_[0]{self}{field}[$_], ], 1..$_[0]{self}{depth}+1; }, mode => 'L', 1689 }, 1690 { label => _"cloud mode", code => sub { my $self=$_[0]{self}; $self->set_mode(($self->{mode} eq 'cloud' ? 'list' : 'cloud'),1); }, 1691 check => sub {$_[0]{mode} eq 'C'}, notmode => 'S', }, 1692 { label => _"mosaic mode", code => sub { my $self=$_[0]{self}; $self->set_mode(($self->{mode} eq 'mosaic' ? 'list' : 'mosaic'),1);}, 1693 check => sub {$_[0]{mode} eq 'M'}, notmode => 'S', 1694 test => sub { Songs::FilterListProp($_[0]{field},'picture') }, 1695 }, 1696 { label => _"show the 'All' row", code => sub { $_[0]{self}->SetOption; }, toggleoption => '!self/noall', mode => 'L', 1697 }, 1698 { label => _"show histogram background",code => sub { $_[0]{self}->SetOption; }, toggleoption => 'self/histogram', mode => 'L', 1699 }, 1700); 1701 1702our @cMenu= 1703( { label=> _"Play", code => sub { ::Select(filter=>$_[0]{filter},song=>'first',play=>1); }, 1704 isdefined => 'filter', stockicon => 'gtk-media-play', id => 'play' 1705 }, 1706 { label=> _"Append to playlist", code => sub { ::DoActionForList('addplay',$_[0]{filter}->filter); }, 1707 isdefined => 'filter', stockicon => 'gtk-add', id => 'addplay', 1708 }, 1709 { label=> _"Enqueue", code => sub { ::EnqueueFilter($_[0]{filter}); }, 1710 isdefined => 'filter', stockicon => 'gmb-queue', id => 'enqueue', 1711 }, 1712 { label=> _"Set as primary filter", 1713 code => sub {my $fp=$_[0]{filterpane}; ::SetFilter( $_[0]{self}, $_[0]{filter}, 1, $fp->{group} ); }, 1714 test => sub {my $fp=$_[0]{filterpane}; $fp->{nb}>1 && $_[0]{filter};} 1715 }, 1716 #songs submenu : 1717 { label => sub { my $IDs=$_[0]{filter}->filter; ::__n("%d song","%d songs",scalar @$IDs); }, 1718 submenu => sub { ::BuildMenuOptional(\@::SongCMenu, { mode => 'F', IDs=>$_[0]{filter}->filter }); }, 1719 isdefined => 'filter', 1720 }, 1721 { label=> _"Rename folder", code => sub { ::AskRenameFolder($_[0]{rawpathlist}[0]); }, onlyone => 'rawpathlist', test => sub {!$::CmdLine{ro}}, }, 1722 { label=> _"Open folder", code => sub { ::openfolder( $_[0]{rawpathlist}[0] ); }, onlyone => 'rawpathlist', }, 1723 #{ label=> _"move folder", code => sub { ::MoveFolder($_[0]{pathlist}[0]); }, onlyone => 'pathlist', test => sub {!$::CmdLine{ro}}, }, 1724 { label=> _"Scan for new songs", code => sub { ::IdleScan( @{$_[0]{rawpathlist}} ); }, 1725 notempty => 'rawpathlist' }, 1726 { label=> _"Check for updated/removed songs", code => sub { ::IdleCheck( @{ $_[0]{filter}->filter } ); }, 1727 isdefined => 'filter', stockicon => 'gtk-refresh', istrue => 'pathlist' }, #doesn't really need pathlist, but makes less sense for non-folder pages 1728 { label=> _"Set Picture", stockicon => 'gmb-picture', 1729 code => sub { my $gid=$_[0]{gidlist}[0]; ::ChooseAAPicture(undef,$_[0]{field},$gid); }, 1730 onlyone=> 'gidlist', test => sub { Songs::FilterListProp($_[0]{field},'picture') && $_[0]{gidlist}[0]>0; }, 1731 }, 1732 { label => _"Auto-select Pictures", code => sub { ::AutoSelPictures( $_[0]{field}, @{ $_[0]{gidlist} } ); }, 1733 onlymany=> 'gidlist', test => sub { $_[0]{field} eq 'album' }, #test => sub { Songs::FilterListProp($_[0]{field},'picture'); }, 1734 stockicon => 'gmb-picture', 1735 }, 1736 { label=> _"Set icon", stockicon => 'gmb-picture', 1737 code => sub { my $gid=$_[0]{gidlist}[0]; Songs::ChooseIcon($_[0]{field},$gid); }, 1738 onlyone=> 'gidlist', test => sub { Songs::FilterListProp($_[0]{field},'icon') && $_[0]{gidlist}[0]>0; }, 1739 }, 1740 { label=> _"Remove label", stockicon => 'gtk-remove', 1741 code => sub { my $gid=$_[0]{gidlist}[0]; ::RemoveLabel($_[0]{field},$gid); }, 1742 onlyone=> 'gidlist', test => sub { $_[0]{field} eq 'label' && $_[0]{gidlist}[0] !=0 }, #FIXME make it generic rather than specific to field label ? #FIXME find a better way to check if gid is special than comparing it to 0 1743 }, 1744 { label=> _"Rename label", 1745 code => sub { my $gid=$_[0]{gidlist}[0]; ::RenameLabel($_[0]{field},$gid); }, 1746 onlyone=> 'gidlist', test => sub { $_[0]{field} eq 'label' && $_[0]{gidlist}[0] !=0 }, #FIXME make it generic rather than specific to field label ? #FIXME find a better way to check if gid is special than comparing it to 0 1747 }, 1748# { separator=>1 }, 1749 { label => _"Options", submenu => \@MenuPageOptions, stock => 'gtk-preferences', isdefined => 'field' }, 1750 { label => _"Show buttons", toggleoption => '!filterpane/hidebb', code => sub { my $fp=$_[0]{filterpane}; $fp->{bottom_buttons}->set_visible(!$fp->{hidebb}); }, }, 1751 { label => _"Show tabs", toggleoption => '!filterpane/hidetabs', code => sub { my $fp=$_[0]{filterpane}; $fp->{notebook}->set_show_tabs( !$fp->{hidetabs} ); }, }, 1752); 1753 1754our @DefaultOptions= 1755( pages => 'savedtree|artists|album|genre|date|label|folder|added|lastplay|rating', 1756 nb => 1, # filter level 1757 min => 1, # filter out entries with less than $min songs 1758 hidebb => 0, # hide button box 1759 tabmode => 'text', # text, icon or both 1760 hscrollbar=>1, 1761); 1762 1763sub new 1764{ my ($class,$opt)=@_; 1765 my $self = bless Gtk2::VBox->new(FALSE, 6), $class; 1766 $self->{SaveOptions}=\&SaveOptions; 1767 %$opt=( @DefaultOptions, %$opt ); 1768 my @pids=split /\|/, $opt->{pages}; 1769 $self->{$_}=$opt->{$_} for qw/nb group min hidetabs tabmode/, grep(m/^activate\d?$/, keys %$opt); 1770 $self->{main_opt}{$_}=$opt->{$_} for qw/group no_typeahead searchbox rules_hint hscrollbar/; #options passed to children 1771 my $nb=$self->{nb}; 1772 my $group=$self->{group}; 1773 1774 my $spin=Gtk2::SpinButton->new( Gtk2::Adjustment->new($self->{min}, 1, 9999, 1, 10, 0) ,10,0 ); 1775 $spin->signal_connect( value_changed => sub { $self->update_children($_[0]->get_value); } ); 1776 my $ResetB=::NewIconButton('gtk-clear',undef,sub { ::SetFilter($_[0],undef,$nb,$group); }); 1777 $ResetB->set_sensitive(0); 1778 my $InterB=Gtk2::ToggleButton->new; 1779 my $InterBL=Gtk2::Label->new; 1780 $InterBL->set_markup('<b>&</b>'); #bold '&' 1781 $InterB->add($InterBL); 1782 my $InvertB=Gtk2::ToggleButton->new; 1783 my $optB=Gtk2::Button->new; 1784 $InvertB->add(Gtk2::Image->new_from_stock('gmb-invert','menu')); 1785 $optB->add(Gtk2::Image->new_from_stock('gtk-preferences','menu')); 1786 $InvertB->signal_connect( toggled => sub {$self->{invert}=$_[0]->get_active;} ); 1787 $InterB->signal_connect( toggled => sub {$self->{inter} =$_[0]->get_active;} ); 1788 $optB->signal_connect( button_press_event => \&PopupOpt ); 1789 $optB->set_relief('none'); 1790 my $hbox = Gtk2::HBox->new (FALSE, 6); 1791 $hbox->pack_start($_, FALSE, FALSE, 0) for $spin, $ResetB, $InvertB, $InterB, $optB; 1792 $ResetB ->set_tooltip_text( ( $nb==1? _"reset primary filter" : 1793 $nb==2? _"reset secondary filter": 1794 ::__x(_"reset filter {nb}",nb =>$nb) 1795 ) ); 1796 $InterB ->set_tooltip_text(_"toggle Intersection mode"); 1797 $InvertB->set_tooltip_text(_"toggle Invert mode"); 1798 $spin ->set_tooltip_text(_"only show entries with at least n songs"); #FIXME 1799 $optB ->set_tooltip_text(_"options"); 1800 1801 my $notebook = Gtk2::Notebook->new; 1802 $notebook->set_scrollable(TRUE); 1803 if (my $tabpos=$opt->{tabpos}) 1804 { ($tabpos,$self->{angle})= $tabpos=~m/^(left|right|top|bottom)?(90|180|270)?/; 1805 $notebook->set_tab_pos($tabpos) if $tabpos; 1806 } 1807 #$notebook->popup_enable; 1808 $self->{hidetabs}= (@pids==1) unless defined $self->{hidetabs}; 1809 $notebook->set_show_tabs( !$self->{hidetabs} ); 1810 $self->{notebook}=$notebook; 1811 1812 my $setpage; 1813 for my $pid (@pids) 1814 { my $n=$self->AppendPage($pid,$opt->{'page_'.$pid}); 1815 if ($opt->{page} && $opt->{page} eq $pid) { $setpage=$n } 1816 } 1817 $self->AppendPage('album') if $notebook->get_n_pages == 0; # fallback in case no pages has been added 1818 1819 $self->pack_end($hbox, FALSE, FALSE, 0); 1820 $notebook->show_all; #needed to set page in this sub 1821 1822 $hbox->show_all; 1823 $_->set_no_show_all(1) for $hbox,$spin,$InterB,$optB; 1824 $self->{bottom_buttons}=$hbox; 1825 $notebook->signal_connect( button_press_event => \&button_press_event_cb); 1826 $notebook->signal_connect( switch_page => sub 1827 { my $p=$_[0]->get_nth_page($_[2]); 1828 my $self=::find_ancestor($_[0],__PACKAGE__); 1829 $self->{DefaultFocus}=$p; 1830 my $pid= $self->{page}= $p->{pid}; 1831 my $mask= $Pages{$pid} ? $Pages{$pid}[2] : 1832 Songs::FilterListProp($pid,'multi') ? 'oni' : 'on'; 1833 $optB->set_visible ( scalar $mask=~m/o/ ); 1834 $spin->set_visible ( scalar $mask=~m/n/ ); 1835 $InterB->set_visible( scalar $mask=~m/i/ ); 1836 }); 1837 1838 $self->add($notebook); 1839 $notebook->set_current_page( $setpage||0 ); 1840 1841 $self->{hidebb}=$opt->{hidebb}; 1842 $hbox->hide if $self->{hidebb}; 1843 $self->{resetbutton}=$ResetB; 1844 ::Watch($self, Icons => \&icons_changed); 1845 ::Watch($self, SongsChanged=> \&SongsChanged_cb); 1846 ::Watch($self, SongsAdded => \&SongsAdded_cb); 1847 ::Watch($self, SongsRemoved=> \&SongsRemoved_cb); 1848 ::Watch($self, SongsHidden => \&SongsRemoved_cb); 1849 $self->signal_connect(destroy => \&cleanup); 1850 $self->{needupdate}=1; 1851 ::WatchFilter($self,$opt->{group},\&updatefilter); 1852 ::IdleDo('9_FPfull'.$self,100,\&updatefilter,$self); 1853 return $self; 1854} 1855 1856sub SaveOptions 1857{ my $self=shift; 1858 my @opt= 1859 ( hidebb => $self->{hidebb}, 1860 min => $self->{min}, 1861 page => $self->{page}, 1862 hidetabs=> $self->{hidetabs}, 1863 pages => (join '|', map $_->{pid}, $self->{notebook}->get_children), 1864 ); 1865 for my $page (grep $_->isa('FilterList'), $self->{notebook}->get_children) 1866 { my %pageopt=$page->SaveOptions; 1867 push @opt, 'page_'.$page->{pid}, { %pageopt } if keys %pageopt; 1868 } 1869 return \@opt; 1870} 1871 1872sub AppendPage 1873{ my ($self,$pid,$opt)=@_; 1874 my ($package,$col,$label); 1875 if ($Pages{$pid}) 1876 { ($package,$col,undef,$label)=@{ $Pages{$pid} }; 1877 } 1878 elsif ( grep $_ eq $pid, Songs::FilterListFields() ) 1879 { $package='FilterList'; 1880 $col=$pid; 1881 $label=Songs::FieldName($col); 1882 } 1883 else {return} 1884 $opt||={}; 1885 my %opt=( %{$self->{main_opt}}, %$opt); 1886 my $page=$package->new($col,\%opt); #create new page 1887 $page->{pid}=$pid; 1888 $page->{page_name}=$label; 1889 if ($package eq 'FilterList' || $package eq 'FolderList') 1890 { $page->{Depend_on_field}=$col; 1891 } 1892 my $notebook=$self->{notebook}; 1893 my $n=$notebook->append_page( $page, $self->create_tab($page) ); 1894 $n=$notebook->get_n_pages-1; # $notebook->append_page doesn't returns the page number before Gtk2-Perl 1.080 1895 $notebook->set_tab_reorderable($page,TRUE); 1896 $page->show_all; 1897 return $n; 1898} 1899sub create_tab 1900{ my ($self,$page)=@_; 1901 my $pid=$page->{pid}; 1902 my $img; 1903 my $angle= $self->{angle} || 0; 1904 my $label= Gtk2::Label->new( $page->{page_name} ); 1905 $label->set_angle($angle) if $angle; 1906 1907 # set base gravity to auto so that rotated tabs handle vertical scripts (asian languages) better 1908 $label->get_pango_context->set_base_gravity('auto'); 1909 $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 1910 1911 if ($self->{tabmode} ne 'text') 1912 { my $icon= "gmb-tab-$pid"; 1913 $img= Gtk2::Image->new_from_stock($icon,'menu') if Gtk2::IconFactory->lookup_default($icon); 1914 $label=undef if $img && $self->{tabmode} eq 'icon'; 1915 } 1916 my $tab; 1917 if ($img && $label) 1918 { $tab= $angle%180 ? Gtk2::VBox->new(FALSE,0) : Gtk2::HBox->new(FALSE,0); 1919 my @pack= $angle%180 ? ($label,TRUE,$img,FALSE) : ($img,FALSE,$label,TRUE); 1920 $tab->pack_start( $pack[$_], $pack[$_+1],$pack[$_+1],0 ) for 0,2; 1921 } 1922 else { $tab= $img || $label; } 1923 $tab->show_all; 1924 return $tab; 1925} 1926sub icons_changed 1927{ my $self=shift; 1928 if ($self->{tabmode} ne 'text') 1929 { my $notebook=$self->{notebook}; 1930 for my $page ($notebook->get_children) 1931 { $notebook->set_tab_label( $page, $self->create_tab($page) ); 1932 } 1933 } 1934} 1935sub RemovePage_cb 1936{ my $self=$_[1]; 1937 my $nb=$self->{notebook}; 1938 my $n=$nb->get_current_page; 1939 my $page=$nb->get_nth_page($n); 1940 my $pid=$page->{pid}; 1941 my $col; 1942 if ($Pages{$pid}) { $col=$Pages{$pid}[1] if $Pages{$pid}[0] eq 'FolderList'; } 1943 else { $col=$pid; } 1944 $nb->remove_page($n); 1945} 1946 1947sub button_press_event_cb 1948{ my ($nb,$event)=@_; 1949 return 0 if $event->button != 3; 1950 return 0 unless ::IsEventInNotebookTabs($nb,$event); #to make right-click on tab arrows work 1951 my $self=::find_ancestor($nb,__PACKAGE__); 1952 my $menu=Gtk2::Menu->new; 1953 my $cb=sub { $nb->set_current_page($_[1]); }; 1954 my %pages; 1955 $pages{$_}= $Pages{$_}[3] for keys %Pages; 1956 $pages{$_}= Songs::FieldName($_) for Songs::FilterListFields; 1957 for my $page ($nb->get_children) 1958 { my $pid=$page->{pid}; 1959 my $name=delete $pages{$pid}; 1960 my $item=Gtk2::MenuItem->new_with_label($name); 1961 $item->signal_connect(activate=>$cb,$nb->page_num($page)); 1962 $menu->append($item); 1963 } 1964 $menu->append(Gtk2::SeparatorMenuItem->new); 1965 1966 if (keys %pages) 1967 { my $new=Gtk2::ImageMenuItem->new(_"Add tab"); 1968 $new->set_image( Gtk2::Image->new_from_stock('gtk-add','menu') ); 1969 my $submenu=Gtk2::Menu->new; 1970 for my $pid (sort {$pages{$a} cmp $pages{$b}} keys %pages) 1971 { my $item=Gtk2::ImageMenuItem->new_with_label($pages{$pid}); 1972 $item->set_image( Gtk2::Image->new_from_stock("gmb-tab-$pid",'menu') ); 1973 $item->signal_connect(activate=> sub { my $n=$self->AppendPage($pid); $self->{notebook}->set_current_page($n) }); 1974 $submenu->append($item); 1975 } 1976 $menu->append($new); 1977 $new->set_submenu($submenu); 1978 } 1979 if ($nb->get_n_pages>1) 1980 { my $item=Gtk2::ImageMenuItem->new(_"Remove this tab"); 1981 $item->set_image( Gtk2::Image->new_from_stock('gtk-remove','menu') ); 1982 $item->signal_connect(activate=> \&RemovePage_cb,$self); 1983 $menu->append($item); 1984 } 1985 #::PopupContextMenu(\@MenuTabbedL, { self=>$self, list=>$listname, pagenb=>$pagenb, page=>$page, pagetype=>$page->{tabbed_page_type} } ); 1986 ::PopupMenu($menu,event=>$event,nomenupos=>1); 1987 return 1; 1988} 1989 1990sub SongsAdded_cb 1991{ my ($self,$IDs)=@_; 1992 return if $self->{needupdate}; 1993 if ( $self->{filter}->added_are_in($IDs) ) 1994 { $self->{needupdate}=1; 1995 ::IdleDo('9_FPfull'.$self,5000,\&updatefilter,$self); 1996 } 1997} 1998 1999sub SongsChanged_cb 2000{ my ($self,$IDs,$fields)=@_; 2001 return if $self->{needupdate}; 2002 if ( $self->{filter}->changes_may_affect($IDs,$fields) ) 2003 { $self->{needupdate}=1; 2004 ::IdleDo('9_FPfull'.$self,5000,\&updatefilter,$self); 2005 } 2006 else 2007 { for my $page ( $self->get_field_pages ) 2008 { next unless $page->{valid} && $page->{hash}; 2009 my @depends= Songs::Depends( $page->{Depend_on_field} ); 2010 next unless ::OneInCommon(\@depends,$fields); 2011 $page->{valid}=0; 2012 $page->{hash}=undef; 2013 ::IdleDo('9_FP'.$self,1000,\&refresh_current_page,$self) if $page->mapped; 2014 } 2015 } 2016} 2017sub SongsRemoved_cb 2018{ my ($self,$IDs)=@_; 2019 return if $self->{needupdate}; 2020 my $list=$self->{list}; 2021 my $changed=1; 2022 if ($list!=$::Library) #CHECKME use $::Library or a copy ? 2023 { my $isin=''; 2024 vec($isin,$_,1)=1 for @$IDs; 2025 my $before=@$list; 2026 @$list=grep !vec($isin,$_,1), @$list; 2027 $changed=0 if $before==@$list; 2028 } 2029 $self->invalidate_children if $changed; 2030} 2031 2032sub updatefilter 2033{ my ($self,undef,$nb)=@_; 2034 my $mynb=$self->{nb}; 2035 return if $nb && $nb> $mynb; 2036 2037 delete $::ToDo{'9_FPfull'.$self}; 2038 my $force=delete $self->{needupdate}; 2039 warn "Filtering list for FilterPane$mynb\n" if $::debug; 2040 my $group=$self->{group}; 2041 my $currentf=$::Filters{$group}[$mynb+1]; 2042 $self->{resetbutton}->set_sensitive( !Filter::is_empty($currentf) ); 2043 my $filt=Filter->newadd(TRUE, map($::Filters{$group}[$_+1],0..($mynb-1)) ); 2044 return if !$force && $self->{list} && Filter::are_equal($filt,$self->{filter}); 2045 $self->{filter}=$filt; 2046 2047 my $lref=$filt->is_empty ? $::Library #CHECKME use $::Library or a copy ? 2048 : $filt->filter; 2049 $self->{list}=$lref; 2050 2051 #warn "filter :".$filt->{string}.($filt->{source}? " with source" : '')." songs=".scalar(@$lref)."\n"; 2052 $self->invalidate_children; 2053} 2054 2055sub invalidate_children 2056{ my $self=shift; 2057 for my $page ( $self->get_field_pages ) 2058 { $page->{valid}=0; 2059 $page->{hash}=undef; 2060 } 2061 ::IdleDo('9_FP'.$self,1000,\&refresh_current_page,$self); 2062} 2063sub update_children 2064{ my ($self,$min)=@_; 2065 $self->{min}=$min; 2066 if (!$self->{list} || $self->{needupdate}) { $self->updatefilter; return; } 2067 warn "Updating FilterPane".$self->{nb}."\n" if $::debug; 2068 for my $page ( $self->get_field_pages ) 2069 { $page->{valid}=0; # set dirty flag for this page 2070 } 2071 $self->refresh_current_page; 2072} 2073sub refresh_current_page 2074{ my $self=shift; 2075 delete $::ToDo{'9_FP'.$self}; 2076 my ($current)=grep $_->mapped, $self->get_field_pages; 2077 if ($current) { $current->Fill } # update now if page is displayed 2078} 2079sub get_field_pages 2080{ grep $_->{Depend_on_field}, $_[0]->{notebook}->get_children; 2081} 2082 2083sub cleanup 2084{ my $self=shift; 2085 delete $::ToDo{'9_FP'.$self}; 2086 delete $::ToDo{'9_FPfull'.$self}; 2087} 2088 2089sub Activate 2090{ my ($page,$button,$filter)=@_; 2091 my $self=::find_ancestor($page,__PACKAGE__); 2092 $button||=1; 2093 my $action= $self->{"activate$button"} || $self->{activate} || ($button==2 ? 'queue' : 'play'); 2094 my $aftercmd; 2095 $aftercmd=$1 if $action=~s/&(.*)$//; 2096 ::DoActionForFilter($action,$filter); 2097 ::run_command($self,$aftercmd) if $aftercmd; 2098} 2099 2100sub PopupContextMenu 2101{ my ($page,$hash,$menu)=@_; 2102 my $self=::find_ancestor($page,__PACKAGE__); 2103 $hash->{filterpane}=$self; 2104 $menu||=\@cMenu; 2105 ::PopupContextMenu($menu, $hash); 2106} 2107 2108sub PopupOpt #Only for FilterList #FIXME should be moved in FilterList::, and/or use a common function with FilterList::PopupContextMenu 2109{ my $self=::find_ancestor($_[0],__PACKAGE__); 2110 my $nb=$self->{notebook}; 2111 my $page=$nb->get_nth_page( $nb->get_current_page ); 2112 my $field=$page->{field}[0]; 2113 my $mainfield=Songs::MainField($field); 2114 my $aa= ($mainfield eq 'artist' || $mainfield eq 'album') ? $mainfield : undef; #FIXME 2115 my $mode= uc(substr $page->{mode},0,1); # C => cloud, M => mosaic, L => list 2116 ::PopupContextMenu(\@MenuPageOptions, { self=>$page, aa=>$aa, field => $field, mode => $mode, subfield => $field, depth =>0, usemenupos => 1,} ); 2117 return 1; 2118} 2119 2120package FilterList; 2121use base 'Gtk2::Box'; 2122use constant { GID_ALL => 2**31-1, GID_TYPE => 'Glib::Long' }; 2123 2124our %defaults= 2125( mode => 'list', 2126 type => '', 2127 lmarkup => 0, 2128 lpicsize=> 0, 2129 'sort' => 'default', 2130 depth => 0, 2131 noall => 0, 2132 histogram=>0, 2133 mmarkup => 0, 2134 mpicsize=> 64, 2135 cloud_min=> 5, 2136 cloud_max=> 20, 2137 cloud_stat=> 'count', 2138); 2139 2140sub new 2141{ my ($class,$field,$opt)=@_; 2142 my $self = bless Gtk2::VBox->new, $class; 2143 2144 $opt= { %defaults, %$opt }; 2145 $self->{$_} = $opt->{$_} for qw/mode noall histogram depth mmarkup mpicsize cloud_min cloud_max cloud_stat no_typeahead rules_hint hscrollbar/; 2146 $self->{$_} = [ split /\|/, $opt->{$_} ] for qw/sort type lmarkup lpicsize/; 2147 2148 $self->{type}[0] ||= $field.'.'.(Songs::FilterListProp($field,'type')||''); $self->{type}[0]=~s/\.$//; #FIXME 2149 ::Watch($self, Picture_artist => \&AAPicture_Changed); #FIXME PHASE1 2150 ::Watch($self, Picture_album => \&AAPicture_Changed); #FIXME PHASE1 2151 2152 for my $d (0..$self->{depth}) 2153 { my ($field)= $self->{type}[$d] =~ m#^([^.]+)#; 2154 $self->{field}[$d]=$field; 2155 $self->{icons}[$d]= Songs::FilterListProp($field,'icon') ? (Gtk2::IconSize->lookup('menu'))[0] : 0; 2156 } 2157 2158 #search box 2159 if ($opt->{searchbox} && Songs::FilterListProp($field,'search')) 2160 { $self->pack_start( make_searchbox() ,::FALSE,::FALSE,1); 2161 } 2162 ::Watch($self,'SearchText_'.$opt->{group},\&set_text_search); 2163 2164 #interactive search box 2165 $self->{isearchbox}=GMB::ISearchBox->new($opt,$self->{type}[0],'nolabel'); 2166 $self->pack_end( $self->{isearchbox} ,::FALSE,::FALSE,1); 2167 $self->signal_connect(key_press_event => \&key_press_cb); #only used for isearchbox 2168 $self->signal_connect(map => \&Fill); 2169 2170 $self->set_mode($self->{mode}); 2171 return $self; 2172} 2173 2174sub SaveOptions 2175{ my $self=$_[0]; 2176 my %opt; 2177 $opt{$_} = join '|', @{$self->{$_}} for qw/type lmarkup lpicsize sort/; 2178 $opt{$_} = $self->{$_} for qw/mode noall histogram depth mmarkup mpicsize cloud_min cloud_max cloud_stat/; 2179 for (keys %opt) { delete $opt{$_} if $opt{$_} eq $defaults{$_}; } #remove options equal to default value 2180 delete $opt{type} if $opt{type} eq $self->{pid}; #remove unneeded type options 2181 return %opt, $self->{isearchbox}->SaveOptions; 2182} 2183 2184sub SetField 2185{ my ($self,$field,$depth)=@_; 2186 $self->{field}[$depth]=$field; 2187 my $type=Songs::FilterListProp($field,'type'); 2188 $self->{type}[$depth]= $type ? $field.'.'.$type : $field; 2189 $self->{lpicsize}[$depth]||=0; 2190 $self->{lmarkup}[$depth]||=0; 2191 $self->{'sort'}[$depth]||='default'; 2192 $self->{icons}[$depth]||= Songs::FilterListProp($field,'icon') ? (Gtk2::IconSize->lookup('menu'))[0] : 0; 2193 2194 my $i=0; 2195 $i++ while $self->{field}[$i]; 2196 $self->{depth}=$i-1; 2197 2198 $self->Fill('optchanged'); 2199} 2200 2201sub SetOption 2202{ my ($self,$key,$value)=@_; 2203 $self->{$key}=$value if $key; 2204 $self->Fill('optchanged'); 2205} 2206 2207sub set_mode 2208{ my ($self,$mode,$fillnow)=@_; 2209 for my $child ($self->get_children) 2210 { $self->remove($child) if $child->{is_a_view}; 2211 } 2212 2213 my ($child,$view)= $mode eq 'cloud' ? $self->create_cloud : 2214 $mode eq 'mosaic'? $self->create_mosaic : 2215 $self->create_list; 2216 $self->{view}=$view; 2217 $self->{DefaultFocus}=$view; 2218 $child->{is_a_view}=1; 2219 $view->signal_connect(focus_in_event => sub { my $self=::find_ancestor($_[0],__PACKAGE__); $self->{isearchbox}->parent_has_focus; 0; }); #hide isearchbox when focus goes to the view 2220 2221 my $drag_type= Songs::FilterListProp( $self->{field}[0], 'drag') || ::DRAG_FILTER; 2222 ::set_drag( $view, source => [$drag_type,\&drag_cb]); 2223 MultiTreeView::init($view,__PACKAGE__) if $mode eq 'list'; #should be in create_list but must be done after set_drag 2224 2225 $child->show_all; 2226 $self->add($child); 2227 $self->{valid}=0; 2228 $self->Fill if $fillnow; 2229} 2230 2231sub create_list 2232{ my $self=$_[0]; 2233 $self->{mode}='list'; 2234 my $field=$self->{field}[0]; 2235 my $sw=Gtk2::ScrolledWindow->new; 2236# $sw->set_shadow_type('etched-in'); 2237 $sw->set_policy('automatic','automatic'); 2238 ::set_biscrolling($sw); 2239 2240 my $store=Gtk2::TreeStore->new(GID_TYPE); 2241 my $treeview=Gtk2::TreeView->new($store); 2242 $treeview->set_rules_hint(1) if $self->{rules_hint}; 2243 $sw->add($treeview); 2244 $treeview->set_headers_visible(::FALSE); 2245 $treeview->set_search_column(-1); #disable gtk interactive search, use my own instead 2246 $treeview->set_enable_search(::FALSE); 2247 #$treeview->set('fixed-height-mode' => ::TRUE); #only if fixed-size column 2248 my $renderer= CellRendererGID->new; 2249 my $column=Gtk2::TreeViewColumn->new_with_attributes('',$renderer); 2250 2251 $renderer->set(prop => [@$self{qw/type lmarkup lpicsize icons hscrollbar/}]); #=> $renderer->get('prop')->[0] contains $self->{type} (which is a array ref) 2252 #$column->add_attribute($renderer, gid => 0); 2253 $column->set_cell_data_func($renderer, sub 2254 { my (undef,$cell,$store,$iter)=@_; 2255 my $gid=$store->get($iter,0); 2256 my $depth=$store->iter_depth($iter); 2257 $cell->set( gid=>$gid, depth=>$depth);# 'is-expander'=> $depth < $store->{depth}); 2258 }); 2259 $treeview->append_column($column); 2260 $treeview->signal_connect(row_expanded => \&row_expanded_cb); 2261 #$treeview->signal_connect(row_collapsed => sub { my $store=$_[0]->get_model;my $iter=$_[1]; while (my $iter=$store->iter_nth_child($iter,1)) { $store->remove($iter) } }); 2262 2263 my $selection=$treeview->get_selection; 2264 $selection->set_mode('multiple'); 2265 $selection->signal_connect(changed =>\&selection_changed_cb); 2266 2267 $treeview->signal_connect( row_activated => sub { Activate($_[0],1); }); 2268 return $sw,$treeview; 2269} 2270 2271sub Activate 2272{ my ($view,$button)=@_; 2273 my $self=::find_ancestor($view,__PACKAGE__); 2274 my $filter= $self->get_selected_filters; 2275 return unless $filter; #nothing selected 2276 FilterPane::Activate($self,$button,$filter); 2277} 2278 2279sub create_cloud 2280{ my $self=$_[0]; 2281 $self->{mode}='cloud'; 2282 my $sw=Gtk2::ScrolledWindow->new; 2283 $sw->set_policy('never','automatic'); 2284 my $sub=Songs::DisplayFromGID_sub($self->{type}[0]); 2285 my $cloud= GMB::Cloud->new(\&child_selection_changed_cb,\&get_fill_data, \&Activate,\&PopupContextMenu,$sub); 2286 $sw->add_with_viewport($cloud); 2287 return $sw,$cloud; 2288} 2289sub create_mosaic 2290{ my $self=$_[0]; 2291 $self->{mode}='mosaic'; 2292 $self->{mpicsize}||=64; 2293 my $hbox=Gtk2::HBox->new(0,0); 2294 my $vscroll=Gtk2::VScrollbar->new; 2295 $hbox->pack_end($vscroll,0,0,0); 2296 my $mosaic= GMB::Mosaic->new(\&child_selection_changed_cb,\&get_fill_data,\&Activate,\&PopupContextMenu,$self->{type}[0],$vscroll); 2297 $hbox->add($mosaic); 2298 return $hbox,$mosaic; 2299} 2300 2301sub get_cursor_row 2302{ my $self=$_[0]; 2303 if ($self->{mode} eq 'list') 2304 { my ($path)=$self->{view}->get_cursor; 2305 return $path ? $path->to_string : undef; 2306 } 2307 else { return $self->{view}->get_cursor_row; } 2308} 2309sub set_cursor_to_row 2310{ my ($self,$row)=@_; 2311 if ($self->{mode} eq 'list') 2312 { $self->{view}->set_cursor(Gtk2::TreePath->new_from_indices($row)); 2313 } 2314 else { $self->{view}->set_cursor_to_row($row); } 2315} 2316 2317sub make_searchbox 2318{ my $entry=Gtk2::Entry->new; #FIXME tooltip 2319 my $clear=::NewIconButton('gtk-clear',undef,sub { $_[0]->{entry}->set_text(''); },'none' ); #FIXME tooltip 2320 $clear->{entry}=$entry; 2321 my $hbox=Gtk2::HBox->new(0,0); 2322 $hbox->pack_end($clear,0,0,0); 2323 $hbox->pack_start($entry,1,1,0); 2324 $entry->signal_connect(changed => 2325 sub { ::IdleDo('6_UpdateSearch'.$entry,300,sub 2326 { my $entry=$_[0]; 2327 my $self=::find_ancestor($entry,__PACKAGE__); 2328 my $s=$entry->get_text; 2329 $self->set_text_search( $entry->get_text, 0,0 ) 2330 },$_[0]); 2331 }); 2332 $entry->signal_connect(activate => 2333 sub { ::DoTask('6_UpdateSearch'.$entry); 2334 }); 2335 return $hbox; 2336} 2337sub set_text_search 2338{ my ($self,$search,$is_regexp,$is_casesens)=@_; 2339 return if defined $self->{search} && $self->{search} eq $search 2340 && !($self->{search_is_regexp} xor $is_regexp) 2341 && !($self->{search_is_casesens} xor $is_casesens); 2342 $self->{search}=$search; 2343 $self->{search_is_regexp}= $is_regexp||0; 2344 $self->{search_is_casesens}= $is_casesens||0; 2345 $self->{valid}=0; 2346 $self->Fill if $self->mapped; 2347} 2348 2349sub AAPicture_Changed 2350{ my ($self,$key)=@_; 2351 return if $self->{mode} eq 'cloud'; 2352 return unless $self->{valid} && $self->{hash} && $self->{hash}{$key} && $self->{hash}{$key} >= ::find_ancestor($self,'FilterPane')->{min}; 2353 $self->queue_draw; 2354} 2355 2356sub selection_changed_cb 2357{ my $treesel=$_[0]; 2358 child_selection_changed_cb($treesel->get_tree_view); 2359} 2360 2361sub child_selection_changed_cb 2362{ my $child=$_[0]; 2363 my $self=::find_ancestor($child,__PACKAGE__); 2364 return if $self->{busy}; 2365 my $filter=$self->get_selected_filters; 2366 return unless $filter; 2367 my $filterpane=::find_ancestor($self,'FilterPane'); 2368 ::SetFilter( $self, $filter, $filterpane->{nb}, $filterpane->{group} ); 2369} 2370 2371sub get_selected_filters 2372{ my $self=::find_ancestor($_[0],__PACKAGE__); 2373 my @filters; 2374 my $types=$self->{type}; 2375 if ($self->{mode} eq 'list') 2376 { my $store=$self->{view}->get_model; 2377 my $sel=$self->{view}->get_selection; 2378 my @rows=$sel->get_selected_rows; 2379 for my $path (@rows) 2380 { my $iter=$store->get_iter($path); 2381 if ($store->get_value($iter,0)==GID_ALL) { return Filter->new; } 2382 my @parents= $iter; 2383 unshift @parents,$iter while $iter=$store->iter_parent($iter); 2384 next if grep $sel->iter_is_selected($parents[$_]), 0..$#parents-1;#skip if one parent is selected 2385 my @f=map Songs::MakeFilterFromGID( $types->[$_], $store->get_value($parents[$_],0)), 0..$#parents; 2386 push @filters,Filter->newadd(1, @f); 2387 } 2388 } 2389 else 2390 { my $vals=$self->get_selected; 2391 @filters=map Songs::MakeFilterFromGID($types->[0],$_), @$vals; 2392 } 2393 return undef unless @filters; 2394 my $field=$self->{field}[0]; 2395 my $filterpane=::find_ancestor($self,'FilterPane'); 2396 my $i= $filterpane->{inter} && Songs::FilterListProp($field,'multi'); 2397 my $filter=Filter->newadd($i,@filters); 2398 $filter->invert if $filterpane->{invert}; 2399 return $filter; 2400} 2401sub get_selected #not called for list => only called for cloud or mosaic 2402{ return [$_[0]->{view}->get_selected]; 2403} 2404 2405sub get_selected_list 2406{ my $self=$_[0]; 2407 my $field=$self->{field}[0]; 2408 my @vals; 2409 if ($self->{mode} eq 'list') #only returns selected rows if they are all at the same depth 2410 {{ my $store=$self->{view}->get_model; 2411 my @iters=map $store->get_iter($_), $self->{view}->get_selection->get_selected_rows; 2412 last unless @iters; 2413 if ($store->get_value($iters[0],0)==GID_ALL) # assumes "All row" first iter 2414 { my $iter= $store->get_iter_first; # this iter is "All row" -> not added 2415 # "all row" is selected, replace iters list by list of all iters of first depth 2416 @iters=(); 2417 push @iters,$iter while $iter=$store->iter_next($iter); 2418 last unless @iters; 2419 } 2420 my $depth=$store->iter_depth($iters[0]); 2421 last if grep $depth != $store->iter_depth($_), @iters; 2422 @vals=map $store->get_value($_,0) , @iters; 2423 $field=$self->{field}[$depth]; 2424 }} 2425 else { @vals=$self->{view}->get_selected } 2426 return $field,\@vals; 2427} 2428 2429sub drag_cb 2430{ my $self=::find_ancestor($_[0],__PACKAGE__); 2431 my $field=$self->{field}[0]; 2432 if (my $drag=Songs::FilterListProp($field,'drag')) #return artist or album gids 2433 { if ($self->{mode} eq 'list') 2434 { my $store=$self->{view}->get_model; 2435 my @rows=$self->{view}->get_selection->get_selected_rows; 2436 unless (grep $_->get_depth>1, @rows) 2437 { my @gids=map $store->get_value($store->get_iter($_),0), @rows; 2438 warn "dnd : gids=@gids\n"; 2439 if (grep $_==GID_ALL, @gids) {return ::DRAG_FILTER,'';} #there is an "all-row" 2440 return $drag,@gids; 2441 } 2442 #else : rows of depth>0 selected => fallback to get_selected_filters 2443 } 2444 } 2445 my $filter=$self->get_selected_filters; 2446 return ($filter? (::DRAG_FILTER,$filter->{string}) : undef); 2447} 2448 2449sub row_expanded_cb 2450{ my ($treeview,$piter,$path)=@_; 2451 my $self=::find_ancestor($treeview,__PACKAGE__); 2452 my $filterpane=::find_ancestor($self,'FilterPane'); 2453 my $store=$treeview->get_model; 2454 my $depth=$store->iter_depth($piter); 2455 my @filters; 2456 for (my $iter=$piter; $iter; $iter=$store->iter_parent($iter) ) 2457 { push @filters, Songs::MakeFilterFromGID($self->{type}[$store->iter_depth($iter)], $store->get($iter,0)); 2458 } 2459 my $list=$filterpane->{list}; 2460 $list= Filter->newadd(1,@filters)->filter($list); 2461 my $type=$self->{type}[$depth+1]; 2462 my $h=Songs::BuildHash($type,$list,'gid'); 2463 my $children=AA::SortKeys($type,[keys %$h],$self->{'sort'}[$depth+1]); 2464 for my $i (0..$#$children) 2465 { my $iter= $store->iter_nth_child($piter,$i) || $store->append($piter); 2466 $store->set($iter,0,$children->[$i]); 2467 } 2468 while (my $iter=$store->iter_nth_child($piter,$#$children+1)) { $store->remove($iter) } 2469 2470 if ($depth<$self->{depth}-1) #make sure every child has a child if $depth not the deepest 2471 { for (my $iter=$store->iter_children($piter); $iter; $iter=$store->iter_next($iter) ) 2472 { $store->append($iter) unless $store->iter_children($iter); 2473 } 2474 } 2475} 2476 2477sub get_fill_data 2478{ my ($child,$opt)=@_; 2479 my $self=::find_ancestor($child,__PACKAGE__); 2480 my $filterpane=::find_ancestor($self,'FilterPane'); 2481 my $type=$self->{type}[0]; 2482 $self->{hash}=undef if $opt && $opt eq 'rehash'; 2483 my $href= $self->{hash} ||= Songs::BuildHash($type,$filterpane->{list},'gid'); 2484 $self->{valid}=1; 2485 $self->{all_count}= keys %$href; #used to display how many artists/album/... there is in this filter 2486 my $min=$filterpane->{min}; 2487 my $search=$self->{search}; 2488 my @list; 2489 if ($min>1) 2490 { @list=grep $$href{$_} >= $min, keys %$href; 2491 } 2492 else { @list=keys %$href; } 2493 if (defined $search && $search ne '') 2494 { @list= @{ AA::GrepKeys($type,$search,$self->{search_is_regexp},$self->{search_is_casesens},\@list) }; 2495 } 2496 AA::SortKeys($type,\@list,$self->{'sort'}[0]); 2497 2498 my $always_first= Songs::Field_property($type,'always_first_gid'); 2499 if (defined $always_first) #special gid that should always appear first 2500 { my $before=@list; 2501 @list= grep $_!=$always_first, @list; 2502 unshift @list,$always_first if $before!=@list; 2503 } 2504 2505 $self->{array}=\@list; #used for interactive search 2506 2507 if ($self->{mode} eq 'cloud' && $self->{cloud_stat} ne 'count') #FIXME update cloud when used fields change 2508 { $href=Songs::BuildHash($type,$filterpane->{list},'gid',$self->{cloud_stat}); 2509 } 2510 2511 return \@list,$href; 2512} 2513 2514sub Fill 2515{ warn "filling @_\n" if $::debug; 2516 my ($self,$opt)=@_; 2517 $opt=undef unless $opt && ($opt eq 'optchanged' || $opt eq 'rehash'); 2518 return if $self->{valid} && !$opt; 2519 if ($self->{mode} eq 'list') 2520 { my $treeview=$self->{view}; 2521 $treeview->set('show-expanders', ($self->{depth}>0) ) if Gtk2->CHECK_VERSION(2,12,0); 2522 my $store=$treeview->get_model; 2523 my $col=$self->{col}; 2524 my ($renderer)=($treeview->get_columns)[0]->get_cell_renderers; 2525 $renderer->reset; 2526 $self->{busy}=1; 2527 $store->clear; #FIXME keep selection ? FIXME at least when opt is true (ie lmarkup or lpicsize changed) 2528 my ($list,$href)=$self->get_fill_data($opt); 2529 $renderer->set('all_count', $self->{all_count}); 2530 my $max= $self->{histogram} ? ::max(values %$href) : 0; 2531 $renderer->set( hash=>$href, max=> $max ); 2532 $self->{array_offset}= $self->{noall} ? 0 : 1; #row number difference between store and $list, needed by interactive search 2533 $store->set($store->prepend(undef),0,$_) for reverse @$list; # prepend because filling is a bit faster in reverse 2534 $store->set($store->prepend(undef),0,GID_ALL) unless $self->{noall}; 2535 2536 if ($self->{field}[1]) # add a children to every row 2537 { my $first=$store->get_iter_first; 2538 $first=$store->iter_next($first) if $first && $store->get($first,0)==GID_ALL; #skip "all" row 2539 for (my $iter=$first; $iter; $iter=$store->iter_next($iter)) 2540 { $store->append($iter); 2541 } 2542 } 2543 $self->{busy}=undef; 2544 } 2545 else 2546 { $self->{view}->reset_selection unless $opt; 2547 $self->{view}->Fill($opt); 2548 } 2549} 2550 2551sub PopupContextMenu 2552{ my $self=::find_ancestor($_[0],__PACKAGE__); 2553 my ($field,$gidlist)=$self->get_selected_list; 2554 my $mainfield=Songs::MainField($field); 2555 my $aa= ($mainfield eq 'artist' || $mainfield eq 'album') ? $mainfield : undef; #FIXME 2556 my $mode= uc(substr $self->{mode},0,1); # C => cloud, M => mosaic, L => list 2557 FilterPane::PopupContextMenu($self,{ self=> $self, filter => $self->get_selected_filters, field => $field, aa => $aa, gidlist =>$gidlist, mode => $mode, subfield => $field, depth =>0 }); 2558} 2559 2560sub key_press_cb 2561{ my ($self,$event)=@_; 2562 my $key=Gtk2::Gdk->keyval_name( $event->keyval ); 2563 my $unicode=Gtk2::Gdk->keyval_to_unicode($event->keyval); # 0 if not a character 2564 my $state=$event->get_state; 2565 my $ctrl= $state * ['control-mask'] && !($state * [qw/mod1-mask mod4-mask super-mask/]); #ctrl and not alt/super 2566 my $mod= $state * [qw/control-mask mod1-mask mod4-mask super-mask/]; # no modifier ctrl/alt/super 2567 my $shift=$state * ['shift-mask']; 2568 if (lc$key eq 'f' && $ctrl) { $self->{isearchbox}->begin(); } #ctrl-f : search 2569 elsif (lc$key eq 'g' && $ctrl) { $self->{isearchbox}->search($shift ? -1 : 1);} #ctrl-g : next/prev match 2570 elsif ($key eq 'F3' && !$mod) { $self->{isearchbox}->search($shift ? -1 : 1);} #F3 : next/prev match 2571 elsif (!$self->{no_typeahead} && $unicode && $unicode!=32 && !$mod) 2572 { $self->{isearchbox}->begin( chr $unicode ); #begin typeahead search 2573 } 2574 else {return 0} 2575 return 1; 2576} 2577 2578package FolderList; 2579use base 'Gtk2::ScrolledWindow'; 2580 2581sub new 2582{ my ($class,$col,$opt)=@_; 2583 my $self = bless Gtk2::ScrolledWindow->new, $class; 2584 #$self->set_shadow_type ('etched-in'); 2585 $self->set_policy ('automatic', 'automatic'); 2586 ::set_biscrolling($self); 2587 2588 my $store=Gtk2::TreeStore->new('Glib::String'); 2589 my $treeview=Gtk2::TreeView->new($store); 2590 $treeview->set_headers_visible(::FALSE); 2591 $treeview->set_search_equal_func(\&search_equal_func); 2592 $treeview->set_enable_search(!$opt->{no_typeahead}); 2593 #$treeview->set('fixed-height-mode' => ::TRUE); #only if fixed-size column 2594 $treeview->signal_connect(row_expanded => \&row_expanded_changed_cb); 2595 $treeview->signal_connect(row_collapsed => \&row_expanded_changed_cb); 2596 $treeview->{expanded}={}; 2597 my $renderer= Gtk2::CellRendererText->new; 2598 $store->{displayfunc}= Songs::DisplayFromHash_sub('path'); 2599 my $column=Gtk2::TreeViewColumn->new_with_attributes(Songs::FieldName($col),$renderer); 2600 $column->set_cell_data_func($renderer, sub 2601 { my (undef,$cell,$store,$iter)=@_; 2602 my $folder=::decode_url($store->get($iter,0)); 2603 $cell->set( text=> $store->{displayfunc}->($folder)); 2604 }); 2605 $treeview->append_column($column); 2606 $self->add($treeview); 2607 $self->{treeview}=$treeview; 2608 $self->{DefaultFocus}=$treeview; 2609 2610 $self->signal_connect(map => \&Fill); 2611 2612 my $selection=$treeview->get_selection; 2613 $selection->set_mode('multiple'); 2614 $selection->signal_connect (changed =>\&selection_changed_cb); 2615 ::set_drag($treeview, source => [::DRAG_FILTER,sub 2616 { my @paths=_get_path_selection( $_[0] ); 2617 return undef unless @paths; 2618 my $filter=_MakeFolderFilter(@paths); 2619 return ::DRAG_FILTER,($filter? $filter->{string} : undef); 2620 }]); 2621 MultiTreeView::init($treeview,__PACKAGE__); 2622 return $self; 2623} 2624 2625sub search_equal_func 2626{ #my ($store,$col,$string,$iter)=@_; 2627 my $store=$_[0]; 2628 my $folder= $store->{displayfunc}( ::decode_url($store->get($_[3],0)) ); 2629 #use ::superlc instead of uc ? 2630 my $string=uc $_[2]; 2631 index uc($folder), $string; 2632} 2633 2634sub Fill 2635{ warn "filling @_\n" if $::debug; 2636 my $self=$_[0]; 2637 return if $self->{valid}; 2638 my $treeview=$self->{treeview}; 2639 my $filterpane=::find_ancestor($self,'FilterPane'); 2640 my $href=$self->{hash}||= do 2641 { my $h= Songs::BuildHash('path',$filterpane->{list}); 2642 my @hier; 2643 while (my ($f,$n)=each %$h) 2644 { my $ref=\@hier; 2645 $ref=$ref->[1]{$_}||=[] and $ref->[0]+=$n for split /$::QSLASH/o,$f; 2646 } 2647 for my $dir (keys %{$treeview->{expanded}}) 2648 { my $ref=\@hier; my $notfound; 2649 $ref=$ref->[1]{$_} or $notfound=1, last for split /$::QSLASH/o,$dir; 2650 if ($notfound) {delete $treeview->{expanded}{$dir}} 2651 else { $ref->[2]=1; } 2652 } 2653 $hier[1]{::SLASH}=delete $hier[1]{''} if exists $hier[1]{''}; 2654 $hier[1]; 2655 }; 2656 my $min=$filterpane->{min}; 2657 my $store=$treeview->get_model; 2658 $self->{busy}=1; 2659 $store->clear; #FIXME keep selection 2660 2661 #fill the store 2662 my @toadd; my @toexpand; 2663 push @toadd,$href->{$_},$_,undef for sort grep $href->{$_}[0]>=$min, keys %$href; 2664 while (my ($ref,$name,$iter)=splice @toadd,0,3) 2665 { my $iter=$store->append($iter); 2666 $store->set($iter,0, Songs::filename_escape($name)); 2667 push @toexpand,$store->get_path($iter) if $ref->[2]; 2668 if ($ref->[1]) #sub-folders 2669 { push @toadd, $ref->[1]{$_},$_,$iter for sort grep $ref->[1]{$_}[0]>=$min, keys %{$ref->[1]}; } 2670 } 2671 2672 # expand tree to first fork 2673 if (my $iter=$store->get_iter_first) 2674 { $iter=$store->iter_children($iter) while $store->iter_n_children($iter)==1; 2675 $treeview->expand_to_path( $store->get_path($iter) ); 2676 } 2677 #expand previously expanded rows 2678 $treeview->expand_row($_,::FALSE) for @toexpand; 2679 2680 $self->{busy}=undef; 2681 $self->{valid}=1; 2682} 2683 2684sub row_expanded_changed_cb #keep track of which rows are expanded 2685{ my ($treeview,$iter,$path)=@_; 2686 my $self=::find_ancestor($treeview,__PACKAGE__); 2687 return if $self->{busy}; 2688 my $expanded=$treeview->row_expanded($path); 2689 $path= ::decode_url(_treepath_to_foldername($treeview->get_model,$path)); 2690 my $ref=[undef,$self->{hash}]; 2691 $ref=$ref->[1]{($_ eq '' ? ::SLASH : $_)} for split /$::QSLASH/o,$path; 2692 if ($expanded) 2693 { $ref->[2]=1; #for when reusing the hash 2694 $treeview->{expanded}{$path}=undef; #for when reconstructing the hash 2695 } 2696 else 2697 { delete $ref->[2]; 2698 delete $treeview->{expanded}{$path}; 2699 } 2700} 2701 2702sub selection_changed_cb 2703{ my $treesel=$_[0]; 2704 my $self=::find_ancestor($treesel->get_tree_view,__PACKAGE__); 2705 return if $self->{busy}; 2706 my @paths=_get_path_selection( $self->{treeview} ); 2707 return unless @paths; 2708 my $filter=_MakeFolderFilter(@paths); 2709 my $filterpane=::find_ancestor($self,'FilterPane'); 2710 $filter->invert if $filterpane->{invert}; 2711 ::SetFilter( $self, $filter, $filterpane->{nb}, $filterpane->{group} ); 2712} 2713 2714sub _MakeFolderFilter 2715{ return Filter->newadd(::FALSE,map( "path:i:$_", @_ )); 2716} 2717 2718sub Activate 2719{ my ($self,$button)=@_; 2720 my @paths=_get_path_selection( $self->{treeview} ); 2721 my $filter= _MakeFolderFilter(@paths); 2722 FilterPane::Activate($self,$button,$filter); 2723} 2724sub PopupContextMenu 2725{ my $self=shift; 2726 my $tv=$self->{treeview}; 2727 my @paths=_get_path_selection($tv); 2728 my @raw= map ::decode_url($_), @paths; 2729 FilterPane::PopupContextMenu($self,{self=>$tv, rawpathlist=> \@raw, pathlist => \@paths, filter => _MakeFolderFilter(@paths) }); 2730} 2731 2732sub _get_path_selection 2733{ my $treeview=$_[0]; 2734 my $store=$treeview->get_model; 2735 my @paths=$treeview->get_selection->get_selected_rows; 2736 return () if @paths==0; #if no selection 2737 @paths=map _treepath_to_foldername($store,$_), @paths; 2738 return @paths; 2739} 2740sub _treepath_to_foldername 2741{ my $store=$_[0]; my $tp=$_[1]; 2742 my @folders; 2743 my $iter=$store->get_iter($tp); 2744 while ($iter) 2745 { unshift @folders, $store->get_value($iter,0); 2746 $iter=$store->iter_parent($iter); 2747 } 2748 $folders[0]='' if $folders[0] eq ::SLASH; 2749 return join(::SLASH,@folders); 2750} 2751 2752package Filesystem; #FIXME lots of common code with FolderList => merge it 2753use base 'Gtk2::ScrolledWindow'; 2754 2755sub new 2756{ my ($class,$col,$opt)=@_; 2757 my $self = bless Gtk2::ScrolledWindow->new, $class; 2758 #$self->set_shadow_type ('etched-in'); 2759 $self->set_policy ('automatic', 'automatic'); 2760 ::set_biscrolling($self); 2761 2762 my $store=Gtk2::TreeStore->new('Glib::String','Glib::Uint'); 2763 my $treeview=Gtk2::TreeView->new($store); 2764 $treeview->set_headers_visible(::FALSE); 2765 $treeview->set_enable_search(!$opt->{no_typeahead}); 2766 #$treeview->set('fixed-height-mode' => ::TRUE); #only if fixed-size column 2767 $treeview->signal_connect(test_expand_row => \&row_expand_cb); 2768 my $renderer= Gtk2::CellRendererText->new; 2769 my $column=Gtk2::TreeViewColumn->new_with_attributes('',$renderer); 2770 $column->set_cell_data_func($renderer, \&cell_data_func_cb); 2771 $treeview->append_column($column); 2772 2773 $self->add($treeview); 2774 $self->{treeview}=$treeview; 2775 $self->{DefaultFocus}=$treeview; 2776 2777 $self->signal_connect(map => \&Fill); 2778 2779 my $selection=$treeview->get_selection; 2780 $selection->set_mode('multiple'); 2781 $selection->signal_connect (changed =>\&selection_changed_cb); 2782 # drag and drop doesn't work with filter using a special source, which is the case here 2783# ::set_drag($treeview, source => [::DRAG_FILTER,sub 2784# { my @paths=_get_path_selection( $_[0] ); 2785# return undef unless @paths; 2786# my $filter=_MakeFolderFilter(@paths); 2787# return ::DRAG_FILTER,($filter? $filter->{string} : undef); 2788# }]); 2789 ::set_drag($treeview, source => [::DRAG_ID,sub 2790 { my @paths=_get_path_selection( $_[0] ); 2791 return undef unless @paths; 2792 my $filter=_MakeFolderFilter(@paths); 2793 return undef unless $filter; 2794 my @list= @{$filter->filter}; 2795 ::SortList(\@list); 2796 return ::DRAG_ID,@list; 2797 }]); 2798 MultiTreeView::init($treeview,__PACKAGE__); 2799 return $self; 2800} 2801 2802sub Fill 2803{ warn "filling @_\n" if $::debug; 2804 my $self=$_[0]; 2805 return if $self->{valid}; 2806 my $treeview=$self->{treeview}; 2807 my $store=$treeview->get_model; 2808 my $iter=$store->append(undef); 2809 my $root= ::SLASH; 2810 $root='C:' if $^O eq 'MSWin32'; #FIXME Win32 find a way to list the drives 2811 $store->set($iter,0, ::url_escape($root)); 2812 my $treepath= $store->get_path($iter); 2813 #expand to home dir 2814 for my $folder (split /$::QSLASH/o, ::url_escape(Glib::get_home_dir)) 2815 { next if $folder eq ''; 2816 $self->refresh_path($treepath,1); 2817 $iter=$store->iter_children($iter); 2818 while ($iter) 2819 { last if $folder eq $store->get($iter,0); 2820 $iter=$store->iter_next($iter); 2821 $treepath=$store->get_path($iter); 2822 } 2823 last unless $iter; 2824 } 2825 $self->refresh_path($treepath,1); 2826 $treeview->expand_to_path($treepath); 2827 $self->{valid}=1; 2828} 2829 2830sub cell_data_func_cb 2831{ my ($tvcolumn,$cell,$store,$iter)=@_; 2832 my $folder=::decode_url($store->get($iter,0)); 2833 $cell->set( text=> ::filename_to_utf8displayname($folder) ); 2834 my $treeview= $tvcolumn->get_tree_view; 2835 Glib::Timeout->add(10,\&idle_load,$treeview) unless $treeview->{queued_load}; 2836 push @{$treeview->{queued_load}}, $store->get_path($iter); 2837} 2838sub idle_load 2839{ my $treeview=shift; 2840 my $queue=$treeview->{queued_load}; 2841 return 0 unless $queue; 2842 my ($first,$last)= $treeview->get_visible_range; 2843 unless ($first && $last) { @$queue=(); return 0 } 2844 while (my $path=shift @$queue) 2845 { next unless $path->compare($first)>=0 && $path->compare($last)<=0; # ignore if out of view 2846 my $self=::find_ancestor($treeview,__PACKAGE__); 2847 my $partial=$self->refresh_path($path); 2848 if ($partial) { unshift @$queue,$path; return 1 } 2849 last if Gtk2->events_pending; 2850 } 2851 return 1 if @$queue; 2852 delete $treeview->{queued_load}; 2853 return 0; 2854} 2855 2856sub row_expand_cb 2857{ my ($treeview,$iter,$path)=@_; 2858 my $self=::find_ancestor($treeview,__PACKAGE__); 2859 $self->refresh_path($path,1); 2860 return !$treeview->get_model->iter_children($iter); 2861} 2862 2863sub refresh_path 2864{ my ($self,$path,$force)=@_; 2865 my $treeview=$self->{treeview}; 2866 my $store=$treeview->get_model; 2867 my $parent=$store->get_iter($path); 2868 my $folder=_treepath_to_foldername($store,$path); 2869 return 0 unless $folder; 2870 $folder= ::decode_url($folder); 2871 my @subfolders; 2872 my $full= $force || $treeview->row_expanded($path); 2873 my $continue; 2874 if ($self->{in_progress}) 2875 { if ($full || $self->{in_progress}{folder} ne $folder) { delete $self->{in_progress}; } 2876 else {$continue=1} 2877 } 2878 my $dh; my $lastmodif; 2879 if (!$continue) # check folder is there and if treeview up-to-date 2880 { my $ok=opendir $dh,$folder; 2881 unless ($ok) { $store->remove($parent) unless -d $folder; return 0; } 2882 $lastmodif= (stat $dh)[9] || 1;# ||1 to ùake sure it isn't 0, as 0 means not read 2883 my $lastrefresh=$store->get($parent,1); 2884 return 0 if $lastmodif==$lastrefresh && !$force; 2885 } 2886 if ($full) 2887 { @subfolders= grep !m#^\.# && -d $folder.::SLASH.$_, readdir $dh; 2888 close $dh; 2889 } 2890 else # the content of the folder will be search for subfolders in chunks (-d can sometimes be slow) 2891 { my $progress= $self->{in_progress} ||= { list=>[], found=>[], lastmodif=>$lastmodif, folder=>$folder }; 2892 my $list= $progress->{list}; 2893 my $found= $progress->{found}; 2894 if (!$continue) 2895 { @$list= grep !m#^\.#, readdir $dh; 2896 close $dh; 2897 } 2898 while (@$list) 2899 { return 1 if Gtk2->events_pending; # continue later 2900 my $dir=shift @$list; 2901 push @$found,$dir if -d $folder.::SLASH.$dir; 2902 } 2903 @subfolders=@$found; 2904 $lastmodif= $progress->{lastmodif}; 2905 delete $self->{in_progress}; 2906 } 2907 # got the list of subfolders, update the treeview 2908 $store->set($parent,1,$lastmodif); 2909 my $iter=$store->iter_children($parent); 2910 NEXTDIR: for my $dir (sort @subfolders) 2911 { $dir= ::url_escape($dir); 2912 while ($iter) 2913 { my $c= $dir cmp $store->get($iter,0); 2914 unless ($c) { $iter=$store->iter_next($iter); next NEXTDIR; } #folder already there 2915 last if $c<0; 2916 # there should be no subfolders before => remove them 2917 my $iter2=$store->iter_next($iter); 2918 $store->remove($iter); 2919 $iter=$iter2; 2920 } 2921 # add subfolder 2922 my $iter2=$store->insert_before($parent,$iter); 2923 $store->set($iter2,0,$dir,1,0); 2924 my $dummy=$store->insert_after($iter2,undef); 2925 $store->set($dummy,0,"",1,0); #add dummy child 2926 } 2927 while ($iter) #no more subfolders => remove any trailing folders 2928 { my $iter2=$store->iter_next($iter); 2929 $store->remove($iter); 2930 $iter=$iter2; 2931 } 2932 return 0; 2933} 2934 2935sub selection_changed_cb 2936{ my $treesel=$_[0]; 2937 my $self=::find_ancestor($treesel->get_tree_view,__PACKAGE__); 2938 #return if $self->{busy}; 2939 my @paths=_get_path_selection( $self->{treeview} ); 2940 return unless @paths; 2941 my $filter=_MakeFolderFilter(@paths); 2942 my $filterpane=::find_ancestor($self,'FilterPane'); 2943 #$filter->invert if $filterpane->{invert}; 2944 ::SetFilter( $self, $filter, $filterpane->{nb}, $filterpane->{group} ); 2945} 2946 2947sub _MakeFolderFilter 2948{ my @paths= map ::decode_url($_), @_; 2949 my @list= ::FolderToIDs(0,0,@paths); 2950 my $filter= Filter->new('',\@list); #FIXME use a filter on path rather than a list ? 2951 return $filter; 2952} 2953 2954sub Activate 2955{ my ($self,$button)=@_; 2956 my @paths=_get_path_selection( $self->{treeview} ); 2957 my $filter= _MakeFolderFilter(@paths); 2958 FilterPane::Activate($self,$button,$filter); 2959} 2960sub PopupContextMenu 2961{ my $self=$_[0]; 2962 my $tv=$self->{treeview}; 2963 my @paths=_get_path_selection($tv); 2964 my @raw= map ::decode_url($_), @paths; 2965 FilterPane::PopupContextMenu($self,{self=>$tv, rawpathlist=> \@raw, pathlist => \@paths, filter => _MakeFolderFilter(@paths) }); 2966} 2967 2968sub _get_path_selection 2969{ my $treeview=$_[0]; 2970 my $store=$treeview->get_model; 2971 my @paths=$treeview->get_selection->get_selected_rows; 2972 return () if @paths==0; #if no selection 2973 @paths=map _treepath_to_foldername($store,$_), @paths; 2974 return @paths; 2975} 2976sub _treepath_to_foldername 2977{ my $store=$_[0]; my $tp=$_[1]; 2978 my @folders; 2979 my $iter=$store->get_iter($tp); 2980 while ($iter) 2981 { unshift @folders, $store->get_value($iter,0); 2982 $iter=$store->iter_parent($iter); 2983 } 2984 if ($^O eq 'MSWin32') { $folders[0].=::SLASH if @folders==1 } 2985 else { $folders[0]='' if @folders>1; } 2986 return join(::SLASH,@folders); 2987} 2988 2989package SavedTree; 2990use base 'Gtk2::Box'; 2991 2992use constant { TRUE => 1, FALSE => 0, }; 2993 2994our @cMenu; our %Modes; 2995INIT 2996{ @cMenu= 2997 ( { label => _"New filter", code => sub { ::EditFilter($_[0]{self},undef,''); }, stockicon => 'gtk-add' }, 2998 { label => _"Edit filter", code => sub { ::EditFilter($_[0]{self},undef,$_[0]{names}[0]); }, 2999 mode => 'F', onlyone => 'names' }, 3000 { label => _"Remove filter", code => sub { ::SaveFilter($_[0]{names}[0],undef); }, 3001 mode => 'F', onlyone => 'names', stockicon => 'gtk-remove' }, 3002 { label => _"Save current filter as", code => sub { ::EditFilter($_[0]{self},$_[0]{curfilter},''); }, 3003 stockicon => 'gtk-save', isdefined => 'curfilter', test => sub { ! $_[0]{curfilter}->is_empty; } }, 3004 { label => _"Save current list as", code => sub { $_[0]{self}->CreateNewFL('L',[@{ $_[0]{songlist}{array} }]); }, 3005 stockicon => 'gtk-save', isdefined => 'songlist' }, 3006 { label => _("Edit list").'...', code => sub { ::WEditList( $_[0]{names}[0] ); }, 3007 mode => 'L', onlyone => 'names' }, 3008 { label => _"Remove list", code => sub { ::SaveList($_[0]{names}[0],undef); }, 3009 stockicon => 'gtk-remove', mode => 'L', onlyone => 'names', }, 3010 { label => _"Rename", code => sub { my $tv=$_[0]{self}{treeview}; $tv->set_cursor($_[0]{treepaths}[0],$tv->get_column(0),TRUE); }, 3011 notempty => 'names', onlyone => 'treepaths' }, 3012 { label => _("Import list").'...', code => sub { ::Choose_and_import_playlist_files($_[0]{self}); }, mode => 'L', }, 3013 ); 3014 3015 %Modes= 3016 ( F => [_"Saved filters", 'sfilter', 'SavedFilters', \&UpdateSavedFilters, 'gmb-filter' ,\&::SaveFilter, 'filter000'], 3017 L => [_"Saved lists", 'slist', 'SavedLists', \&UpdateSavedLists, 'gmb-list' ,\&::SaveList, 'list000'], 3018 P => [_"Playing", 'play', undef, \&UpdatePlayingFilters, 'gtk-media-play' ], 3019 ); 3020} 3021 3022sub new 3023{ my ($class,$mode,$opt)=@_; 3024 my $self = bless Gtk2::VBox->new(FALSE, 4), $class; 3025 my $store=Gtk2::TreeStore->new(('Glib::String')x4,'Glib::Boolean'); 3026 $self->{treeview}=my $treeview=Gtk2::TreeView->new($store); 3027 $self->{DefaultFocus}=$treeview; 3028 $treeview->set_headers_visible(FALSE); 3029 my $renderer0=Gtk2::CellRendererPixbuf->new; 3030 my $renderer1=Gtk2::CellRendererText->new; 3031 $renderer1->signal_connect(edited => \&name_edited_cb,$self); 3032 my $column=Gtk2::TreeViewColumn->new; 3033 $column->pack_start($renderer0,0); 3034 $column->pack_start($renderer1,1); 3035 $column->add_attribute($renderer0, 'stock-id' => 2); 3036 $column->add_attribute($renderer1, text => 0); 3037 $column->add_attribute($renderer1, editable => 4); 3038 $treeview->append_column($column); 3039 3040 ::set_drag($treeview, source => 3041 [::DRAG_FILTER,sub 3042 { my $self=::find_ancestor($_[0],__PACKAGE__); 3043 my $filter=$self->get_selected_filters; 3044 return ::DRAG_FILTER,($filter? $filter->{string} : undef); 3045 }], 3046 dest => 3047 [::DRAG_FILTER,::DRAG_ID,sub #targets are modified in drag_motion callback 3048 { my ($treeview,$type,$dest,@data)=@_; 3049 my $self=::find_ancestor($treeview,__PACKAGE__); 3050 my (undef,$path)=@$dest; 3051 my ($name,$rowtype)=$store->get_value( $store->get_iter($path) ); 3052 if ($type == ::DRAG_ID) 3053 { if ($rowtype eq 'slist') 3054 { $::Options{SavedLists}{$name}->Push(\@data); 3055 } 3056 else 3057 { $self->CreateNewFL('L',\@data); 3058 } 3059 } 3060 elsif ($type == ::DRAG_FILTER) 3061 { $self->CreateNewFL('F', Filter->new($data[0]) ); 3062 } 3063 }], 3064 motion => \&drag_motion_cb); 3065 3066 MultiTreeView::init($treeview,__PACKAGE__); 3067 $treeview->signal_connect( row_activated => \&row_activated_cb); 3068 my $selection=$treeview->get_selection; 3069 $selection->set_mode('multiple'); 3070 $selection->signal_connect( changed => \&sel_changed_cb); 3071 3072 my $sw= ::new_scrolledwindow($treeview); 3073 ::set_biscrolling($sw); 3074 $self->add($sw); 3075 $self->{store}=$store; 3076 3077 $mode||='FPL'; 3078 my $n=0; 3079 for (split //,$mode) 3080 { my ($label,$id,$watchid,$sub,$stock)=@{ $Modes{$_} }; 3081 if (length($mode)!=1) 3082 { $store->set($store->append(undef),0,$label,1,'root-'.$id,2,$stock); 3083 $self->{$id}=$n++; #path of the root for this id 3084 } 3085 ::Watch($self,$watchid,$sub) if $watchid; 3086 $sub->($self); 3087 } 3088 $treeview->expand_all; 3089 3090 return $self; 3091} 3092 3093sub UpdatePlayingFilters 3094{ my $self=$_[0]; 3095 my ($path,$iter); 3096 my $treeview=$self->{treeview}; 3097 my $store=$treeview->get_model; 3098 if (defined $self->{play}) 3099 { $iter=$store->get_iter_from_string($self->{play}); 3100 } 3101 my @list=( playfilter => _"Playing Filter", 3102 'f=artists' => _"Playing Artist", 3103 'f=album' => _"Playing Album", 3104 'f=title' => _"Playing Title", 3105 ); 3106 while (@list) 3107 { my $id=shift @list; 3108 my $name=shift @list; 3109 $store->set($store->append($iter),0,$name,1,'play',3,$id); 3110 } 3111 $treeview->expand_to_path($path); 3112} 3113 3114sub UpdateSavedFilters 3115{ $_[0]->fill_savednames('sfilter','SavedFilters'); 3116} 3117sub UpdateSavedLists 3118{ return if $_[2] && $_[2] eq 'push'; 3119 $_[0]->fill_savednames('slist','SavedLists'); 3120} 3121sub fill_savednames 3122{ my ($self,$type,$hkey)=@_; 3123 $self->{busy}=1; 3124 my $treeview=$self->{treeview}; 3125 my $store=$treeview->get_model; 3126 my $path; 3127 my $expanded; my $iter; 3128 if (defined $self->{$type}) 3129 { $path=Gtk2::TreePath->new( $self->{$type} ); 3130 $expanded=$treeview->row_expanded($path); 3131 $iter=$store->get_iter($path); 3132 $expanded=1 unless $store->iter_has_child($iter); 3133 } 3134 while (my $child=$store->iter_children($iter)) 3135 { $store->remove($child); 3136 } 3137 $store->set($store->append($iter),0,$_,1,$type,4,TRUE) for sort keys %{$::Options{$hkey}}; #FIXME use case and accent insensitive sort #should use GetListOfSavedLists() for SavedLists 3138 $treeview->expand_to_path($path) if $expanded; 3139 $self->{busy}=undef; 3140} 3141 3142sub PopupContextMenu 3143{ my $self=shift; 3144 my $tv=$self->{treeview}; 3145 my @rows=$tv->get_selection->get_selected_rows; 3146 my $store=$tv->get_model; 3147 my %sel; 3148 for my $path (@rows) 3149 { my ($name,$type)=$store->get_value($store->get_iter($path)); 3150 next if $type=~m/^root-/; 3151 push @{ $sel{$type} },$name; 3152 } 3153 my %args=( self=> $self, treepaths=>\@rows, curfilter=>::GetFilter($self), filter=> $self->get_selected_filters ); 3154 if ((keys %sel)==1) 3155 { my ($mode)=($args{mode})=keys %sel; 3156 $args{mode}= $mode eq 'sfilter' ? 'F' : 3157 $mode eq 'slist' ? 'L' : 3158 ''; 3159 $args{names}=$sel{$mode}; 3160 } 3161 else { $args{mode}=''; } 3162 my $songlist=::GetSonglist($self); 3163 $args{songlist}=$songlist if $songlist; 3164 FilterPane::PopupContextMenu($self,\%args, [@cMenu,{ separator=>1 },@FilterPane::cMenu] ); 3165} 3166 3167sub drag_motion_cb 3168{ my ($treeview,$context,$x,$y,$time)=@_; 3169 ::drag_checkscrolling($treeview,$context,$y); 3170 my $store=$treeview->get_model; 3171 my ($path,$pos)=$treeview->get_dest_row_at_pos($x,$y); 3172 my $status; 3173 { last if !$path || $treeview->{drag_is_source}; 3174 my $type=$store->get_value( $store->get_iter($path) ,1); 3175 last unless $type; 3176 my $target_id=[$::DRAGTYPES[::DRAG_ID][0],'same-app',::DRAG_ID]; 3177 my $target_filter=[$::DRAGTYPES[::DRAG_FILTER][0],[],::DRAG_FILTER]; 3178 my $lookfor; my @targets; 3179 if ($type eq 'root-sfilter') 3180 { $lookfor=::DRAG_FILTER; 3181 @targets=($target_filter,$target_id); 3182 } 3183 elsif ($type=~m/slist$/) 3184 { $lookfor=::DRAG_ID; 3185 @targets=($target_id,$target_filter); 3186 } 3187 else {last} 3188 3189 if ($lookfor && grep $::DRAGTYPES{$_->name} ==$lookfor, $context->targets) 3190 { $status='copy'; 3191 $treeview->drag_dest_set_target_list(Gtk2::TargetList->new( @targets )); 3192 } 3193 } 3194 unless ($status) { $status='default'; $path=undef; } 3195 $context->{dest}=[$treeview,$path]; 3196 $treeview->set_drag_dest_row($path,'into-or-after'); 3197 $context->status($status, $time); 3198 return 1; 3199} 3200 3201sub row_activated_cb 3202{ my ($treeview,$path,$column)=@_; 3203 # rename, not sure if i 3204 $treeview->set_cursor($path,$column,TRUE); 3205} 3206 3207sub Activate 3208{ my ($self,$button)=@_; 3209 my $filter= $self->get_selected_filters; 3210 FilterPane::Activate($self,$button,$filter); 3211} 3212 3213sub name_edited_cb 3214{ my ($cell, $path_string, $newname,$self) = @_; 3215 my $store=$self->{store}; 3216 my $iter=$store->get_iter_from_string($path_string); 3217 my ($name,$type)=$store->get($iter,0,1); 3218 my $sub= $type eq 'sfilter' ? \&::SaveFilter : \&::SaveList; 3219 #$self->{busy}=1; 3220 $sub->($name,undef,$newname); 3221 #$self->{busy}=undef; 3222 #$store->set($iter, 0, $newname); 3223} 3224 3225sub CreateNewFL 3226{ my ($self,$mode,$data)=@_; 3227 my ($type,$hkey,$savesub,$name)= @{$Modes{$mode}}[1,2,5,6]; 3228 while ($::Options{$hkey}{$name}) {$name++} 3229 return if $::Options{$hkey}{$name}; 3230 $savesub->($name,$data); 3231 3232 my $treeview=$self->{treeview}; 3233 my $store=$treeview->get_model; 3234 my $iter; 3235 if (defined $self->{$type}) 3236 { $iter=$store->get_iter_from_string( $self->{$type} ); 3237 } 3238 $iter=$store->iter_children($iter); 3239 while ($iter) 3240 { last if $store->get($iter,0) eq $name; 3241 $iter=$store->iter_next($iter); 3242 } 3243 return unless $iter; 3244 my $path=$store->get_path($iter); 3245 $self->{busy}=1; 3246 $treeview->set_cursor($path,$treeview->get_column(0),TRUE); 3247 $self->{busy}=undef; 3248} 3249 3250sub sel_changed_cb 3251{ my $treesel=$_[0]; 3252 my $self=::find_ancestor($treesel->get_tree_view,__PACKAGE__); 3253 return if $self->{busy}; 3254 my $filter=$self->get_selected_filters; 3255 return unless $filter; 3256 my $filterpane=::find_ancestor($self,'FilterPane'); 3257 ::SetFilter( $self, $filter, $filterpane->{nb}, $filterpane->{group} ); 3258} 3259 3260sub get_selected_filters 3261{ my $self=$_[0]; 3262 my $store=$self->{store}; 3263 my @filters; 3264 for my $path ($self->{treeview}->get_selection->get_selected_rows) 3265 { my ($name,$type,undef,$extra)=$store->get_value($store->get_iter($path)); 3266 next unless $type; 3267 if ($type eq 'sfilter') {push @filters,$::Options{SavedFilters}{$name};} 3268 elsif ($type eq 'slist'){push @filters,'list:~:'.$name;} 3269 elsif ($type eq 'play') {push @filters,_getplayfilter($extra);} 3270 } 3271 return undef unless @filters; 3272 my $filterpane=::find_ancestor($self,'FilterPane'); 3273 my $filter=Filter->newadd( $filterpane->{inter},@filters ); 3274 $filter->invert if $filterpane->{invert}; 3275 return $filter; 3276} 3277 3278sub _getplayfilter 3279{ my $extra=$_[0]; 3280 my $filter; 3281 if ($extra eq 'playfilter') { $filter=$::PlayFilter } 3282 elsif (defined $::SongID && $extra=~s/^f=//) 3283 { $filter= Songs::MakeFilterFromID($extra,$::SongID); 3284 } 3285 return $filter; 3286} 3287 3288package GMB::AABox; 3289use base 'Gtk2::Bin'; 3290 3291our @DefaultOptions= 3292( aa => 'album', 3293 filternb=> 1, 3294 #nopic => 0, 3295); 3296 3297sub new 3298{ my ($class,$opt)= @_; 3299 my $self=bless Gtk2::EventBox->new, $class; 3300 %$opt=( @DefaultOptions, %$opt ); 3301 my $aa=$opt->{aa}; 3302 $aa='artists' if $aa eq 'artist'; 3303 $aa= 'album' unless $aa eq 'artists'; #FIXME PHASE1 change artist to artists 3304 $self->{aa}=$aa; 3305 $self->{filternb}=$opt->{filternb}; 3306 $self->{group}=$opt->{group}; 3307 $self->{nopic}=1 if $opt->{nopic}; 3308 my $hbox= Gtk2::HBox->new; 3309 $self->add($hbox); 3310 $self->{Sel}=$self->{SelID}=undef; 3311 my $vbox=Gtk2::VBox->new(::FALSE, 0); 3312 for my $name (qw/Ltitle Lstats/) 3313 { my $l=Gtk2::Label->new(''); 3314 $self->{$name}=$l; 3315 $l->set_justify('center'); 3316 if ($name eq 'Ltitle') 3317 { $l->set_line_wrap(1);$l->set_ellipsize('end'); #FIXME find a better way to deal with long titles 3318 my $b=Gtk2::Button->new; 3319 $b->set_relief('none'); 3320 $b->signal_connect(button_press_event => \&AABox_button_press_cb); 3321 $b->add($l); 3322 $l=$b; 3323 } 3324 $vbox->pack_start($l, ::FALSE,::FALSE, 2); 3325 } 3326 3327 my $pixbox=Gtk2::EventBox->new; 3328 $self->{img}=my $img=Gtk2::Image->new; 3329 $img->{size}=0; 3330 $img->signal_connect(size_allocate => \&size_allocate_cb) unless $self->{nopic}; 3331 $pixbox->add($img); 3332 $pixbox->signal_connect(button_press_event => \&GMB::Picture::pixbox_button_press_cb,1); # 1 : mouse button 1 3333 3334 my $buttonbox=Gtk2::VBox->new; 3335 my $Bfilter=::NewIconButton('gmb-filter',undef,sub { my $self=::find_ancestor($_[0],__PACKAGE__); $self->filter },'none'); 3336 my $Bplay=::NewIconButton('gtk-media-play',undef,sub 3337 { my $self=::find_ancestor($_[0],__PACKAGE__); 3338 return unless defined $self->{SelID}; 3339 my $filter=Songs::MakeFilterFromGID($self->{aa},$self->{Sel}); 3340 ::Select(filter=> $filter, song=>'first',play=>1); 3341 },'none'); 3342 $Bplay->signal_connect(button_press_event => sub #enqueue with middle-click 3343 { my $self=::find_ancestor($_[0],__PACKAGE__); 3344 return 0 if $_[1]->button !=2; 3345 my $filter= Songs::MakeFilterFromGID($self->{aa},$self->{Sel}); 3346 if (defined $self->{SelID}) { ::EnqueueFilter($filter); } 3347 1; 3348 }); 3349 $Bfilter->set_tooltip_text( ($aa eq 'album' ? _"Filter on this album" : _"Filter on this artist") ); 3350 $Bplay ->set_tooltip_text( ($aa eq 'album' ? _"Play all songs from this album" : _"Play all songs from this artist") ); 3351 $buttonbox->pack_start($_, ::FALSE, ::FALSE, 0) for $Bfilter,$Bplay; 3352 3353 $hbox->pack_start($pixbox, ::FALSE, ::TRUE, 0); 3354 $hbox->pack_start($vbox, ::TRUE, ::TRUE, 0); 3355 $hbox->pack_start($buttonbox, ::FALSE, ::FALSE, 0); 3356 3357 if ($aa eq 'artists') 3358 { $self->{'index'}=0; 3359 $self->signal_connect(scroll_event => \&AABox_scroll_event_cb); 3360 my $BAlblist=::NewIconButton('gmb-playlist',undef,undef,'none'); 3361 $BAlblist->signal_connect(button_press_event => \&AlbumListButton_press_cb); 3362 $BAlblist->set_tooltip_text(_"Choose Album From this Artist"); 3363 $buttonbox->pack_start($BAlblist, ::FALSE, ::FALSE, 0); 3364 } 3365 3366 my $drgsrc=$aa eq 'album' ? ::DRAG_ALBUM : ::DRAG_ARTIST; 3367 ::set_drag($self, source => 3368 [$drgsrc, sub { $drgsrc,$_[0]{Sel}; } ], 3369 dest => [::DRAG_ID,::DRAG_FILE,sub 3370 { my ($self,$type,@values)=@_; 3371 if ($type==::DRAG_FILE) 3372 { return unless defined $self->{Sel}; 3373 my $file=$values[0]; 3374 if ($file=~s#^file://##) 3375 { AAPicture::SetPicture($self->{aa},$self->{Sel},::decode_url($file)); 3376 } 3377 #else #FIXME download http link, ask filename 3378 } 3379 else # $type is ID 3380 { $self->id_set($values[0]); 3381 } 3382 }]); 3383 3384 $self->signal_connect(button_press_event => \&AABox_button_press_cb); 3385 ::Watch($self,"Picture_".($aa eq 'album' ? 'album' : 'artist') =>\&AAPicture_Changed); 3386 ::WatchSelID($self,\&id_set); 3387 ::Watch($self, SongsChanged=> \&SongsChanged_or_added_cb); 3388 ::Watch($self, SongsAdded => \&SongsChanged_or_added_cb); 3389 ::Watch($self, SongsRemoved=> \&SongsRemoved_cb); 3390 ::Watch($self, SongsHidden => \&SongsRemoved_cb); 3391 $self->signal_connect(destroy => \&remove); 3392 return $self; 3393} 3394sub remove 3395{ my $self=$_[0]; 3396 delete $::ToDo{'9_AABox'.$self}; 3397} 3398 3399sub AAPicture_Changed 3400{ my ($self,$key)=@_; 3401 return unless defined $self->{Sel}; 3402 return unless $key eq $self->{Sel}; 3403 $self->pic_update; 3404} 3405 3406sub update_id 3407{ my $self=$_[0]; 3408 my $ID=$self->{SelID}; 3409 $self->{SelID}=$self->{Sel}=undef; 3410 $self->id_set($ID); 3411} 3412 3413sub clear 3414{ my $self=$_[0]; 3415 $self->{SelID}=$self->{Sel}=undef; 3416 $self->pic_update; 3417 $self->{$_}->set_text('') for qw/Ltitle Lstats/; 3418 delete $::ToDo{'9_AABox'.$self}; 3419} 3420 3421sub id_set 3422{ my ($self,$ID)=@_; 3423 return if defined $self->{SelID} && $self->{SelID}==$ID; 3424 $self->{SelID}=$ID; 3425 my $key= Songs::Get_gid($ID,$self->{aa}); 3426 if ( $self->{aa} eq 'artists' ) #$key is an array ref 3427 { $self->{'index'}%= @$key; 3428 $key= $key->[ $self->{'index'} ]; 3429 } 3430 $self->update($key) unless defined $self->{Sel} && $key == $self->{Sel}; 3431} 3432 3433sub update 3434{ my ($self,$key)=@_; 3435 #return if $self->{Sel} == $key; 3436 if (defined $key) { $self->{Sel}=$key; } 3437 else { $key=$self->{Sel}; } 3438 return unless defined $key; 3439 my $aa=$self->{aa}; 3440 $self->pic_update; 3441 $self->{Ltitle}->set_markup( AA::ReplaceFields($key,"<big><b>%a</b></big>",$aa,1) ); 3442 $self->{Lstats}->set_markup( AA::ReplaceFields($key,"%s\n%X\n<small>%L\n%y</small>",$aa,1) ); 3443 3444 delete $::ToDo{'9_AABox'.$self}; 3445 $self->{needupdate}=0; 3446} 3447 3448sub SongsChanged_or_added_cb 3449{ my ($self,$IDs,$fields)=@_; #fields is undef if SongsAdded 3450 return if $self->{needupdate}; 3451 # could check if is in list or in filter, is it worth it ? 3452 return if $fields && !::OneInCommon($fields,[qw/artist album length size year/]); 3453 $self->{needupdate}=1; 3454 ::IdleDo('9_AABox'.$self,1000,\&update,$self); 3455} 3456sub SongsRemoved_cb 3457{ my ($self,$IDs)=@_; 3458 return if $self->{needupdate}; 3459 $self->{needupdate}=1; 3460 ::IdleDo('9_AABox'.$self,1000,\&update,$self); 3461} 3462 3463sub filter 3464{ my $self=$_[0]; 3465 return unless defined $self->{Sel}; 3466 ::SetFilter( $self, Songs::MakeFilterFromGID($self->{aa},$self->{Sel}), $self->{filternb}, $self->{group} ); 3467} 3468 3469sub pic_update 3470{ my $self=shift; 3471 return if $self->{nopic}; 3472 my $img=$self->{img}; 3473 delete $img->{pixbuf}; 3474 ::IdleDo('3_AABscaleimage'.$img,200,\&setpic,$img); 3475} 3476 3477sub size_allocate_cb 3478{ my ($img,$alloc)=@_; 3479 my $h=$alloc->height; 3480 $h=200 if $h>200; #FIXME use a relative max value (to what?) 3481 return unless abs($img->{size}-$h); 3482 $img->{size}=$h; 3483 ::IdleDo('3_AABscaleimage'.$img,200,\&setpic,$img); 3484} 3485sub setpic 3486{ my $img=shift; 3487 my $self= ::find_ancestor($img,__PACKAGE__); 3488 return unless defined $self->{SelID}; 3489 my $file= $img->{filename}= AAPicture::GetPicture($self->{aa},$self->{Sel}); 3490 my $pixbuf= $file ? GMB::Picture::pixbuf($file,$img->{size}) : undef; 3491 $img->set_from_pixbuf($pixbuf); 3492} 3493 3494sub AABox_button_press_cb #popup menu 3495{ my ($widget,$event)=@_; 3496 my $self=::find_ancestor($widget,__PACKAGE__); 3497 return 0 unless $self; 3498 return 0 if $self == $widget && $event->button != 3; 3499 return unless defined $self->{SelID}; 3500 ::PopupAAContextMenu({self=>$self, field=>$self->{aa}, gid=>$self->{Sel}, ID=>$self->{SelID}, filternb => $self->{filternb}, mode => 'B'}); 3501 return 1; 3502} 3503 3504sub AABox_scroll_event_cb 3505{ my ($self,$event)=@_; 3506 my $l= Songs::Get_gid($self->{SelID},'artists'); 3507 return 0 unless @$l>1; 3508 $self->{'index'}+=($event->direction eq 'up')? 1 : -1; 3509 $self->{'index'}%=@$l; 3510 $self->update( $l->[$self->{'index'}] ); 3511 1; 3512} 3513 3514sub AlbumListButton_press_cb 3515{ my ($widget,$event)=@_; 3516 my $self=::find_ancestor($widget,__PACKAGE__); 3517 return unless defined $self->{Sel}; 3518 ::PopupAA('album', from => $self->{Sel}, cb=>sub 3519 { my $filter= $_[0]{filter}; 3520 ::SetFilter( $self, $filter, $self->{filternb}, $self->{group} ); 3521 }); 3522 1; 3523} 3524 3525package SimpleSearch; 3526use base 'Gtk2::Entry'; 3527 3528our @SelectorMenu= #the first one is the default 3529( [_"Search Title, Artist and Album", 'title|artist|album' ], 3530 [_"Search Title, Artist, Album, Comment, Label and Genre", 'title|artist|album|comment|label|genre' ], 3531 [_"Search Title, Artist, Album, Comment, Label, Genre and Filename", 'title|artist|album|comment|label|genre|file' ], 3532 [_"Search Title", 'title'], 3533 [_"Search Artist", 'artist'], 3534 [_"Search Album", 'album'], 3535 [_"Search Comment", 'comment'], 3536 [_"Search Label", 'label'], 3537 [_"Search Genre", 'genre'], 3538); 3539 3540our %Options= 3541( casesens => _"Case sensitive", 3542 literal => _"Literal search", 3543 regexp => _"Regular expression", 3544); 3545our %Options2= 3546( autofilter => _"Auto filter", 3547 suggest => _"Show suggestions", 3548); 3549our @DefaultOptions= 3550( nb => 1, 3551 fields => $SelectorMenu[2][1], 3552 autofilter =>1, 3553); 3554 3555sub new 3556{ my ($class,$opt)=@_; 3557 my $self= bless Gtk2::Entry->new, $class; 3558 %$opt=( @DefaultOptions, %$opt ); 3559 $self->signal_connect(changed => \&EntryChanged_cb); 3560 $self->signal_connect(activate => \&DoFilter); 3561 $self->signal_connect(activate => \&CloseSuggestionMenu); 3562 $self->signal_connect(key_press_event => sub { my ($self,$event)=@_; return 0 unless Gtk2::Gdk->keyval_name($event->keyval) eq 'Escape'; $self->set_text(''); return 1; }); 3563 $self->signal_connect_after(activate => sub {::run_command($_[0],$opt->{activate});}) if $opt->{activate}; 3564 #$self->set_width_chars($opt->{width_chars}) if $opt->{width_chars}; 3565 unless ($opt->{noselector}) 3566 { if (*Gtk2::Entry::set_icon_from_stock{CODE}) # requires gtk>=2.16 && perl-Gtk2 version >=1.211 3567 { $self->set_icon_from_stock('primary','gtk-find'); 3568 $self->set_icon_from_stock('secondary','gtk-clear'); 3569 $self->set_icon_activatable($_,1) for qw/primary secondary/; 3570 $self->set_icon_tooltip_text('primary',_"Search options"); 3571 $self->set_icon_tooltip_text('secondary',_"Reset filter"); 3572 $self->set_icon_sensitive('secondary',0); 3573 $self->signal_connect(changed => \&UpdateClearButton); 3574 $self->signal_connect(icon_press => sub { my ($self,$iconpos)=@_; if ($iconpos eq 'primary') {$self->PopupSelectorMenu} else {$self->ClearFilter} }); 3575 $self->signal_connect(focus_out_event => \&focus_changed_cb); 3576 $self->signal_connect(focus_in_event => \&focus_changed_cb); 3577 $self->signal_connect(scroll_event => \&scroll_event_cb); 3578 } 3579 else # old version => use old hackish entry with icons 3580 { $self= SimpleSearch::old->new($opt); 3581 } 3582 } 3583 $self->{$_}=$opt->{$_} for qw/nb fields group searchfb/,keys %Options,keys %Options2; 3584 $self->{SaveOptions}=\&SaveOptions; 3585 ::WatchFilter($self, $self->{group},sub { $_[0]->Update_bg(0); $_[0]->UpdateClearButton;}) unless $opt->{noselector}; #to update background color and clear button 3586 return $self; 3587} 3588 3589sub SaveOptions 3590{ my $self=$_[0]; 3591 my %opt=(fields => $self->{fields}); 3592 $opt{$_}= $self->{$_} ? 1 : 0 for keys %Options, keys %Options2; 3593 return \%opt; 3594} 3595 3596sub ClearFilter 3597{ my $self=shift; 3598 my $event=Gtk2->get_current_event; 3599 my $text=''; 3600 if ($event->isa('Gtk2::Gdk::Event::Button') && $event->button == 2) #paste clipboard if middle-click 3601 { my $clip= $self->get_clipboard(Gtk2::Gdk::Atom->new('PRIMARY',1))->wait_for_text; 3602 $text=$1 if $clip=~m/([^\n\r]+)/; 3603 } 3604 $self->set_text($text); 3605 $self->DoFilter; 3606} 3607sub UpdateClearButton 3608{ my $self=shift; 3609 my $on= $self->get_text ne '' || !::GetFilter($self)->is_empty; 3610 $self->set_icon_sensitive('secondary',$on); 3611} 3612sub focus_changed_cb { $_[0]->Update_bg; 0; } 3613sub Update_bg 3614{ my ($self,$on)=@_; 3615 $self->{filtered}=$on if defined $on; 3616 $self->set_progress_fraction( !$self->has_focus && $self->{filtered} ); #used to set the background color 3617} 3618 3619sub ChangeOption 3620{ my ($self,$key,$value)=@_; 3621 $self->{$key}=$value; 3622 $self->DoFilter unless $self->get_text eq ''; 3623} 3624 3625sub PopupSelectorMenu 3626{ my $self=shift; 3627 my $menu=Gtk2::Menu->new; 3628 my $cb=sub { $self->ChangeOption( fields => $_[1]); }; 3629 for my $ref (@SelectorMenu) 3630 { my ($label,$fields)=@$ref; 3631 my $item=Gtk2::CheckMenuItem->new($label); 3632 $item->set_active(1) if $fields eq $self->{fields}; 3633 $item->set_draw_as_radio(1); 3634 $item->signal_connect(activate => $cb,$fields); 3635 $menu->append($item); 3636 } 3637 my $item1=Gtk2::MenuItem->new(_"Select search fields"); 3638 $item1->set_submenu( ::BuildChoiceMenu( 3639 { map { $_=>Songs::FieldName($_) } Songs::StringFields(),qw/file path year/,}, 3640 'reverse' =>1, return_list=>1, 3641 check=> sub { [split /\|/,$self->{fields}]; }, 3642 code => sub { $self->ChangeOption(fields=> join '|',@{$_[1]} ); }, 3643 ) ); 3644 $menu->append($item1); 3645 $menu->append(Gtk2::SeparatorMenuItem->new); 3646 for my $key (sort { $Options{$a} cmp $Options{$b} } keys %Options) 3647 { my $item=Gtk2::CheckMenuItem->new($Options{$key}); 3648 $item->set_active(1) if $self->{$key}; 3649 $item->signal_connect(activate => sub 3650 { $self->ChangeOption( $_[1] => $_[0]->get_active); 3651 },$key); 3652 $menu->append($item); 3653 } 3654 $menu->append(Gtk2::SeparatorMenuItem->new); 3655 for my $key (sort { $Options2{$a} cmp $Options2{$b} } keys %Options2) 3656 { my $item=Gtk2::CheckMenuItem->new($Options2{$key}); 3657 $item->set_active(1) if $self->{$key}; 3658 $item->signal_connect(activate => sub 3659 { $self->{$_[1]}= $_[0]->get_active; 3660 },$key); 3661 $menu->append($item); 3662 } 3663 my $item2=Gtk2::MenuItem->new(_"Advanced Search ..."); 3664 $item2->signal_connect(activate => sub 3665 { ::EditFilter($self,::GetFilter($self),undef,sub {::SetFilter($self,$_[0]) if defined $_[0]}); 3666 }); 3667 $menu->append($item2); 3668 ::PopupMenu($menu); 3669} 3670 3671sub GetFilter 3672{ my $self=shift; 3673 my $search= $self->get_text; 3674 3675 my $filter; 3676 if (length $search) 3677 { if ($self->{literal}) 3678 { my $op= $self->{regexp} ? ($self->{casesens} ? 'm' : 'mi') : ($self->{casesens} ? 's' : 'si'); 3679 my $fields=$self->{fields}; 3680 $filter= Filter->newadd(0, map($_.':'.$op.':'.$search, split /\|/, $self->{fields}) ); 3681 } 3682 else 3683 { $filter= Filter->new_from_smartstring($search,$self->{casesens},$self->{regexp},$self->{fields}); 3684 } 3685 # optimization : see if it can use previous search 3686 my $last_filter= delete $self->{last_filter}; 3687 $filter->add_possible_superset($last_filter) if $last_filter; 3688 $self->{last_filter}=$filter; 3689 } 3690 else { $filter= Filter->new } 3691 return $filter; 3692} 3693 3694sub AutoFilter 3695{ my ($self,$event,$force)=@_; 3696 if ($::debug) { warn "AutoFilter: $event".($force ? ' force':'')."\n" } 3697 unless ($event eq 'filter_ready' || $event eq 'time_ready') {warn 'error'; return} 3698 $self->{$event}=1; 3699 return unless $self->{filter_ready} && $self->{time_ready}; 3700 my $idlefilter=$self->{idlefilter}; 3701 if (!$force && $idlefilter && !$idlefilter->is_cached) { warn "AutoFilter: restart\n" if $::debug; $idlefilter->start; return } #for case where filter was finished before first timeout, but since the cache was flushed, retry unless the second timeout has expired 3702 Glib::Source->remove(delete $self->{changed_timeout}) if $self->{changed_timeout}; 3703 Glib::Source->remove(delete $self->{idlefilter_timeout}) if $self->{idlefilter_timeout}; 3704 $self->DoFilter if $self->{autofilter}; 3705} 3706 3707sub StartIdleFilter 3708{ my $self=shift; 3709 my $search=$self->get_text; 3710 my $previous= delete $self->{idlefilter}; 3711 my $filter= ::SimulateSetFilter( $self,$self->GetFilter, $self->{nb} ); 3712 #warn "idle $search\n"; 3713 my $new= IdleFilter->new($filter,sub { $self->AutoFilter('filter_ready')}); 3714 $self->{idlefilter}=$new if ref $new; 3715 $previous->abort if $previous; 3716} 3717 3718sub DoFilter 3719{ my $self=shift; 3720 Glib::Source->remove(delete $self->{changed_timeout}) if $self->{changed_timeout}; 3721 my $idlefilter= delete $self->{idlefilter}; 3722 $idlefilter->abort if $idlefilter; 3723 3724 my $filter= $self->GetFilter; 3725 ::SetFilter($self,$filter,$self->{nb}); 3726 if ($self->{searchfb}) 3727 { my $search= $self->get_text; 3728 ::HasChanged('SearchText_'.$self->{group},$search); #FIXME 3729 } 3730 $self->Update_bg( !$filter->is_empty ); 3731} 3732 3733sub EntryChanged_cb 3734{ my $self=shift; 3735 $self->Update_bg(0); 3736 my $l= length($self->get_text); 3737 delete $self->{filter_ready}; 3738 delete $self->{time_ready}; 3739 Glib::Source->remove(delete $self->{changed_timeout}) if $self->{changed_timeout}; 3740 Glib::Source->remove(delete $self->{idlefilter_timeout}) if $self->{idlefilter_timeout}; 3741 if ($self->{autofilter}) 3742 { # 1st timeout : do not filter before this minimum timeout, even if filter is ready 3743 my $timeout= $l<2 ? 800 : 300; 3744 $self->{changed_timeout}= Glib::Timeout->add($timeout,sub { $self->AutoFilter('time_ready'); 0 }); 3745 # 2nd timeout : filter even if idlefilter not finished 3746 $timeout= $l<4 ? 3000 : 2000; 3747 $self->{idlefilter_timeout}= Glib::Timeout->add($timeout,sub { $self->AutoFilter('filter_ready','force'); 0 }); 3748 } 3749 $self->StartIdleFilter if $self->{autofilter} || ($l>2 && !$self->{suggest}); 3750 if ($self->{suggest}) 3751 { Glib::Source->remove(delete $self->{suggest_timeout}) if $self->{suggest_timeout}; 3752 my $timeout= $l<2 ? 0 : $l==2 ? 200 : 100; 3753 if ($timeout) { $self->{suggest_timeout}= Glib::Timeout->add($timeout,\&UpdateSuggestionMenu,$self); } 3754 else { $self->CloseSuggestionMenu; } 3755 } 3756} 3757 3758sub scroll_event_cb #increase/decrease numbers when using the wheel over them 3759{ my ($self,$event)=@_; 3760 my $dir= $event->direction; 3761 $dir= $dir eq 'up' ? 1 : $dir eq 'down' ? -1 : 0; 3762 return 0 unless $dir; 3763 return 0 unless $event->window == $self->get_text_window; #ignore if pointer outside the text area 3764 my $text0= $self->get_text; 3765 my ($offx,$offy)= $self->get_layout_offsets; 3766 my $x= $event->x - $offx; 3767 my $layout= $self->get_layout; 3768 my ($index,$trailing)=$layout->xy_to_index(Gtk2::Pango->scale *$x,0); #y always 0, as only one line 3769 $index= $x<0 ? 0 : # if pointer before the text 3770 defined $index ? $self->layout_index_to_text_index($index) : 3771 length($text0)-1; # if pointer after the text, do as if at the end of text 3772 my $pos=0; 3773 my $text=''; 3774 my $found; 3775 for my $string (split /(\|| +|(?<=\d)(?=(?:\.\.|[.,]?-)\d))/,$text0) 3776 { my $l= length $string; 3777 if (!$found && $pos<=$index && $pos+$l>=$index && $string=~m/\d/) 3778 { $string= _smart_incdec($string,$dir); 3779 $found= $pos+length $string; 3780 } 3781 $pos+= $l; 3782 $text.=$string; 3783 } 3784 if ($found) { $self->set_text($text); $self->set_position($found); return 1; } 3785 0; 3786} 3787sub _smart_incdec # increase/decrease the lowest significant digit in the number of the string 3788{ my ($string,$inc)=@_; 3789 my @parts= reverse split /(\.\.|\d*[.,]\d+|\d+)/,$string; 3790 3791 for my $part (@parts) 3792 { if ($part=~m#^(\d*)([.,])(\d+)$#) 3793 { my $d=$3+$inc; 3794 my $n=$1; 3795 my $l1=length $3; 3796 my $l2=length $d; 3797 if ($d<0) 3798 { if ($n) { $d="9"x$l1; $n--;} 3799 else { $d="0"x$l1 } 3800 } 3801 elsif ($l2>$l1) { $d="0"x$l1; $n++; } 3802 elsif ($l2<$l1) { $d="0"x($l1-$l2).$d } 3803 $part= $n.$2.$d; 3804 last; 3805 } 3806 elsif ($part=~m#^\d+$#) 3807 { $part+=$inc; 3808 $part=0 if $part<0; 3809 last; 3810 } 3811 } 3812 return join '',reverse @parts; 3813} 3814 3815sub CloseSuggestionMenu 3816{ my $self=shift; 3817 Glib::Source->remove(delete $self->{suggest_timeout}) if $self->{suggest_timeout}; 3818 my $menu= delete $self->{matchmenu}; 3819 return unless $menu; 3820 $menu->cancel; 3821 $menu->destroy; 3822} 3823 3824sub UpdateSuggestionMenu 3825{ my $self=shift; 3826 if ($self->{matchmenu} && !$self->{matchmenu}->mapped) { $self->CloseSuggestionMenu; } 3827 Glib::Source->remove(delete $self->{suggest_timeout}) if $self->{suggest_timeout}; 3828 my $refresh= !!$self->{matchmenu}; 3829 my $menu= $self->{matchmenu} ||= Gtk2::Menu->new; 3830 if ($refresh) { $menu->remove($_) for $menu->get_children; } 3831 3832 my $h=$self->size_request->height; 3833 my $w=$self->size_request->width; 3834 my $screen=$self->get_screen; 3835 my $monitor=$screen->get_monitor_at_window($self->window); 3836 my ($xmin,$ymin,$monitorwidth,$monitorheight)=$screen->get_monitor_geometry($monitor)->values; 3837 my $xmax=$xmin + $monitorwidth; 3838 my $ymax=$ymin + $monitorheight; 3839 my ($x,$y)=$self->window->get_origin; # position of the parent widget on the screen 3840 my ($dx,$dy)=$self->window->get_size; # width,height of the parent widget 3841 if ($self->isa('Gtk2::Widget') && $self->no_window) 3842 { (my$x2,my$y2,$dx,$dy)=$self->allocation->values; 3843 $x+=$x2;$y+=$y2; 3844 } 3845 my $above=0; 3846 my $height=$ymax-$y-$h; 3847 if ($height<$y-$ymin) { $height=$y-$ymin; $above=1; } 3848 $height*=.9; 3849 3850 my $found; 3851 my $text= $self->get_text; 3852 for my $field (qw/artists album genre label title/) 3853 { my $list; 3854 if ($field eq 'title') 3855 { my $filter= Filter->new('title:si:'.$text); 3856 $filter->add_possible_superset($self->{last_suggestion_filter}) if $self->{last_suggestion_filter}; 3857 $self->{last_suggestion_filter}=$filter; 3858 $list= $filter->filter; 3859 next unless @$list; 3860 Songs::SortList($list,'-rating -playcount -lastplay'); 3861 } 3862 else 3863 { $list= AA::GrepKeys($field, $text); 3864 next unless @$list; 3865 #AA::SortKeys($field,$list,'alpha'); 3866 AA::SortKeys($field,$list,'songs'); @$list= reverse @$list; 3867 # remove 0 songs ? 3868 } 3869 $found=1; 3870 my $item0= Gtk2::MenuItem->new; 3871 my $label0= Gtk2::Label->new; 3872 $label0->set_markup_with_format("<i> %s : %d</i>", Songs::FieldName($field), scalar(@$list)); 3873 $label0->set_alignment(1,.5); 3874 $item0->add($label0); 3875 $item0->show_all; 3876 $height-= $item0->size_request->height; 3877 $menu->append($item0); 3878 my $format= $field eq 'album' ? "<b>%a</b>%Y\n<small>%s by %b</small>": 3879 $field=~m/^artists?$/ ? "<b>%a</b>\n<small>%x %s%Y</small>" : 3880 $field eq 'title' ? "<b>%t</b>\n<small><small>by</small> %a <small>from</small> %l</small>": 3881 "<b>%a</b> (<small>%s</small>)"; 3882 if ($field eq 'title') { $item0->set_sensitive(0) } 3883 else 3884 { $item0->{field}=$field; 3885 $item0->{list}=$list; 3886 $item0->{format}=$format; 3887 $item0->signal_connect(button_press_event => \&SuggestionMenu_field_expand) unless $field eq 'title'; 3888 } 3889 for my $i (0..::min(4,$#$list)) 3890 { my $val= $list->[$i]; 3891 my $item; 3892 if ($field eq 'artists' || $field eq 'album') #FIXME be more generic 3893 { if ( my $img=AAPicture::newimg($field,$val,32) ) 3894 { $item=Gtk2::ImageMenuItem->new; 3895 $item->set_image($img); 3896 } 3897 } 3898 elsif ($field eq 'label') #FIXME be more generic 3899 { if (my $icon=Songs::Picture($val,$field,'icon')) 3900 { $item=Gtk2::ImageMenuItem->new; 3901 $item->set_image( Gtk2::Image->new_from_stock($icon,'menu') ); 3902 } 3903 } 3904 $item||=Gtk2::MenuItem->new; 3905 my $markup; 3906 if ($field eq 'title') { $markup=::ReplaceFieldsAndEsc($val,$format); } 3907 else 3908 { $markup=AA::ReplaceFields($val,$format,$field,1); 3909 } 3910 my $label=Gtk2::Label->new; 3911 $label->set_markup($markup); 3912 $label->set_ellipsize('end'); 3913 $label->set_alignment(0,.5); 3914 $item->{val}=$val; 3915 $item->{field}=$field; 3916 $item->signal_connect(button_press_event => sub { $_[0]{middle}=$_[1]->button==2; }); 3917 $item->signal_connect(activate=> \&SuggestionMenu_item_activated_cb); 3918 $item->add($label); 3919 $item->show_all; 3920 $height-= $item->size_request->height; 3921 if ($height<0) 3922 { $menu->remove($item0) if $i==0; 3923 last; 3924 } 3925 $menu->append($item); 3926 } 3927 last if $height<0; 3928 } 3929 unless ($found) 3930 { $self->CloseSuggestionMenu; 3931 return; 3932 } 3933 $menu->set_size_request($w*2,-1); 3934 $menu->show_all; 3935 $menu->set_take_focus(0); 3936 if ($menu->mapped) 3937 { $menu->reposition; 3938 $menu->set_active(0); 3939 } 3940 else 3941 { $menu->attach_to_widget($self, sub {'empty detaching callback'}); 3942 $menu->signal_connect(key_press_event => \&SuggestionMenu_key_press_cb); 3943 $menu->signal_connect(selection_done => sub {$_[0]->get_attach_widget->CloseSuggestionMenu}); 3944 $menu->popup(undef,undef,sub { my $menu=shift; $x, ($above ? $y-$menu->size_request->height : $y+$h); },undef,0,Gtk2->get_current_event_time); 3945 } 3946} 3947sub SuggestionMenu_key_press_cb 3948{ my ($menu,$event)=@_; 3949 my $key=Gtk2::Gdk->keyval_name( $event->keyval ); 3950 if (grep $key eq $_, qw/Up Down Return Right/) 3951 { my @items=$menu->get_children; 3952 if ($key eq 'Up' && $items[0]->state eq 'prelight') { $items[0] ->deselect; return 1 } 3953 if ($key eq 'Down' && $items[-1]->state eq 'prelight') { $items[-1]->deselect; return 1 } 3954 if ($key eq 'Return' || $key eq 'Right') 3955 { my ($item)= grep $_->state eq 'prelight', @items; 3956 if ($item) 3957 { SuggestionMenu_field_expand($item) if $item->{list}; 3958 return 0; 3959 } 3960 } 3961 else { return 0 } 3962 } 3963 #return 0 if grep $key eq $_, qw/Up Down/; 3964 $menu->get_attach_widget->event($event); # redirect the event to the entry 3965 1; 3966} 3967sub SuggestionMenu_item_activated_cb 3968{ my $item=shift; 3969 my $self= ::find_ancestor($item,__PACKAGE__); # use the attach_widget to get back to self 3970 my $val= $item->{val}; 3971 my $field= $item->{field}; 3972 my $filter; 3973 if ($field eq 'title') 3974 { $filter= Songs::MakeFilterFromID($field,$val); 3975 } 3976 else 3977 { $filter= Songs::MakeFilterFromGID($field,$val); 3978 } 3979 if (my $watch=delete $self->{changed_timeout}) { Glib::Source->remove($watch); } 3980 $self->CloseSuggestionMenu; 3981 if ($item->{middle}) 3982 { my $IDs= $field eq 'title' ? [$val] : $filter->filter; 3983 ::DoActionForList('queue', $IDs); 3984 } 3985 else { ::SetFilter($self,$filter,$self->{nb}); } 3986} 3987 3988sub SuggestionMenu_field_expand 3989{ my $item=shift; 3990 return 0 if $item->get_submenu; 3991 my $submenu=::PopupAA($item->{field}, list=>$item->{list}, format=>$item->{format}, cb => sub { my $item=$_[0]{menuitem}; $item->{field}=$_[0]{field}; $item->{val}=$_[0]{key}; SuggestionMenu_item_activated_cb($item); }); 3992 $item->set_submenu($submenu); 3993 return 0; 3994} 3995 3996package SimpleSearch::old; 3997use base 'Gtk2::Box'; 3998our @ISA; 3999BEGIN {unshift @ISA,'SimpleSearch';} 4000 4001sub new 4002{ my ($class,$opt)=@_; 4003 my $self= bless Gtk2::HBox->new(0,0), $class; 4004 my $entry=$self->{entry}=Gtk2::Entry->new; 4005 $self->{DefaultFocus}=$entry; 4006 $entry->signal_connect(changed => sub { $_[0]->parent->EntryChanged_cb }); 4007 $entry->signal_connect(activate => sub { $_[0]->parent->DoFilter; }); 4008 $entry->signal_connect(activate => sub { $_[0]->parent->CloseSuggestionMenu; }); 4009 $entry->signal_connect(key_press_event => sub { my ($entry,$event)=@_; return 0 unless Gtk2::Gdk->keyval_name($event->keyval) eq 'Escape'; $entry->set_text(''); return 1; }); 4010 $entry->signal_connect_after(activate => sub {::run_command($_[0]->parent,$opt->{activate});}) if $opt->{activate}; 4011 4012 for my $aref ( ['gtk-find' => sub {$_[0]->parent->PopupSelectorMenu}, 0, _"Search options"], 4013 ['gtk-clear' => sub {$_[0]->parent->ClearFilter}, 1, _"Reset filter"] 4014 ) 4015 { my ($stock,$cb,$end,$tip)=@$aref; 4016 my $img=Gtk2::Image->new_from_stock($stock,'menu'); 4017 my $but=Gtk2::Button->new; 4018 $but->add($img); 4019 $but->can_focus(0); 4020 $but->set_relief('none'); 4021 $but->set_tooltip_text($tip); 4022 $but->signal_connect(expose_event => sub #prevent the button from beign drawn, but draw its child 4023 { my ($but,$event)=@_; 4024 $but->propagate_expose($but->child,$event); 4025 1; 4026 }); 4027 #$but->signal_connect(realize => sub { $_[0]->window->set_cursor(Gtk2::Gdk::Cursor->new('hand2')); }); 4028 $but->signal_connect(button_press_event=> $cb); 4029 if ($end) { $self->pack_end($but,0,0,0); } 4030 else { $self->pack_start($but,0,0,0); } 4031 if ($stock eq 'gtk-clear') 4032 { $self->{clear_button}=$but; 4033 $entry->signal_connect(changed => sub { $_[0]->parent->UpdateClearButton }); 4034 $but->set_sensitive(0); 4035 } 4036 } 4037 $self->pack_start($entry,1,1,0); 4038 $entry->set('has-frame',0); 4039 $entry->signal_connect($_ => sub {$_[0]->parent->queue_draw}) for qw/focus_in_event focus_out_event/; 4040 $self->signal_connect(expose_event => sub #draw an entry frame inside $self 4041 { my ($self,$event)=@_; 4042 my $entry=$self->{entry}; 4043 if ($entry->state eq 'normal') 4044 { my $s= $self->{filtered} && !$entry->is_focus; 4045 $entry->modify_base('normal', ($s? $entry->style->bg('selected') : undef) ); 4046 $entry->modify_text('normal', ($s? $entry->style->text('selected') : undef) ); 4047 } 4048 $entry->style->paint_flat_box( $self->window, $entry->state, 'none', $event->area, $entry, 'entry_bg', $self->allocation->values ); 4049 $entry->style->paint_shadow( $self->window, 'normal', $entry->get('shadow-type'), $event->area, $entry, 'entry', $self->allocation->values); 4050 #$self->propagate_expose($_,$event) for $self->get_children; 4051 0; 4052 }); 4053 #$entry->set_width_chars($opt->{width_chars}) if $opt->{width_chars}; 4054 return $self; 4055} 4056 4057sub Update_bg 4058{ $_[0]{filtered}=$_[1]; 4059 $_[0]->queue_draw; 4060} 4061sub set_text 4062{ $_[0]{entry}->set_text($_[1]); 4063} 4064sub get_text 4065{ $_[0]{entry}->get_text; 4066} 4067sub set_icon_sensitive 4068{ my ($self,$icon,$on)=@_; 4069 $self->{clear_button}->set_sensitive($on) if $icon eq 'secondary'; 4070} 4071 4072 4073package SongSearch; 4074use base 'Gtk2::Box'; 4075 4076sub new 4077{ my ($class,$opt)=@_; 4078 my $self= bless Gtk2::VBox->new, $class; 4079 my %sl_opt=( type=>'S', headers=>'off', 'sort'=>'title', cols=>'titleaa', group=>"$self", name=>'songsearch' ); 4080 $sl_opt{$_}= $opt->{$_} for grep m/^activate\d?/, keys %$opt; 4081 $sl_opt{activate} ||= 'queue'; 4082 $self->{songlist}= my $songlist= SongList->new(\%sl_opt); 4083 my $hbox1=Gtk2::HBox->new; 4084 my $entry=Gtk2::Entry->new; 4085 $entry->signal_connect(changed => \&EntryChanged_cb,0); 4086 $entry->signal_connect(activate =>\&EntryChanged_cb,1); 4087 $hbox1->pack_start( Gtk2::Label->new(_"Search : ") , ::FALSE,::FALSE,2); 4088 $hbox1->pack_start($entry, ::TRUE,::TRUE,2); 4089 $self->pack_start($hbox1, ::FALSE,::FALSE,2); 4090 $self->add($songlist); 4091 if ($opt->{buttons}) 4092 { my $hbox2=Gtk2::HBox->new; 4093 my $Bqueue=::NewIconButton('gmb-queue', _"Enqueue", sub { $songlist->EnqueueSelected; }); 4094 my $Bplay= ::NewIconButton('gtk-media-play', _"Play", sub { $songlist->PlaySelected; }); 4095 my $Bclose=::NewIconButton('gtk-close', _"Close", sub {$self->get_toplevel->close_window}); 4096 $hbox2->pack_end($_, ::FALSE,::FALSE,4) for $Bclose,$Bplay,$Bqueue; 4097 $self->pack_end($hbox2, ::FALSE,::FALSE,0); 4098 } 4099 4100 $self->{DefaultFocus}=$entry; 4101 return $self; 4102} 4103 4104sub EntryChanged_cb 4105{ my ($entry,$force)=@_; 4106 my $text=$entry->get_text; 4107 my $self=::find_ancestor($entry,__PACKAGE__); 4108 if (!$force && 2>length $text) { $self->{songlist}->Empty } 4109 else { $self->{songlist}->SetFilter( Filter->new('title:si:'.$text) ); } 4110} 4111 4112package AASearch; 4113use base 'Gtk2::Box'; 4114 4115sub new 4116{ my ($class,$opt)=@_; 4117 my $self= bless Gtk2::VBox->new, $class; 4118 my $store=Gtk2::ListStore->new(FilterList::GID_TYPE); 4119 my $treeview= $self->{treeview}= Gtk2::TreeView->new($store); 4120 $treeview->set_headers_visible(::FALSE); 4121 my $sw= ::new_scrolledwindow($treeview,'etched-in'); 4122 ::set_biscrolling($sw); 4123 my $renderer= CellRendererGID->new; 4124 $treeview->append_column( Gtk2::TreeViewColumn->new_with_attributes('', $renderer, gid=>0) ); 4125 $self->{activate}= $opt->{activate} || 'queue'; 4126 $treeview->signal_connect( row_activated => \&Activate); 4127 4128 $self->{field}= $opt->{aa} || 'artists'; 4129 $renderer->set(prop => [[$self->{field}],[1],[32],[0]], depth => 0); # (field markup=1 picsize=32 icons=0) 4130 $self->{drag_type}= Songs::FilterListProp( $self->{field}, 'drag') || ::DRAG_FILTER; 4131 ::set_drag($treeview, source => 4132 [ $self->{drag_type}, 4133 sub 4134 { my $self=::find_ancestor($_[0],__PACKAGE__); 4135 my @rows=$treeview->get_selection->get_selected_rows; 4136 my @gids=map $store->get_value($store->get_iter($_),0) , @rows; 4137 if ($self->{drag_type} != ::DRAG_FILTER) #return artist or album gids 4138 { return $self->{drag_type},@gids; 4139 } 4140 else 4141 { my @f=map Songs::MakeFilterFromGID( $self->{field}, $_ ), @gids; 4142 my $filter= Filter->newadd(::FALSE, @f); 4143 return ($filter? (::DRAG_FILTER,$filter->{string}) : undef); 4144 } 4145 }]); 4146 4147 my $hbox1=Gtk2::HBox->new; 4148 my $entry=Gtk2::Entry->new; 4149 $entry->signal_connect(changed => \&EntryChanged_cb,0); 4150 $entry->signal_connect(activate=> \&EntryChanged_cb,1); 4151 $hbox1->pack_start( Gtk2::Label->new(_"Search : ") , ::FALSE,::FALSE,2); 4152 $hbox1->pack_start($entry, ::TRUE,::TRUE,2); 4153 $self->pack_start($hbox1, ::FALSE,::FALSE,2); 4154 $self->add($sw); 4155 if ($opt->{buttons}) 4156 { my $hbox2=Gtk2::HBox->new; 4157 my $Bqueue=::NewIconButton('gmb-queue', _"Enqueue", \&Enqueue); 4158 my $Bplay= ::NewIconButton('gtk-media-play',_"Play", \&Play); 4159 my $Bclose=::NewIconButton('gtk-close', _"Close", sub {$self->get_toplevel->close_window}); 4160 $hbox2->pack_end($_, ::FALSE,::FALSE,4) for $Bclose,$Bplay,$Bqueue; 4161 $self->pack_end($hbox2, ::FALSE,::FALSE,0); 4162 } 4163 4164 $self->{DefaultFocus}=$entry; 4165 EntryChanged_cb($entry,1); 4166 return $self; 4167} 4168 4169sub GetFilter 4170{ my $self=::find_ancestor($_[0],__PACKAGE__); 4171 my $treeview=$self->{treeview}; 4172 my $path=($treeview->get_cursor)[0]; 4173 return undef unless $path; 4174 my $store=$treeview->get_model; 4175 my $gid=$store->get_value( $store->get_iter($path),0 ); 4176 return Songs::MakeFilterFromGID( $self->{field}, $gid ); 4177} 4178 4179sub EntryChanged_cb 4180{ my ($entry,$force)=@_; 4181 my $text=$entry->get_text; 4182 my $self= ::find_ancestor($entry,__PACKAGE__); 4183 my $store=$self->{treeview}->get_model; 4184 (($self->{treeview}->get_columns)[0]->get_cell_renderers)[0]->reset; 4185 $store->clear; 4186 #return if !$force && 2>length $text; 4187 my $list= AA::GrepKeys($self->{field}, $text); 4188 AA::SortKeys($self->{field},$list,'alpha'); 4189 $store->set($store->append,0,$_) for @$list; 4190} 4191 4192sub Activate 4193{ my $self=::find_ancestor($_[0],__PACKAGE__); 4194 my $filter=GetFilter($self); 4195 my $action= $self->{activate}; 4196 my $aftercmd; 4197 $aftercmd=$1 if $action=~s/&(.*)$//; 4198 ::DoActionForFilter($action,$filter); 4199 ::run_command($self,$aftercmd) if $aftercmd; 4200} 4201 4202sub Enqueue 4203{ my $filter=GetFilter($_[0]); 4204 ::DoActionForFilter('queue',$filter); 4205} 4206sub Play 4207{ my $filter=GetFilter($_[0]); 4208 ::DoActionForFilter('play',$filter); 4209} 4210 4211package CellRendererIconList; 4212use Glib::Object::Subclass 4213 'Gtk2::CellRenderer', 4214 properties => [ Glib::ParamSpec->ulong('ID','ID','Song ID', 0,2**32-1,0, [qw/readable writable/]), 4215 Glib::ParamSpec->string('field','field','field id', 'label', [qw/readable writable/]), 4216 ]; 4217 4218use constant PAD => 2; 4219 4220sub GET_SIZE 4221{ my ($cell, $widget, $cell_area) = @_; 4222 return (0,0,0,0); 4223# my $list=$cell->get('iconlist'); 4224# return (0,0,0,0) unless defined $list; 4225# my $nb=@$list; 4226# #my ($w,$h)=Gtk2::IconSize->lookup( $cell->get('stock-size') ); 4227# my ($w,$h)=Gtk2::IconSize->lookup('menu'); 4228# return (0,0, $nb*($w+PAD)+$cell->get('xpad')*2, $h+$cell->get('ypad')*2); 4229} 4230 4231sub RENDER 4232{ my ($cell, $window, $widget, $background_area, $cell_area, $expose_area, $flags) = @_; 4233 my ($field,$ID)=$cell->get(qw/field ID/); 4234 my @list=Songs::Get_icon_list($field,$ID); 4235 return unless @list; 4236 #my $size=$cell->get('stock-size'); 4237 my $size='menu'; 4238 my @pb=map $widget->render_icon($_, $size), sort @list; 4239 return unless @pb; 4240 my $state= ($flags & 'selected') ? 4241 ( $widget->has_focus ? 'selected' : 'active'): 4242 ( $widget->state eq 'insensitive' ? 'insensitive' : 'normal'); 4243 4244 my ($w,$h)=Gtk2::IconSize->lookup($size); 4245 my $room=PAD + $cell_area->height-2*$cell->get('ypad'); 4246 my $nb=int( $room / ($h+PAD) ); 4247 my $x=$cell_area->x+$cell->get('xpad'); 4248 my $y=$cell_area->y+$cell->get('ypad'); 4249 $y+=int( $cell->get('yalign') * ($room-($h+PAD)*$nb) ) if $nb>0; 4250 my $row=0; my $ystart=$y; 4251 for my $pb (@pb) 4252 { $window->draw_pixbuf( $widget->style->fg_gc($state), $pb,0,0, 4253 $x,$y,-1,-1,'none',0,0); 4254 $row++; 4255 if ($row<$nb) { $y+=PAD+$h; } 4256 else { $row=0; $y=$ystart; $x+=PAD+$w; } 4257 } 4258} 4259 4260package CellRendererGID; 4261use Glib::Object::Subclass 'Gtk2::CellRenderer', 4262properties => [ Glib::ParamSpec->long('gid', 'gid', 'group id', -2**31+1, 2**31-1, 0, [qw/readable writable/]), 4263 Glib::ParamSpec->ulong('all_count', 'all_count', 'all_count', 0, 2**32-1, 0, [qw/readable writable/]), 4264 Glib::ParamSpec->ulong('max', 'max', 'max number of songs', 0, 2**32-1, 0, [qw/readable writable/]), 4265 Glib::ParamSpec->scalar('prop', 'prop', '[[field],[markup],[picsize]]', [qw/readable writable/]), 4266 Glib::ParamSpec->scalar('hash', 'hash', 'gid to song count', [qw/readable writable/]), 4267 Glib::ParamSpec->int('depth', 'depth', 'depth', 0, 20, 0, [qw/readable writable/]), 4268 ]; 4269use constant { PAD => 2, XPAD => 2, YPAD => 2, P_FIELD => 0, P_MARKUP =>1, P_PSIZE=>2, P_ICON =>3, P_HORIZON=>4 }; 4270 4271#sub INIT_INSTANCE 4272#{ #$_[0]->set(xpad=>2,ypad=>2); #Gtk2::CellRendererText has these padding values as default 4273#} 4274sub makelayout 4275{ my ($cell,$widget)=@_; 4276 my ($prop,$gid,$depth)=$cell->get(qw/prop gid depth/); 4277 my $layout=Gtk2::Pango::Layout->new( $widget->create_pango_context ); 4278 my $field=$prop->[P_FIELD][$depth]; 4279 my $markup=$prop->[P_MARKUP][$depth]; 4280 $markup= !$markup ? "%a" : $markup eq 1 ? "<b>%a</b>%Y\n<small>%s <small>%l</small></small>" : $markup; 4281 if ($gid==FilterList::GID_ALL) 4282 { $markup= ::MarkupFormat("<b>%s (%d)</b>", Songs::Field_All_string($field), $cell->get('all_count') ); 4283 } 4284 #elsif ($gid==0) { } 4285 else { $markup=AA::ReplaceFields( $gid,$markup,$field,::TRUE ); } 4286 $layout->set_markup($markup); 4287 return $layout; 4288} 4289 4290sub GET_SIZE 4291{ my ($cell, $widget, $cell_area) = @_; 4292 my $layout=$cell->makelayout($widget); 4293 my ($w,$h)=$layout->get_pixel_size; 4294 my ($prop,$depth)=$cell->get('prop','depth'); 4295 my $s= $prop->[P_PSIZE][$depth] || $prop->[P_ICON][$depth]; 4296 if ($s == -1) {$s=$h} 4297 elsif ($h<$s) {$h=$s} 4298 my $width= $prop->[P_HORIZON] ? $w+$s+PAD+XPAD*2 : 0; 4299 return (0,0,$width,$h+YPAD*2); 4300} 4301 4302sub RENDER 4303{ my ($cell, $window, $widget, $background_area, $cell_area, $expose_area, $flags) = @_; 4304 my $x=$cell_area->x+XPAD; 4305 my $y=$cell_area->y+YPAD; 4306 my ($prop,$gid,$depth,$hash,$max)=$cell->get(qw/prop gid depth hash max/); 4307 my $iconfield= $prop->[P_ICON][$depth]; 4308 my $psize= $iconfield ? (Gtk2::IconSize->lookup('menu'))[0] : $prop->[P_PSIZE][$depth]; 4309 my $layout=$cell->makelayout($widget); 4310 my ($w,$h)=$layout->get_pixel_size; 4311 $psize=$h if $psize == -1; 4312 $w+=PAD+$psize; 4313 my $offy=0; 4314 if ($psize>$h) 4315 { $offy+=int( $cell->get('yalign')*($psize-$h) ); 4316 $h=$psize; 4317 } 4318 4319 my $state= ($flags & 'selected') ? 4320 ( $widget->has_focus ? 'selected' : 'active'): 4321 ( $widget->state eq 'insensitive' ? 'insensitive' : 'normal'); 4322 4323 if ($psize && $gid!=FilterList::GID_ALL) 4324 { my $field=$prop->[P_FIELD][$depth]; 4325 my $pixbuf= $iconfield ? $widget->render_icon(Songs::Picture($gid,$field,'icon'),'menu')||undef: #FIXME could be better 4326 AAPicture::pixbuf($field,$gid,$psize); 4327 if ($pixbuf) #pic cached -> draw now 4328 { my $offy=int(($h-$pixbuf->get_height)/2);#center pic 4329 my $offx=int(($psize-$pixbuf->get_width)/2); 4330 $window->draw_pixbuf( $widget->style->black_gc, $pixbuf,0,0, 4331 $x+$offx, $y+$offy,-1,-1,'none',0,0); 4332 } 4333 elsif (defined $pixbuf) #pic exists but not cached -> load and draw in idle 4334 { my ($tx,$ty)=$widget->widget_to_tree_coords($x,$y); 4335 $cell->{idle}||=Glib::Idle->add(\&idle,$cell); 4336 $cell->{widget}||=$widget; 4337 $cell->{window}||=$window; 4338 $cell->{queue}{$ty}=[$tx,$ty,$gid,$psize,$h,\$field]; 4339 } 4340 } 4341 4342 if ($max && !$depth && !($flags & 'selected') && $gid!=FilterList::GID_ALL) #draw histogram only works for depth==0 4343 { # if parent widget is a scrolledwindow, maxwidth use the visible width instead of the total width of the treeview 4344 my $maxwidth= $widget->parent->isa('Gtk2::ScrolledWindow') ? $widget->parent->get_hadjustment->page_size : $cell_area->width; 4345 $maxwidth-= 3*XPAD+$psize; 4346 $maxwidth=5 if $maxwidth<5; 4347 my $width= $hash->{$gid} / $max * $maxwidth; 4348 $widget->style->paint_flat_box( $window,$state,'none',$expose_area,$widget,'cell_odd_ruled', 4349 $x+$psize+PAD, $cell_area->y, $width, $cell_area->height ); 4350 } 4351 4352 # draw text 4353 $widget-> get_style-> paint_layout($window, $state, 1, 4354 $cell_area, $widget, undef, $x+$psize+PAD, $y+$offy, $layout); 4355 4356 my $field=$prop->[P_FIELD][$depth]; 4357 $field=~s/\..*//; 4358 my $has_stars= $Songs::Def{$field}{starprefix}; #FIXME shouldn't use Songs::Def directly 4359 if ($gid!=FilterList::GID_ALL && $has_stars) 4360 { if (my $pb= Songs::Stars($gid,$field)) 4361 { # FIXME center verticaly or resize ? 4362 $window->draw_pixbuf( $widget->style->black_gc, $pb,0,0, $x+XPAD+$w, $y+$offy,-1,-1,'none',0,0); 4363 } 4364 } 4365} 4366 4367sub reset 4368{ my $cell=$_[0]; 4369 delete $cell->{queue}; 4370 Glib::Source->remove( $cell->{idle} ) if $cell->{idle}; 4371 delete $cell->{idle}; 4372} 4373 4374sub idle 4375{ my $cell=$_[0]; 4376 { last unless $cell->{queue} && $cell->{widget}->mapped; 4377 my ($y,$ref)=each %{ $cell->{queue} }; 4378 last unless $ref; 4379 delete $cell->{queue}{$y}; 4380 _drawpix($cell->{widget},$cell->{window},@$ref); 4381 last unless scalar keys %{ $cell->{queue} }; 4382 return 1; 4383 } 4384 delete $cell->{queue}; 4385 delete $cell->{widget}; 4386 delete $cell->{window}; 4387 return $cell->{idle}=undef; 4388} 4389 4390sub _drawpix 4391{ my ($widget,$window,$ctx,$cty,$gid,$psize,$h,$fieldref)=@_; 4392 my ($vx,$vy,$vw,$vh)=$widget->get_visible_rect->values; 4393 #warn " $gid\n"; 4394 return if $vx > $ctx+$psize || $vy > $cty+$h || $vx+$vw < $ctx || $vy+$vh < $cty; #no longer visible 4395 #warn "DO $gid\n"; 4396 my ($x,$y)=$widget->tree_to_widget_coords($ctx,$cty); 4397 my $pixbuf= AAPicture::pixbuf($$fieldref,$gid, $psize,1); 4398 return unless $pixbuf; 4399 4400 my $offy=int( ($h-$pixbuf->get_height)/2 );#center pic 4401 my $offx=int( ($psize-$pixbuf->get_width )/2 ); 4402 $window->draw_pixbuf( $widget->style->black_gc, $pixbuf,0,0, 4403 $x+$offx, $y+$offy, -1,-1,'none',0,0); 4404} 4405 4406package CellRendererSongsAA; 4407use Glib::Object::Subclass 'Gtk2::CellRenderer', 4408properties => [ Glib::ParamSpec->scalar 4409 ('ref', #name 4410 'ref', #nickname 4411 'array : [r1,r2,row,gid]', #blurb 4412 [qw/readable writable/] #flags 4413 ), 4414 Glib::ParamSpec->string('aa','aa','use album or artist column', 'album',[qw/readable writable/]), 4415 Glib::ParamSpec->string('markup','markup','show info', '', [qw/readable writable/]), 4416 ]; 4417 4418use constant PAD => 2; 4419 4420sub GET_SIZE { (0,0,-1,-1) } 4421 4422 4423sub RENDER 4424{ my ($cell, $window, $widget, $background_area, $cell_area, $expose_area, $flags) = @_; 4425 my ($r1,$r2,$row,$gid)=@{ $cell->get('ref') }; #come from CellRendererSongsAA::get_value : first_row, last_row, this_row, gid 4426 my $field= $cell->get('aa'); 4427 my $format=$cell->get('markup'); 4428 my @format= $format ? (split /\n/,$format) : (); 4429 $format=$format[$row-$r1]; 4430 if ($format) 4431 { my ($x, $y, $width, $height)= $cell_area->values; 4432 my $gc= $widget->get_style->base_gc('normal'); 4433 $window->draw_rectangle($gc, 1, $background_area->values);# if $r1 != $r2; 4434 my $layout=Gtk2::Pango::Layout->new( $widget->create_pango_context ); 4435 my $markup=AA::ReplaceFields( $gid,$format,$field,::TRUE ); 4436 $layout->set_markup($markup); 4437 $gc= $widget->get_style->text_gc('normal'); 4438 $gc->set_clip_rectangle($cell_area); 4439 $window->draw_layout($gc, $x, $y, $layout); 4440 $gc->set_clip_rectangle(undef); 4441# $widget->get_style->paint_layout($window, $widget->state, 0, $cell_area, $widget, undef, $x, $y, $layout); 4442 return; 4443 } 4444 4445 my $gc= $widget->get_style->base_gc('normal'); 4446 $window->draw_rectangle($gc, 1, $background_area->values); 4447 my($x, $y, $width, $height)= $background_area->values; #warn "$row $x, $y, $width, $height\n"; 4448 $y-=$height*($row-$r1 - @format); 4449 $height*=1+$r2-$r1 - @format; 4450# my $ypad=$cell->get('ypad') + $background_area->height - $cell_area->height; 4451# $y+=$ypad; 4452 $x+=$cell->get('xpad'); 4453# $height-=$ypad*2; 4454 $width-=$cell->get('xpad')*2; 4455 my $s= $height > $width ? $width : $height; 4456 $s=200 if $s>200; 4457 4458 if ( my $pixbuf= AAPicture::pixbuf($field,$gid,$s) ) 4459 { my $gc=Gtk2::Gdk::GC->new($window); 4460 $gc->set_clip_rectangle($background_area); 4461 $window->draw_pixbuf( $gc, $pixbuf,0,0, $x,$y, -1,-1,'none',0,0); 4462 } 4463 elsif (defined $pixbuf) 4464 { my ($tx,$ty)=$widget->widget_to_tree_coords($x,$y);#warn "$tx,$ty <= ($x,$y)\n"; 4465 $cell->{queue}{$r1}=[$tx,$ty,$gid,$s,$field]; 4466 $cell->{idle}||=Glib::Idle->add(\&idle,$cell); 4467 $cell->{widget}||=$widget; 4468 $cell->{window}||=$window; 4469 } 4470} 4471 4472sub reset #not used FIXME should be reset when songlist change 4473{ my $cell=$_[0]; 4474 delete $cell->{queue}; 4475 Glib::Source->remove( $cell->{idle} ) if $cell->{idle}; 4476 delete $cell->{idle}; 4477} 4478 4479sub idle 4480{ my $cell=$_[0]; 4481 { last unless $cell->{queue} && $cell->{widget}->mapped; 4482 my ($r1,$ref)=each %{ $cell->{queue} }; 4483 last unless $ref; 4484 delete $cell->{queue}{$r1}; 4485 _drawpix($cell->{widget},$cell->{window},@$ref); 4486 last unless scalar keys %{ $cell->{queue} }; 4487 return 1; 4488 } 4489 delete $cell->{queue}; 4490 delete $cell->{widget}; 4491 delete $cell->{window}; 4492 return $cell->{idle}=undef; 4493} 4494 4495sub _drawpix 4496{ my ($widget,$window,$ctx,$cty,$gid,$s,$col)=@_; #warn "$ctx,$cty,$gid,$s\n"; 4497 my ($vx,$vy,$vw,$vh)=$widget->get_visible_rect->values; 4498 #warn " $gid\n"; 4499 return if $vx > $ctx+$s || $vy > $cty+$s || $vx+$vw < $ctx || $vy+$vh < $cty; #no longer visible 4500 #warn "DO $gid\n"; 4501 my ($x,$y)=$widget->tree_to_widget_coords($ctx,$cty);#warn "$ctx,$cty => ($x,$y)\n"; 4502 my $pixbuf= AAPicture::pixbuf($col,$gid, $s,1); 4503 return unless $pixbuf; 4504 $window->draw_pixbuf( Gtk2::Gdk::GC->new($window), $pixbuf,0,0, $x,$y,-1,-1,'none',0,0); 4505} 4506 4507sub get_value 4508{ my ($field,$array,$row)=@_; 4509 my $r1=my $r2=$row; 4510 my $gid=Songs::Get_gid($array->[$row],$field); 4511 $r1-- while $r1>0 && Songs::Get_gid($array->[$r1-1],$field) == $gid; #find first row with this gid 4512 $r2++ while $r2<$#$array && Songs::Get_gid($array->[$r2+1],$field) == $gid; #find last row with this gid 4513 return [$r1,$r2,$row,$gid]; 4514} 4515 4516package GMB::Cloud; 4517use base 'Gtk2::Widget'; 4518 4519use constant 4520{ XPAD => 2, YPAD => 2, 4521}; 4522 4523sub new 4524{ my ($class,$selectsub,$getdatasub,$activatesub,$menupopupsub,$displaykeysub)=@_; 4525 my $self = bless Gtk2::DrawingArea->new, $class; 4526 $self->can_focus(::TRUE); 4527 $self->signal_connect(expose_event => \&expose_cb); 4528 $self->signal_connect(focus_out_event => \&focus_change); 4529 $self->signal_connect(focus_in_event => \&focus_change); 4530 $self->signal_connect(configure_event => \&configure_cb); 4531 $self->signal_connect(drag_begin => \&drag_begin_cb); 4532 $self->signal_connect(button_press_event=> \&button_press_cb); 4533 $self->signal_connect(button_release_event=> \&button_release_cb); 4534 $self->signal_connect(key_press_event => \&key_press_cb); 4535 $self->{selectsub}=$selectsub; 4536 $self->{get_fill_data_sub}=$getdatasub; 4537 $self->{activatesub}=$activatesub; 4538 $self->{menupopupsub}=$menupopupsub; 4539 $self->{displaykeysub}=$displaykeysub; 4540 $self->{selected}={}; 4541 return $self; 4542} 4543 4544sub get_selected 4545{ sort keys %{$_[0]{selected}}; 4546} 4547sub reset_selection 4548{ $_[0]{selected}={}; 4549 $_[0]{lastclick}=undef; 4550 $_[0]{startgrow}=undef; 4551} 4552 4553sub Fill #FIXME should be called when signals ::style-set and ::direction-changed are received because I keep layout objects 4554{ my ($self)=@_; 4555 my ($list,$href)= $self->{get_fill_data_sub}($self); 4556 my $window=$self->window; 4557 my ($width,$height)=$window->get_size; 4558 4559 if ($width<2 && !$self->{delayed}) {$self->{delayed}=1;::IdleDo('2_resizecloud'.$self,100,\&Fill,$self);return} 4560 delete $self->{delayed}; 4561 delete $::ToDo{'2_resizecloud'.$self}; 4562 4563 unless (keys %$href) 4564 { $self->set_size_request(-1,-1); 4565 $self->queue_draw; 4566 $self->{lines}=[]; 4567 return; 4568 } 4569 my $filterlist= ::find_ancestor($self,'FilterList'); #FIXME should get its options another way (to keep GMB::Cloud generic) 4570 my $scalemin= ($filterlist->{cloud_min}||5) /10; 4571 my $scalemax= ($filterlist->{cloud_max}||20) /10; 4572 warn "Cloud : scalemin=$scalemin scalemax=$scalemax\n" if $::debug; 4573 $scalemax=$scalemin+.5 if $scalemin>=$scalemax; 4574 $scalemax-=$scalemin; 4575 $self->{width}=$width; 4576 my $lastkey; 4577 if ($self->{lastclick}) 4578 { my ($i,$j)=@{ delete $self->{lastclick} }; 4579 $lastkey=$self->{lines}[$i+2][$j+4]; 4580 } 4581 my @lines; 4582 $self->{lines}=\@lines; 4583 my $line=[]; 4584 my ($min,$max)=(0,1); 4585 #for (values %$href) {$max=$_ if $max<$_} 4586 for (map $href->{$_}, @$list) {$max=$_ if $max<$_} 4587 if ($min==$max) {$max++;$min--;} 4588 my ($x,$y)=(XPAD,YPAD); my ($hmax,$bmax)=(0,0); 4589 my $displaykeysub=$self->{displaykeysub}; 4590 my $inverse= ($self->get_direction eq 'rtl'); 4591 ::setlocale(::LC_NUMERIC,'C'); #for the sprintf in the loop 4592 my $pango_context= $self->create_pango_context; 4593 for my $key (@$list) 4594 { my $layout=Gtk2::Pango::Layout->new($pango_context); 4595 my $value=sprintf '%.1f', $scalemin + $scalemax*($href->{$key}-$min)/($max-$min); 4596 #$layout->set_text($key); 4597 #$layout->get_attributes->insert( Gtk2::Pango::AttrScale->new($value) ); #need recent Gtk2 4598 my $text= $displaykeysub ? $displaykeysub->($key) : $key; 4599 $layout->set_markup('<span size="'.(10240*$value).'"> '.::PangoEsc($text).'</span> '); 4600 my ($w,$h)=$layout->get_pixel_size; 4601 my $bl= $self->{baselines}{$h}||= $layout->get_iter->get_baseline / Gtk2::Pango->scale; #cache not needed for $Gtk2::VERSION>1.161 4602 if ( $x+$w+XPAD > $width ) 4603 { push @lines, $y,$y+$bmax,$line; 4604 $x=XPAD; $y+=YPAD*2+$bmax; $hmax=$bmax=0; 4605 $line=[]; 4606 } 4607 if (defined $lastkey && $lastkey eq $key) 4608 { $lastkey=undef; $self->{lastclick}=[scalar@lines,scalar@$line]; } 4609 if ($inverse) { unshift @$line, $width-$x-$w, $width-$x, $bl,$layout,$key; } 4610 else { push @$line, $x, $x+$w, $bl,$layout,$key; } 4611 #push @$line, $x,$x+$w,$bl,$layout,$key; 4612 $hmax=$h if $h>$hmax; $bmax=$bl if $bl>$bmax; 4613 $x+=$w+XPAD*2; 4614 } 4615 ::setlocale(::LC_NUMERIC,''); 4616 push @lines, $y,$y+$bmax,$line; 4617 $y+=YPAD+$bmax; 4618 $self->set_size_request(50,$y); 4619 $self->queue_draw; 4620} 4621 4622sub configure_cb 4623{ my ($self,$event)=@_; 4624 return if !$self->{width} || $self->{width} eq $event->width; 4625 ::IdleDo('2_resizecloud'.$self,500,\&Fill,$self); 4626} 4627 4628sub focus_change 4629{ my $self=$_[0]; 4630 my $sel=$self->{selected}; 4631 return unless keys %$sel; 4632 #FIXME could redraw only selected keys 4633 $self->queue_draw; 4634 0; 4635} 4636 4637sub expose_cb 4638{ my ($self,$event)=@_; 4639 my ($exp_x1,$exp_y1,$exp_x2,$exp_y2)=$event->area->values; 4640 $exp_x2+=$exp_x1; $exp_y2+=$exp_y1; 4641 my $window=$self->window; 4642 my $style=$self->get_style; 4643 #my ($width,$height)=$window->get_size; 4644 #warn "expose_cb : $width,$height\n"; 4645 my $state= $self->state eq 'insensitive' ? 'insensitive' : 'normal'; 4646 my $sstate= $self->has_focus ? 'selected' : 'active'; 4647 my $gc= $style->text_gc($state); 4648 my $bgc= $style->base_gc($state); 4649 my $sgc= $style->text_gc($sstate); 4650 my $sbgc= $style->base_gc($sstate); 4651 $window->draw_rectangle($bgc,::TRUE,$event->area->values); #clear the area with the base bg color 4652 #$style->paint_box($window,$state,'none',undef,$self,undef,$event->area->values); 4653 4654 my $lines=$self->{lines}; 4655 4656 for (my $i=0; $i<=$#$lines; $i+=3) 4657 { my ($y1,$y2,$line)=@$lines[$i,$i+1,$i+2]; 4658 next unless $y2>$exp_y1; 4659 last if $y1>$exp_y2; 4660 for (my $j=0; $j<=$#$line; $j+=5) 4661 { my ($x1,$x2,$bl,$layout,$key)=@$line[$j..$j+4]; 4662 next unless $x2>$exp_x1; 4663 last if $x1>$exp_x2; 4664 my $gc=$gc; 4665 if (exists $self->{selected}{$key}) 4666 { $window->draw_rectangle($sbgc,1,$x1-XPAD(),$y1-YPAD(),$x2-$x1+XPAD*2,$y2-$y1+YPAD*2); 4667 $gc=$sgc; 4668 } 4669 $window->draw_layout($gc,$x1,$y2-$bl,$layout); 4670 #$window->draw_rectangle($bgc,::TRUE,$x1,$y2-$bl,$x2-$x1,$h); 4671 #$style->paint_box($window,$sstate,'none',undef,$self,undef,$x1,$y2-$bl,$x2-$x1,$h) if exists $self->{selected}{$key}; 4672 #$style->paint_layout($window,(exists $self->{selected}{$key}? $sstate : $state),::FALSE,undef,$self,undef,$x1,$y2-$bl,$layout); 4673 } 4674 } 4675 if ($self->{lastclick}) #paint focus indicator 4676 {{ my ($i,$j)=@{ $self->{lastclick} }; 4677 my ($y1,$y2,$line)=@$lines[$i,$i+1,$i+2]; 4678 last unless $y2>$exp_y1; 4679 last if $y1>$exp_y2; 4680 my ($x1,$x2,$bl,undef,$key)=@$line[$j..$j+4]; 4681 last unless $x2>$exp_x1; 4682 last if $x1>$exp_x2; 4683 $style->paint_focus($window, (exists $self->{selected}{$key}? $sstate : $state), undef,$self,undef, $x1-XPAD(),$y1-YPAD(),$x2-$x1+XPAD*2,$y2-$y1+YPAD*2); 4684 }} 4685 ::TRUE; 4686} 4687 4688sub button_press_cb 4689{ my ($self,$event)=@_; 4690 $self->grab_focus; 4691 my $but=$event->button; 4692 if ($event->type eq '2button-press') 4693 { $self->{activatesub}($self,$but); 4694 return 1; 4695 } 4696 if ($but==1) 4697 { my ($i,$j,$key)=$self->coord_to_index($event->get_coords); 4698 return 0 unless defined $j; 4699 if ( $event->get_state * ['shift-mask', 'control-mask'] || !exists $self->{selected}{$key} ) 4700 { $self->key_selected($event,$i,$j);} 4701 else { $self->{pressed}=1; } 4702 return 0; 4703 } 4704 if ($but==3) 4705 { my ($i,$j,$key)=$self->coord_to_index($event->get_coords); 4706 if (defined $key && !exists $self->{selected}{$key}) 4707 { $self->key_selected($event,$i,$j); 4708 } 4709 $self->{menupopupsub}($self,undef,$event); 4710 return 1; 4711 } 4712 1; 4713} 4714sub button_release_cb 4715{ my ($self,$event)=@_; 4716 return 0 unless $event->button==1 && $self->{pressed}; 4717 $self->{pressed}=undef; 4718 my ($i,$j)=$self->coord_to_index($event->get_coords); 4719 return 0 unless defined $j; 4720 $self->key_selected($event,$i,$j); 4721 return 1; 4722} 4723sub drag_begin_cb 4724{ $_[0]->{pressed}=undef; 4725} 4726 4727sub get_cursor_row 4728{ my $self=$_[0]; 4729 return 0 unless $self->{lastclick}; 4730 my ($ci,$cj)=@{$self->{lastclick}}; 4731 my $row=0; 4732 my $lines=$self->{lines}; 4733 for (my $i=0; $i<=$#$lines; $i+=3) 4734 { my $line=$lines->[$i+2]; 4735 for (my $j=0; $j<=$#$line; $j+=5) 4736 { return $row if $i==$ci && $j==$cj; 4737 $row++; 4738 } 4739 } 4740 return 0; 4741} 4742sub set_cursor_to_row 4743{ my ($self,$row)=@_; 4744 my $lines=$self->{lines}; 4745 for (my $i=0; $i<=$#$lines; $i+=3) 4746 { my $line=$lines->[$i+2]; 4747 for (my $j=0; $j<=$#$line; $j+=5) 4748 { unless ($row--) { $self->key_selected(undef,$i,$j); return } 4749 } 4750 } 4751} 4752 4753sub select_all 4754{ my $self=shift; 4755 my $selected=$self->{selected}; 4756 my $lines=$self->{lines}; 4757 for (my $i=0; $i<=$#$lines; $i+=3) 4758 { my $line=$lines->[$i+2]; 4759 for (my $j=0; $j<=$#$line; $j+=5) 4760 { my $key=$line->[$j+4]; 4761 $selected->{$key}=undef; 4762 } 4763 } 4764 $self->queue_draw; 4765 $self->{selectsub}($self); 4766} 4767 4768sub key_selected 4769{ my ($self,$event,$i,$j)=@_; 4770 $self->scroll_to_index($i,$j); 4771 my $key=$self->{lines}[$i+2][$j+4]; 4772 my $selected=$self->{selected}; 4773 unless ($event && $event->get_state >= ['control-mask']) 4774 { %$selected=(); 4775 } 4776 if ($event && $event->get_state >= ['shift-mask'] && $self->{lastclick}) 4777 { my $start=$self->{startgrow}||=$self->{lastclick}; 4778 my ($i2,$j2)=@$start; 4779 my ($i1,$j1)=($i,$j); 4780 if ($i2<$i1 || $i2==$i1 && $j2<$j1) 4781 { ($i1,$j1,$i2,$j2)=($i2,$j2,$i1,$j1); 4782 } 4783 while ($i1 <= $i2) 4784 { my $line=$self->{lines}[$i1+2]; 4785 my $jmax= $i1==$i2 ? $j2 : $#$line; 4786 while ($j1 <= $jmax) 4787 { my $key=$line->[$j1+4]; 4788 $selected->{$key}=undef; 4789 $j1+=5; 4790 } 4791 $j1=0; 4792 $i1+=3; 4793 } 4794 } 4795 elsif (exists $selected->{$key}) 4796 { delete $selected->{$key}; 4797 delete $self->{startgrow}; 4798 } 4799 else 4800 { $selected->{$key}=undef; 4801 delete $self->{startgrow}; 4802 } 4803 $self->{lastclick}=[$i,$j]; 4804 4805 $self->queue_draw; 4806 $self->{selectsub}($self); 4807} 4808 4809sub coord_to_index 4810{ my ($self,$x,$y)=@_; 4811 my $lines=$self->{lines}; 4812 my ($i,$j); 4813 for ($i=0; $i<=$#$lines; $i+=3) 4814 { next if $y > $lines->[$i+1]+YPAD; 4815 last unless $y > $lines->[$i]-YPAD(); 4816 my $line=$lines->[$i+2]; 4817 for ($j=0; $j<=$#$line; $j+=5) 4818 { next if $x > $line->[$j+1]+XPAD; 4819 last unless $x > $line->[$j]-XPAD(); 4820 my $key=$line->[$j+4]; 4821 return ($i,$j,$key); 4822 } 4823 last; 4824 } 4825} 4826 4827sub scroll_to_index 4828{ my ($self,$i,$j)=@_; 4829 my ($y1,$y2)=@{$self->{lines}}[$i,$i+1]; 4830 $self->parent->get_vadjustment->clamp_page($y1,$y2); 4831} 4832 4833sub key_press_cb 4834{ my ($self,$event)=@_; 4835 my $key=Gtk2::Gdk->keyval_name( $event->keyval ); 4836 my $state=$event->get_state; 4837 my $ctrl= $state * ['control-mask'] && !($state * [qw/mod1-mask mod4-mask super-mask/]); #ctrl and not alt/super 4838 my $mod= $state * [qw/control-mask mod1-mask mod4-mask super-mask/]; # no modifier ctrl/alt/super 4839 my $shift=$state * ['shift-mask']; 4840 if (($key eq 'space' || $key eq 'Return') && !$mod && !$shift) 4841 { $self->{activatesub}($self,1); 4842 return 1; 4843 } 4844 elsif (lc$key eq 'a' && $ctrl) { $self->select_all; return 1; } #ctrl-a : select-all 4845 4846 my ($i,$j)=(0,0); 4847 ($i,$j)=@{$self->{lastclick}} if $self->{lastclick}; 4848 my $lines=$self->{lines}; 4849 my $jmax=@{$lines->[$i+2]}-5; 4850 my $j_check; 4851 if ($key eq 'Left') 4852 { if ($j>4) {$j-=5} 4853 elsif ($i>2) {$i-=3; $j_check=2;} 4854 } 4855 elsif ($key eq 'Right') 4856 { if ( $j < $jmax ) {$j+=5} 4857 elsif ( $i< @$lines-3 ) {$i+=3;$j=0;} 4858 } 4859 elsif ($key eq 'Up') 4860 { if ($i>2) {$i-=3; $j_check=1; } 4861 else {$j=0} 4862 } 4863 elsif ($key eq 'Down') 4864 { if ( $i< @$lines-3 ) {$i+=3; $j_check=1;} 4865 else {$j=$jmax;} 4866 } 4867 else {return 0} 4868 if ($j_check) 4869 { $jmax=@{$lines->[$i+2]}-5; 4870 $j=$jmax if $j_check==2 || $j > $jmax; 4871 } 4872 $self->key_selected($event,$i,$j); 4873 return 1; 4874} 4875 4876package GMB::Mosaic; 4877use base 'Gtk2::Widget'; 4878 4879use constant 4880{ XPAD => 2, YPAD => 2, 4881}; 4882 4883sub new 4884{ my ($class,$selectsub,$getdatasub,$activatesub,$menupopupsub,$field,$vscroll)=@_; 4885 my $self = bless Gtk2::DrawingArea->new, $class; 4886 $self->can_focus(::TRUE); 4887 $self->add_events(['pointer-motion-mask','leave-notify-mask']); 4888 $self->{vscroll}=$vscroll; 4889 $vscroll->get_adjustment->signal_connect(value_changed => \&scroll,$self); 4890 $self->signal_connect(scroll_event => \&scroll_event_cb); 4891 $self->signal_connect(expose_event => \&expose_cb); 4892 $self->signal_connect(focus_out_event => \&focus_change); 4893 $self->signal_connect(focus_in_event => \&focus_change); 4894 $self->signal_connect(configure_event => \&configure_cb); 4895 $self->signal_connect(drag_begin => \&GMB::Cloud::drag_begin_cb); 4896 $self->signal_connect(button_press_event=> \&GMB::Cloud::button_press_cb); 4897 $self->signal_connect(button_release_event=> \&GMB::Cloud::button_release_cb); 4898 $self->signal_connect(key_press_event => \&key_press_cb); 4899 $self->signal_connect(motion_notify_event=> \&start_tooltip); #FIXME use set_has_tooltip and 4900 $self->signal_connect(leave_notify_event=> \&abort_tooltip); # query_tooltip instead (requires gtk+ 2.12, Gtk2 1.160) 4901 $self->{selectsub}=$selectsub; 4902 $self->{get_fill_data_sub}=$getdatasub; 4903 $self->{activatesub}=$activatesub; 4904 $self->{menupopupsub}=$menupopupsub; 4905 $self->{field}=$field; 4906 $self->{lastdy}=0; 4907 4908 return $self; 4909} 4910 4911sub get_selected 4912{ sort keys %{$_[0]{selected}}; 4913} 4914sub reset_selection 4915{ $_[0]{selected}={}; 4916 $_[0]{lastclick}=undef; 4917 $_[0]{startgrow}=undef; 4918} 4919 4920sub Fill 4921{ my ($self,$samelist)=@_; 4922 my $window=$self->window; 4923 my ($width,$height)=$window->get_size; 4924 if ($width<2 && !$self->{delayed}) { $self->{delayed}=1; ::IdleDo('2_resizemosaic'.$self,100,\&Fill,$self);return} 4925 delete $self->{delayed}; 4926 delete $::ToDo{'2_resizemosaic'.$self}; 4927 $self->abort_queue; 4928 $self->{width}=$width; 4929 4930 my $list=$self->{list}; 4931 ($list)= $self->{get_fill_data_sub}($self) unless $samelist && $samelist eq 'samelist'; 4932 4933 my $filterlist= ::find_ancestor($self,'FilterList'); #FIXME should get its options another way 4934 my $mpsize=$filterlist->{mpicsize}||64; 4935 $self->{picsize}=$mpsize; 4936 $self->{hsize}=$mpsize; 4937 $self->{vsize}=$mpsize; 4938 4939 $self->{markup}= $self->{markup_pos}= ''; 4940 if ($filterlist->{mmarkup}) 4941 { $self->{markup_pos}= $filterlist->{mmarkup}; 4942 $self->{markup}= my $markup= $self->{field} eq 'album' ? "<small><b>%a</b></small>\n<small>%b</small>" 4943 : "<small><b>%a</b></small>\n<small>%X</small>"; 4944 my @heights; 4945 for my $m (split /\n/, $markup) 4946 { my $lay=$self->create_pango_layout($m); 4947 push @heights, ($lay->get_pixel_size)[1]; 4948 } 4949 $self->{markup_heights}=\@heights; 4950 if ($self->{markup_pos} eq 'right') 4951 { $self->{markup_width}= ::max(120,$mpsize*1.2); 4952 $self->{hsize}+=$self->{markup_width}; 4953 } 4954 else 4955 { $self->{markup_width}=$mpsize; 4956 $self->{vsize}+= 2*YPAD; 4957 $self->{vsize}+=$_ for @heights; 4958 } 4959 } 4960 4961 my $nw= int($width / ($self->{hsize}+2*XPAD)) || 1; 4962 my $nh= int(@$list/$nw); 4963 my $nwlast= @$list % $nw; 4964 $nh++ if $nwlast; 4965 $nwlast=$nw unless $nwlast; 4966 $self->{dim}=[$nw,$nh,$nwlast]; 4967 $self->{list}=$list; 4968 $self->set_size_request($self->{hsize}+2*XPAD,$self->{vsize}+2*YPAD); 4969 $self->{viewsize}[1]= $nh*($self->{vsize}+2*YPAD); 4970 $self->{viewwindowsize}=[$self->window->get_size]; 4971 $self->update_scrollbar; 4972 $self->queue_draw; 4973 $self->start_tooltip; 4974} 4975sub update_scrollbar 4976{ my $self=$_[0]; 4977 my $scroll= $self->{vscroll}; 4978 my $pagesize=$self->{viewwindowsize}[1]||0; 4979 my $upper=$self->{viewsize}[1]||0; 4980 my $adj=$scroll->get_adjustment; 4981 my $oldpos= $adj->value; 4982 my $oldupper=$adj->upper; 4983 # calculate the old position in a 0 to 1 scale 4984 $oldpos= !($oldupper && $oldpos) ? 0: # at the beginning => stay there 4985 $oldupper<=$oldpos+$adj->page_size ? 1: # at the end => stay there 4986 ($adj->page_size/2+$oldpos) / $oldupper; #base position on middle of current position 4987 $adj->page_size($pagesize); 4988 if ($upper>$pagesize) {$scroll->show; $adj->upper($upper); $scroll->queue_draw; } 4989 else {$scroll->hide; $adj->upper(0);} 4990 $adj->step_increment($pagesize*.125); 4991 $adj->page_increment($pagesize*.75); 4992 my $newval= $oldpos*$adj->upper - $adj->page_size/2; 4993 $newval=$adj->upper-$pagesize if $newval > $adj->upper-$pagesize; 4994 $newval=0 if $newval<0; 4995 $adj->set_value($newval); 4996} 4997sub scroll_event_cb 4998{ my ($self,$event,$pageinc)=@_; 4999 my $dir= ref $event ? $event->direction : $event; 5000 $dir= $dir eq 'up' ? -1 : $dir eq 'down' ? 1 : 0; 5001 return unless $dir; 5002 if ($event->state >= 'control-mask') # increase/decrease picture size 5003 { my $filterlist= ::find_ancestor($self,'FilterList'); 5004 my $size= $filterlist->{mpicsize} - 8*$dir; 5005 return if $size<16 || $size>1024; 5006 $filterlist->SetOption(mpicsize=>$size); 5007 return 1; 5008 } 5009 my $adj=$self->{vscroll}->get_adjustment; 5010 my $max= $adj->upper - $adj->page_size; 5011 my $value= $adj->value + $dir* ($pageinc? $adj->page_increment : $adj->step_increment); 5012 $value=$max if $value>$max; 5013 $value=0 if $value<0; 5014 $adj->set_value($value); 5015 1; 5016} 5017sub scroll 5018{ my ($adj,$self)=@_; 5019 my $new=int $adj->value; 5020 my $old=$self->{lastdy}; 5021 return if $new==$old; 5022 $self->{lastdy}=$new; 5023 $self->window->scroll(0,$old-$new); #copy still valid parts and queue_draw new parts 5024} 5025 5026sub show_tooltip 5027{ my $self=$_[0]; 5028 Glib::Source->remove(delete $self->{tooltip_t}) if $self->{tooltip_t}; 5029 $self->{tooltip_t}=Glib::Timeout->add(5000, \&abort_tooltip,$self); 5030 5031 my ($window,$px,$py)=Gtk2::Gdk::Display->get_default->get_window_at_pointer; 5032 return 0 unless $window && $window==$self->window; 5033 my ($i,$j,$key)=$self->coord_to_index($px,$py); 5034 return 0 unless defined $key; 5035 my $win=$self->{tooltip_w}=Gtk2::Window->new('popup'); 5036 #$win->{key}=$key; 5037 #$win->set_border_width(3); 5038 my $label=Gtk2::Label->new; 5039 $label->set_markup(AA::ReplaceFields($key,"<b>%a</b>%Y\n<small>%s <small>%l</small></small>",$self->{field},1)); 5040 my $request=$label->size_request; 5041 my ($x,$y,$w,$h)=$self->index_to_rect($i,$j); 5042 my ($rx,$ry)=$self->window->get_origin; 5043 $x+= $rx + $w/2 - $request->width/2; 5044 $y+= $ry + $h+YPAD+1; 5045 5046 my $screen=$self->get_screen; 5047 my $monitor=$screen->get_monitor_at_window($self->window); 5048 my ($x0,$y0,$xmax,$ymax)=$screen->get_monitor_geometry($monitor)->values; 5049 $xmax+= $x0-$request->width; 5050 $ymax+= $y0-$request->height; 5051 $x=$xmax if $x>$xmax; 5052 $y-=$h+$request->height if $y>$ymax; 5053 $x=$x0 if $x<$x0; 5054 $y=$y0 if $y<$y0; 5055 5056 my $frame=Gtk2::Frame->new; 5057 $frame->add($label); 5058 $win->add($frame); 5059 $win->move($x,$y); 5060 $win->show_all; 5061 return 0; 5062} 5063 5064sub start_tooltip 5065{ my ($self,$event)=@_; 5066 my $timeout= $self->{tooltip_browsemode} ? 100 : 1000; 5067 $self->abort_tooltip; 5068 $self->{tooltip_t}=Glib::Timeout->add($timeout, \&show_tooltip,$self); 5069 return 0; 5070} 5071sub abort_tooltip 5072{ my $self=$_[0]; 5073 Glib::Source->remove(delete $self->{tooltip_t}) if $self->{tooltip_t}; 5074 if ($self->{tooltip_w}) 5075 { $self->{tooltip_browsemode}=1; 5076 Glib::Source->remove($self->{tooltip_t2}) if $self->{tooltip_t2}; 5077 $self->{tooltip_t2}=Glib::Timeout->add(500, sub{$_[0]{tooltip_browsemode}=$_[0]{tooltip_t2}=0;} ,$self); 5078 $self->{tooltip_w}->destroy; 5079 } 5080 $self->{tooltip_w}=undef; 5081 0; 5082} 5083 5084sub configure_cb ## FIXME I think it redraws everything even when it's not needed 5085{ my ($self,$event)=@_; 5086 return 1 unless $self->{width}; 5087 $self->{viewwindowsize}=[$event->width,$event->height]; 5088 my $iw= $self->{hsize}+2*XPAD; 5089 if ( int($self->{width}/$iw) == int($event->width/$iw)) 5090 { $self->update_scrollbar; 5091 return 1; 5092 } 5093 $self->abort_queue; 5094 ::IdleDo('2_resizecloud'.$self,100,\&Fill,$self,'samelist'); 5095 return 1; 5096} 5097 5098sub expose_cb 5099{ my ($self,$event)=@_; 5100 my ($exp_x1,$exp_y1,$exp_x2,$exp_y2)=$event->area->values; 5101 $exp_x2+=$exp_x1; $exp_y2+=$exp_y1; 5102 my $dy=int $self->{vscroll}->get_adjustment->value; 5103 $self->start_tooltip if $self->{lastdy}!=$dy; 5104 $self->{lastdy}=$dy; 5105 my $window=$self->window; 5106 my $field=$self->{field}; 5107 my $style=$self->get_style; 5108 #my ($width,$height)=$window->get_size; 5109 #warn "expose_cb : $width,$height\n"; 5110 my $state= $self->state eq 'insensitive' ? 'insensitive' : 'normal'; 5111 my $sstate= $self->has_focus ? 'selected' : 'active'; 5112 #my $gc= $style->text_gc($state); 5113 my $bgc= $style->base_gc($state); 5114 #my $sgc= $style->text_gc($sstate); 5115 my $sbgc= $style->base_gc($sstate); 5116 $window->draw_rectangle($bgc,::TRUE,$event->area->values); #clear the area with the base bg color 5117 #$style->paint_flat_box( $window,$state,'none',$event->area,$self,'',$event->area->values); 5118 5119 return unless $self->{list}; 5120 my ($nw,$nh,$nwlast)=@{$self->{dim}}; 5121 my $list=$self->{list}; 5122 my $vsize=$self->{vsize}; 5123 my $hsize=$self->{hsize}; 5124 my $picsize=$self->{picsize}; 5125 my @markup= $self->{markup} ? (split /\n/,$self->{markup}) : (); 5126 my $markup_width= $self->{markup_width}; 5127 my $mheights= $self->{markup_heights}; 5128 my $i1=int($exp_x1/($hsize+2*XPAD)); 5129 my $i2=int($exp_x2/($hsize+2*XPAD)); 5130 my $j1=int(($dy+$exp_y1)/($vsize+2*YPAD)); 5131 my $j2=int(($dy+$exp_y2)/($vsize+2*YPAD)); 5132 $i2=$nw-1 if $i2>=$nw; 5133 $j2=$nh-1 if $j2>=$nh; 5134 for my $j ($j1..$j2) 5135 { my $y=$j*($vsize+2*YPAD)+YPAD - $dy; 5136 $i2=$nwlast-1 if $j==$nh-1; 5137 for my $i ($i1..$i2) 5138 { my $pos=$i+$j*$nw; 5139 #last if $pos>$#$list; 5140 my $key=$list->[$pos]; 5141 my $x=$i*($hsize+2*XPAD)+XPAD; 5142 my $state=$state; 5143 if (exists $self->{selected}{$key}) 5144 { $window->draw_rectangle($sbgc,1,$x-XPAD(),$y-YPAD(),$hsize+XPAD*2,$vsize+YPAD*2); 5145 $state=$sstate; 5146 #$style->paint_flat_box( $window,$state,'none',$event->area,$self,'', 5147 # $x-XPAD(),$y-YPAD(),$hsize+XPAD*2,$vsize+YPAD*2 ); 5148 } 5149 #$window->draw_rectangle($style->text_gc($state),1,$x+20,$y+20,24,24); #DEBUG 5150 my $pixbuf= AAPicture::draw($window,$x,$y,$field,$key,$picsize); 5151 if ($pixbuf) {} 5152 elsif (defined $pixbuf) 5153 { #warn "add idle\n" unless $self->{idle}; 5154 $self->{idle}||=Glib::Idle->add(\&idle,$self); 5155 $self->{window}||=$window; 5156 $self->{queue}{$i+$j*$nw}=[$x,$y+$dy,$key,$picsize]; 5157 } 5158 elsif (!@markup) # draw text in place of picture if no picture 5159 { my $layout=Gtk2::Pango::Layout->new( $self->create_pango_context ); 5160 $layout->set_markup(AA::ReplaceFields($key,"<small>%a</small>",$field,1)); 5161 $layout->set_wrap('word-char'); 5162 $layout->set_width($hsize * Gtk2::Pango->scale); 5163 $layout->set_height($vsize * Gtk2::Pango->scale); 5164 my $yoffset=0; 5165 my (undef,$logical_rect)= $layout->get_pixel_extents; 5166 my $free_height= $vsize - $logical_rect->{height}; 5167 if ($free_height>1) { $yoffset= int($free_height/2); } #center vertically 5168 $layout->set_ellipsize('end'); 5169 $layout->set_alignment('center'); 5170 $style->paint_layout($window, $state, 1, 5171 Gtk2::Gdk::Rectangle->new($x,$y,$hsize,$vsize), $self, undef, $x, $y+$yoffset, $layout); 5172 next; 5173 } 5174 my ($xm,$ym,$align)= $self->{markup_pos} eq 'right' ? ($x+$picsize+XPAD,$y,'left') : ($x,$y+$picsize+YPAD,'center'); 5175 my $i=0; 5176 for my $markup (@markup) 5177 { my $layout=Gtk2::Pango::Layout->new( $self->create_pango_context ); 5178 $layout->set_markup(AA::ReplaceFields($key,$markup,$field,1)); 5179 $layout->set_width($markup_width * Gtk2::Pango->scale); 5180 $layout->set_alignment($align); 5181 my $height= $mheights->[$i++]; 5182 $layout->set_height($height * Gtk2::Pango->scale); 5183 $layout->set_ellipsize('end'); 5184 $style->paint_layout($window, $state, 1, 5185 Gtk2::Gdk::Rectangle->new($xm,$ym,$markup_width,$height), $self, undef, $xm, $ym, $layout); 5186 $ym+=$height; 5187 } 5188 } 5189 } 5190 1; 5191} 5192 5193sub focus_change 5194{ my $self=$_[0]; 5195 $self->redraw_keys($self->{selected}); 5196 0; 5197} 5198 5199sub coord_to_index 5200{ my ($self,$x,$y)=@_; 5201 $y+=int $self->{vscroll}->get_adjustment->value; 5202 my ($nw,$nh,$nwlast)=@{$self->{dim}}; 5203 my $i=int($x/($self->{hsize}+2*XPAD)); 5204 return undef if $i>=$nw; 5205 my $j=int($y/($self->{vsize}+2*YPAD)); 5206 return undef if $j>=$nh; 5207 return undef if $j==$nh-1 && $i>=$nwlast; 5208 my $key=$self->{list}[$i+$j*$nw]; 5209 return $i,$j,$key; 5210} 5211sub index_to_rect 5212{ my ($self,$i,$j)=@_; 5213 my $x=$i*($self->{hsize}+2*XPAD)+XPAD; 5214 my $y=$j*($self->{vsize}+2*YPAD)+YPAD; 5215 $y-=int $self->{vscroll}->get_adjustment->value; 5216 return $x,$y,$self->{hsize},$self->{vsize}; 5217} 5218 5219sub redraw_keys 5220{ my ($self,$keyhash)=@_; 5221 return unless keys %$keyhash; 5222 my $hsize2=$self->{hsize}+2*XPAD; 5223 my $vsize2=$self->{vsize}+2*YPAD; 5224 my $y=int $self->{vscroll}->get_adjustment->value; 5225 my ($nw,$nh,$nwlast)=@{$self->{dim}}; 5226 my $height= $self->{viewwindowsize}[1]; 5227 my $j1=int($y/($self->{vsize}+2*YPAD)); 5228 my $j2=int(($y+$height)/($self->{vsize}+2*YPAD)); 5229 for my $j ($j1..$j2) 5230 { for my $i (0..$nw-1) 5231 { my $key=$self->{list}[$i+$j*$nw]; 5232 next unless defined $key; 5233 next unless exists $keyhash->{$key}; 5234 $self->queue_draw_area($i*$hsize2,$j*$vsize2-$y,$hsize2,$vsize2); 5235 } 5236 } 5237} 5238 5239sub key_selected 5240{ my ($self,$event,$i,$j)=@_; 5241 $self->scroll_to_row($j); 5242 my ($nw)=@{$self->{dim}}; 5243 my $list=$self->{list}; 5244 my $pos=$i+$j*$nw; 5245 my $key=$list->[$pos]; 5246 my $selected=$self->{selected}; 5247 my %changed; 5248 $changed{$_}=1 for keys %$selected; 5249 unless ($event && $event->get_state >= ['control-mask']) 5250 { %$selected=(); 5251 } 5252 if ($event && $event->get_state >= ['shift-mask'] && defined $self->{lastclick}) 5253 { $self->{startgrow}=$self->{lastclick} unless defined $self->{startgrow}; 5254 my $i1=$self->{startgrow}; 5255 my $i2=$pos; 5256 ($i1,$i2)=($i2,$i1) if $i1>$i2; 5257 $selected->{ $list->[$_] }=undef for $i1..$i2; 5258 } 5259 elsif (exists $selected->{$key}) 5260 { delete $selected->{$key}; 5261 delete $self->{startgrow}; 5262 } 5263 else 5264 { $selected->{$key}=undef; 5265 delete $self->{startgrow}; 5266 } 5267 $self->{lastclick}=$pos; 5268 $changed{$_}-- for keys %$selected; 5269 $changed{$_} or delete $changed{$_} for keys %changed; 5270 $self->redraw_keys(\%changed); 5271 $self->{selectsub}($self); 5272} 5273 5274sub get_cursor_row 5275{ my $self=$_[0]; 5276 return $self->{lastclick}||0; 5277} 5278sub set_cursor_to_row 5279{ my ($self,$row)=@_; 5280 my ($nw,$nh,$nwlast)=@{$self->{dim}}; 5281 my $i=$row % $nw; 5282 my $j=int($row/$nw); 5283 $self->key_selected(undef,$i,$j); 5284} 5285 5286sub scroll_to_row 5287{ my ($self,$j)=@_; 5288 my $y1=$j*($self->{vsize}+2*YPAD)+YPAD; 5289 my $y2=$y1+$self->{vsize}; 5290 $self->{vscroll}->get_adjustment->clamp_page($y1,$y2); 5291} 5292 5293sub key_press_cb 5294{ my ($self,$event)=@_; 5295 my $key=Gtk2::Gdk->keyval_name( $event->keyval ); 5296 my $state=$event->get_state; 5297 my $ctrl= $state * ['control-mask'] && !($state * [qw/mod1-mask mod4-mask super-mask/]); #ctrl and not alt/super 5298 my $mod= $state * [qw/control-mask mod1-mask mod4-mask super-mask/]; # no modifier ctrl/alt/super 5299 my $shift=$state * ['shift-mask']; 5300 if ( ($key eq 'space' || $key eq 'Return') && !$mod && !$shift ) 5301 { $self->{activatesub}($self,1); 5302 return 1; 5303 } 5304 my $pos=0; 5305 $pos=$self->{lastclick} if $self->{lastclick}; 5306 my ($nw,$nh,$nwlast)=@{$self->{dim}}; 5307 my $page= int($self->{vscroll}->get_adjustment->page_size / ($self->{vsize}+2*YPAD)); 5308 my $i=$pos % $nw; 5309 my $j=int($pos/$nw); 5310 if ($key eq 'Left') {$i--} 5311 elsif ($key eq 'Right') {$i++} 5312 elsif ($key eq 'Up') {$j--} 5313 elsif ($key eq 'Down') {$j++} 5314 elsif ($key eq 'Home') {$i=$j=0; } 5315 elsif ($key eq 'End') {$i=$nwlast-1; $j=$nh-1;} 5316 elsif ($key eq 'Page_Up') { $j-=$page; } 5317 elsif ($key eq 'Page_Down') { $j+=$page; } 5318 elsif (lc$key eq 'a' && $ctrl) #ctrl-a : select-all 5319 { $self->{selected}{$_}=undef for @{ $self->{list} }; $self->queue_draw; $self->{selectsub}($self); return 1; } 5320 else {return 0} 5321 if ($i<0) {$j--;$i=$nw-1;} 5322 elsif ($i>=$nw) {$j++;$i=0;} 5323 if ($j<0) {$j=0;$i=0} 5324 elsif ($j==$nh-1) {$i=$nwlast-1 if $i>=$nwlast} 5325 elsif ($j>$nh-1) {$j=$nh-1; $i=$nwlast-1 } 5326 $self->key_selected($event,$i,$j); 5327 return 1; 5328} 5329 5330sub abort_queue 5331{ my $self=$_[0]; 5332 delete $self->{queue}; 5333 Glib::Source->remove( $self->{idle} ) if $self->{idle}; 5334 delete $self->{idle}; 5335} 5336 5337sub idle 5338{ my $self=$_[0]; 5339 { last unless $self->{queue} && $self->mapped; 5340 my ($y,$ref)=each %{ $self->{queue} }; 5341 last unless $ref; 5342 delete $self->{queue}{$y}; 5343 _drawpix($self,$self->{window},@$ref); 5344 last unless scalar keys %{ $self->{queue} }; 5345 return 1; 5346 } 5347 delete $self->{queue}; 5348 delete $self->{window}; 5349 return $self->{idle}=undef; 5350} 5351 5352sub _drawpix 5353{ my ($self,$window,$x,$y,$key,$s)=@_; 5354 my $vadj=$self->{vscroll}->get_adjustment; 5355 my $dy=int $vadj->get_value; 5356 my $page=$vadj->page_size; 5357 return if $dy > $y+$s || $dy+$page < $y; #no longer visible 5358 AAPicture::draw($window,$x,$y-$dy,$self->{field},$key, $s,1); 5359} 5360 5361package GMB::ISearchBox; #interactive search box (search as you type) 5362use base 'Gtk2::Box'; 5363 5364our %OptCodes= 5365( casesens => 'i', onlybegin => 'b', onlyword => 'w', hidenomatch => 'h', 5366); 5367our @OptionsMenu= 5368( { label => _"Case-sensitive", toggleoption => 'self/casesens', code => sub { $_[0]{self}->changed; }, }, 5369 { label => _"Begin with", toggleoption => 'self/onlybegin', code => sub { $_[0]{self}{onlyword}=0; $_[0]{self}->changed;}, }, 5370 { label => _"Words that begin with", toggleoption => 'self/onlyword',code => sub { $_[0]{self}{onlybegin}=0; $_[0]{self}->changed;}, }, 5371 { label => _"Hide non-matching",toggleoption=> 'self/hidenomatch', code => sub { $_[0]{self}{close_button}->set_visible($_[0]{self}{hidenomatch}); $_[0]{self}->changed;}, test=> sub { $_[0]{self}{type} } }, 5372 { label => _"Fields", submenu => sub { return {map { $_=>Songs::FieldName($_) } Songs::StringFields}; }, submenu_reverse => 1, 5373 check => sub { $_[0]{self}{fields}; }, test => sub { !$_[0]{self}{type} }, 5374 code => sub { my $toggle=$_[1]; my $l=$_[0]{self}{fields}; my $n=@$l; @$l=grep $toggle ne $_, @$l; push @$l,$toggle if @$l==$n; @$l=('title') unless @$l; $_[0]{self}->changed; }, #toggle selected field 5375 }, 5376); 5377 5378sub new ##currently the returned widget must be put in ->{isearchbox} of a parent widget, and this parent must have the array to search in ->{array} and have the methods get_cursor_row and set_cursor_to_row. And also select_by_filter for SongList/SongTree 5379{ my ($class,$opt,$type,$nolabel)=@_; 5380 my $self=bless Gtk2::HBox->new(0,0), $class; 5381 $self->{type}=$type; 5382 5383 #restore options 5384 my $optcodes= $opt->{isearch} || ''; 5385 for my $key (keys %OptCodes) 5386 { $self->{$key}=1 if index($optcodes, $OptCodes{$key}) !=-1; 5387 } 5388 unless ($type) { $self->{fields}= [split /\|/, ($opt->{isearchfields} || 'title')]; } 5389 5390 $self->{entry}=my $entry=Gtk2::Entry->new; 5391 $entry->signal_connect( changed => \&changed ); 5392 $entry->signal_connect(key_press_event => \&key_press_event_cb); 5393 my $select=::NewIconButton('gtk-index', undef, \&select,'none',_"Select matches"); 5394 my $next=::NewIconButton('gtk-go-down', ($nolabel ? undef : _"Next"), \&button_cb,'none'); 5395 my $prev=::NewIconButton('gtk-go-up', ($nolabel ? undef : _"Previous"),\&button_cb,'none'); 5396 $prev->{is_previous}=1; 5397 my $close= $self->{close_button}= ::NewIconButton('gtk-close', undef, \&close,'none'); 5398 my $label=Gtk2::Label->new(_"Find :"); 5399 my $options=Gtk2::Button->new; 5400 $options->add(Gtk2::Image->new_from_stock('gtk-preferences','menu')); 5401 $options->signal_connect( button_press_event => \&PopupOpt ); 5402 $options->set_relief('none'); 5403 $options->set_tooltip_text(_"options"); 5404 5405 $self->pack_start($close,0,0,0); 5406 $self->pack_start($label,0,0,2) unless $nolabel; 5407 $self->add($entry); 5408 #$_->set_focus_on_click(0) for $prev,$next,$options; 5409 $self->pack_start($_,0,0,0) for $prev,$next; 5410 $self->pack_start($select,0,0,0) unless $self->{type}; 5411 $self->pack_end($options,0,0,0); 5412 $self->show_all; 5413 $self->set_no_show_all(1); 5414 $self->hide; 5415 $close->set_no_show_all(1); 5416 $close->hide unless $self->{hidenomatch}; 5417 5418 return $self; 5419} 5420 5421sub SaveOptions 5422{ my $self=$_[0]; 5423 my $opt=join '', map $OptCodes{$_}, grep $self->{$_}, sort keys %OptCodes; 5424 my @opt; 5425 push @opt, isearch => $opt if $opt ne ''; 5426 unless ($self->{type}) { push @opt, isearchfields => join '|',@{$self->{fields}}; } 5427 return @opt; 5428} 5429 5430sub set_colors 5431{ my ($self,$mode)=@_; #mode : -1 not found, 0 : neutral, 1 : found 5432 my $entry=$self->{entry}; 5433 if (*Gtk2::Entry::set_icon_from_stock{CODE}) # requires gtk>=2.16 && perl-Gtk2 version >=1.211 5434 { $entry->set_progress_fraction( $mode<1 ? 0 : 1 ); 5435 } 5436 elsif ($mode<1) 5437 { $entry->modify_base('normal', undef ); 5438 $entry->modify_text('normal', undef ); 5439 } 5440 else 5441 { $entry->modify_base('normal', $entry->style->bg('selected') ); 5442 $entry->modify_text('normal', $entry->style->text('selected') ); 5443 } 5444 5445} 5446 5447sub key_press_event_cb # hide with Escape 5448{ my ($entry,$event)=@_; 5449 return 0 unless Gtk2::Gdk->keyval_name($event->keyval) eq 'Escape'; 5450 my $self= ::find_ancestor($entry,__PACKAGE__); 5451 my $newfocus= $self->get_parent; 5452 $newfocus= $newfocus->{DefaultFocus} while $newfocus->{DefaultFocus}; 5453 $newfocus->grab_focus; 5454 $self->close; 5455 return 1; 5456} 5457sub close 5458{ my $self= ::find_ancestor($_[0],__PACKAGE__); 5459 if ($self->{hidenomatch}) { $self->{entry}->set_text(''); $self->hide; } 5460} 5461 5462sub parent_has_focus 5463{ my $self=shift; 5464 $self->hide unless length($self->{entry}->get_text) && $self->{hidenomatch}; 5465} 5466 5467sub begin 5468{ my ($self,$text)=@_; 5469 $self->show; 5470 my $entry=$self->{entry}; 5471 $entry->grab_focus; 5472 if (defined $text) 5473 { $entry->set_text($text); 5474 $entry->set_position(-1); 5475 } 5476 else { $self->set_colors(0); } 5477} 5478 5479sub changed 5480{ my $self=::find_ancestor($_[0],__PACKAGE__); 5481 $self->{searchsub}=undef; 5482 my $entry=$self->{entry}; 5483 my $text=$entry->get_text; 5484 if ($text eq '' && !$self->{hidenomatch}) 5485 { $self->set_colors(0); 5486 return; 5487 } 5488 $text=::superlc($text) unless $self->{casesens}; 5489 my $re= $self->{onlybegin} ? '^' : $self->{onlyword} ? '\b' : ''; 5490 $re.= quotemeta $text; 5491 my $type= $self->{type}; 5492 if (!$type) #search song IDs 5493 { my $fields= $self->{fields}; 5494 my $filter= $self->{casesens} ? ':m:' : ':mi:'; 5495 $filter.=$re; 5496 $filter=Filter->newadd(::FALSE,map $_.$filter, @$fields); 5497 my $code=$filter->singlesong_code(); 5498 $self->{filter}=$filter; 5499 $self->{searchsub}= eval 'sub { my $array=$_[0]; my $rows=$_[1]; for my $row (@$rows) { local $_=$array->[$row]; return $row if '.$code.'; } return undef; }'; 5500 #$self->{searchsub}= eval "sub { local \$_=\$_[0]; return $code }"; 5501 } 5502 elsif ($self->{hidenomatch}) 5503 { $self->get_parent->set_text_search($re,1,$self->{casesens}); 5504 } 5505 else # #search gid of type $type 5506 { my $code= Songs::Code($type,'gid_to_display', GID => '$array->[$row]'); 5507 $re= $self->{casesens} ? qr/$re/ : qr/$re/i; 5508 $self->{searchsub}= eval 'sub { my $array=$_[0]; my $rows=$_[1]; for my $row (@$rows) { return $row if ::superlc('.$code.')=~m/$re/; } return undef; }'; 5509 } 5510 if ($@) { warn "Error compiling search code : $@\n"; $self->{searchsub}=undef; } 5511 $self->search(0); 5512} 5513 5514sub select 5515{ my $widget=$_[0]; 5516 my $self=::find_ancestor($widget,__PACKAGE__); 5517 my $parent= $self->get_parent; 5518 $parent->select_by_filter($self->{filter}) if $self->{filter}; 5519} 5520sub button_cb 5521{ my $widget=$_[0]; 5522 my $self=::find_ancestor($widget,__PACKAGE__); 5523 my $dir= $widget->{is_previous} ? -1 : 1; 5524 $self->search($dir); 5525} 5526sub search 5527{ my ($self,$direction)=@_; 5528 my $search=$self->{searchsub}; 5529 return unless $search; 5530 my $parent= $self->get_parent; 5531 my $array= $parent->{array}; #FIXME could be better 5532 return unless @$array; 5533 my $offset=$parent->{array_offset}||0; 5534 my $start= $parent->get_cursor_row; 5535 $start-= $offset; 5536 my @rows= ($start..$#$array, 0..$start-1); 5537 shift @rows if $direction; 5538 @rows=reverse @rows if $direction<0; 5539 my $found=$search->($array,\@rows); 5540 if (defined $found) 5541 { $parent->set_cursor_to_row($found+$offset); 5542 $self->set_colors(1); 5543 } 5544 else { $self->set_colors(-1); } 5545} 5546 5547sub get_parent 5548{ my $parent=shift; 5549 $parent=$parent->parent until $parent->{isearchbox}; #FIXME could be better, maybe pass a package name to new and use ::find_ancestor($self,$self->{targetpackage}); 5550 return $parent; 5551} 5552 5553sub PopupOpt 5554{ my $self=::find_ancestor($_[0],__PACKAGE__); 5555 ::PopupContextMenu(\@OptionsMenu, { self=>$self, usemenupos => 1,} ); 5556 return 1; 5557} 5558 5559package SongTree::ViewVBox; 5560use Glib::Object::Subclass 5561Gtk2::VBox::, 5562 signals => { 5563 set_scroll_adjustments => { 5564 class_closure => sub {}, 5565 flags => [qw(run-last action)], 5566 return_type => undef, 5567 param_types => [Gtk2::Adjustment::,Gtk2::Adjustment::], 5568 }, 5569 }, 5570 #properties => [Glib::ParamSpec->object ('hadjustment','hadj','', Gtk2::Adjustment::, [qw/readable writable construct/] ), 5571 # Glib::ParamSpec->object ('vadjustment','vadj','', Gtk2::Adjustment::, [qw/readable writable construct/] )], 5572 ; 5573 5574 5575package SongTree; 5576use base 'Gtk2::Box'; 5577our @ISA; 5578our %STC; 5579INIT { unshift @ISA, 'SongList::Common'; } 5580 5581sub init_textcolumns #FIXME support calling it multiple times => remove columns for removed fields, update added columns ? 5582{ for my $key (Songs::ColumnsKeys()) 5583 { my $align= Songs::ColumnAlign($key) ? ',x=-text:w' : ''; #right-align if Songs::ColumnAlign($key) 5584 $STC{$key}= 5585 { title => Songs::FieldName($key), 5586 sort => Songs::SortField($key), 5587 width => Songs::FieldWidth($key), 5588 #elems => ['text=text(text=$'.$id.')'], 5589 elems => ['text=text(markup=playmarkup(pesc($'.$key."))$align)"], 5590 songbl =>'text', 5591 hreq => 'text:h', 5592 }; 5593 } 5594} 5595 5596our %GroupSkin; 5597#=( default => { head => 'title:h', 5598# vcollapse =>'head', 5599# elems => 5600# [ 'title=text(markup=\'<b><big>\'.pesc($title).\'</big></b>\',pad=4)', 5601# ], 5602# }, 5603# ); 5604 5605our @DefaultOptions= 5606( headclick => 'collapse', # 'select' 5607 # FIXME could try to get SongTree style GtkTreeView::horizontal-separator and others as default values 5608 songxpad => 4, # space between columns 5609 songypad => 4, # space between rows 5610 headers => 'on', 5611 no_typeahead => 0, 5612 cols => 'playandqueue title artist album year length track file lastplay playcount rating', 5613); 5614 5615sub new 5616{ my ($class,$opt)=@_; 5617 my $self = bless Gtk2::HBox->new(0,0), $class; 5618 #my $self = bless Gtk2::Frame->new, $class; 5619 #$self->set_shadow_type('etched-in'); 5620 #my $frame=Gtk2::Frame->new;# $frame->set_shadow_type('etched-in'); 5621 5622 #use default options for this songlist type 5623 my $name= 'songtree_'.$opt->{name}; $name=~s/\d+$//; 5624 my $default= $::Options{"DefaultOptions_$name"} || {}; 5625 5626 %$opt=( @DefaultOptions, %$default, %$opt ); 5627 $self->{$_}=$opt->{$_} for qw/headclick songxpad songypad no_typeahead grouping/; 5628 5629 #create widgets used to draw the songtree as a treeview, would be nice to do without but it's not possible currently 5630 $self->{stylewidget}=Gtk2::TreeView->new; 5631 $self->{stylewparent}=Gtk2::VBox->new; $self->{stylewparent}->add($self->{stylewidget}); #some style engines (gtk-qt) call ->parent on the parent => Gtk-CRITICAL messages if stylewidget doesn't have a parent. And needs to hold a reference to it or bad things happen 5632 for my $i (1,2,3) 5633 { my $column=Gtk2::TreeViewColumn->new; 5634 my $label=Gtk2::Label->new; 5635 $column->set_widget($label); 5636 $self->{stylewidget}->append_column($column); 5637 my $button=::find_ancestor($label,'Gtk2::Button'); 5638 $self->{'stylewidget_header'.$i}=$button; #must be a button which has a treeview for parent 5639 #$button->remove($button->child); #don't need the child 5640 } 5641 5642 $self->{isearchbox}=GMB::ISearchBox->new($opt); 5643 my $view=$self->{view}=Gtk2::DrawingArea->new; 5644 my $vbox=SongTree::ViewVBox->new; 5645 my $sw= ::new_scrolledwindow($vbox,'etched-in'); 5646 ::set_biscrolling($sw); 5647 $self->CommonInit($opt); 5648 5649 $self->add($sw); 5650 $self->{headers}=SongTree::Headers->new($sw->get_hadjustment) unless $opt->{headers} eq 'off'; 5651 $self->{vadj}=$sw->get_vadjustment; 5652 $self->{hadj}=$sw->get_hadjustment; 5653 $vbox->pack_start($self->{headers},0,0,0) if $self->{headers}; 5654 $vbox->pack_end($self->{isearchbox},0,0,0); 5655 $vbox->add($view); 5656 $view->can_focus(::TRUE); 5657 $self->{DefaultFocus}=$view; 5658 $self->{$_}->signal_connect(value_changed => sub {$self->has_scrolled($_[1])},$_) for qw/hadj vadj/; 5659 $self->signal_connect(scroll_event => \&scroll_event_cb); 5660 $self->signal_connect(key_press_event => \&key_press_cb); 5661 $self->signal_connect(destroy => \&destroy_cb); 5662 $view->signal_connect(expose_event => \&expose_cb); 5663 $view->signal_connect(focus_in_event => sub { my $self=::find_ancestor($_[0],__PACKAGE__); $self->{isearchbox}->parent_has_focus; 0; }); 5664 $view->signal_connect(focus_in_event => \&focus_change); 5665 $view->signal_connect(focus_out_event => \&focus_change); 5666 $view->signal_connect(configure_event => \&configure_cb); 5667 $view->signal_connect(drag_begin => \&drag_begin_cb); 5668 $view->signal_connect(drag_leave => \&drag_leave_cb); 5669 $view->signal_connect(button_press_event=> \&button_press_cb); 5670 $view->signal_connect(button_release_event=> \&button_release_cb); 5671 $view->signal_connect(query_tooltip=> \&query_tooltip_cb) if *Gtk2::Widget::set_has_tooltip{CODE}; # requires gtk+ 2.12, Gtk2 1.160 5672 $self->SetRowTip($opt->{rowtip}); 5673 5674 ::Watch($self, CurSongID => \&CurSongChanged); 5675 ::Watch($self, SongArray => \&SongArray_changed_cb); 5676 ::Watch($self, SongsChanged => \&SongsChanged_cb); 5677 5678 ::set_drag($view, 5679 source=>[::DRAG_ID,sub { my $view=$_[0]; my $self=::find_ancestor($view,__PACKAGE__); return ::DRAG_ID,$self->GetSelectedIDs; }], 5680 dest => [::DRAG_ID,::DRAG_FILE,\&drag_received_cb], 5681 motion=>\&drag_motion_cb, 5682 ); 5683 5684 #$self->{grouping}='album|pic' unless defined $self->{grouping}; 5685 $self->{grouping}= ($self->{type}=~m/[QLA]/ ? '' : 'album|pic') unless defined $self->{grouping}; 5686 5687 $self->AddColumn($_) for split / +/,$opt->{cols}; 5688 unless ($self->{cells}) { $self->AddColumn('title'); } #to ensure there is at least 1 column 5689 5690 $self->{selected}=''; 5691 $self->{lastclick}=$self->{startgrow}=-1; 5692 $self->set_head_columns; 5693 return $self; 5694} 5695 5696sub destroy_cb 5697{ my $self=$_[0]; 5698 delete $self->{$_} for keys %$self;#it's important to delete $self->{queue} to destroy references cycles, better delete all keys to be sure 5699} 5700 5701sub SaveOptions 5702{ my $self=shift; 5703 my %opt=( $self->{isearchbox}->SaveOptions ); 5704 $opt{$_}=$self->{$_} for qw/grouping/; 5705 $opt{cols}= join ' ', map $_->{colid}, @{$self->{cells}}; 5706 #save cols width 5707 $opt{colwidth}= join ' ',map $_.' '.$self->{colwidth}{$_}, sort keys %{$self->{colwidth}}; 5708 #warn "$_ $opt{$_}\n" for sort keys %opt; 5709 return \%opt; 5710} 5711 5712sub AddColumn 5713{ my ($self,$colid,$pos)=@_; 5714 return unless $STC{$colid}; 5715 my $cells=$self->{cells}||=[]; 5716 $pos=@{$self->{cells}} unless defined $pos; 5717 my $width= $self->{colwidth}{$colid} || $STC{$colid}{width} || 50; 5718 splice @$cells, $pos,0,GMB::Cell->new_songcol($colid,$width); 5719 $self->{cols_changed}=1; 5720 $self->update_columns if $self->{ready}; 5721} 5722sub remove_column 5723{ my ($self,$cellnb)=@_; 5724 my $cell= $self->{cells}[$cellnb]; 5725 splice @{$self->{cells}}, $cellnb, 1; 5726 $self->{cols_changed}=1; 5727 unless (@{$self->{cells}}) { $self->AddColumn('title'); } #to ensure there is at least 1 column 5728 $self->update_columns if $self->{ready}; 5729} 5730sub update_columns 5731{ my ($self,$nosavepos)=@_; 5732 my $savedpos; 5733 my $songswidth=0; 5734 if (my $ew=$self->{events_to_watch}) {::UnWatch($self->{view},$_) for keys %$ew;} 5735 delete $self->{events_to_watch}; 5736 my $baseline=0; 5737 my $vsizesong=$self->{vsizesong}; 5738 $vsizesong=GMB::Cell::init_songs($self,$self->{cells},$self->{songxpad},$self->{songypad}) if $self->{cols_changed}; 5739 $self->{cols_changed}=undef; 5740 my %fields_to_watch; 5741 for my $cell (@{ $self->{cells} }) 5742 { my $colid=$cell->{colid}; 5743 $cell->{width}= $self->{colwidth}{$colid} || $STC{$colid}{width} unless exists $cell->{width}; 5744 delete $cell->{last}; 5745 if (my $watch=$cell->{event}) { $self->{events_to_watch}{$_}=undef for @$watch; } 5746 if (my $fields=$cell->{watchfields}) { $fields_to_watch{$_}=undef for @$fields; } 5747 $songswidth+=$cell->{width}; 5748 } 5749 $self->{cells}[-1]{last}=1; #this column gets the extra width 5750 $self->{songswidth}=$songswidth; 5751 if (!$self->{vsizesong} || $self->{vsizesong}!=$vsizesong) #if height of rows has changed 5752 { $self->{vsizesong}=$vsizesong; 5753 #warn "new vsizesong : $vsizesong\n"; 5754 $savedpos=$self->coord_to_path(0,int($self->{vadj}->page_size/2)) unless $nosavepos; 5755 $self->compute_height if $self->{ready}; 5756 } 5757 my $w= $songswidth; 5758 for my $cell (reverse @{$self->{headcells}} ) 5759 { $w+= $cell->{left} + $cell->{right}; 5760 $cell->{width}=$w; 5761 if (my $watch=$cell->{event}) { $self->{events_to_watch}{$_}=undef for @$watch; } 5762 if (my $fields=$cell->{watchfields}) { $fields_to_watch{$_}=undef for @$fields; } 5763 } 5764 $self->{viewsize}[0]= $w; 5765 5766 $self->{fields_to_watch2}=[keys %fields_to_watch]; #FIXME could call Songs::Depends only once now rather than multiple times for each column in GMB::Expression::parse ???? 5767 if (my $ew=$self->{events_to_watch}) {::Watch($self->{view},$_,sub {$_[0]->queue_draw;}) for keys %$ew;} 5768 5769 $self->updateextrawidth(0); 5770 $self->scroll_to_row($savedpos->{hirow}||0,1) if $savedpos; 5771 $self->update_scrollbar; 5772 delete $self->{queue}; 5773 $self->{view}->queue_draw; 5774 $self->update_sorted_column; 5775 $self->{headers}->update if $self->{headers}; 5776} 5777sub set_head_columns 5778{ my ($self,$grouping)=@_; 5779 $grouping=$self->{grouping} unless defined $grouping; 5780 $self->{grouping}=$grouping; 5781 my @cols= $grouping=~m#([^|]+\|[^|]+)(?:\||$)#g; #split into pairs : "word|word" 5782 my $savedpos= $self->coord_to_path(0,int($self->{vadj}->page_size/2)) if $self->{ready}; #save vertical pos 5783 $self->{headcells}=[]; 5784 $self->{colgroup}=[]; 5785 $self->{songxoffset}=0; 5786 $self->{songxright}=0; 5787 my $depth=0; 5788 my @fields; 5789 if (@cols) 5790 { for my $colskin (@cols) 5791 { my ($col,$skin)=split /\|/,$colskin; 5792 next unless $col; 5793 push @fields,$col; 5794 my $cell= GMB::Cell->new_group( $self,$depth,$col,$skin ); 5795 #$cell->{skin}=$skin; 5796 $cell->{x}=$self->{songxoffset}; 5797 $self->{songxoffset}+=$cell->{left}; 5798 $self->{songxright}+=$cell->{right}; 5799 #$cell->{width}=$col->{width}; #FIXME use saved width ? 5800 push @{$self->{colgroup}}, $col; 5801 push @{$self->{headcells}}, $cell; 5802 $depth++; 5803 } 5804 } 5805 else 5806 { $self->{headcells}[0]{$_}=0 for qw/left right x head tail vmin/; 5807 } 5808 $self->{fields_to_watch1}=[Songs::Depends(@fields)]; 5809 5810 $self->update_columns(1); 5811 $self->BuildTree unless $self->{need_init}; 5812 $self->scroll_to_row($savedpos->{hirow}||0,1) if $savedpos; 5813} 5814 5815sub set_has_tooltip { $_[0]{view}->set_has_tooltip($_[1]) } 5816 5817sub GetCurrentRow 5818{ my $self=shift; 5819 my $row=$self->{lastclick}; 5820 return $row; 5821} 5822 5823sub GetSelectedRows 5824{ my $self=$_[0]; 5825 my $songarray=$self->{array}; 5826 return [grep vec($self->{selected},$_,1), 0..$#$songarray]; 5827} 5828 5829sub focus_change 5830{ my $view=$_[0]; 5831 #my $sel=$self->{selected}; 5832 #return unless keys %$sel; 5833 #FIXME could redraw only selected rows 5834 $view->queue_draw; 5835 1; 5836} 5837 5838sub buildexpstate 5839{ my $self=$_[0]; 5840#my $time=times; #DEBUG 5841 my @exp; 5842 my $maxdepth=$#{ $self->{headcells} }; 5843 for my $depth (0..$maxdepth) 5844 { my $string=''; 5845 my $expanded= $self->{TREE}{expanded}[$depth]; 5846 my $lastrows= $self->{TREE}{lastrows}[$depth]; 5847 my $firstrow=-1; 5848 for my $i (1..$#$lastrows) 5849 { my $lastrow=$lastrows->[$i]; 5850 $string.= $expanded->[$i]x($lastrow-$firstrow); 5851 $firstrow=$lastrow; 5852 } 5853 push @exp,$string;#warn $string; 5854 } 5855 $self->{new_expand_state}=\@exp; 5856#warn 'buildexpstate '.(times-$time)."s\n"; #DEBUG 5857 return \@exp; 5858} 5859 5860sub BuildTree 5861{ my $self=shift; 5862#my $time=times; 5863 my $expstate=delete $self->{new_expand_state}; 5864 my $list=$self->{array}; 5865 return unless $list; 5866 5867 my $colgroup=$self->{colgroup}; 5868 delete $self->{queue}; 5869 5870 my $vsizesong=$self->{vsizesong}; 5871 5872 my $maxdepth=$#$colgroup; 5873 #warn "Building Tree\n"; 5874 my $defaultexp=1; 5875 $self->{TREE}{lastrows}=$self->{TREE}{expanded}=undef; 5876 for my $depth (0..$maxdepth) 5877 { my $col= $colgroup->[$depth]; 5878 #my $func= Songs::GroupSub($col); 5879 my $lastrows_parent= $depth==0 ? [-1,$#$list] : $self->{TREE}{lastrows}[$depth-1]; 5880 my ($lastrows,$lastchild)= @$list ? Songs::GroupSub($col)->($list,$lastrows_parent) : ([-1],[0]); 5881 #my @lastrows;my @lastchild; 5882 #my $firstrow=0; 5883 #for my $lastrow (@$lastrows_parent) 5884 #{ push @lastrows, map $_-1, @{ $func->($list,$firstrow,$lastrow) } if $lastrow-$firstrow>1; 5885 # push @lastrows, $lastrow; 5886 # push @lastchild, $#lastrows; 5887 # $firstrow=$lastrow+1; 5888 #} 5889 #$self->{TREE}{lastrows}[$depth]=\@lastrows; 5890 #$self->{TREE}{lastchild}[$depth-1]=\@lastchild if $depth>=1; 5891 $self->{TREE}{lastrows}[$depth]=$lastrows; 5892 $self->{TREE}{lastchild}[$depth-1]=$lastchild if $depth>=1; 5893 my $exp; 5894 if (!$expstate) { $exp=[($defaultexp)x@$lastrows]; } 5895 else 5896 { $exp=shift @$expstate; 5897 $exp= [0,map substr($exp,$lastrows->[$_]+1,1), 0..$#$lastrows-1]; 5898 } 5899 $self->{TREE}{expanded}[$depth]=$exp; 5900 } 5901 $self->{TREE}{expanded}[0]||=[0,1]; 5902 $self->{TREE}{lastrows}[0]||=[-1,$#$list]; 5903#warn 'BuildTree 1st part '.(times-$time)."s\n"; 5904 $self->compute_height; 5905 #$self->{viewsize}[1]= $height; 5906 #$self->update_scrollbar; 5907 #$self->{view}->queue_draw; 5908 $self->{ready}=1; 5909#warn 'BuildTree total '.(times-$time)."s\n"; 5910} 5911 5912sub update_scrollbar 5913{ my $self=$_[0]; 5914 for my $i (0,1) 5915 { my $adj= $self->{ (qw/hadj vadj/)[$i] }; 5916 my $pagesize= $self->{viewwindowsize}[$i] ||0; 5917 my $upper= $self->{viewsize}[$i] ||0; 5918 $adj->page_size($pagesize); 5919 $adj->upper($upper); 5920 $adj->step_increment($pagesize*.125); 5921 $adj->page_increment($pagesize*.75); 5922 if ($adj->value > $adj->upper-$pagesize) {$adj->set_value($adj->upper-$pagesize);} 5923 $adj->changed; 5924 } 5925} 5926sub has_scrolled 5927{ my ($self,$adj)=@_; 5928 delete $self->{queue}; 5929 delete $self->{action_rectangles}; 5930 $self->{view}->queue_draw;# FIXME replace by something like $self->{view}->window->scroll($xold-$xnew,$yold-$ynew); (must be integers), will need to clean up $self->{action_rectangles} 5931} 5932 5933sub configure_cb 5934{ my ($view,$event)=@_; 5935 my $self=::find_ancestor($view,__PACKAGE__); 5936 $self->{viewwindowsize}=[$event->width,$event->height]; 5937 $self->updateextrawidth; 5938 $self->update_scrollbar; 5939 1; 5940} 5941sub updateextrawidth 5942{ my ($self,$old)=@_; 5943 $old=$self->{extra} unless defined $old; 5944 my $extra= ($self->{viewwindowsize}[0]||0) - $self->{viewsize}[0]; 5945 $extra=0 if $extra<0; 5946 my $diff= $extra - $old; 5947 $_->{width}+=$diff for @{$self->{headcells}}; 5948 $self->{songswidth}+=$diff; 5949 $self->{extra}=$extra; 5950} 5951 5952sub SongsChanged_cb 5953{ my ($self,$IDs,$fields)=@_; 5954 return if $IDs && !@{ $self->{array}->AreIn($IDs) }; #ignore changes to songs not in the list 5955 if ( ::OneInCommon($fields,$self->{fields_to_watch1}) ) #changes include a field used to group songs => rebuild 5956 { $self->buildexpstate; #save expanded state for each song 5957 $self->BuildTree; 5958 } 5959 elsif ( ::OneInCommon($fields,$self->{fields_to_watch2}) ) 5960 { $self->{view}->queue_draw; #could redraw only affected visible rows, but probably not worth it => so just redraw everything 5961 } 5962} 5963 5964sub SongArray_changed_cb 5965{ my ($self,$songarray,$action,@extra)=@_; 5966 #if ($self->{mode} eq 'playlist' && $songarray==$::ListPlay) 5967 #{ $self->{array}->Mirror($songarray,$action,@extra); 5968 #} 5969 return unless $self->{array}==$songarray; 5970 #warn "SongArray_changed $action,@extra\n"; 5971 my $center; 5972 my $selected=\$self->{selected}; 5973 if ($action eq 'sort') 5974 { my ($sort,$oldarray)=@extra; 5975 $self->{'sort'}=$sort; 5976 my @selected=grep vec($$selected,$_,1), 0..$#$songarray; 5977 my @order; 5978 $order[ $songarray->[$_] ]=$_ for reverse 0..$#$songarray; #reverse so that in case of duplicates ID, $order[$ID] is the first row with this $ID 5979 my @IDs= map $oldarray->[$_], @selected; 5980 @selected= map $order[$_]++, @IDs; # $order->[$ID]++ so that in case of duplicates ID, the next row (with same $ID) are used 5981 $self->update_sorted_column; 5982 $self->{headers}->update if $self->{headers}; #to update sort indicator 5983 $$selected=''; vec($$selected,$_,1)=1 for @selected; 5984 $self->{new_expand_state}=0; 5985 $self->{lastclick}=$self->{startgrow}=-1; 5986 $center=1; 5987 } 5988 elsif ($action eq 'update') #should only happen when in filter mode, so no duplicates IDs 5989 { my $oldarray=$extra[0]; 5990 # translate selection to new order : 5991 my @selected; 5992 $selected[$oldarray->[$_]]=vec($$selected,$_,1) for 0..$#$oldarray; 5993 $$selected=''; 5994 vec($$selected,$_,1)=1 for grep $selected[$songarray->[$_]], 0..$#$songarray; 5995 5996 #translate expstate to new order : 5997 my @newexp; 5998 for my $string (@{ $self->buildexpstate }) 5999 { my @exp; 6000 $exp[$oldarray->[$_]]=substr($string,$_,1) for 0..$#$oldarray; 6001 my $new=''; 6002 $new.= defined($_) ? $_ : 1 for map $exp[$_], @$songarray; 6003 push @newexp, $new; 6004 } 6005 $self->{new_expand_state}=\@newexp; 6006 $self->{lastclick}=$self->{startgrow}=-1; 6007 } 6008 elsif ($action eq 'insert') 6009 { my ($destrow,$IDs)=@extra; 6010 vec($$selected,$#$songarray,1)||=0; #make sure $$selected has a value for every row 6011 my $string=unpack 'b*',$$selected; 6012 substr($string,$destrow,0,'0'x@$IDs); 6013 $$selected=pack 'b*',$string; 6014 my $exp=$self->buildexpstate; 6015 substr($_,$destrow,0,'1'x@$IDs) for @$exp; 6016 $_>=$destrow and $_+=@$IDs for $self->{lastclick}, $self->{startgrow}; 6017 } 6018 elsif ($action eq 'move') 6019 { my (undef,$rows,$destrow)=@extra; 6020 vec($$selected,$#$songarray,1)||=0; 6021 my $string=unpack 'b*',$$selected; 6022 for my $s ($string,@{ $self->buildexpstate }) 6023 { my $toinsert=''; 6024 $toinsert.=substr($s,$_,1,'') for reverse @$rows; 6025 substr($s,$destrow,0,reverse $toinsert); 6026 } 6027 $$selected=pack 'b*',$string; 6028 } 6029 elsif ($action eq 'up') 6030 { my $rows=$extra[0]; 6031 for my $row (@$rows) 6032 { ( vec($$selected,$row-1,1), vec($$selected,$row,1) ) 6033 = ( vec($$selected,$row,1), vec($$selected,$row-1,1) ); 6034 $self->{lastclick}-- if $self->{lastclick}==$row; 6035 $self->{startgrow}-- if $self->{startgrow}==$row; 6036 } 6037 for my $exp (@{ $self->buildexpstate }) 6038 { (substr($exp,$_-1,1), substr($exp,$_,1) ) 6039 =(substr($exp,$_,1), substr($exp,$_-1,1)) for @$rows 6040 } 6041 } 6042 elsif ($action eq 'down') 6043 { my $rows=$extra[0]; 6044 for my $row (reverse @$rows) 6045 { ( vec($$selected,$row+1,1), vec($$selected,$row,1) ) 6046 = ( vec($$selected,$row,1), vec($$selected,$row+1,1) ); 6047 $self->{lastclick}++ if $self->{lastclick}==$row; 6048 $self->{startgrow}++ if $self->{startgrow}==$row; 6049 } 6050 for my $exp (@{ $self->buildexpstate }) 6051 { (substr($exp,$_+1,1), substr($exp,$_,1) ) 6052 =(substr($exp,$_,1), substr($exp,$_+1,1)) for reverse @$rows 6053 } 6054 } 6055 elsif ($action eq 'remove') 6056 { if (@$songarray) 6057 { my $rows=$extra[0]; 6058 vec($$selected,@$rows+$#$songarray,1)||=0; #make sure $$selected has a value for every row, unlike $songarray $selected is not yet updated, so its last_row= @$rows+$#$songarray 6059 my $string=unpack 'b*',$$selected; 6060 for my $s ($string,@{ $self->buildexpstate }) 6061 { substr($s,$_,1,'') for reverse @$rows; 6062 } 6063 $$selected=pack 'b*',$string; 6064 for my $refrow ($self->{lastclick},$self->{startgrow}) 6065 { $refrow >= $_ and $refrow-- for reverse @$rows; 6066 } 6067 } 6068 else {$self->{lastclick}=$self->{startgrow}=-1;$$selected='';} 6069 } 6070 elsif ($action eq 'mode' || $action eq 'proxychange') {return} #the list itself hasn't changed 6071 else #'replace' or unknown action 6072 { #FIXME if replace : check if a filter is in $extra[0] 6073 $$selected=''; #clear selection 6074 $self->{lastclick}=$self->{startgrow}=-1; 6075 if ($action eq 'replace') 6076 { $self->{new_expand_state}=0; 6077 $center=1; 6078 } 6079 } 6080 $self->BuildTree; 6081 if ($center) 6082 { $self->{vadj}->set_value(0); 6083 my $ID=::GetSelID($self); 6084 if (defined $ID && $songarray->IsIn($ID)) #scroll to last selected ID if in the list 6085 { my $row= ::first { $songarray->[$_]==$ID } 0..$#$songarray; 6086 if ($$selected eq '') { $self->set_cursor_to_row($row); } # scroll to row and select it 6087 else { $self->scroll_to_row($row,1,1); } # scroll to row but keep selection 6088 } 6089 elsif ($self->{follow}) { $self->FollowSong; } 6090 } 6091 ::HasChanged('Selection_'.$self->{group}); 6092 $self->Hide(!scalar @$songarray) if $self->{hideif} eq 'empty'; 6093} 6094 6095sub update_sorted_column 6096{ my $self=shift; 6097 my $sort= $self->{'sort'}; 6098 my $invsort= join ' ', map { s/^-// && $_ || '-'.$_ } split / /,$sort; 6099 for my $cell (@{$self->{cells}}) 6100 { my $s= $cell->{sort} || ''; 6101 my $arrow= $s eq $sort ? 'down': 6102 $s eq $invsort ? 'up' : 6103 undef; 6104 if ($arrow) { $cell->{sorted}=$arrow; } # used by SongTree to draw background of cells differently for sorted column 6105 else { delete $cell->{sorted}; } # and by SongTree::Headers to draw up/down arrow 6106 } 6107} 6108 6109sub scroll_event_cb 6110{ my ($self,$event,$pageinc)=@_; 6111 my $dir= ref $event ? $event->direction : $event; 6112 (my $adj,$dir)= $dir eq 'up' ? (vadj =>-1) : 6113 $dir eq 'down' ? (vadj => 1) : 6114 $dir eq 'left' ? (hadj =>-1) : 6115 $dir eq 'right' ? (hadj => 1) : 6116 undef; 6117 return 0 unless $adj; 6118 $adj=$self->{$adj}; 6119 my $max= $adj->upper - $adj->page_size; 6120 my $value= $adj->value + $dir* ($pageinc? $adj->page_increment : $adj->step_increment); 6121 $value=$max if $value>$max; 6122 $value=0 if $value<0; 6123 $adj->set_value($value); 6124 1; 6125} 6126sub key_press_cb 6127{ my ($self,$event)=@_; 6128 my $key=Gtk2::Gdk->keyval_name( $event->keyval ); 6129 my $unicode=Gtk2::Gdk->keyval_to_unicode($event->keyval); # 0 if not a character 6130 my $state=$event->get_state; 6131 my $ctrl= $state * ['control-mask'] && !($state * [qw/mod1-mask mod4-mask super-mask/]); #ctrl and not alt/super 6132 my $mod= $state * [qw/control-mask mod1-mask mod4-mask super-mask/]; # no modifier ctrl/alt/super 6133 my $shift=$state * ['shift-mask']; 6134 my $row= $self->{lastclick}; 6135 $row=0 if $row<0; 6136 my $list=$self->{array}; 6137 if (($key eq 'space' || $key eq 'Return') && !$mod && !$shift) 6138 { $self->Activate(1); } 6139 elsif ($key eq 'Up') { $row-- if $row>0; $self->song_selected($event,$row); } 6140 elsif ($key eq 'Down') { $row++ if $row<$#$list;$self->song_selected($event,$row); } 6141 elsif ($key eq 'Home') { $self->song_selected($event,0); } 6142 elsif ($key eq 'End') { $self->song_selected($event,$#$list); } 6143 elsif ($key eq 'Left') { $self->scroll_event_cb('left'); } 6144 elsif ($key eq 'Right') { $self->scroll_event_cb('right'); } 6145 elsif ($key eq 'Page_Up') { $self->scroll_event_cb('up',1); } 6146 elsif ($key eq 'Page_Down') { $self->scroll_event_cb('down',1); } 6147 elsif ($key eq 'Delete') { $self->RemoveSelected; } 6148 elsif (lc$key eq 'a' && $ctrl) #ctrl-a : select-all 6149 { vec($self->{selected},$_,1)=1 for 0..$#$list; $self->UpdateSelection;} 6150 elsif (lc$key eq 'f' && $ctrl) { $self->{isearchbox}->begin(); } #ctrl-f : search 6151 elsif (lc$key eq 'g' && $ctrl) { $self->{isearchbox}->search($shift ? -1 : 1);} #ctrl-g : next/prev match 6152 elsif ($key eq 'F3' && !$mod) { $self->{isearchbox}->search($shift ? -1 : 1);} #F3 : next/prev match 6153 elsif (!$self->{no_typeahead} && $unicode && $unicode!=32 && !$mod) # character except space, no modifier 6154 { $self->{isearchbox}->begin( chr $unicode ); #begin typeahead search 6155 } 6156 else {return 0} 6157 return 1; 6158} 6159 6160sub expose_cb 6161{ my ($view,$event)=@_;# my $time=times; 6162 my $self=::find_ancestor($view,__PACKAGE__); 6163 my $expose=$event->area; 6164 my ($exp_x1,$exp_y1,$exp_x2,$exp_y2)=$expose->values; 6165 $exp_x2+=$exp_x1; $exp_y2+=$exp_y1; 6166 my $window=$view->window; 6167 my $style=Gtk2::Rc->get_style_by_paths($self->{stylewidget}->get_settings, '.GtkTreeView', '.GtkTreeView','Gtk2::TreeView') 6168 || Gtk2::Rc->get_style($self->{stylewidget}) 6169 || $self->get_style; 6170 $style=$style->attach($window); 6171 my $nstate= $self->state eq 'insensitive' ? 'insensitive' : 'normal'; 6172 my $sstate=$view->has_focus ? 'selected' : 'active'; 6173 $self->{stylewidget}->has_focus($view->has_focus); #themes engine check if the widget has focus 6174 my $selected= \$self->{selected}; 6175 my $list= $self->{array}; 6176 my $songcells= $self->{cells}; 6177 my $headcells= $self->{headcells}; 6178 my $vsizesong= $self->{vsizesong}; 6179 #$window->draw_rectangle($style->base_gc($state), 1, $expose->values); 6180 my $gc=$style->base_gc($nstate); 6181 $window->draw_rectangle($gc, 1, $expose->values); 6182 unless ($list && @$list) 6183 { $self->DrawEmpty($window); 6184 return 1; 6185 } 6186 6187 my $xadj=int $self->{hadj}->value; 6188 my $yadj=int $self->{vadj}->value; 6189 my @next; 6190 my ($depth,$i)=(0,1); 6191 my ($x,$y)=(0-$xadj, 0-$yadj); 6192 my $songs_x= $x+$self->{songxoffset}; 6193 my $songs_width=$self->{songswidth}; 6194 6195 my $maxy=$self->{viewsize}[1]-$yadj; 6196 $exp_y2=$maxy if $exp_y2>$maxy; #don't try to draw past the end 6197 6198 my $heights= $self->{TREE}{height}; 6199 my $max=$#{$heights->[$depth]}; 6200 my $maxdepth=$#$heights; 6201 while ($y<=$exp_y2) 6202 { if ($i>$max) 6203 { last unless $depth; 6204 $depth--; 6205 ($y,$i,$max)=splice @next,-3; 6206 next; 6207 } 6208 my $bh= $heights->[$depth][$i]; 6209 my $yend=$y+$bh; 6210 6211 if ($yend>$exp_y1) 6212 { my $cell=$headcells->[$depth]; 6213 my $expanded=$self->{TREE}{expanded}[$depth][$i]; 6214 if ($cell->{head} || $cell->{left} || $cell->{right}) 6215 { my $clip=$expose->intersect( Gtk2::Gdk::Rectangle->new( $x+$cell->{x},$y,$cell->{width},$bh) ); 6216 if ($clip) 6217 { my $start= $self->{TREE}{lastrows}[$depth][$i-1]+1; 6218 my $end= $self->{TREE}{lastrows}[$depth][$i]; 6219 my %arg= 6220 ( self => $cell, widget => $self, style => $style, 6221 window => $window, clip => $clip, state => $nstate, 6222 depth => $depth, expanded=> $expanded, 6223 vx => $xadj+$x+$cell->{x}, vy => $yadj+$y, 6224 x => $x+$cell->{x}, y => $y, 6225 w => $cell->{width}, h => $bh, 6226 grouptype => $cell->{grouptype}, 6227 groupsongs=> [@$list[$start..$end]], 6228 odd => $i%2, row=>$i, 6229 ); 6230 my $q= $cell->{draw}(\%arg); 6231 my $qid=$depth.'g'.($yadj+$y); 6232 delete $self->{queue}{$qid}; 6233 $self->{queue}{$qid}=$q if $q; 6234 } 6235 } 6236 if ($expanded) 6237 { $y+=$cell->{head}; 6238 last if $y>$exp_y2; 6239 if ($depth<$maxdepth) 6240 { push @next, $yend,$i+1,$max; 6241 $max=$self->{TREE}{lastchild}[$depth][$i]; 6242 $i= $self->{TREE}{lastchild}[$depth][$i-1]+1; 6243 $depth++; 6244 next; 6245 } 6246 else #songs 6247 { my $first= $self->{TREE}{lastrows}[$depth][$i-1]+1; 6248 my $last= $self->{TREE}{lastrows}[$depth][$i]; 6249 my $h=($last-$first+1)*$vsizesong; 6250 if ($y+$h>$exp_y1) 6251 { my $skip=0; 6252 $last-= int(-.5+($y+$h-$exp_y2)/$vsizesong) if $y+$h>$exp_y2; 6253 if ($y<$exp_y1) 6254 { $skip=int(($exp_y1-$y)/$vsizesong); 6255 $first+=$skip; 6256 $y+=$vsizesong*$skip; 6257 } 6258 my $odd=$skip%2; 6259 for my $row ($first..$last) 6260 { my $ID=$list->[$row]; 6261 my $state= vec($$selected,$row,1) ? $sstate : $nstate; 6262 my $detail= $odd? 'cell_odd_ruled' : 'cell_even_ruled'; 6263 #detail can have these suffixes (in order) : _ruled _sorted _start|_last|_middle 6264 $style->paint_flat_box( $window,$state,'none',$expose,$self->{stylewidget},$detail, 6265 $songs_x,$y,$songs_width,$vsizesong ); 6266 my $x=$songs_x; 6267 for my $cell (@$songcells) 6268 { my $width=$cell->{width}; 6269 $width+=$self->{extra} if $cell->{last}; 6270 my $clip=$expose->intersect( Gtk2::Gdk::Rectangle->new($x,$y,$width,$vsizesong) ); 6271 if ($clip) 6272 { if ($cell->{sorted}) # if column is sorted, redraw background with '_sorted' hint 6273 { $style->paint_flat_box( $window,$state,'none',$expose,$self->{stylewidget},$detail.'_sorted', 6274 $x,$y,$width,$vsizesong ); 6275 } 6276 6277 my %arg= 6278 (state => $state, self => $cell, widget => $self, 6279 style => $style, window => $window, clip => $clip, 6280 ID => $ID, firstrow=> $first, lastrow => $last, row=>$row, 6281 vx => $xadj+$x, vy => $yadj+$y, 6282 x => $x, y => $y, 6283 w => $width, h => $vsizesong, 6284 odd => $odd, 6285 currentsong => ($::SongID && $ID==$::SongID && ($self->{mode} ne 'playlist' || !defined $::Position || $::Position==$row)), 6286 ); 6287 6288 my $q= $cell->{draw}(\%arg); 6289 my $qid=$x.'s'.$y; 6290 delete $self->{queue}{$qid}; 6291 $self->{queue}{$qid}=$q if $q; 6292 } 6293 $x+=$width; 6294 } 6295 if (exists $view->{drag_highlight} && $view->{drag_highlight}==$row) 6296 { my $gc=$style->fg_gc('normal'); 6297 $gc->set_clip_rectangle($expose); 6298 $window->draw_line($gc,$songs_x,$y,$x,$y); 6299 $gc->set_clip_rectangle(undef); 6300 } 6301 $y+=$vsizesong; 6302 $odd^=1; 6303 } 6304 } 6305 #else {$y+=$h} 6306 } 6307 #$y+=$cell->{tail}; 6308 } 6309 } 6310 $y=$yend; #end of branch 6311 $i++; 6312 } 6313 if ($self->{queue}) 6314 { $self->{idle} ||= Glib::Idle->add(\&expose_queue,$self); 6315 } 6316 #warn 'expose : '.(times-$time)."s\n"; 6317 1; 6318} 6319 6320sub expose_queue 6321{ my $self=$_[0]; 6322 { last unless $self->{queue} && $self->mapped; 6323 my ($qid,$ref)=each %{ $self->{queue} }; 6324 last unless $ref; 6325 my $context=$ref->[-1]; 6326 my $qsub=shift @$ref; 6327 delete $self->{queue}{$qid} if @$ref<=1; 6328 my $hadj=$self->{hadj}; $context->{x}= $context->{vx} - int($hadj->value); 6329 my $vadj=$self->{vadj}; $context->{y}= $context->{vy} - int($vadj->value); 6330 &$qsub unless $context->{x}+$context->{w}<0 6331 || $context->{y}+$context->{h}<0 6332 || $context->{x}>$hadj->page_size 6333 || $context->{y}>$vadj->page_size; 6334 last unless scalar keys %{ $self->{queue} }; 6335 return 1; 6336 } 6337 delete $self->{queue}; 6338 return $self->{idle}=undef; 6339} 6340 6341sub coord_to_path 6342{ my ($self,$x,$y)=@_; 6343 $x+=int($self->{hadj}->value); 6344 $y+=int($self->{vadj}->value); 6345 return undef unless @{$self->{array}}; 6346 my $vsizesong= $self->{vsizesong}; 6347 my (@next,@path); 6348 my ($depth,$i)=(0,1); 6349 my ($hirow,$area,$row); 6350 my $heights= $self->{TREE}{height}; 6351 my $max=$#{$heights->[$depth]}; 6352 my $maxdepth=$#$heights; 6353 ############# find vertical position 6354 while (1) 6355 { if ($i>$max) 6356 { last unless $depth; 6357 $depth--; 6358 ($y,$i,$max)=splice @next,-3; 6359 pop @path; 6360 if ($y<0) 6361 { $area='tail'; 6362 $hirow= $self->{TREE}{lastrows}[$depth][$i]+1; 6363 last; 6364 } 6365 next; 6366 } 6367 my $bh= $heights->[$depth][$i]; 6368 my $yend=$y-$bh; 6369 if ($y>=0 && $yend<0) 6370 { if ($self->{TREE}{expanded}[$depth][$i]) #expanded 6371 { my $head= $self->{headcells}[$depth]{head}; 6372 if ($y-$head<=0) #head 6373 { my $after= $y > $head/2; 6374 $hirow= $self->{TREE}{lastrows}[$depth][$i-1]+1; 6375 $area='head'; 6376 last; 6377 } 6378 $y-=$head; 6379 if ($depth<$maxdepth) 6380 { push @next, $yend,$i+1,$max; 6381 push @path,$i; 6382 $max=$self->{TREE}{lastchild}[$depth][$i]; 6383 $i= $self->{TREE}{lastchild}[$depth][$i-1]+1; 6384 $depth++; 6385 next; 6386 } 6387 else #songs 6388 { my $first= $self->{TREE}{lastrows}[$depth][$i-1]+1; 6389 my $last= $self->{TREE}{lastrows}[$depth][$i]; 6390 my $h=($last-$first+1)*$vsizesong; 6391 if ($y-$h<=0) 6392 { $row= int( $y/$vsizesong ); 6393 my $after= int( .5+$y/$vsizesong ) <= $row; 6394 $row+=$first; 6395 $hirow=$row+1-$after; 6396 $area='songs'; 6397 last; 6398 } 6399 } 6400 $hirow= $self->{TREE}{lastrows}[$depth][$i]+1; 6401 $area='tail'; 6402 last; 6403 } 6404 else #collapsed group 6405 { my $after= $y > $bh/2; 6406 my $i2= $after ? $i-1 : $i; 6407 $hirow= $self->{TREE}{lastrows}[$depth][$i2]+1; 6408 $area='collapsed'; 6409 last; 6410 } 6411 } 6412 $y=$yend; 6413 $i++; 6414 } 6415 unless (@path) 6416 { $area||='end'; #empty space at the end 6417 } 6418 return undef unless $area; 6419 my $depth0=$depth; 6420 6421 ############# find horizontal position 6422 push @path,$i; 6423 my $hdepth=0; my ($x2,$col); 6424 my $harea='left'; 6425 for my $cell (@{$self->{headcells}}) 6426 { $x-=$cell->{left}; 6427 last if $x<=0; 6428 $hdepth++; 6429 $x2=$x; 6430 } 6431 if ($x>0 && $x<$self->{songswidth} && $area eq 'songs') 6432 { $harea='songs'; 6433 $depth=undef; 6434 $col=0; 6435 while ($x>0) 6436 { $x-=$self->{cells}[$col]{width}; 6437 last if $x<0; 6438 $col++; 6439 $x2=$x; 6440 last unless $self->{cells}[$col]; 6441 } 6442 $y %= $vsizesong; 6443 } 6444 if ($x>$self->{songswidth}) 6445 { $x-=$self->{songswidth}; 6446 $harea='right'; 6447 for my $cell (reverse @{$self->{headcells}}) 6448 { $x-=$cell->{right}; 6449 last if $x<0; 6450 $hdepth--; 6451 $x2=$x; 6452 } 6453 } 6454 if (defined $depth && $hdepth<$depth) 6455 { $depth=$hdepth; 6456 $#path=$depth; 6457 } 6458 return { path => \@path, 6459 start => $self->{TREE}{lastrows}[$depth0][$i-1]+1, 6460 end => $self->{TREE}{lastrows}[$depth0][$i], 6461 depth => $depth, 6462 row => $row, 6463 hirow => $hirow, 6464 area => $area, 6465 harea => $harea, 6466 x => $x2, 6467 y => $y, 6468 col => $col, 6469 branch => $i, 6470 }; 6471} 6472 6473sub row_to_y 6474{ my ($self,$row)=@_; 6475 my $y=0; 6476 my $depth=0; 6477 my $i=1; 6478 my $maxdepth= $#{ $self->{TREE}{lastrows} }; 6479 my $lastrows= $self->{TREE}{lastrows}[$depth]; 6480 my $heights= $self->{TREE}{height}[$depth]; 6481 while ($i<=$#$lastrows) 6482 { if ($row>$lastrows->[$i-1] && $row<=$lastrows->[$i]) 6483 { return $y unless $self->{TREE}{expanded}[$depth][$i]; 6484 $y+= $self->{headcells}[$depth]{head}; 6485 if ($depth<$maxdepth) 6486 { $i=$self->{TREE}{lastchild}[$depth][$i-1]+1; 6487 $depth++; 6488 $lastrows= $self->{TREE}{lastrows}[$depth]; 6489 $heights= $self->{TREE}{height}[$depth]; 6490 next; 6491 } 6492 my $first= $self->{TREE}{lastrows}[$depth][$i-1]+1; 6493 $y+= $self->{vsizesong}*($row-$first); 6494 return $y; 6495 } 6496 $y+=$heights->[$i]; 6497 $i++; 6498 } 6499 return 0; 6500} 6501sub row_to_rect 6502{ my ($self,$row)=@_; 6503 my $y=$self->row_to_y($row); 6504 return unless defined $y; 6505 my $x= $self->{songxoffset} - int($self->{hadj}->value); 6506 $y-= $self->{vadj}->value; 6507 return Gtk2::Gdk::Rectangle->new($x, $y, $self->{songswidth}, $self->{vsizesong}); 6508} 6509sub update_row 6510{ my ($self,$row)=@_; 6511 my $rect=$self->row_to_rect($row); 6512 my $gdkwin= $self->{view}->window; 6513 $gdkwin->invalidate_rect($rect,0) if $rect && $gdkwin; 6514} 6515#sub update_row 6516#{ my ($self,$row)=@_; 6517# my $y=$self->row_to_y($row); 6518# return unless defined $y; 6519# my $x= $self->{songxoffset} - int($self->{hadj}->value); 6520# $y-= $self->{vadj}->value; 6521# $self->{view}->queue_draw_area($x, $y, $self->{songswidth}, $self->{vsizesong}); 6522#} 6523 6524 6525sub Scroll_to_TopEnd 6526{ my ($self,$end)=@_; 6527 my $adj=$self->{vadj}; 6528 if ($end) { $adj->set_value($adj->upper-$adj->page_size); } 6529 else { $adj->set_value(0); } 6530} 6531 6532sub drag_received_cb 6533{ my ($view,$type,$dest,@IDs)=@_; 6534 if ($type==::DRAG_FILE) #convert filenames to IDs 6535 { @IDs=::FolderToIDs(1,0,map ::decode_url($_), @IDs); 6536 return unless @IDs; 6537 } 6538 my $self=::find_ancestor($view,__PACKAGE__); 6539 my (undef,$row)=@$dest; 6540 return unless defined $row; #FIXME 6541#warn "dropped, insert before row $row, song : ".Songs::Display($self->{array}[$row],'title')."\n"; 6542 my $songarray=$self->{array}; 6543 if ($view->{drag_is_source}) 6544 { $songarray->Move($row,$self->GetSelectedRows); 6545 } 6546 else { $songarray->Insert($row,\@IDs); } 6547} 6548sub drag_motion_cb 6549{ my ($view,$context,$x,$y,$time)=@_; 6550 my $self=::find_ancestor($view,__PACKAGE__); 6551 if ($self->{autoupdate}) { $context->status('default',$time); return } # refuse any drop if autoupdate is on 6552 6553 #check scrolling 6554 if ($y-$self->{vsizesong}<=0) {$view->{scroll}='up'} 6555 elsif ($y+$self->{vsizesong} >= $self->{viewwindowsize}[1]) {$view->{scroll}='down'} 6556 else {delete $view->{context};delete $view->{scroll}} 6557 if ($view->{scroll}) 6558 { $view->{scrolling}||=Glib::Timeout->add(200, \&drag_scrolling_cb,$view); 6559 $view->{context}||=$context; 6560 } 6561 6562 my $answer=$self->coord_to_path($x,$y); 6563 my $row=$answer->{hirow}; 6564 $row=@{$self->{array}} unless defined $row; 6565 $self->update_row($view->{drag_highlight}) if defined $view->{drag_highlight}; 6566 $view->{drag_highlight}=$row; 6567 $self->update_row($row); 6568 $context->{dest}=[$view,$row]; 6569 $context->status(($view->{drag_is_source} ? 'move' : 'copy'),$time); 6570 return 1; 6571} 6572sub drag_scrolling_cb 6573{ my $view=$_[0]; 6574 if (my $s=$view->{scroll}) 6575 { my $self=::find_ancestor($view,__PACKAGE__); 6576 $self->scroll_event_cb($s); 6577 drag_motion_cb($view,$view->{context}, ($view->window->get_pointer)[1,2], 0 ); 6578 return 1; 6579 } 6580 else 6581 { delete $view->{scrolling}; 6582 return 0; 6583 } 6584} 6585sub drag_leave_cb 6586{ my $view=$_[0]; 6587 my $self=::find_ancestor($view,__PACKAGE__); 6588 my $row=delete $view->{drag_highlight}; 6589 $self->update_row($row) if defined $row; 6590} 6591 6592sub expand_collapse 6593{ my ($self,$depth,$i)=@_; 6594 $self->{TREE}{expanded}[$depth][$i]^=1; 6595 $self->compute_height; # FIXME could compute only ($depth,$i) 6596} 6597 6598sub compute_height 6599{ my ($self)=@_; 6600 delete $self->{queue}; 6601 $self->{TREE}{height}=[]; 6602 my $vsizesong=$self->{vsizesong}; 6603 my $headcells=$self->{headcells}; 6604 my $maxdepth=$#$headcells; 6605 for my $depth (reverse 0..$maxdepth) 6606 { my $headcell=$headcells->[$depth]; 6607 my $vmin=$headcell->{vmin}; 6608 my $headtail= $headcell->{head} + $headcell->{tail}; 6609 my $vcollapsed=$headcell->{vcollapse}; 6610 my $expanded= $self->{TREE}{expanded}[$depth]; 6611 my $height= $self->{TREE}{height}[$depth]=[0]; 6612 if ($depth==$maxdepth) 6613 { my $lastrows=$self->{TREE}{lastrows}[$depth]; 6614 my $firstrow=-1; 6615 for my $i (1..$#$lastrows) 6616 { my $lastrow=$lastrows->[$i]; 6617 my $h; 6618 if ($expanded->[$i]) 6619 { $h= $headtail+ $vsizesong * ($lastrow-$firstrow); 6620 $h=$vmin if $h<$vmin; 6621 } 6622 else { $h=$vcollapsed } 6623 $height->[$i]=$h; 6624 $firstrow=$lastrow; 6625 } 6626 } 6627 else 6628 { my $lastchild= $self->{TREE}{lastchild}[$depth]; 6629 my $hchildren= $self->{TREE}{height}[$depth+1]; 6630 my $firstchild=1; 6631 for my $i (1..$#$lastchild) 6632 { my $lastchild=$lastchild->[$i]; 6633 my $h; 6634 if ($expanded->[$i]) 6635 { $h= $headtail; 6636 $h+= $hchildren->[$_] for $firstchild..$lastchild; 6637 $h=$vmin if $h<$vmin; 6638 } 6639 else { $h=$vcollapsed } 6640 $height->[$i]=$h; 6641 $firstchild=$lastchild+1; 6642 } 6643 } 6644 } 6645 6646 my $height0=$self->{TREE}{height}[0]; 6647 my $h=0; 6648 $h+=$_ for @$height0; 6649 $self->{viewsize}[1]= $h; 6650#warn "total height=$h"; 6651 6652 $self->update_scrollbar; 6653 $self->{view}->queue_draw; 6654} 6655 6656sub button_press_cb 6657{ my ($view,$event)=@_; 6658 $view->grab_focus; 6659 my $self=::find_ancestor($view,__PACKAGE__); 6660 my $but=$event->button; 6661 my $answer=$self->coord_to_path($event->coords); 6662 my $row= $answer && $answer->{row}; 6663 my $depth= $answer && $answer->{depth}; 6664 if ((my $ref=$self->{action_rectangles}) && 0) #TESTING 6665 { my $x= $event->x + int($self->{hadj}->value); 6666 my $y= $event->y + int($self->{vadj}->value); 6667 my $found; 6668 for my $dim (keys %$ref) 6669 { my ($rx,$ry,$rw,$rh)=split /,/,$dim; 6670 next if $ry>$y || $ry+$rh<$y || $rx>$x || $rx+$rw<$x; 6671 $found=$ref->{$dim}; 6672 } 6673 if ($found) {warn "actions : $_ => $found->{$_}" for keys %$found} 6674 } 6675 if ($event->type eq '2button-press') 6676 { return 0 unless $answer; #empty list 6677 return 0 unless $answer->{area} eq 'songs'; 6678 $self->Activate($but); 6679 return 1; 6680 } 6681 if ($but==3) 6682 { if ($answer && !defined $depth && !vec($self->{selected},$row,1)) 6683 { $self->song_selected($event,$row); 6684 } 6685 $self->PopupContextMenu; 6686 return 1; 6687 } 6688 else# ($but==1) 6689 { return 0 unless $answer; 6690 if (defined $depth && $answer->{area} eq 'head' || $answer->{area} eq 'collapsed') 6691 { if ($answer->{area} eq 'head' && $self->{headclick} eq 'select') 6692 { $self->song_selected($event,$answer->{start},$answer->{end}); return 0} 6693 else { $self->expand_collapse($depth,$answer->{branch}); } 6694 return 1; 6695 } 6696 elsif (defined $depth && $answer->{harea} eq 'left' || $answer->{harea} eq 'right') 6697 { $self->song_selected($event,$answer->{start},$answer->{end}); 6698 return 0; 6699 } 6700 if (defined $row) 6701 { if ( $event->get_state * ['shift-mask', 'control-mask'] || !vec($self->{selected},$row,1) ) 6702 { $self->song_selected($event,$row); } 6703 else { $view->{pressed}=1; } 6704 } 6705 return 0; 6706 } 6707 1; 6708} 6709sub button_release_cb 6710{ my ($view,$event)=@_; 6711 return 0 unless $view->{pressed}; 6712 $view->{pressed}=undef; 6713 my $self=::find_ancestor($view,__PACKAGE__); 6714 my $answer=$self->coord_to_path($event->coords); 6715 $self->song_selected($event,$answer->{row}); 6716 return 1; 6717} 6718sub drag_begin_cb 6719{ $_[0]->{pressed}=undef; 6720} 6721 6722sub scroll_to_row #FIXME simplify 6723{ my ($self,$row,$center,$not_if_visible)=@_; 6724 my $vsize=$self->{vsizesong}; 6725 my $y1=my $y2=$self->row_to_y($row); 6726 my $vadj=$self->{vadj}; 6727 if ($not_if_visible) {return if $y1-$vadj->value>0 && $y1+$vsize-$vadj->value-$vadj->page_size<0;} 6728 if ($center) 6729 { my $half= $center * $vadj->page_size/2; 6730 $y1-=$half-$vsize/2; 6731 $y2+=$half+$vsize/2; 6732 } 6733 else 6734 { $y1-=$vsize; 6735 $y2+=$vsize*2; 6736 } 6737 $vadj->clamp_page($y1,$y2+2); 6738} 6739 6740sub CurSongChanged 6741{ my $self=$_[0]; 6742 $self->FollowSong if $self->{follow}; 6743} 6744sub FollowSong 6745{ my $self=$_[0]; 6746 return unless defined $::SongID; 6747 my $array=$self->{array}; 6748 return unless $array; 6749 my $row; 6750 if ($self->{mode} eq 'playlist') { $row=$::Position; } 6751 if ($array->IsIn($::SongID)) 6752 { $row= ::first { $array->[$_]==$::SongID } 0..$#$array unless defined $row && $row>=0; 6753 $self->set_cursor_to_row($row); 6754 } 6755 ::HasChangedSelID($self->{group},$::SongID); 6756} 6757 6758sub get_cursor_row 6759{ my $self=$_[0]; 6760 my $row=$self->{lastclick}; 6761 if ($row<0) 6762 { my $path=$self->coord_to_path(0,0); 6763 $row= ref $path ? $path->{row} : 0 ; 6764 } 6765 return $row; 6766} 6767 6768sub set_cursor_to_row 6769{ my ($self,$row)=@_; 6770 $self->song_selected(undef,$row,undef,'noscroll'); 6771 $self->scroll_to_row($row,1,1); 6772} 6773 6774sub song_selected 6775{ my ($self,$event,$idx1,$idx2,$noscroll)=@_; 6776 return if $idx1<0 || $idx1 >= @{$self->{array}}; 6777 $idx2=$idx1 unless defined $idx2; 6778 $self->scroll_to_row($idx1) unless $noscroll; 6779 ::HasChangedSelID($self->{group},$self->{array}[$idx1]); 6780 unless ($event && $event->get_state >= ['control-mask']) 6781 { $self->{selected}=''; 6782 } 6783 if ($event && $event->get_state >= ['shift-mask'] && $self->{lastclick}>=0) 6784 { $self->{startgrow}=$self->{lastclick} unless $self->{startgrow}>=0; 6785 my $i1=$self->{startgrow}; 6786 my $i2=$idx1; 6787 if ($i1>$i2) { ($i1,$i2)=($i2,$i1) } 6788 else { $i2=$idx2 } 6789 vec($self->{selected},$_,1)=1 for $i1..$i2; 6790 } 6791 elsif (!grep !vec($self->{selected},$_,1), $idx1..$idx2) 6792 { vec($self->{selected},$_,1)=0 for $idx1..$idx2; 6793 $self->{startgrow}=-1; 6794 } 6795 #elsif (vec($self->{selected},$idx,1)) 6796 #{ vec($self->{selected},$idx,1)=0 6797 # $self->{startgrow}=-1; 6798 #} 6799 else 6800 { vec($self->{selected},$_,1)=1 for $idx1..$idx2; 6801 $self->{startgrow}=-1; 6802 } 6803 $self->{lastclick}=$idx1; 6804 $self->UpdateSelection; 6805} 6806sub select_by_filter 6807{ my ($self,$filter)=@_; 6808 my $array=$self->{array}; 6809 my $IDs= $filter->filter($array); 6810 my %h; $h{$_}=undef for @$IDs; 6811 $self->{selected}=''; #clear selection 6812 vec($self->{selected},$_,1)=1 for grep exists $h{$array->[$_]}, 0..$#$array; 6813 $self->{startgrow}=$self->{lastclick}=-1; 6814 $self->UpdateSelection; 6815} 6816 6817sub UpdateSelection 6818{ my $self=shift; 6819 ::HasChanged('Selection_'.$self->{group}); 6820 $self->{view}->queue_draw; 6821} 6822 6823sub query_tooltip_cb 6824{ my ($view, $x, $y, $keyb, $tooltip)=@_; 6825 return 0 if $keyb; 6826 my $self=::find_ancestor($view,__PACKAGE__); 6827 my $path=$self->coord_to_path($x,$y); 6828 my $row=$path->{row}; 6829 return 0 unless defined $row; 6830 my $ID=$self->{array}[$row]; 6831 return unless defined $ID; 6832 my $markup= ::ReplaceFieldsAndEsc($ID,$self->{rowtip}); 6833 $tooltip->set_markup($markup); 6834 my $rect=$self->row_to_rect($row); 6835 $tooltip->set_tip_area($rect) if $rect; 6836 1; 6837} 6838 6839package SongTree::Headers; 6840use base 'Gtk2::Viewport'; 6841use constant TREE_VIEW_DRAG_WIDTH => 6; 6842 6843our @ColumnMenu= 6844( { label => _"_Sort by", submenu => sub { Browser::make_sort_menu($_[0]{songtree}); } 6845 }, 6846 { label => _"Set grouping", submenu => sub {$::Options{SavedSTGroupings}}, check =>sub { $_[0]{songtree}{grouping} }, 6847 code => sub { $_[0]{songtree}->set_head_columns($_[1]); }, 6848 }, 6849 { label => _"Edit grouping ...", code => sub { my $songtree=$_[0]{songtree}; ::EditSTGroupings($songtree,$songtree->{grouping},undef,sub{ $songtree->set_head_columns($_[0]) if defined $_[0]; }); }, 6850 }, 6851 { label => _"_Insert column", submenu => sub 6852 { my %names; $names{$_}= $SongTree::STC{$_}{menutitle}||$SongTree::STC{$_}{title} for keys %SongTree::STC; 6853 delete $names{$_->{colid}} for grep $_->{colid}, $_[0]{self}->child->get_children; 6854 return \%names; 6855 }, submenu_reverse =>1, 6856 code => sub { $_[0]{songtree}->AddColumn($_[1],$_[0]{insertpos}); }, stockicon => 'gtk-add', 6857 }, 6858 { label=> sub { _('_Remove this column').' ('.($SongTree::STC{$_[0]{colid}}{menutitle}||$SongTree::STC{$_[0]{colid}}{title}).')' }, 6859 code => sub { $_[0]{songtree}->remove_column($_[0]{cellnb}) }, stockicon => 'gtk-remove', isdefined => 'colid', 6860 }, 6861 { label => _("Edit row tip").'...', code => sub { $_[0]{songtree}->EditRowTip; }, 6862 }, 6863 { label => _"Keep list filtered and sorted", code => sub { $_[0]{songtree}{array}->SetAutoUpdate( $_[0]{songtree}{autoupdate} ); }, 6864 toggleoption => 'songtree/autoupdate', mode => 'B', 6865 }, 6866 { label => _"Follow playing song", code => sub { $_[0]{songtree}->FollowSong if $_[0]{songtree}{follow}; }, 6867 toggleoption => 'songtree/follow', 6868 }, 6869 { label => _"Go to playing song", code => sub { $_[0]{songtree}->FollowSong; }, }, 6870); 6871 6872sub new 6873{ my ($class,$adj)=@_; 6874 my $self=bless Gtk2::Viewport->new($adj,undef), $class; 6875 $self->set_size_request(1,-1); 6876 $self->add_events(['pointer-motion-mask','button-press-mask','button-release-mask']); 6877 $self->signal_connect(realize => \&update); 6878 $self->signal_connect(button_release_event => \&button_release_cb); 6879 $self->signal_connect(motion_notify_event => \&motion_notify_cb); 6880 $self->signal_connect(button_press_event => \&button_press_cb); 6881 my $rcstyle0=Gtk2::RcStyle->new; 6882 $rcstyle0->ythickness(0); 6883 $rcstyle0->xthickness(0); 6884 $self->modify_style($rcstyle0); 6885 return $self; 6886} 6887 6888sub button_press_cb #begin resize 6889{ my ($self,$event)=@_; 6890 for my $button ($self->child->get_children) 6891 { if ($button->{dragwin} && ($event->window == $button->{dragwin})) 6892 { my $x= $event->x + $button->allocation->width; 6893 $self->{resizecol}=[$x,$button]; 6894 last; 6895 } 6896 #elsif ($button->window==$event->window) {}#FIXME add column drag and drop 6897 } 6898 return 0 unless $self->{resizecol}; 6899 Gtk2->grab_add($self); 6900 1; 6901} 6902sub button_release_cb #end resize 6903{ my $self=$_[0]; 6904 return 0 unless $self->{resizecol}; 6905 Gtk2->grab_remove($self); 6906 my $songtree=::find_ancestor($self,'SongTree'); 6907 my $cell= $songtree->{cells}[ $self->{resizecol}[1]->{cellnb} ]; 6908 $songtree->{colwidth}{$cell->{colid}}= $cell->{width}; #set width as default for this colid 6909 delete $self->{resizecol}; 6910 _update_dragwin($_) for $self->child->get_children; 6911 1; 6912} 6913sub motion_notify_cb #resize column 6914{ my ($self,$event)=@_; 6915 return 0 unless $self->{resizecol}; 6916 my $songtree=::find_ancestor($self,'SongTree'); 6917 my ($xstart,$button)=@{ $self->{resizecol} }; 6918 my $cell= $songtree->{cells}[$button->{cellnb}]; 6919 my $width=$cell->{width}; 6920 my $newwidth= $xstart + $event->x; 6921 my $min= $cell->{minwidth} || 0; 6922 $newwidth=$min if $newwidth<$min; 6923 return 1 if $width==$newwidth; 6924 $cell->{width}=$newwidth; 6925 $self->{busy}=1; 6926 $songtree->update_columns; 6927 $self->{busy}=0; 6928 $button->set_size_request($newwidth,-1); 6929 1; 6930} 6931 6932sub update 6933{ my $self=$_[0]; 6934 return if $self->{busy}; 6935 my $songtree=::find_ancestor($self,'SongTree'); 6936 #return unless $songtree->{ready}; 6937 $self->remove($self->child) if $self->child; 6938 my $hbox=Gtk2::HBox->new(0,0); 6939 $self->add($hbox); 6940 6941 if (my $w=$songtree->{songxoffset}) 6942 { my $button=Gtk2::Button->new; 6943 $button->set_size_request($w,-1); 6944 $hbox->pack_start($button,0,0,0); 6945 $button->{insertpos}=0; 6946 } 6947 if (my $w=$songtree->{songxright}) 6948 { my $button=Gtk2::Button->new; 6949 $button->set_size_request($w,-1); 6950 $hbox->pack_end($button,0,0,0); 6951 $button->{insertpos}=@{$songtree->{cells}}; 6952 } 6953 my $i=0; 6954 for my $cell (@{$songtree->{cells}}) 6955 { my $button=Gtk2::Button->new; 6956 my $hbox2=Gtk2::HBox->new; 6957 my $label=Gtk2::Label->new( $SongTree::STC{ $cell->{colid} }{title} ); 6958 $button->add($hbox2); 6959 $hbox2->add($label); 6960 if (my $arrow=$cell->{sorted}) 6961 { $hbox2->pack_end(Gtk2::Arrow->new($arrow,'in'),0,0,0); 6962 } 6963 $button->{sort}=$cell->{sort}; 6964 $label->set_alignment(0,.5); 6965 #FIXME the drag_wins need to be destroyed, but this sometimes 6966 # create "GdkWindow unexpectedly destroyed" warnings 6967 # $button->signal_connect(unrealize => \&_destroy_dragwin); 6968 # $button->signal_connect(hide => \&_destroy_dragwin); 6969 $button->{cellnb}=$i++; 6970 $button->{colid}=$cell->{colid}; 6971 $button->set_size_request($cell->{width},-1); 6972 my $expand= $i==@{$songtree->{cells}}; 6973 $hbox->pack_start($button,$expand,$expand,0); 6974 } 6975 my $rcstyle=Gtk2::RcStyle->new; 6976 $rcstyle->ythickness(1); 6977 $rcstyle->xthickness(1); 6978 my @buttons=$hbox->get_children; 6979 for my $button (@buttons) 6980 { $button->signal_connect(expose_event => \&button_expose_cb); 6981 $button->signal_connect(clicked => \&clicked_cb); 6982 $button->signal_connect(button_press_event => \&popup_col_menu); 6983 $button->{stylewidget}=$songtree->{stylewidget_header2}; 6984 $button->modify_style($rcstyle); 6985 } 6986 $buttons[-1]{stylewidget}=$songtree->{stylewidget_header3}; 6987 $buttons[0]{stylewidget}=$songtree->{stylewidget_header1}; 6988 $hbox->show_all; 6989} 6990 6991sub clicked_cb 6992{ my $button=$_[0]; 6993 my $songtree=::find_ancestor($button,'SongTree'); 6994 my $sort= $button->{colid} ? $button->{sort} : join ' ',map Songs::SortGroup($_), @{$songtree->{colgroup}}; 6995 return unless defined $sort; 6996 $sort='-'.$sort if $sort eq $songtree->{sort}; 6997 $songtree->Sort($sort); 6998} 6999 7000sub popup_col_menu 7001{ my ($button,$event)=@_; 7002 return 0 unless $event->button == 3; 7003 my $self= ::find_ancestor($button,__PACKAGE__); 7004 my $songtree= ::find_ancestor($self,'SongTree'); 7005 my $insertpos= exists $button->{cellnb} ? $button->{cellnb}+1 : $button->{insertpos}; 7006 ::PopupContextMenu(\@ColumnMenu, { self => $self, colid => $button->{colid}, cellnb =>$button->{cellnb}, insertpos =>$insertpos, songtree => $songtree, mode=>$songtree->{type}, }); 7007 return 1; 7008} 7009 7010sub button_expose_cb 7011{ my ($button,$event)=@_; 7012 #my $style=Gtk2::Rc->get_style($button->{stylewidget}); 7013 my $style=Gtk2::Rc->get_style_by_paths($button->get_settings, '.GtkTreeView.GtkButton', '.GtkTreeView.GtkButton','Gtk2::Button') 7014 || Gtk2::Rc->get_style($button->{stylewidget}); 7015 $style=$style->attach($button->window); 7016 $style->paint_box($button->window,$button->state,'out',$event->area,$button->{stylewidget},'button',$button->allocation->values); 7017 $button->propagate_expose($button->child,$event) if $button->child; 7018 if ($button->{colid}) 7019 { _create_dragwin($button) unless $button->{dragwin}; 7020 #$button->{dragwin}->raise; 7021 } 7022 1; 7023} 7024 7025sub _create_dragwin 7026{ my $button=$_[0]; 7027 my ($x,$y,$w,$h)=$button->allocation->values; 7028 my %attr= 7029 ( window_type => 'child', 7030 wclass => 'only', 7031 cursor => Gtk2::Gdk::Cursor->new('sb-h-double-arrow'), 7032 x => $x+$w-(TREE_VIEW_DRAG_WIDTH/2), 7033 y => $y, 7034 width => TREE_VIEW_DRAG_WIDTH, 7035 height => $h, 7036 event_mask => ['pointer-motion-mask','button-press-mask','button-release-mask'], 7037 ); 7038 $button->{dragwin}=Gtk2::Gdk::Window->new($button->window,\%attr); 7039 $button->{dragwin}->set_user_data($button->window->get_user_data); 7040 $button->{dragwin}->show; 7041} 7042sub _destroy_dragwin 7043{ my $button=$_[0]; 7044 my $dragwin=delete $button->{dragwin}; 7045 return unless $dragwin; 7046 warn "destroying $dragwin\n" if $::debug; 7047 $dragwin->set_user_data(0); #needed ? 7048 $dragwin->destroy; 7049} 7050sub _update_dragwin 7051{ my ($button)=@_; 7052 return unless $button->{dragwin}; 7053 my ($x,$y,$w)=$button->allocation->values; 7054 $button->{dragwin}->move($x+$w-(TREE_VIEW_DRAG_WIDTH/2), $y); 7055 0; 7056} 7057 7058package GMB::Cell; 7059 7060my $drawpix= ['pixbuf_draw','draw = pixbuf xd yd wd hd']; 7061my $padandalignx=['pad_and_align', 'xd wd = x xpad pad xalign wr w']; 7062my $padandaligny=['pad_and_align', 'yd hd = y ypad pad yalign hr h']; 7063my $optpad= ['optpad', 'xpad ypad = pad']; 7064sub optpad # 7065{ return $_[1],$_[1]; 7066} 7067our %GraphElem= 7068( text =>{ functions => 7069 [ ['layout_draw','draw = layout xd yd wd hd'], 7070 ['markup_layout','layout = text markup rotate hide'], 7071 ($Gtk2::VERSION<1.161 ? ['layout_size2','wr hr bl = layout markup'] : # work-around for bug #482795 in $Gtk2::VERSION<1.161 7072 ['layout_size','wr hr bl = layout'] 7073 ), 7074 $padandalignx,$padandaligny, 7075 ], 7076 defaults => 7077 'w=___wr+2*___xpad,h=___hr+2*___ypad,xpad=xpad,ypad=ypad,yalign=.5,rotate=0,blp=___bl+___ypad', 7078 optional => 7079 [ $optpad, 7080 ], 7081 }, 7082 rect =>{ functions => 7083 [ ['box_draw','draw = x y w h color filled width hide'], 7084 ], 7085 defaults => 'color=0,filled=0,x=0,y=0,w=$_w-___x,h=$_h-___y,width=1', 7086 }, 7087 pbar =>{ functions => 7088 [ ['pbar_draw','draw = x y w h fill hide'], 7089 ], 7090 defaults => 'fill=0,x=0,y=0,w=$_w-___x,h=$_h-___y', 7091 }, 7092 line =>{ functions => 7093 [ ['line_draw','draw = x1 y1 x2 y2 color width hide'], 7094 ], 7095 defaults => 'color=0,x1=0,y1=0,x2=___x1,y2=___y1,width=1', 7096 }, 7097 aapic =>{ functions => 7098 [ ['aapic_size','pixbuf wr hr = aap'], 7099 ['aapic_cached','aap queue = picsize aa ids aanb hide'], 7100 $drawpix,$padandalignx,$padandaligny, 7101 ], 7102 defaults => 7103 'x=0,y=0,w=___picsize+2*___xpad,h=___picsize+2*___ypad,xpad=xpad,ypad=ypad,xalign=.5,yalign=.5,aanb=0,aa=$_grouptype,ids=$ids,picsize=min(___w+2*___xpad,___h+2*___ypad)', 7104 optional => 7105 [ $optpad, 7106 ], 7107 }, 7108 picture =>{ functions => 7109 [ ['pic_cached','cached queue = file resize? w? h? xpad ypad crop hide'], 7110 ['pic_size','pixbuf wr hr = cached file crop hide'], 7111 $drawpix,$padandalignx,$padandaligny, 7112 ], 7113 defaults => 'x=0,y=0,xalign=.5,yalign=.5,resize=0,w=0,h=0,crop=0,xpad=xpad,ypad=ypad,w=___wr+2*___xpad,h=___hr+2*___ypad', 7114 optional => 7115 [ $optpad, 7116 ], 7117 }, 7118 icon =>{ functions => 7119 [ ['icon_size','wr hr nbh w1 h1 = size icon y h xpad ypad hide'], 7120 ['icon_draw','draw = icon size xd yd wd hd nbh w1 h1 hide'], 7121 $padandalignx,$padandaligny, 7122 ], 7123 defaults => 'w=___wr+2*___xpad,h=$_h,xpad=xpad,ypad=ypad,xalign=0,yalign=.5,size=\'menu\'', 7124 optional => 7125 [ $optpad, 7126 ], 7127 }, 7128 action=>{ functions => 7129 [ ['set_action','draw = x y w h actions hide'], 7130 ], 7131 defaults => 'x=0,y=0,w=$_w,h=$_h', 7132 }, 7133# expander=>{ functions => 7134# [ ['exp_size','wr hr = hide'], 7135# ['exp_draw','draw = xd yd wd hd hide'], 7136# $padandalignx,$padandaligny, 7137# ], 7138# }, 7139 7140 blalign =>{ functions => 7141 [ ['blalign','h = y ref','y = blp h'], 7142 ], 7143 defaults => 'y=0,ref=0', 7144 }, 7145 xalign =>{ functions => 7146 [ ['align','w = align x ref','x = w'], 7147 ], 7148 defaults => 'ref=___align', 7149 }, 7150 yalign =>{ functions => 7151 [ ['align','h = align y ref','y = h'], 7152 ], 7153 defaults => 'ref=___align', 7154 }, 7155 xpack =>{ functions => 7156 [ ['epack','w = x pad','x = w'], 7157 ], 7158 defaults => 'ref=0,pad=0', 7159 }, 7160 ypack =>{ functions => 7161 [ ['epack','h = y pad','y = h'], 7162 ], 7163 defaults => 'ref=0,pad=0', 7164 }, 7165); 7166 7167sub new_songcol 7168{ my ($class,$colid,$width)=@_; 7169 my $sort= $SongTree::STC{$colid}{sort}; 7170 my $self=bless {colid => $colid, width => $width, 'sort' => $sort }, $class; 7171 return $self; 7172} 7173 7174sub init_songs 7175{ my ($widget,$cells,$xpad,$ypad)=@_; 7176 my $initcontext={ widget => $widget, init=>1, }; 7177 my $constant={ xpad=>$xpad, ypad=>$ypad, playmarkup=> 'weight="bold"' }; #FIXME should be quoted : q('weight="bold"') 7178 my @blh; my @y_refs; 7179 my @Deps; 7180 for my $cell (@$cells) 7181 { my $colid=$cell->{colid}; 7182 my $def= $SongTree::STC{$colid} || {}; 7183 my (@draw,@elems); 7184 for my $part (@{ $def->{elems} }) 7185 { my ($eid,$elem,$opt)= $part=~m/^(\w+)=(\w+)\s*\((.*)\)$/; 7186 next unless $elem; 7187 push @elems,[$eid.':',$elem,$opt]; 7188 push @draw,$eid; 7189 } 7190 my $h = $def->{hreq}; 7191 my $bl= $def->{songbl}; 7192 push @elems, ['',undef,"hreq=$h"] if $h; 7193 my ($dep,$update)=createdep(\@elems,'song',$constant); 7194 $cell->{event}= [keys %{$update->{event}}] if $update->{event}; 7195 $cell->{watchfields}=[keys %{$update->{col}}] if $update->{col}; 7196 $cell->{draw}=\@draw; 7197 7198 m/^(\w+:)init_(\w+)$/ and ($dep->{$_},$dep->{$1.$2})=($dep->{$1.$2},$dep->{$_}) for keys %$dep; #exchange init_* keys with normal keys 7199 push @Deps,$dep; 7200 if ($bl) 7201 { my @init; 7202 push @init, $_.':blp', $_.':h' for split /\|/,$bl; 7203 $dep->{init}=[undef, @init]; 7204 my $var=GMB::Expression::Make($dep,'init',$initcontext); 7205 push @blh,$var->{$_} for @init; 7206 push @y_refs, \$dep->{$_.':y'} for split /\|/,$bl; 7207 } 7208 } 7209 if (@blh) 7210 { my ($h,@y)=blalign(undef,0,0,@blh); #compute the y of elements aligned with songbl 7211 ${$y_refs[$_]}= [ $y[$_]||0 ] for 0..$#y; #set the y 7212 } 7213 my $maxh=1; 7214 for my $cell (@$cells) 7215 { my $dep=shift @Deps; 7216 if ($dep->{hreq}) 7217 { my $var=GMB::Expression::Make($dep,'hreq',$initcontext); 7218 my $h= $var->{hreq}||0; 7219 $maxh=$h if $h > $maxh; 7220 } 7221 m/^(\w+:)init_(\w+)$/ and $dep->{$1.$2}=$dep->{$_} for keys %$dep; #revert init_ keys 7222 $cell->{draw}=GMB::Expression::MakeMake($dep,$cell->{draw}); 7223 } 7224 return $maxh; 7225} 7226 7227sub new_group 7228{ my ($class,$widget,$depth,$grouptype,$skin)=@_; 7229 my $constant={ xpad=>0, ypad=>0, }; 7230 if ($skin=~s#\((.*)\)$##) #skin options 7231 { my $opt=::ParseOptions($1); 7232 for my $key (keys %$opt) 7233 { my $v=::decode_url($opt->{$key}); 7234 $v=~s#'#\\'#g; 7235 $constant->{$key}="'$v'"; 7236 } 7237 } 7238 if (my $ref0=$SongTree::GroupSkin{$skin}{options}) 7239 { for my $key (keys %$ref0) { $constant->{$key}="'".$ref0->{$key}{default}."'" unless exists $constant->{$key} } 7240 } 7241 my $def=$SongTree::GroupSkin{$skin} || {}; 7242 my $self=bless 7243 { grouptype=> $grouptype, 7244 depth => $depth, 7245 }, $class; 7246 my @elems; 7247 my @draw; my %hide; 7248 for my $part (@{$def->{elems}}) 7249 { my ($eid,$exp,$elem,$opt)= $part=~m/^(\w+)=([+-])?(\w+)\s*\((.*)\)$/; 7250 next unless $elem; 7251 push @elems,[$eid.':',$elem,$opt]; 7252 $hide{$eid.':hide'}= ($exp eq '+') if defined $exp; 7253 push @draw,$eid; 7254 } 7255 my @init=map $_.'='.($def->{$_}||0), qw/head tail left right vmin vcollapse/; 7256 push @elems, ['',undef,join ',',@init]; 7257 my ($dep,$update)=createdep(\@elems,'group',$constant); 7258 $self->{event}=[keys %{$update->{event}}] if $update->{event}; 7259 $self->{watchfields}=[keys %{$update->{col}}] if $update->{col}; 7260 for my $key (keys %hide) 7261 { my $hide; 7262 $hide='!' if $hide{$key}; 7263 $hide.= '$arg->{expanded}'; 7264 $hide.= '|| ('.$dep->{$key}[0].')' if exists $dep->{$key}; 7265 $dep->{$key}[0]= $hide; 7266 } 7267 7268 my $initcontext={widget => $widget, expanded =>1, init=>1, depth => $depth, grouptype =>$grouptype}; 7269 $dep->{init}=[undef,qw/head tail left right vmin/]; 7270 m/^(\w+:)init_(\w+)$/ and ($dep->{$_},$dep->{$1.$2})=($dep->{$1.$2},$dep->{$_}) for keys %$dep; #exchange init_* keys with normal keys 7271 my $var=GMB::Expression::Make($dep,'init',$initcontext); 7272 $self->{$_}=$var->{$_} for qw/head tail left right vmin/; 7273 7274 $dep->{init0}=[undef,'vcollapse']; 7275 $initcontext->{expanded}=0; 7276 my $var0=GMB::Expression::Make($dep,'init0',$initcontext); 7277 $self->{vcollapse}=$var0->{vcollapse}; 7278 7279 m/^(\w+:)init_(\w+)$/ and $dep->{$1.$2}=$dep->{$_} for keys %$dep; #revert init_ keys 7280 $self->{draw}=GMB::Expression::MakeMake($dep,\@draw); 7281 return $self; 7282} 7283 7284sub createdep 7285{ my ($elems,$context,$constant)=@_; 7286 my (%update,%dep,%default,%children); 7287 #process options 7288 for my $elem (@$elems) 7289 { my ($eid,$elem,$opt)=@$elem; 7290 $opt=GMB::Expression::split_options($opt,$eid); 7291 $children{$eid.'children'}= delete $opt->{$eid.'children'}; 7292 GMB::Expression::parse($opt,$context,\%update,\%dep,$constant); 7293 if ($elem && $GraphElem{$elem} && $GraphElem{$elem}{defaults}) 7294 { my $default= $GraphElem{$elem}{defaults}; 7295 $default=~s/___/$eid/g; 7296 $default=GMB::Expression::split_options($default,$eid); 7297 delete $default->{$_} for keys %$opt; 7298 GMB::Expression::parse($default,$context,\%update,\%default,$constant); 7299 } 7300 } 7301 #process functions 7302 for my $elem (@$elems) 7303 { my ($eid,$elem)=@$elem; 7304 next unless $elem && $GraphElem{$elem}; 7305 for my $ref (@{ $GraphElem{$elem}{functions} }) 7306 { my ($code,$params,$cparams)=@$ref; 7307 my ($out,$in)=split /\s*=\s*/, $params,2; 7308 my @in= map $eid.$_, split / +/,$in; 7309 my @out= map $eid.$_, split / +/,$out; 7310 $code=__PACKAGE__.'::'.$code unless $code=~m/::/; 7311 if ($children{$eid.'children'} && $cparams) 7312 { ($out,$in)=split /\s*=\s*/, $cparams,2; 7313 for my $child (split /\|/,$children{$eid.'children'}) 7314 { push @in, map $child.':'.$_, split / +/,$in; 7315 push @out, map $child.':'.$_, split / +/,$out; 7316 } 7317 } 7318 $code=[$code,@out];# if @out >1; 7319 $default{$_}=[$code,@in] for @out; 7320 #warn "$_ code=$code->[0] with (@in)\n" for @out; 7321 } 7322 } 7323 #process optional functions 7324 for my $elem (@$elems) 7325 { my ($eid,$elem)=@$elem; 7326 next unless $elem && $GraphElem{$elem}; 7327 my $optional= $GraphElem{$elem}{optional}; 7328 next unless $optional; 7329 for my $ref (@$optional) 7330 { my ($code,$params)=@$ref; 7331 my ($out,$in)=split /\s*=\s*/, $params,2; 7332 my @in= map $eid.$_, split / +/,$in; 7333 my @out= map $eid.$_, split / +/,$out; 7334 my $present= grep exists $dep{$_}, @in; 7335 next unless $present==@in; 7336 $code=__PACKAGE__.'::'.$code unless $code=~m/::/; 7337 $code=[$code,@out];# if @out >1; 7338 $dep{$_}=[$code,@in] for @out; 7339 #warn "$_ code=$code->[0] with (@in)\n" for @out; 7340 } 7341 } 7342 $dep{'@DEFAULT'}=\%default; 7343 return \%dep,\%update; 7344} 7345 7346sub markup_layout 7347{ my ($arg,$text,$markup,$rotate,$hide)=@_; 7348 return if $hide; 7349 my $pangocontext=$arg->{widget}->create_pango_context; 7350 if ($rotate && $Gtk2::VERSION >= ($Gtk2::VERSION<1.150 ? 1.146 : 1.154)) 7351 { #$pangocontext->set_base_gravity('east'); 7352 my $matrix=Gtk2::Pango::Matrix->new; 7353 $matrix->rotate($rotate); 7354 $pangocontext->set_matrix($matrix); 7355 } 7356 my $layout=Gtk2::Pango::Layout->new($pangocontext); 7357 if (defined $markup) { $markup=~s#(?:\\n|<br>)#\n#g; $layout->set_markup($markup); } 7358 else { $text='' unless defined $text; $layout->set_text($text); } 7359 return $layout; 7360} 7361sub layout_size 7362{ my ($arg,$layout)=@_; 7363 return 0,0,0 unless $layout; 7364 my $bl=$layout->get_iter->get_baseline / Gtk2::Pango->scale; 7365 return $layout->get_pixel_size, $bl; 7366} 7367sub layout_size2 #version using a cache because of a memory leak in layout->get_iter (http://bugzilla.gnome.org/show_bug.cgi?id=482795) only used with gtk2-perl version <1.161 7368 #FIXME might not work correctly in all cases 7369{ my ($arg,$layout,$markup)=@_; 7370 return 0,0,0 unless $layout; 7371 my ($w,$h)=$layout->get_pixel_size; 7372 $markup||=''; $markup=~s#>[^<]+<#>.<#g; $markup=~s#^[^<]+##g; $markup=~s#[^>]+$##g; 7373 my $bl= $arg->{self}{baseline}{$h.$markup}||= $layout->get_iter->get_baseline / Gtk2::Pango->scale; 7374 return $w,$h,$bl; 7375} 7376sub layout_draw 7377{ my ($arg,$layout,$x,$y,$w,$h)=@_; 7378 return unless $layout; 7379#warn "drawing layout at x=$x y=$y text=".$layout->get_text."\n"; 7380 $x+=$arg->{x}; 7381 $y+=$arg->{y}; 7382 my $clip= Gtk2::Gdk::Rectangle->new($x,$y,$w,$h)->intersect($arg->{clip}); 7383 return unless $clip; 7384 $layout->set_width($w * Gtk2::Pango->scale); $layout->set_ellipsize('end'); #ellipsize 7385 $arg->{style}->paint_layout($arg->{window},$arg->{state},1,$clip,$arg->{widget}{stylewidget},'cellrenderertext',$x,$y,$layout); 7386# my $gc=$arg->{style}->text_gc($arg->{state}); 7387# $gc->set_clip_rectangle($clip); 7388# $arg->{window}->draw_layout($gc,$x,$y,$layout); 7389# $gc->set_clip_rectangle(undef); 7390} 7391sub box_draw 7392{ my ($arg,$x,$y,$w,$h,$color,$filled,$width,$hide)=@_; 7393 return if $hide; 7394 $x+=$arg->{w} if $x<0; 7395 $y+=$arg->{h} if $y<0; 7396 $w+=$arg->{w} if $w<=0; 7397 $h+=$arg->{h} if $h<=0; 7398 $x+=$arg->{x}; 7399 $y+=$arg->{y}; 7400 my $gc=Gtk2::Gdk::GC->new($arg->{window}); 7401 $gc->set_clip_rectangle($arg->{clip}); 7402 $color||= 'fg'; 7403 $color= $color eq 'fg' ? $arg->{style}->fg('normal') : Gtk2::Gdk::Color->parse($color); 7404 $gc->set_rgb_fg_color($color); 7405 my $line='solid';#'on-off-dash' 'double-dash' 7406 my $cap='not-last'; #'butt' 'round' 'projecting' 7407 my $join='round';# 'miter' 'bevel' 7408 $gc->set_line_attributes($width,$line,$cap,$join); 7409 #my $dashes='5 5 0 5 5'; 7410 #$gc->set_dashes(split / +/, $dashed); 7411# warn "rect : $x,$y,$w,$h\n"; 7412 $arg->{window}->draw_rectangle($gc,$filled||0,$x,$y,$w,$h); 7413 7414} 7415sub pbar_draw 7416{ my ($arg,$x,$y,$w,$h,$fill,$hide)=@_; 7417 return if $hide; 7418 $x+=$arg->{w} if $x<0; 7419 $y+=$arg->{h} if $y<0; 7420 $w+=$arg->{w} if $w<=0; 7421 $h+=$arg->{h} if $h<=0; 7422 $x+=$arg->{x}; 7423 $y+=$arg->{y}; 7424 $fill=0 if $fill<0; 7425 $fill=1 if $fill>1; 7426 my $stylew=$arg->{self}{progressbar}||=Gtk2::ProgressBar->new; 7427 $arg->{style}->paint_box($arg->{window}, 'normal', 'in', $arg->{clip}, $stylew, 'though', $x, $y, $w, $h); 7428 $arg->{style}->paint_box($arg->{window}, 'prelight', 'out', $arg->{clip}, $stylew, 'bar', $x, $y, $w*$fill, $h); 7429} 7430sub line_draw 7431{ my ($arg,$x1,$y1,$x2,$y2,$color,$width,$hide)=@_; 7432 return if $hide; 7433 my ($offx,$offy)= @{$arg}{'x','y'}; 7434 $x1+=$arg->{w} if $x1<0; 7435 $x2+=$arg->{w} if $x2<0; 7436 $y1+=$arg->{h} if $y1<0; 7437 $y2+=$arg->{h} if $y2<0; 7438 $x1+=$offx; $y1+=$offy; 7439 $x2+=$offx; $y2+=$offy; 7440 my $gc=Gtk2::Gdk::GC->new($arg->{window}); 7441 my $line='solid';#'on-off-dash' 'double-dash' 7442 my $cap='not-last'; #'butt' 'round' 'projecting' 7443 my $join='round';# 'miter' 'bevel' 7444 $gc->set_line_attributes($width,$line,$cap,$join); 7445 $gc->set_clip_rectangle($arg->{clip}); 7446 $color||= 'fg'; 7447 $color= $color eq 'fg' ? $arg->{style}->fg('normal') : Gtk2::Gdk::Color->parse($color); 7448 $gc->set_rgb_fg_color($color); 7449 $arg->{window}->draw_line($gc,$x1,$y1,$x2,$y2); 7450} 7451 7452sub pic_cached 7453{ my ($arg,$file,$resize,$w,$h,$xpad,$ypad,$crop,$hide)=@_; 7454 return undef,0 if $hide || !$file; 7455 if (defined $w || defined $h) 7456 { if (defined $w) { $w-=2*$xpad; return undef,0 if $w<=0 } 7457 else {$w=0; $resize='ratio'} 7458 if (defined $h) { $h-=2*$ypad; return undef,0 if $h<=0 } 7459 else {$h=0; $resize='ratio'} 7460 $resize||='s'; 7461 $resize.="_$w"."_$h"; 7462 } 7463 my $cached=GMB::Picture::load_skinfile($file,$crop,$resize); 7464 return $cached||$resize, !$cached; 7465} 7466sub pic_size 7467{ my ($arg,$cached,$file,$crop,$hide)=@_; 7468 return undef,0,0 if $hide || !$file; 7469 my $pixbuf=$cached; 7470 unless (ref $cached) #=> cached is resize_w_h 7471 { $pixbuf=GMB::Picture::load_skinfile($file,$crop,$cached,1); 7472 } 7473 return undef,0,0 unless $pixbuf; 7474 return $pixbuf,$pixbuf->get_width,$pixbuf->get_height; 7475} 7476sub icon_size 7477{ my ($arg,$size,$icon,$y,$h,$xpad,$ypad,$hide)=@_; 7478 return 0,0,0,0,0 if $hide; 7479 my ($w1,$h1)=Gtk2::IconSize->lookup($size); 7480 my $nb= ref $icon ? @$icon : (defined $icon && $icon ne ''); 7481 return 0,0 unless $nb; 7482 $y||=0; 7483 $y+=$arg->{h} if $y<0; 7484 $h||=0; 7485 $h+=$arg->{h}-$y if $h<=0; 7486 $h+=$ypad; 7487 $w1+=$xpad; 7488 $h1+=$ypad; 7489 my $nbh=$nb; 7490 if ($nb*$h1>$h) { $nbh=int($h/$h1) } 7491 $nbh=1 unless $nbh; 7492 my $hr= $nbh*$h1-$ypad; 7493 my $wr= $w1*(int($nb/$nbh) + (($nb % $nbh) ? 1 : 0)); 7494 return $wr,$hr,$nbh,$w1,$h1; 7495} 7496sub icon_draw 7497{ my ($arg,$icon,$size,$x,$y,$w,$h,$nbh,$w1,$h1,$hide)=@_; 7498 return if $hide; 7499 return unless defined $icon && $icon ne ''; 7500 $x+=$arg->{x}; 7501 $y+=$arg->{y}; 7502 my $clip= Gtk2::Gdk::Rectangle->new($x,$y,$w,$h)->intersect($arg->{clip}); 7503 return unless $clip; 7504 my $gc=Gtk2::Gdk::GC->new($arg->{window}); 7505 $gc->set_clip_rectangle($clip); 7506 my $i=0; my $y0=$y; 7507 for my $icon (ref $icon ? @$icon : $icon) 7508 { my $pixbuf=$arg->{widget}->render_icon($icon,$size); 7509 next unless $pixbuf; 7510 $arg->{window}->draw_pixbuf($gc, $pixbuf,0,0, $x,$y, -1,-1,'none',0,0); 7511 $i++; 7512 if ($i>=$nbh) {$y=$y0; $x+=$w1; $i=0;} else {$y+=$h1} 7513 } 7514} 7515 7516sub pad_and_align 7517{ my ($context,$x,$xpad,$pad,$xalign,$wr,$w)=@_; 7518 $xpad||= $pad||0; 7519 $xalign||= 0; 7520 $x||=0; 7521 $x+=$context->{w} if $x<0; 7522 $w||=$wr+2*$xpad; 7523 $w+=$context->{w}-$x if $w<=0; 7524 my $wd= $w -2*$xpad; 7525 $x+= $xpad + $xalign *($wd-$wr); 7526 return $x,$wd; 7527} 7528 7529sub aapic_cached 7530{ my ($arg,$picsize,$aa,$ids,$aanb,$hide)=@_; 7531 return undef,0 if $hide; 7532 #$aa||=$arg->{grouptype}; 7533 #$now=1 if $param->{notdelayed}; 7534 my $gid; 7535 if (ref $ids) { $gid= (::uniq( Songs::Map_to_gid($aa,$ids)))[$aanb]; } 7536 elsif (!$aanb){ $gid= Songs::Get_gid($ids,$aa); } 7537 my $pixbuf= defined $gid ? AAPicture::pixbuf($aa,$gid,$picsize) : undef; 7538 my ($aap,$queue)= $pixbuf ? ($pixbuf,undef) : 7539 defined $pixbuf ? ([$aa,$gid,$picsize],1) : 7540 (undef,undef); 7541 return $aap,$queue; 7542} 7543sub aapic_size 7544{ my ($arg,$aap,$queue)=@_; 7545 return undef,0,0 unless $aap; 7546 my $pixbuf= (ref $aap eq 'ARRAY') ? AAPicture::pixbuf(@$aap,1) : $aap; 7547 return undef,0,0 unless $pixbuf; 7548 return $pixbuf,$pixbuf->get_width,$pixbuf->get_height; 7549} 7550sub pixbuf_draw 7551{ my ($arg,$pixbuf,$x,$y,$w,$h)=@_; 7552 return unless $pixbuf; 7553 $x+=$arg->{x}; 7554 $y+=$arg->{y}; 7555 my $clip= Gtk2::Gdk::Rectangle->new($x,$y,$w,$h)->intersect($arg->{clip}); 7556 return unless $clip; 7557 my $gc=Gtk2::Gdk::GC->new($arg->{window}); 7558 $gc->set_clip_rectangle($clip); 7559 $arg->{window}->draw_pixbuf($gc, $pixbuf,0,0, $x,$y, -1,-1,'none',0,0); 7560} 7561 7562#sub exp_size 7563#{ my ($arg,$hide)=@_; 7564# return 0,0 if $hide; 7565# return wr hr; 7566#} 7567#sub exp_draw 7568#{ my ($arg,$xd,$yd,$wd,$hd,$hide)=@_; 7569# $style->paint_expander($window, $state_type, $area, $widget, $detail, $x, $y, $expander_style); 7570#} 7571 7572sub set_action #TESTING 7573{ my ($arg,$x,$y,$w,$h,$actions,$hide)=@_; 7574 return if $hide || !ref $actions; 7575 $x+=$arg->{vx}; 7576 $y+=$arg->{vy}; 7577 my %ac=@$actions; 7578 $arg->{widget}{action_rectangles}{join ',',$x,$y,$w,$h}{$_}=$ac{$_} for keys %ac; 7579} 7580 7581sub blalign #align baselines 7582{ my (undef,$y,$ref,@blh)=@_; #warn "blalign <- ($y,$ref,@blh)\n"; 7583 my @y; my ($min,$max)=($y,0); 7584 for (my $i=0;$i<@blh;$i+=2) 7585 { my $cy= $y - $blh[$i]; 7586 $min=$cy if $min>$cy; 7587 push @y,$cy; 7588 $cy+=$blh[$i+1]; 7589 $max=$cy if $max<$cy; 7590 }#warn " @y max=$max min=$min\n"; 7591 my $h=$max-$min; 7592 $_-= $min+$h*$ref-$y for @y; 7593 #warn "blalign -> ($h,@y)\n"; 7594 return $h,@y; 7595} 7596sub align 7597{ my (undef,$align,$x,$ref,@cw)=@_;# warn "align <- ($align,$x,$ref,@cw)\n"; 7598 $ref=$align unless defined $ref; 7599 my $max=0; 7600 $max<$_ and $max=$_ for @cw; 7601 $max*=$align; 7602 my @x=map $x - $max*$ref + $align*($max-$_), @cw; 7603 #warn "align -> ($max,@x)\n"; 7604 return $max,@x; 7605} 7606sub epack 7607{ my (undef,$x,$pad,@cw)=@_; 7608 $pad||=0; 7609 my @x; 7610 for my $cw (@cw) 7611 { push @x,$x; 7612 $x+= $cw+$pad; 7613 } 7614 return $x,@x; 7615} 7616 7617package GMB::Edit::STGroupings; 7618use base 'Gtk2::Box'; 7619 7620my %opt_types= 7621( Text => [ sub {my $entry=Gtk2::Entry->new;$entry->set_text($_[0]); return $entry}, sub {$_[0]->get_text},1 ], 7622 Color => [ sub { Gtk2::ColorButton->new_with_color( Gtk2::Gdk::Color->parse($_[0]) ); }, 7623 sub {my $c=$_[0]->get_color; sprintf '#%02x%02x%02x',$c->red/256,$c->green/256,$c->blue/256; }, 1 ], 7624 Font => [ sub { Gtk2::FontButton->new_with_font($_[0]); }, sub {$_[0]->get_font_name}, 1 ], 7625 Boolean => [ sub { my $c=Gtk2::CheckButton->new($_[1]); $c->set_active(1) if $_[0]; return $c }, sub {$_[0]->get_active}, 0 ], 7626 Number => [ sub { my $s=Gtk2::SpinButton->new_with_range($_[2]{min}||0, $_[2]{max}||9999, $_[2]{step}||1); 7627 $s->set_digits($_[2]{digits}) if $_[2]{digits}; 7628 #::setlocale(::LC_NUMERIC,'C'); 7629 $s->set_value($_[0]); 7630 #::setlocale(::LC_NUMERIC,''); 7631 return $s; 7632 }, 7633 sub { ::setlocale(::LC_NUMERIC,'C'); my $v=''.$_[0]->get_value; ::setlocale(::LC_NUMERIC,''); return $v}, 1 ], 7634 Combo => [ sub { my @l=split( /\|/,$_[2]{list} ); 7635 my @l2; 7636 while (@l) { my $w=shift @l; $w.="|".shift(@l) while @l && $w=~s/\\$//; push @l2,$w; } 7637 TextCombo->new( \@l2, $_[0]); 7638 }, 7639 sub {$_[0]->get_value},1 ], 7640); 7641 7642sub new 7643{ my ($class,$dialog,$init) = @_; 7644 my $self = bless Gtk2::VBox->new, $class; 7645 my $vbox=Gtk2::VBox->new; 7646 my $sw = Gtk2::ScrolledWindow->new; 7647 $sw->set_shadow_type('etched-in'); 7648 $sw->set_policy('never','automatic'); 7649 $sw->add_with_viewport($vbox); 7650 $self->{vbox}=$vbox; 7651 my $badd= ::NewIconButton('gtk-add',_"Add a group",sub {$_[0]->parent->AddRow('album|default');} ); 7652 $self->add($sw); 7653 $self->pack_start($badd,0,0,2); 7654 $self->Set($init); 7655 return $self; 7656} 7657 7658sub Set 7659{ my ($self,$string)=@_; 7660 my $vbox=$self->{vbox}; 7661 $vbox->remove($_) for $vbox->get_children; 7662 for my $group ($string=~m#([^|]+\|[^|]+)(?:\||$)#g) #split into "word|word" 7663 { $self->AddRow($group); 7664 } 7665} 7666 7667sub AddRow 7668{ my ($self,$string)=@_; 7669 my ($type,$skin)=split /\|/,$string; 7670 my $opt; 7671 if ($skin=~s/\((.*)\)$//) { $opt=::ParseOptions($1) } 7672 my $typelist=TextCombo::Tree->new( Songs::ListGroupTypes(), $type ); 7673 my $skinlist=TextCombo->new({map {$_ => $SongTree::GroupSkin{$_}{title}||$_} keys %SongTree::GroupSkin}, $skin, \&skin_changed_cb ); 7674 my $button=::NewIconButton('gtk-remove',undef,sub 7675 { my $button=$_[0]; 7676 my $box=$button->parent->parent; 7677 $box->parent->remove($box); 7678 },'none'); 7679 my $fopt=Gtk2::Expander->new; 7680 my $vbox=Gtk2::VBox->new; 7681 my $hbox=Gtk2::HBox->new; 7682 $hbox->pack_start($_,0,0,2) for $button, 7683 Gtk2::Label->new(_"Group by :"), $typelist, 7684 Gtk2::Label->new(_"using skin :"), $skinlist; 7685 my $optbox=Gtk2::HBox->new; 7686 my $filler=Gtk2::HBox->new; 7687 my $sg=Gtk2::SizeGroup->new('horizontal'); 7688 $sg->add_widget($_) for $button,$filler; 7689 $optbox->pack_start($_,0,0,2) for $filler,$fopt; 7690 $vbox->pack_start($_,0,0,2) for $hbox,$optbox; 7691 $vbox->{type}=$typelist; 7692 $vbox->{skin}=$skinlist; 7693 $vbox->{fopt}=$fopt; 7694 $fopt->set_no_show_all(1); 7695 $vbox->show_all; 7696 skin_changed_cb($skinlist,$opt); 7697 $self->{vbox}->pack_start($vbox,0,0,2); 7698} 7699 7700sub skin_changed_cb 7701{ my ($combo,$opt)=@_; 7702 my $skin=$combo->get_value; 7703 my $hbox=$combo; $hbox=$hbox->parent until $hbox->{fopt}; 7704 my $fopt=$hbox->{fopt}; 7705 $fopt->remove($fopt->child) if $fopt->child; 7706 delete $fopt->{entry}; 7707 $fopt->set_label( _"skin options" ); 7708 my $table=Gtk2::Table->new(2,1,0); my $row=0; 7709 my $ref0=$SongTree::GroupSkin{$skin}{options}; 7710 for my $key (sort keys %$ref0) 7711 { my $ref=$ref0->{$key}; 7712 my $type=$ref->{type}; 7713 $type='Text' unless exists $opt_types{$type}; 7714 my $l=$ref->{name}||$key; 7715 my $label=Gtk2::Label->new($l); 7716 $label->set_alignment(0,.5); 7717 my $v=$ref->{default}; 7718 $v=::decode_url($opt->{$key}) if $opt && exists $opt->{$key}; 7719 $v='' unless defined $v; 7720 my $entry= $opt_types{$type}[0]($v,$l,$ref); 7721 my $x=0; 7722 if ($opt_types{$type}[2]) 7723 { $table->attach($label, 0, 1, $row, $row+1, ['expand','fill'], [], 2, 2); 7724 $x=1; 7725 } 7726 $table->attach($entry, $x, 2, $row, $row+1, ['expand','fill'], [], 2, 2); 7727 $row++; 7728 $fopt->{entry}{$key}=$entry; 7729 } 7730 if ($fopt->{entry}) 7731 { $fopt->add($table); 7732 $table->show_all; 7733 $fopt->show; 7734 } 7735 else {$fopt->hide} 7736} 7737 7738sub Result 7739{ my $self=shift; 7740 my $vbox=$self->{vbox}; 7741 my @groups; 7742 for my $hbox ($vbox->get_children) 7743 { my $type=$hbox->{type}->get_value; 7744 my $skin=$hbox->{skin}->get_value; 7745 my $group="$type|$skin"; 7746 if (my $h=$hbox->{fopt}{entry}) 7747 { my @opt; 7748 for my $key (sort keys %$h) 7749 { my $type=$SongTree::GroupSkin{$skin}{options}{$key}{type}; 7750 my $v= $opt_types{$type}[1]($h->{$key}); 7751 push @opt,$key.'='.::url_escapeall($v); 7752 } 7753 $group.='('.join(',',@opt).')'; 7754 } 7755 push @groups,$group; 7756 } 7757 return join '|',@groups; 7758} 7759 7760package GMB::Expression; 7761no warnings; 7762 7763our %alias=( 'if' => 'iff', pesc => '::PangoEsc', min =>'::min', max =>'::max', sum =>'::sum',); 7764our %functions= 7765( formattime=> ['do {my ($f,$t,$z)=(', '); !$t && defined $z ? $z : ::strftime_utf8($f,localtime($t)); }'], 7766 #sum => ['do {my $sum; $sum+=$_ for ', ';$sum}'], 7767 average => ['do {my $sum=::sum(', '); @l ? $sum/@l : undef}'], 7768 #max => ['do {my ($max,@l)=(', '); $_>$max and $max=$_ for @l; $max}'], 7769 #min => ['do {my ($min,@l)=(', '); $_<$min and $min=$_ for @l; $min}'], 7770 iff => ['do {my ($cond,$res,@l)=(', '); while (@l>1) {last if $cond; $cond=shift @l;$res=shift @l;} $cond ? $res : $l[0] }'], 7771 size => ['do {my ($l)=(', '); ref $l ? scalar @$l : 1}'], 7772 ratingpic=> ['Songs::Stars(', ',"rating");'], 7773 playmarkup=> \&playmarkup, 7774); 7775$functions{$_}||=undef for qw/ucfirst uc lc chr ord not index length substr join sprintf warn abs int rand/, values %alias; 7776our %vars2= 7777(song=> 7778 { #ufile #REMOVED PHASE1 fix the doc 7779 #upath #REMOVED PHASE1 fix the doc 7780 progress=> ['$arg->{ID}==$::SongID ? $::PlayTime/Songs::Get($arg->{ID},"length") : 0', 'length','CurSong Time'], 7781 queued => ['do {my $i;my $f;for (@$::Queue) {$i++; $f=$i,last if $arg->{ID}==$_};$f}',undef,'Queue'], 7782 playing => ['$arg->{ID}==$::SongID', undef,'CurSong'], 7783 playicon=> ['::Get_PPSQ_Icon($arg->{ID},!$arg->{currentsong})', undef,'Playing Queue CurSong'], 7784 labelicons=>['[Songs::Get_icon_list("label",$arg->{ID})]', 'label','Icons'], 7785 ids => ['$arg->{ID}'], 7786 }, 7787 group=> 7788 { ids => ['$arg->{groupsongs}'], 7789 year => ['groupyear($arg->{groupsongs})', 'year'], 7790 artist => ['groupartist("artist",$arg->{groupsongs})', 'artist'], 7791 album_artist=> ['groupartist("album_artist",$arg->{groupsongs})', 'album_artist'], 7792 album_artistid=>['groupartistid("album_artist",$arg->{groupsongs})', 'album_artist'], 7793 album => ['groupalbum($arg->{groupsongs},0)', 'album'], 7794 albumraw=> ['groupalbum($arg->{groupsongs},1)', 'album'], 7795 artistid=> ['groupartistid($arg->{groupsongs})','artist'], 7796 albumid => ['groupalbumid($arg->{groupsongs})', 'album'], 7797 genres => ['groupgenres($arg->{groupsongs},"genre")', 'genre'], 7798 labels => ['groupgenres($arg->{groupsongs},"label")', 'label'], 7799 gid => ['Songs::Get_gid($arg->{groupsongs}[0],$arg->{grouptype})'], #FIXME PHASE1 7800 title => ['($arg->{groupsongs} ? Songs::Get_grouptitle($arg->{grouptype},$arg->{groupsongs}) : "")'], #FIXME should the init case ($arg->{groupsongs}==undef) be treated here ? 7801 rating_avrg => ['do {my $sum; $sum+= $_ for Songs::Map(ratingnumber=>$arg->{groupsongs}); $sum/@{$arg->{groupsongs}}; }', 'rating'], #FIXME round, int ? 7802 'length' => ['do {my (undef,$v)=Songs::ListLength($arg->{groupsongs}); sprintf "%d:%02d",$v/60,$v%60;}', 'length'], 7803 nbsongs => ['scalar @{$arg->{groupsongs}}'], 7804 disc => ['groupdisc($arg->{groupsongs})', 'disc'], 7805 discname=> ['groupdiscname($arg->{groupsongs})','discname'], 7806 } 7807); 7808 7809my %PCompl=( '{','}', '(',')', '[',']' , '"', =>0, "'"=>0, ); 7810 7811sub split_options #doesn't work the same as ParseOptions : count parens and don't remove quotes #FIXME find a way to merge them ? 7812{ (local $_,my $prefix)=@_; 7813 my %opt; 7814 while (1) 7815 { my ($key,$begin,$end,@closing); 7816 if (m#\G\s*(\w+)=#gc) {$key=$1;$begin=pos} 7817 else {last} 7818 while (m#\G[^]["'{}(),]*#gc && m#\G(.)#gc) 7819 { if ($1 eq ',') 7820 { next if @closing; 7821 $end=pos()-1; 7822 last; 7823 } 7824 my $c= $PCompl{$1}; 7825 if ($c) { push @closing,$c; } #opening (, [ or { 7826 elsif (defined $c) #quote " or ' 7827 { if ($1 eq '"') {m#\G(?:[^"\\]|\\.)*"#gc} 7828 else {m#\G(?:[^'\\]|\\.)*'#gc} 7829 } 7830 else #closing ), ], or } 7831 { shift @closing if $closing[0] eq $1; 7832 } 7833 } 7834 my $l= ($end||pos) -$begin; 7835 $opt{$prefix.$key}= substr $_,$begin,$l; 7836 $key=undef; 7837 last unless $end; 7838 } 7839 return \%opt; 7840} 7841 7842sub parse 7843{ my ($hash,$context,$update,$code_dep,$constant)=@_; 7844 my (%funcused,%var2used,%argused,@watch); 7845 7846 while ( (my$key,local $_)=each %$hash ) 7847 { my (@f_end,@pcount,$pcount,%var1used); my $r=''; my $depth=0; 7848 while (1) 7849 { while (m#\G\s*([([])#gc) # opening ( or [ 7850 { $depth++; 7851 $r.=$1; 7852 $pcount++; 7853 } 7854 m#\s*#g; 7855 if (m#\G(-?\d*\.?\d+)#gc) {$r.=$1} #number 7856 elsif (m#\G(''|'.*?[^\\]')#gc) {$r.=$1} #string between ' ' 7857 #variable or function 7858 elsif (m#\G([-!]\s*)?(\$_?)?([a-zA-Z][:0-9_a-zA-Z]*)(\()?#gc) 7859 { last if $2 && $4; 7860 $r.=$1 if $1; 7861 my $v=$3; 7862 $v=$alias{$v} if $alias{$v}; 7863 if ($4) #functions 7864 { my ($pre,$post); 7865 if (exists $functions{$v}) 7866 { $funcused{$v}++; 7867 if (my $ref=$functions{$v}) 7868 { $ref=$ref->($constant) if ref $ref eq 'CODE'; 7869 $pre=$ref->[0]; 7870 $post=$ref->[1]; 7871 push @watch,[undef,$ref->[2],$ref->[3]] if @$ref>2; 7872 } 7873 else { $pre=$v.'('; $post=')'; } 7874 } 7875 else { $pre='error('; $post=')'; } 7876 $r.= $pre; 7877 push @f_end, $post; 7878 push @pcount,$pcount; 7879 $pcount=0; 7880 $depth++; 7881 next; 7882 } 7883 elsif ($2) 7884 { if ($2 eq '$') 7885 { $var2used{$v}++; 7886 my $ref=$vars2{$context}{$v}; 7887 if ($ref) 7888 { push @watch,$ref; 7889 $r.='('.$ref->[0].')'; 7890 } 7891 elsif ($context eq 'song') #for normal fields 7892 { my $action= $v=~s/_$// ? 'get' : 'display'; 7893 push @watch,[undef,$v]; 7894 $r.='('.Songs::Code($v,$action, ID => '$arg->{ID}').')'; 7895 } 7896 else {$r.= "'unknown var \\'$v\\''"}; 7897 } 7898 else { $argused{$v}++; $r.="\$arg->{$v}"; } 7899 } 7900 elsif (exists $constant->{$v}) { $r.=$constant->{$v}; } 7901 else { $var1used{$v}++; $r.="\$var{'$v'}"; } 7902 } 7903 else {last} 7904 while (m#\G\s*([])])#gc) # closing ) or ] 7905 { next if $depth==0; 7906 $depth--; 7907 if ($pcount) {$pcount--;$r.=$1;} 7908 elsif (@f_end) {$r.=pop @f_end; $pcount=pop @pcount} 7909 else {$r.=$1;} 7910 } 7911 if ( m#\G\s*([!=<>]=|[-+.,%*/<>]|&&|\|\|)#gc 7912 || m#\G\s*((?:x|eq|lt|gt|cmp|le|ge|ne|or|xor|and)\s)#gc) {$r.=$1} 7913 else {last} 7914 } # end of parsing for $key 7915 7916 $code_dep->{$key}= [$r,(keys %var1used ? keys %var1used : ())]; 7917 if (length $_!=pos) 7918 { warn "$_\n->$r\n"; 7919 warn "** error at ".pos()." **\n\n"; 7920 } 7921 } #done all keys 7922 7923 if ($update) 7924 { for my $ref (@watch) 7925 { my (undef,$c,$e)=@$ref; 7926 if (defined $c) 7927 { $update->{col}{$_}=undef for Songs::Depends(split / /,$c); 7928 } 7929 if (defined $e) 7930 { $update->{event}{$_}=undef for split / /,$e; 7931 } 7932 } 7933 } 7934} 7935 7936sub MakeMake 7937{ my ($dep,$targets)=@_; 7938 my (@targets0,@targets1); 7939 my $dep0=$dep->{'@DEFAULT'}; 7940 for my $eid (@$targets) 7941 { if ($dep0->{$eid.':queue'}) { push @targets0,$eid.':queue'; push @targets1,$eid; } 7942 elsif ($dep0->{$eid.':draw'}) { push @targets0,$eid.':draw'; } 7943 } 7944 my $sub='sub {my $arg=$_[0]; my %var; my @queued; my @queuedif;'; 7945 my @notdone; my %done; 7946 my $inqueue; 7947 while (1) 7948 { my $target=shift @targets0; 7949 unless ($target) 7950 { $target= shift @targets1; 7951 last unless $target; 7952 push @targets0,@notdone; 7953 @notdone=(); 7954 $inqueue=1; 7955 $sub.="push \@queuedif,'$target:queue'; push \@queued, sub {"; 7956 $target.=':draw'; 7957 } 7958 ($sub,my $notdone)=Make($dep,$target,undef,\%done,$sub); 7959 push @notdone,$target if exists $notdone->{$target}; 7960 if ($inqueue) 7961 { $sub.='};' unless @targets0; 7962 } 7963 } 7964 if ($inqueue) 7965 { $sub.='while (my $if=shift @queuedif) { if ($var{$if}) {last} else {my $sub=shift @queued; &$sub} }'; 7966 $sub.='return @queued ? [@queued,$arg] : undef;'; 7967 } 7968 $sub.='}'; 7969 warn "Couldn't evaluate : @notdone\n" if @notdone; 7970 7971 my $coderef=eval $sub; warn "GMBMakeMake : $sub\n" if $::debug; 7972 if ($@) {warn "\n";my $c=1; for (split "\n",$sub) {warn "$c $_\n";$c++};warn "$sub\n** error : $@\n"; $coderef=sub {};} 7973 return $coderef; 7974} 7975 7976sub Make 7977{ my ($dep,$target,$var,$done,$sub)=@_; 7978 my $compile= $done? 0 : 1; 7979 my $dep0=$dep->{'@DEFAULT'}; 7980 $done||={}; 7981 $sub||='my $arg=$_[0]; my %var;'; 7982 my %todo=($target => undef); 7983 while (exists $todo{$target}) 7984 { #warn "\ntodo :",(join ',',keys %todo),"\n"; 7985 my $previous=join ',',keys %todo; 7986 for my $key (keys %todo) 7987 { #warn "key=$key -- $dep->{$key} --- $dep0->{$key}\n"; 7988 my ($code,@deps)=@{ $dep->{$key}||$dep0->{$key} }; 7989 my $notnow; 7990 for (@deps) 7991 { my $d=$_; 7992 my $opt= $d=~s#\?$##; 7993 next if exists $done->{$d} || !exists $dep->{$d} && ($opt || !exists $dep0->{$d}); 7994 #warn " -> todo $d\n"; 7995 $todo{$d}=undef; 7996 $notnow=1; 7997 } 7998 unless ($notnow) 7999 { #warn "$key ---found in ($code,@deps)\n"; 8000 if (ref $code) 8001 { my ($func,@keys)=@$code; #warn " -> ($func, @keys)\n"; 8002 my $out=join ',',map "'$_'", @keys; 8003 my $in= join ',',map "'$_'", @deps; $in=~s#\?##g; 8004 $out= @keys>1 ? "\@var{$out}" : "\$var{$out}"; 8005 $in = @deps>1 ? "\@var{$in}" : "\$var{$in}"; 8006 $sub.= "$out=$func(\$arg,$in);\n"; 8007 for (@keys) { delete $todo{$_}; $done->{$_}=undef; } 8008 last; 8009 } 8010 else 8011 { $sub.="\$var{'$key'}=$code;\n" if defined $code; 8012 delete $todo{$key}; 8013 $done->{$key}=undef; 8014 next; 8015 } 8016 } 8017 } 8018 my $new=join ',',keys %todo; 8019 if ($previous eq $new) 8020 { warn "** column definition unsolvable for $new **\n" if $compile; 8021 last; 8022 } 8023 } 8024 unless ($compile) { return $sub,\%todo } 8025 $sub.='return \%var;'; warn "\nGMBMake : sub=\n".$sub."\n\n" if $::debug; 8026 my $coderef=eval "sub {$sub}"; 8027 if ($@) {warn "\n";my $c=1; for (split "\n",$sub) {warn "$c $_\n";$c++};warn "** error : $@\n"; $coderef=sub {};} 8028 elsif ($var) { $coderef->($var); } 8029 else { return $coderef} 8030} 8031 8032=unused 8033sub average #not used 8034{ my $sum; 8035 $sum+=$_ for @_; 8036 return (@_? $sum/@_ : undef); 8037} 8038sub max #not used 8039{ my $max=shift; 8040 $_>$max and $max=$_ for @_; 8041 return $max; 8042} 8043sub min #not used 8044{ my $min=shift; 8045 $_<$min and $min=$_ for @_; 8046 return $min; 8047} 8048sub iff #not used 8049{ my $cond=shift; 8050 while (@_>2) 8051 { my $res=shift; 8052 return $res if $cond; 8053 $cond=shift; 8054 } 8055 return $cond ? $_[0] : $_[1]; 8056} 8057 8058=cut 8059 8060sub groupyear 8061{ my $songs=$_[0]; 8062 my %h; 8063 my @y=sort { $a <=> $b } grep $_,Songs::Map('year',$songs); 8064 my $years=''; 8065 if (@y) {$years=$y[0]; $years.=' - '.$y[-1] if $y[-1]!=$years; } 8066 return $years; 8067} 8068 8069sub groupalbumid 8070{ my $songs=$_[0]; 8071 my $l= Songs::UniqList('album',$songs); 8072 return @$l==1 ? $l->[0] : $l; 8073} 8074sub groupartistid ##FIXME PHASE1 use artists instead ? 8075{ my ($field,$songs)=@_; 8076 my $l= Songs::UniqList($field,$songs); 8077 return @$l==1 ? $l->[0] : $l; 8078} 8079 8080sub groupalbum 8081{ my ($songs,$raw)=@_; 8082 my $l= Songs::UniqList('album',$songs); 8083 if (@$l==1) 8084 { my $album= $raw ? Songs::Gid_to_Get('album',$l->[0]) : Songs::Gid_to_Display('album',$l->[0]); 8085 $album='' unless defined $album; 8086 return $album; 8087 } 8088 return ::__("%d album","%d albums",scalar @$l); 8089} 8090sub groupartist #FIXME optimize PHASE1 8091{ my ($field,$songs)=@_; 8092 my $h=Songs::BuildHash($field,$songs); 8093 my $nb=keys %$h; 8094 return Songs::Gid_to_Display($field,(keys %$h)[0]) if $nb==1; 8095 my @l=map split(/$Songs::Artists_split_re/), keys %$h; 8096 my %h2; $h2{$_}++ for @l; 8097 my @common; 8098 for (@l) { if ($h2{$_}>=$nb) { push @common,$_; delete $h2{$_}; } } 8099 return @common ? join ' & ',@common : ::__("%d artist","%d artists",scalar(keys %h2)); 8100} 8101sub groupgenres 8102{ my ($songs,$field,$common)=@_; 8103 my $h=Songs::BuildHash($field,$songs,'name'); 8104 delete $h->{''}; 8105 return join ', ',sort ($common? grep($h->{$_}==@$songs,keys %$h) : keys %$h); 8106} 8107sub groupdisc 8108{ my $songs=$_[0]; 8109 my $h=Songs::BuildHash('disc',$songs); 8110 delete $h->{''}; 8111 if ((keys %$h)==1 && (values %$h)[0]==@$songs) {return (keys %$h)[0]} 8112 else {return ''} 8113} 8114sub groupdiscname 8115{ my $songs=$_[0]; 8116 if (Songs::FieldEnabled('discname')) 8117 { my $h=Songs::BuildHash('discname',$songs); 8118 if ((keys %$h)==1 && (values %$h)[0]==@$songs) 8119 { my $name= Songs::Gid_to_Display('discname',(keys %$h)[0]); 8120 return $name if length $name; 8121 } 8122 else { return '' } #no common discname 8123 } 8124 # if discname field not enabled or no discname, try to make a discname using the disc number 8125 my $d=groupdisc($songs); 8126 return $d ? ::__x(_"disc {disc}",disc =>$d) : ''; 8127} 8128sub error 8129{ warn "unknown function : '$_[0]'\n"; 8130} 8131 8132sub playmarkup 8133{ my $constant=$_[0]; 8134 return ['do { my $markup=', '; $arg->{currentsong} ? \'<span '.$constant->{playmarkup}.'>\'.$markup."</span>" : $markup }',undef,'CurSong']; 8135} 8136 8137 8138=toremove 8139package GMB::RadioList; 8140use base 'Gtk2::Box'; 8141 8142sub new 8143{ my ($class)=@_; 8144 my $self=bless Gtk2::VBox->new, $class; 8145 my $Badd=::NewIconButton('gtk-add',_"Add a radio",\&add_radio_cb); 8146 my $store=Gtk2::ListStore->new('Glib::Uint'); 8147 $self->{treeview}=my $treeview=Gtk2::TreeView->new($store); 8148 my $sw=Gtk2::ScrolledWindow->new; 8149 $sw->set_shadow_type('etched-in'); 8150 $sw->set_policy('automatic','automatic'); 8151 $self->add_column(title => _"Radio title"); 8152 $self->add_column(url => 'url'); 8153 $self->pack_start($Badd,0,0,2); 8154 $self->add($sw); 8155 $sw->add($treeview); 8156 ::Watch($self,RadioList=>\&Refresh); 8157 ::Watch($self,CurSong=> sub {$_[0]->queue_draw}); 8158 $treeview->signal_connect( row_activated => sub 8159 { my ($tv,$path,$column)=@_; 8160 my $store=$tv->get_model; 8161 my $ID=$store->get($store->get_iter($path),0); 8162 ::Select(song=>$ID,play=>1,staticlist => [$ID]); 8163 }); 8164 $treeview->signal_connect(key_release_event => sub 8165 { my ($tv,$event)=@_; 8166 if (Gtk2::Gdk->keyval_name( $event->keyval ) eq 'Delete') 8167 { my $store=$tv->get_model; 8168 my $path=($treeview->get_cursor)[0]; 8169 return 0 unless $path; 8170 my $ID=$store->get($store->get_iter($path),0); 8171 ::SongsRemove([$ID]); 8172 $tv->parent->parent->Refresh; 8173 return 1; 8174 } 8175 return 0; 8176 }); 8177 $self->Refresh; 8178 return $self; 8179} 8180 8181sub add_column 8182{ my ($self,$field,$title)=@_; 8183 my $renderer=Gtk2::CellRendererText->new; 8184 my $column=Gtk2::TreeViewColumn->new_with_attributes($title,$renderer); 8185 $column->set_resizable(1); 8186 $column->{field}=$field; 8187 $column->set_cell_data_func($renderer,\&set_cell_data_cb); 8188 $self->{treeview}->append_column($column); 8189} 8190 8191sub set_cell_data_cb 8192{ my ($column,$cell,$store,$iter)=@_; 8193 my $ID=$store->get($iter,0); 8194 my $song=$::Songs[$ID]; 8195 my $text= $column->{field} eq 'title' ? $song->[::SONG_TITLE] : 8196 $column->{field} eq 'url' ? $song->[::SONG_UPATH].'/'.$song->[::SONG_UFILE] : ''; 8197 my $w= (defined $::SongID && $::SongID==$ID) ? Gtk2::Pango::PANGO_WEIGHT_BOLD : Gtk2::Pango::PANGO_WEIGHT_NORMAL; 8198 $cell->set(text => $text); 8199 $cell->set(weight => $w); 8200} 8201 8202sub add_radio_cb 8203{ my $self=::find_ancestor($_[0],__PACKAGE__); 8204 my $dialog=Gtk2::Dialog->new( _"Adding a radio", $self->get_toplevel,'destroy-with-parent', 8205 'gtk-add' => 'ok', 8206 'gtk-cancel' => 'none'); 8207 my $table=Gtk2::Table->new(2,2,1); 8208 for my $ref ( ['entry1',0,_"Radio title"], 8209 ['entry2',1,_"Radio url"], ) 8210 { my ($key,$row,$label)=@$ref; 8211 $dialog->{$key}=Gtk2::Entry->new; 8212 $table->attach_defaults($dialog->{$key},1,2,$row,$row+1); 8213 $table->attach_defaults(Gtk2::Label->new($label),0,1,$row,$row+1); 8214 } 8215 $dialog->vbox->pack_start($_,0,0,2) for Gtk2::Label->new(_"Add new radio"),$table; 8216 $dialog->signal_connect( response => sub 8217 { my ($dialog,$response)=@_; 8218 if ($response eq 'ok') 8219 { my $name=$dialog->{entry1}->get_text; 8220 my $url =$dialog->{entry2}->get_text; 8221 ::AddRadio($url,$name); 8222 } 8223 $dialog->destroy 8224 }); 8225 $dialog->show_all; 8226} 8227 8228sub Refresh 8229{ my $self=$_[0]; 8230 my $store=$self->{treeview}->get_model; 8231 $store->clear; 8232 $store->set($store->append,0,$_) for @::Radio; 8233} 8234=cut 8235 82361; 8237