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>&amp;</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