#!/usr/bin/perl
# Copyright (c) 2005-2006 Quentin Sculo <squentin@free.fr>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2, as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.


package main;
use strict;
#use warnings;
use Gtk2 '-init';
use Gtk2::Gdk::Keysyms;
use Glib qw/filename_from_unicode filename_to_unicode/;
*filename_to_utf8displayname=\&Glib::filename_display_name if *Glib::filename_display_name{CODE};
*PangoEsc=\&Glib::Markup::escape_text if *Glib::Markup::escape_text{CODE}; #needs perl-Gtk2 version >=1.092
use POSIX qw/locale_h/;
use File::Copy;
use Time::Local 'timelocal';
use Fcntl 'O_NONBLOCK';
use Encode qw/_utf8_on _utf8_off/;
#close STDINPUT;

use constant SLASH => ($^O  eq 'MSWin32')? '\\' : '/';

# Find dir containing other files (*.pm & pix/) -> $DATADIR
use FindBin;
our $DATADIR;
BEGIN
{  ($DATADIR)=grep -e $_.SLASH.'gmusicbrowser_player.pm',
	$FindBin::Bin,
	$FindBin::Bin.SLASH.'..'.SLASH.'share'.SLASH.'gmusicbrowser';
  die "Can't find folder containing data files.\n" unless $DATADIR;
}
use lib $DATADIR;

use constant
{
 TRUE  => 1,
 FALSE => 0,
 VERSION => '0.955',
 PIXPATH => $DATADIR.SLASH.'pix'.SLASH,
 PROGRAM_NAME => 'gmusicbrowser',

 #fields of @{$Songs[$ID]} :
 SONG_UFILE	=> 0,	SONG_UPATH	=> 1,	SONG_MODIF	=> 2,
 SONG_LENGTH	=> 3,	SONG_SIZE	=> 4,	SONG_BITRATE	=> 5,
 SONG_FORMAT	=> 6,	SONG_CHANNELS	=> 7,	SONG_SAMPRATE	=> 8,
 SONG_TITLE	=> 9,	SONG_ARTIST	=> 10,	SONG_ALBUM	=> 11,
 SONG_DISC	=> 12,	SONG_TRACK	=> 13,	SONG_DATE	=> 14,
 SONG_VERSION	=> 15,	SONG_GENRE	=> 16,	SONG_COMMENT	=> 17,
 SONG_AUTHOR	=> 18,
 SONG_ADDED	=> 19,	SONG_LASTPLAY	=> 20,	SONG_NBPLAY	=> 21,
 SONG_RATING	=> 22,	SONG_FLAGS	=> 23,
 SONG_MISSINGSINCE => 24,
 SONGLASTSAVED	=> 24,
 SONG_FILE	=> 25,	SONG_PATH	=> 26,
 SONGLAST	=> 26,	# nb of last field
 # LYRICS ?

 #SongListColumns
 SLC_UFILE	=> 0,	SLC_UPATH	=> 1,	SLC_MODIF	=> 2,
 SLC_LENGTH	=> 3,	SLC_SIZE	=> 4,	SLC_BITRATE	=> 5,
 SLC_FORMAT	=> 6,	SLC_CHANNELS	=> 7,	SLC_SAMPRATE	=> 8,
 SLC_TITLE	=> 9,	SLC_ARTIST	=> 10,	SLC_ALBUM	=> 11,
 SLC_DISC	=> 12,	SLC_TRACK	=> 13,	SLC_DATE	=> 14,
 SLC_VERSION	=> 15,	SLC_GENRE	=> 16,	SLC_COMMENT	=> 17,
 SLC_AUTHOR	=> 18,
 SLC_ADDED	=> 19,	SLC_LASTPLAY	=> 20,	SLC_NBPLAY	=> 21,
 SLC_RATING	=> 22,	SLC_FLAGS	=> 23,
 SLC_PR_BOLD	=> 24,
 SLC_TITLEAA	=> 25,
 SLC_PLAYICO	=> 26,
 SLC_ICOFLAGS	=> 27,
 SLCLAST	=> 27,

 DRAG_STRING	=> 0, DRAG_FILE		=> 1, DRAG_ID		=> 2,
 DRAG_ARTIST	=> 3, DRAG_ALBUM	=> 4, DRAG_FILTER	=> 5,
 DRAG_MARKUP	=> 6,

 #contents of @{$Artist{$artist}} and @{$Album{$album}} :
 AALIST => 0, AAXREF => 1, AALENGTH => 2, AAYEAR => 3, AAPIXLIST => 4,

};

sub _ ($) {$_[0]}	#dummy translation function
sub __ { sprintf( ($_[2]>1 ? $_[0] : $_[1]), $_[2]); }
sub __x { my ($s,%h)=@_; $s=~s/{(\w+)}/$h{($1)}/g; $s; }
BEGIN
{
 eval {require Locale::gettext};
 if ($@) { warn "Locale::gettext not found -> no translations\n"; }
 else
 {	my $localedir=$DATADIR;
	$localedir= $FindBin::Bin.SLASH.'..'.SLASH.'share' unless -d $localedir.SLASH.'locale';
	my $d=Locale::gettext->domain('gmusicbrowser');
	$d->dir( $localedir.SLASH.'locale' );
	*_=sub ($) { $d->get($_[0]); };
	*__=sub { sprintf $d->nget(@_),$_[2]; };
 }
}

BEGIN{
require 'gmusicbrowser_tags.pm';
require 'gmusicbrowser_player.pm';
require 'gmusicbrowser_list.pm';
require 'simple_http.pm';
}

our $Gtk2TrayIcon;
BEGIN
{ if (grep -f $_."/Gtk2/TrayIcon.pm",@INC)  { require Gtk2::TrayIcon; $Gtk2TrayIcon=1; }
  else { warn "Gtk2::TrayIcon not found -> tray icon won't be available\n"; }
}

our $HomeDir=Glib::get_home_dir.SLASH.'.'.PROGRAM_NAME.SLASH;
unless (-d $HomeDir)
{	warn "Creating folder $HomeDir\n";
	mkdir $HomeDir or warn "Error creating $HomeDir : $!\n";
}

my $SaveFile=(-d $HomeDir)
	?  $HomeDir.'tags'
	: 'gmusicbrowser.tags';
my $FIFOFile=(-d $HomeDir && $^O ne 'MSWin32')
	?  $HomeDir.'gmusicbrowser.fifo'
	: undef;

our $debug;
our %CmdLine;
our (@Songs,@Shuffle);
our (%Artist,%Album);

########## cmdline
{
my $help=PROGRAM_NAME.' v'.VERSION." (c)2005-2006 Quentin Sculo
options :
-c	: don't check for updated/deleted songs on startup
-s	: don't scan folders for songs on startup
-demo	: don't check if current song has been updated/deleted
-ro	: read-only : don't modify any .mp3/.ogg files
-play	: start playing on startup
-gst	: use gstreamer
-nogst  : do not use gstreamer
-server	: send playing song to connected icecast clent
-port N : listen for connection on port N in icecast server mode
-C FILE	: use FILE as configuration file (instead of $SaveFile)
-F FIFO : use FIFO as named pipe to receive commans (instead of $FIFOFile)
-nofifo : do not use/create named pipe $FIFOFile
-debug	: print lots of useless informations
-layout NAME : use layout NAME for player window
-load FILE : Load FILE as a plugin
-tagedit FOLDER_OR_FILE ... : Edittag mode
-listcmd : list the available fifo commands and exit
";
unshift @ARGV,'-tagedit' if $0=~m/tagedit/;
$CmdLine{gst}=0;
 while (defined (my $arg=shift))
 {	if   ($arg eq '-c')	{$CmdLine{nocheck}=1}
	elsif($arg eq '-s')	{$CmdLine{noscan}=1}
	elsif($arg eq '-demo')	{$CmdLine{demo}=1}
	elsif($arg eq '-play')	{$CmdLine{play}=1}
	elsif($arg eq '-server'){$CmdLine{server}=1}
	elsif($arg eq '-nogst')	{$CmdLine{gst}=0}
	elsif($arg eq '-gst')	{$CmdLine{gst}=1}
	elsif($arg eq '-ro')	{$CmdLine{ro}=1}
	elsif($arg eq '-port')	{$CmdLine{port}=shift if $ARGV[0]}
	elsif($arg eq '-debug')	{$debug=1}
	elsif($arg eq '-nofifo'){$FIFOFile=undef}
	elsif($arg eq '-C')	{$SaveFile=shift if $ARGV[0]}
	elsif($arg eq '-F')	{$FIFOFile=shift if $ARGV[0]}
	elsif($arg eq '-layout'){$CmdLine{layout}=shift if $ARGV[0]}
	elsif($arg eq '-load')	{ push @{$CmdLine{plugins}},shift if $ARGV[0]}
	elsif($arg eq '-tagedit'){$CmdLine{tagedit}=1;last; }
	elsif($arg eq '-listcmd'){$CmdLine{cmdlist}=1;last; }
	elsif($arg eq '-remotecmd'){ RunRemoteCommand(@ARGV); $CmdLine{runcmd}="@ARGV"; last; }
	else
	{	warn "unknown option '$arg'\n" unless $arg=~/^--?h(elp)?$/;
		print $help;
		exit;
	}
 }
}

sub RunRemoteCommand
{	if (defined $FIFOFile && -p $FIFOFile)
	{	open my$fifofh,'>',$FIFOFile;
		print $fifofh "@_";
		close $fifofh;
		exit;
	}
}

##########

our $ILLEGALCHAR=qr#[/:><\*\?\"\\]#;	# regex that matches characters that shouldn't be used in filenames
our $ILLEGALCHARDIR=qr#[:><\*\?\"\\]#;	# regex that matches characters that shouldn't be used in pathnames

our $re_spaces_unlessinbrackets=qr/([^( ]+(?:\(.*?\))?)(?: +|$)/; #breaks "widget1(options with spaces) widget2" in "widget1(options with spaces)" and "widget2"


my $browsercmd='gnome-open';
#my $browsercmd='galeon -n';
$browsercmd='start' if $^O eq 'MSWin32';

our @Encodings=grep $_ ne 'null', Encode->encodings(':all');

our @TagProp;	# TagProp[columnnumber]=[name,data_type]
# names :
{ my @names=
  (	_"Filename"	=> SONG_UFILE,
	_"Folder"	=> SONG_UPATH,
	_"Modification"	=> SONG_MODIF,
	_"Title"	=> SONG_TITLE,
	_"Artist"	=> SONG_ARTIST,
	_"Date"		=> SONG_DATE,
	_"Album"	=> SONG_ALBUM,
	_"Comment"	=> SONG_COMMENT,
	_"Track"	=> SONG_TRACK,
	_"Genres"	=> SONG_GENRE,
	_"Length"	=> SONG_LENGTH,
	_"Size"		=> SONG_SIZE,
	_"Disc"		=> SONG_DISC,
	_"Version"	=> SONG_VERSION,
	_"Rating"	=> SONG_RATING,
	_"Added"	=> SONG_ADDED,
	_"Last played"	=> SONG_LASTPLAY,
	_"Nb played"	=> SONG_NBPLAY,	# replace by 'play count' ?
	_"Flags"	=> SONG_FLAGS,
	_"Bitrate"	=> SONG_BITRATE,
	_"Type"		=> SONG_FORMAT,
	_"Channels"	=> SONG_CHANNELS,
	_"Sampling Rate"=> SONG_SAMPRATE,
    );
while (defined (my$name=shift @names)) { $TagProp[ shift @names ]=[ $name ]; }
}
# data_type :	d : date in seconds since epoch (01-01-1970)
#		s : string
#		n : number
#		l : length in seconds
#		f : genres && flags (list of \x00 terminated strings)
$TagProp[$_][1]='d' for SONG_MODIF,SONG_ADDED,SONG_LASTPLAY;
$TagProp[$_][1]='s' for SONG_UFILE,SONG_UPATH,SONG_TITLE,SONG_ARTIST,SONG_ALBUM,SONG_COMMENT,SONG_VERSION,SONG_FORMAT;
$TagProp[$_][1]='n' for SONG_DATE,SONG_TRACK,SONG_DISC,SONG_NBPLAY,SONG_RATING,SONG_SIZE,SONG_BITRATE,SONG_CHANNELS,SONG_SAMPRATE;
$TagProp[SONG_LENGTH][1]='l';
$TagProp[SONG_FLAGS][1]='f';
$TagProp[SONG_GENRE][1]='f';

our %QActions=		#icon		#short		#long description
(	''	=> [ 'gmb-empty',	_"normal",	_"normal play when queue empty"],
	autofill=> [ 'gtk-refresh',	_"autofill",	_"autofill queue" ],
	'wait'	=> [ 'gmb-wait',	_"wait for more",_"wait for more when queue empty"],
	stop	=> [ 'gtk-media-stop',	_"stop",	_"stop when queue empty"],
	quit	=> [ 'gtk-quit',	_"quit",	_"quit when queue empty"],
);


our @DRAGTYPES;
@DRAGTYPES[DRAG_FILE,DRAG_STRING,DRAG_MARKUP,DRAG_ID,DRAG_ARTIST,DRAG_ALBUM,DRAG_FILTER]=
(	['text/uri-list'],
	['text/plain'],
	['markup'],
	[SongID =>
		{ DRAG_FILE, sub { map 'file://'.url_escape($Songs[$_][SONG_PATH].SLASH.$Songs[$_][SONG_FILE]), @_;},
#		  DRAG_ARTIST,	sub { keys %{ &{$FilterList::hashsub[SONG_ARTIST]}(\@_) } },
#		  DRAG_ALBUM,	sub { keys %{ &{$FilterList::hashsub[SONG_ALBUM]}(\@_) } },
		  DRAG_ARTIST,	sub { my %h; $h{ $Songs[$_][SONG_ARTIST] }=undef for @_; sort keys %h; },
		  DRAG_ALBUM,	sub { my %h; $h{ $Songs[$_][SONG_ALBUM ] }=undef for @_; sort keys %h; },
		  DRAG_STRING,	sub { (@_==1)? $Songs[$_[0]][SONG_TITLE] : __("%d song","%d songs",scalar@_) },
		  DRAG_FILTER,	sub {Filter->newadd(FALSE,map SONG_TITLE.'~'.$Songs[$_][SONG_TITLE],@_)->{string}},
		  DRAG_MARKUP,	sub { (@_==1)	? ReplaceFieldsAndEsc($_[0],_"<b>%t</b>\n<small><small>by</small> %a\n<small>from</small> %l</small>")
			  			: __x(	_("{songs} by {artists}") . "\n<small>{length}</small>",
							songs => __("%d song","%d songs",scalar@_),
							artists => do {my %h; $h{$Songs[$_][SONG_ARTIST]}=undef for @_; (keys %h ==1)? ::PangoEsc($Songs[$_[0]][SONG_ARTIST]) : __("%d artist","%d artists",scalar(keys %h)) },
							'length' => CalcListLength(\@_,'short')
							)},
		}],
	[Artist => {	DRAG_STRING,	sub { (@_==1)? $_[0] : __("%d artist","%d artists",$_[0]) },
			DRAG_FILTER,	sub {   Filter->newadd(FALSE,map SONG_ARTIST.'~'.$_,@_)->{string} },
			DRAG_ID,	sub { my $l=Filter->newadd(FALSE,map SONG_ARTIST.'~'.$_,@_)->filter; SortList($l); @$l; },
		}],
	[Album  => {	DRAG_STRING,	sub { (@_==1)? $_[0] : __("%d album","%d albums",scalar@_) },
			DRAG_FILTER,	sub {   Filter->newadd(FALSE,map SONG_ALBUM.'e'.$_,@_)->{string} },
			DRAG_ID,	sub { my $l=Filter->newadd(FALSE,map SONG_ALBUM.'e'.$_,@_)->filter; SortList($l); @$l; },
		}],
	['Filter' =>
		{	DRAG_STRING,	sub {Filter->new($_[0])->explain},
			DRAG_ID,	sub { my $l=Filter->new($_[0])->filter; SortList($l); @$l; },
		}
	],
);
our %DRAGTYPES;
$DRAGTYPES{$DRAGTYPES[$_][0]}=$_ for DRAG_FILE,DRAG_STRING,DRAG_ID,DRAG_ARTIST,DRAG_ALBUM,DRAG_FILTER,DRAG_MARKUP;

our @submenuRemove=
(	{ label => _"Remove from list",	code => sub { $_[0]{self}->get_toplevel->{songlist}->RemoveSelected; }, mode => 'B'},
#(	{ label => _"Remove from list',	code => sub { $_[0]{self}->get_toplevel->{songlist}->RemoveID(@{ $_[0]{IDs} }); }, mode => 'B'},
	{ label => _"Remove from library",	code => sub { SongsRemove($_[0]{IDs}); }, },
	{ label => _"Remove from disk",		code => sub { DeleteFiles($_[0]{IDs}); },	stockicon => 'gtk-delete' },
);
#modes : S:Search, B:Browser, Q:Queue, L:List, P:Playing song in the player window
our @SongCMenu=
(	{ label => _"Song Properties",	code => sub { DialogSongProp (@{ $_[0]{IDs} }); },	onlyone => 'IDs', stockicon => 'gtk-edit' },
	{ label => _"Songs Properties",	code => sub { DialogSongsProp(@{ $_[0]{IDs} }); },	onlymany=> 'IDs', stockicon => 'gtk-edit' },
	{ label => _"Play Only Selected",code => sub { Select(undef,undef,'first','play', $_[0]{IDs} ); },
		onlymany => 'IDs',	stockicon => 'gtk-media-play'},
	{ label => _"Play Only Displayed",code => sub { Select(undef,undef,'first','play', \@{$_[0]{listIDs}} ); },
		test => sub { @{$_[0]{IDs}}<2 },	onlymany => 'listIDs',	stockicon => 'gtk-media-play' },
	{ label => _"Enqueue Selected",	code => sub { Enqueue(@{ $_[0]{IDs} }); },
		notempty => 'IDs', notmode => 'QP', stockicon => 'gmb-queue' },
	{ label => _"Enqueue Displayed",	code => sub { Enqueue(@{ $_[0]{listIDs} }); },
		empty => 'IDs',	notempty=> 'listIDs', notmode => 'QP', stockicon => 'gmb-queue' },
	{ label => _"Add to list",	submenu => \&AddToListMenu,	notempty => 'IDs' },
	{ label => _"Edit Flags",	submenu => \&FlagEditMenu,	notempty => 'IDs' },
	{ label => _"Find songs with the same names", code => sub { SearchSame(SONG_TITLE,$_[0]) },	mode => 'B',	notempty => 'IDs' },
	{ label => _"Find songs with same artists", code => sub { SearchSame(SONG_ARTIST,$_[0]) },	mode => 'B',	notempty => 'IDs' },
	{ label => _"Find songs in same albums", code => sub { SearchSame(SONG_ALBUM,$_[0]) },	mode => 'B',	notempty => 'IDs' },
	{ label => _"Rename file",	code => sub { DialogRename(	@{ $_[0]{IDs} }); },	onlyone => 'IDs', },
	{ label => _"Mass Rename",	code => sub { DialogMassRename(	@{ $_[0]{IDs} }); },	onlymany=> 'IDs', },
	{ label => _"Copy",	code => sub { CopyMoveFiles('Copy',undef,@{ $_[0]{IDs} }); },
		notempty => 'IDs',	stockicon => 'gtk-copy', notmode => 'P' },
	{ label => _"Move",	code => sub { CopyMoveFiles('Move',undef,@{ $_[0]{IDs} }); },	notempty => 'IDs',	notmode => 'P' },
	#{ label => sub {'Remove from '.($_[0]{mode} eq 'Q' ? 'queue' : 'this list')}, code => sub { $_[0]{self}->RemoveSelected; },	stockicon => 'gtk-remove',	notempty => 'IDs', mode => 'LQ' }, #FIXME
	{ label => _"Remove",	submenu => \@submenuRemove,	stockicon => 'gtk-remove',	notempty => 'IDs',	notmode => 'P' },
	{ label => _"Re-read tags",	code => sub { ReReadTags(@{ $_[0]{IDs} }); },
		notempty => 'IDs',	notmode => 'P',	stockicon => 'gtk-refresh' },
	{ label => _"Same Title",     submenu => sub { ChooseSongsTitle(	$_[0]{IDs}[0] ); },	mode => 'P' },
	{ label => _"Edit Lyrics",	code => sub { EditLyrics(	$_[0]{IDs}[0] ); },	mode => 'P' },
	{ label => _"Lookup in google",	code => sub { Google(		$_[0]{IDs}[0] ); },	mode => 'P' },
);
our @cMenuAA=
(	{ label => _"Lock",	code => sub { ToggleLock($_[0]{col}); }, check => sub {$::TogLock==$_[0]{col}}, mode => 'P' },
	{ label => _"Lookup in AMG",	code => sub { ::AMGLookup( $_[0]{col},$_[0]{key} ); }, },
	{ label => _"Filter",		code => \&filterAA,	stockicon => 'gmb-filter', mode => 'P' },
	{ label => \&SongsSubMenuTitle,		submenu => \&SongsSubMenu, },
	{ label => sub {$_[0]{mode} eq 'P' ? _"Display Songs" : _"Filter"},	code => \&FilterOnAA,
		test => sub {$_[0]{self}->get_toplevel->{songlist}} },
	{ label => _"Set Picture",	code => sub { ChooseAAPicture($_[0]{ID},$_[0]{col},$_[0]{key}); },
		stockicon => 'gmb-picture' },
);

sub url_escape
{	local $_=$_[0];
	s/([^\/\$_.+!*'(),A-Za-z0-9-])/sprintf('%%%02X', ord($1))/seg;
	#s/(%)/sprintf('%%%02X', ord($1))/seg;
	return $_;
}
sub decode_url
{	local $_=$_[0];
	s/%([0-9A-F]{2})/chr(hex $1)/ieg;
	return $_;
}

sub PangoEsc	# escape special chars for pango ( & < > ) #replaced by Glib::Markup::escape_text if available
{	local $_=$_[0];
	return '' unless defined;
	s/&/&amp;/g; s/</&lt;/g; s/>/&gt;/g;
	s/"/&quot;/g; s/'/&apos;/g; # doesn't seem to be needed
	return $_;
}

our %ReplaceFields=
(	'%' => [sub {'%'},'%'],
	t => [sub { $Songs[$_[0]][SONG_TITLE]	}, $TagProp[SONG_TITLE][0],	[SONG_TITLE]],
	a => [sub { $Songs[$_[0]][SONG_ARTIST]	}, $TagProp[SONG_ARTIST][0],	[SONG_ARTIST]],
	l => [sub { $Songs[$_[0]][SONG_ALBUM]	}, $TagProp[SONG_ALBUM][0],	[SONG_ALBUM]],
	d => [sub { $Songs[$_[0]][SONG_DISC]	}, $TagProp[SONG_DISC][0],	[SONG_DISC]],
	n => [sub { $Songs[$_[0]][SONG_TRACK]	}, $TagProp[SONG_TRACK][0],	[SONG_TRACK]],
	y => [sub { $Songs[$_[0]][SONG_DATE]	}, $TagProp[SONG_DATE][0],	[SONG_DATE]],
	C => [sub { $Songs[$_[0]][SONG_COMMENT]	}, $TagProp[SONG_COMMENT][0],	[SONG_COMMENT]],
	m => [sub { my $v=$Songs[$_[0]][SONG_LENGTH];sprintf "%d:%02d",$v/60,$v%60;}, $TagProp[SONG_LENGTH][0],	[SONG_LENGTH]],
	o => [sub { my$s=$Songs[$_[0]][SONG_UFILE]; $s=~s/\.[^\.]+$//; $s; },_"old filename",[SONG_UFILE]],
	f => [sub { $Songs[$_[0]][SONG_PATH].SLASH.$Songs[$_[0]][SONG_FILE]; }, _"File",[SONG_PATH,SONG_FILE]],
	u => [sub { $Songs[$_[0]][SONG_UPATH].SLASH.$Songs[$_[0]][SONG_UFILE]; }, _"File",[SONG_UPATH,SONG_UFILE]],
	S => [sub { my $s=$Songs[$_[0]][SONG_TITLE]; $s=$Songs[$_[0]][SONG_UFILE] if $s eq ''; $s;},undef,[SONG_TITLE,SONG_UFILE]],
	V => [sub { my $v=$Songs[$_[0]][SONG_VERSION]; return (!defined $v || $v eq '') ? '' : " ($v)" },undef,[SONG_VERSION]],
	c => [sub { my $alb=$Songs[$_[0]][SONG_ALBUM]; $alb=$Album{$alb}[AAPIXLIST]; $alb='' unless $alb; return $alb; }, _"Cover",[SONG_ALBUM]],
);

sub UsedFields
{	my $s=$_[0];
	return map @{ $ReplaceFields{$_}[2] || [] }, $s=~m/%([CSVumtalydnfco%])/g;
}
sub ReplaceFields
{	my ($ID,$s)=($_[0],$_[1]);
	$s=~s/%([CSVumtalydnfco%])/&{$ReplaceFields{$1}[0]}($ID)/ge;
	return $s;
}
sub ReplaceFieldsAndEsc
{	my ($ID,$s)=($_[0],$_[1]);
	$s=~s/%([CVSumtalydnfco%])/PangoEsc(&{$ReplaceFields{$1}[0]}($ID))/ge;
	return $s;
}
sub MakeReplaceTable
{	my $fields=$_[0];
	my $table=Gtk2::Table->new (4, 2, FALSE);
	my $row=0; my $col=0;
	for my $tag (split //,$fields)
	{  $table->attach( Gtk2::Label->new($_),$col++,$col,$row,$row+1,'shrink','shrink',1,1)
		for ('%'.$tag.' : ' , $ReplaceFields{$tag}[1]);
	   if ($col++>3) { $row++; $col=0; }
	}
	$table->set_col_spacing(2, 30);
	my $align=Gtk2::Alignment->new(.5, .5, 0, 0);
	$align->add($table);
	return $align;
}

our %ReplaceAAFields=
(	'%'	=>	sub {'%'},
	a	=>	sub { $_[0] },
	l	=>	sub { my $l=$_[1]->[AALENGTH]; $l=__x( ($l>=3600 ? _"{hours}h{min}m{sec}s" : _"{min}m{sec}s"), hours => (int $l/3600), min => ($l>=3600 ? sprintf('%02d',$l/60%60) : $l/60%60), sec => sprintf('%02d',$l%60)); },
	L	=>	sub { CalcListLength( $_[1]->[AALIST],'short' ); },
	y	=>	sub { $_[1]->[AAYEAR] || ''; },
	Y	=>	sub { my $y=$_[1]->[AAYEAR]; return $y? " ($y)" : '' },
	s	=>	sub { __('%d song','%d songs',scalar@{$_[1]->[AALIST]}) },
	x	=>	sub { my $nb=keys %{$_[1]->[::AAXREF]}; return $_[2]==SONG_ARTIST ? __("%d Album","%d Albums",$nb) : __("%d Artist","%d Artists",$nb);  },
	X	=>	sub { my $nb=keys %{$_[1]->[::AAXREF]}; return $_[2]==SONG_ARTIST ? __("%d Album","%d Albums",$nb) : $nb>1 ? __("%d Artist","%d Artists",$nb) : '';  },
	b	=>	sub { my %h; $h{ $Songs[$_][::SONG_ARTIST] }=undef for @{$_[1]->[AALIST]}; return keys(%h)==1 ? (keys(%h))[0] : __("%d artist","%d artists", scalar keys(%h));  },
);
sub ReplaceAAFields
{	my ($aa,$format,$col,$esc)=@_;
	my $ref= $col == SONG_ARTIST ? \%Artist : \%Album;
	$ref=$ref->{$aa};
	return '' unless ref $ref;
	if($esc){ $format=~s/%([alLyYsxXb%])/PangoEsc(&{$ReplaceAAFields{$1}}($aa,$ref,$col))/ge; }
	else	{ $format=~s/%([alLyYsxXb%])/&{$ReplaceAAFields{$1}}($aa,$ref,$col)/ge; }
	return $format;
}

our %DATEUNITS=
(		s => [1,_"seconds"],
		m => [60,_"minutes"],
		h => [3600,_"hours"],
		d => [86400,_"days"],
		w => [604800,_"weeks"],
		M => [2592000,_"months"],
		y => [31536000,_"years"],
);
our %SIZEUNITS=
(		b => [1,_"bytes"],
		k => [1000,_"KB"],
		m => [1000000,_"MB"],
);
sub ConvertTime	# convert date pattern into nb of seconds
{	my $pat=$_[0];
	my ($d1,$d2)=$pat=~m/^(\S+)\s?(.*)$/;
	for ($d1,$d2)
	{	if (m/(\d\d\d\d)-(\d\d?)-(\d\d?)/) { $_=timelocal(0,0,0,$3,$2-1,$1); }
		elsif (m/^(\d+\.?\d*)([smhdwMy])$/){ $_=time-$1*$DATEUNITS{$2}[0];   }
	}
	return ($d2?  join(' ',sort { $a <=> $b } $d1,$d2) : $d1 );
}

our $NBVolIcons;
{		# load icons
 my %icons;

 unless (Gtk2::Stock->lookup('gtk-media-play'))	#for gtk version 2.4
 { for (map 'gtk-media-'.$_, qw/play pause stop previous next/)
   {$icons{$_}=[PIXPATH.$_.'.png'];}
 }

 opendir my$dh,PIXPATH;
 for my $file (grep m/^gmb-/, readdir $dh)
 { my $name=$file; next unless $name=~s/\.png$//; $icons{$name}=[PIXPATH.$file]; }
 closedir $dh;

 my $dir=$::HomeDir.'icons';
 if (-d $dir)		#load customs icons
 {	opendir my$dh,$dir;
 	for my $file (grep m/\.(?:png|svg)$/, readdir $dh)
 	{	my $name=$file;
		$name=~s/\.[^.]+$//;
		$icons{$name}=[$dir.SLASH.$file];
	}
 	closedir $dh;
 }
 $NBVolIcons=0;
 $NBVolIcons++ while $icons{'gmb-vol'.$NBVolIcons};

 my $icon_factory = Gtk2::IconFactory->new;
 while (my ($stock_id,$ref)=each %icons)
 {	Gtk2::Stock->add
	({	stock_id => $stock_id,
		#label    => $$ref[1],
		#modifier => [],
		#keyval   => $Gtk2::Gdk::Keysyms{L},
		#translation_domain => 'gtk2-perl-example',
	});
	my $icon_set = Gtk2::IconSet->new_from_pixbuf
		( Gtk2::Gdk::Pixbuf->new_from_file($$ref[0]) );
	$icon_factory->add($stock_id,$icon_set);
 }
 $icon_factory->add_default;
}

#---------------------------------------------------------------
#  -------------------------------------------------------------
our @TAGSREADFROMTAGS=(SONG_TITLE,SONG_ARTIST,SONG_DATE,SONG_ALBUM,SONG_COMMENT,SONG_TRACK,SONG_GENRE,SONG_DISC,SONG_VERSION);
our @TAGSELECTION=(SONG_TITLE,SONG_ARTIST,SONG_ALBUM,SONG_DATE,SONG_TRACK,SONG_DISC,SONG_VERSION,SONG_GENRE,SONG_RATING,SONG_FLAGS,SONG_NBPLAY,SONG_LASTPLAY,SONG_ADDED,SONG_MODIF,SONG_COMMENT,SONG_UFILE,SONG_UPATH,SONG_LENGTH,SONG_SIZE,SONG_BITRATE,SONG_FORMAT,SONG_CHANNELS,SONG_SAMPRATE);
my @SAVEDFIELDS=(0..SONGLASTSAVED); $SAVEDFIELDS[SONG_UFILE]=SONG_FILE; $SAVEDFIELDS[SONG_UPATH]=SONG_PATH;
my $DAYNB=int(time/86400)-12417;
our $QSLASH=quotemeta SLASH;	#quoted SLASH for use in regex

our (@LibraryPath,@Library);
our (%GlobalBoundKeys,%CustomBoundKeys);
#our (@Songs,@Shuffle);
#our (%Artist,%Album);
our @Flags;
my (%GetIDFromFile,%MissingSTAAT,$MissingCount);
my @STAAT=(SONG_SIZE,SONG_TITLE,SONG_ALBUM,SONG_ARTIST,SONG_TRACK); #Fields used to check if same song
our %SavedFilters; our ($SelectedFilter,$PlayFilter);
our (%SavedSorts,%SavedWRandoms);
our %SavedLists; my $SavedListsWatcher;
our @ListPlay;
our ($TogPlay,$TogLock);
my $VolInc=10;
our ($RandomMode,$SortFields,$ListMode);
our ($SongID,@Recent,@Queue,$QueueAction); our $Position=0;
our ($MainWindow,$BrowserWindow,$ContextWindow); my $OptionsDialog;
our $QueueWindow; my $TrayIcon;
our %Context;
my %Editing;
our $PlayTime; our $PlayTimeMMSS='--:--';
our ($StartTime,$StartedAt); my ($PlayingID,$PlayedPartial);
our $CurrentDir=$ENV{'PWD'};

our ($LEvent,$LEventW);
our (%ToDo,%TimeOut);
my %EventWatchers;#for Save Vol Time Queue Lock Sort Filter Pos SongID Playing SavedWRandoms SavedSorts SavedFilters AAPicture

my $SFWatch=0;
my @SongsWatchers=($SFWatch);
my (@Watched,@WatchedFilt);
$Watched[$_]=[] for 0..SONGLAST;
$WatchedFilt[$_]=[] for 0..SONGLAST;
my ($IdleLoop,@ToCheck,@ToScan,%FollowedDirs,@ToAdd,@LengthEstimated,$FoundCover,%ToUpdateYAr,%ToUpdateYAl);
my ($ProgressWin,$ProgressNBSongs,$ProgressNBFolders);
my ($TrayTipWin,$NoTrayTip);
our %Plugins;
my $ScanRegex;

#Default values
our %Options=
(	Layout		=> 'default player layout',
	LayoutT		=> 'info',
	LayoutB		=> 'Browser',
	MaxAutoFill	=> 5,
	Repeat		=> 1,
	Sort		=> 's',		#default sort order
	AltSort		=> SONG_UPATH.' '.SONG_UFILE,
	WSRename	=> '300 180',
	WSMassTag	=> '520 560',
	WSAdvTag	=> '538 503',
	WSSongInfo	=> '420 482',
	WSEditSort	=> '600 320',
	WSEditFilter	=> '600 260',
	WSEditWRandom	=> '600 450',
	Sessions	=> '',
	StartCheck	=> 0,	#check if songs have changed on startup
	StartScan	=> 0,	#scan @LibraryPath on startup for new songs
	#Path		=> '',	#contains join "\x1D",@LibraryPath
	Flags => join("\x1D",'favorite','bootleg','broken','bonus tracks','interview','another example'),
	PlayedPercent	=> .85,	#percent of a song played to increase play count
	DefaultRating	=> 50,
	Device		=> 'default',
	amixerSMC	=> 'PCM',
	gst_sink	=> 'alsa',
	Icecast_port	=> '8000',
	UseTray		=> 1,
	CloseToTray	=> 0,
	ShowTipOnSongChange => 0,
	TAG_use_latin1_if_possible => 1,
	TAG_no_desync	=> 1,
	TAG_keep_id3v2_ver  => 0,
	'TAG_write_id3v2.4' => 0,
	TAG_id3v1_encoding => 'iso-8859-1',
	Simplehttp_CacheSize => 50*1024,
	CustomKeyBindings => '',
);

our $GlobalKeyBindings='Insert OpenSearch c-q EnqueueSelected p PlayPause c OpenContext q OpenQueue';
%GlobalBoundKeys=%{ make_keybindingshash($GlobalKeyBindings) };


sub make_keybindingshash
{	my $string=$_[0];
	my @list=  $string=~m/$re_spaces_unlessinbrackets/g;
	my %h;
	while (@list>1)
	{	my $key=shift @list;
		my $mod;
		$mod=$1 if $key=~s/^(c?a?s?-)//;
		$key=$Gtk2::Gdk::Keysyms{$key};
		$key=$mod.$key if $mod;
		$h{$key}=shift @list;
	}
	return \%h;
}

##########
our %Command=		#contains sub,description,argument_tip, argument_regex or code returning a widget
(	NextSongInPlaylist=> [\&NextSongInPlaylist,		_"Next Song In Playlist"],
	PrevSongInPlaylist=> [\&PrevSongInPlaylist,		_"Previous Song In Playlist"],
	NextSong	=> [\&NextSong,				_"Next Song"],
	PrevSong	=> [\&PrevSong,				_"Previous Song"],
	PlayPause	=> [\&PlayPause,			_"Play/Pause"],
	Forward		=> [\&Forward,				_"Forward",_"Number of seconds",qr/^\d+$/],
	Rewind		=> [\&Rewind,				_"Rewind",_"Number of seconds",qr/^\d+$/],
	Stop		=> [\&Stop,				_"Stop"],
	Browser		=> [\&Playlist,				_"Open Browser"],
	OpenQueue	=> [\&EditQueue,			_"Open Queue window"],
	OpenSearch	=> [sub { Player->new('Search'); },	_"Open Search window"],
	OpenContext	=> [sub { Player->new('Context');},	_"Open Context window"],
	OpenCustom	=> [sub { Player->new($_[1]); },	_"Open Custom window",_"Name of layout", sub { TextCombo->new({map {$_ => _ ($_)} sort { _ ($a) cmp _ ($b) } keys %Player::Layouts}) }],
	OpenPref	=> [\&PrefDialog,			_"Open Preference window"],
	OpenSongProp	=> [sub { DialogSongProp($SongID) if defined $SongID }, _"Edit Current Song Properties"],
	ShowHide	=> [\&ShowHide,				_"Show/Hide"],
	Quit		=> [\&Quit,				_"Quit"],
	Save		=> [\&SaveTags,				_"Save Tags/Options"],
	ChangeDisplay	=> [\&ChangeDisplay,			_"Change Display",_"Display (:1 or host:0 for example)",qr/:\d/],
	EnqueueSelected => [\&Player::EnqueueSelected,		_"Enqueue Selected Songs"],
	EnqueueAction	=> [sub {EnqueueAction($_[1])},		_"Enqueue Action", _"Queue mode" ,sub { TextCombo->new({map {$_ => $QActions{$_}[1]} sort keys %QActions}) }],
	ClearQueue	=> [\&::ClearQueue,			_"Clear queue"],
	IncVolume	=> [sub {ChangeVol('up')},		_"Increase Volume"],
	DecVolume	=> [sub {ChangeVol('down')},		_"Decrease Volume"],
	TogMute		=> [sub {ChangeVol('mute')},		_"Mute/Unmute"],
	RunSysCmd	=> [\&run_system_cmd,			_"Run system command",_"Shell command",qr/./],
	TogArtistLock	=> [sub {ToggleLock(SONG_ARTIST)},	_"Toggle Artist Lock"],
	TogAlbumLock	=> [sub {ToggleLock(SONG_ALBUM)},	_"Toggle Album Lock"],
);

sub run_command
{	my ($self,$cmd)=@_;
	$cmd="$1($2)" if $cmd=~m/^(\w+) (.*)/;
	($cmd, my$arg)= $cmd=~m/^(\w+)(?:\((.*)\))?$/;
	warn "executing $cmd($arg) (with self=$self)" if $::debug;
	$Command{$cmd}[0]->($self,$arg) if $Command{$cmd};
}

sub run_system_cmd
{	my $syscmd=$_[1];
	#my @cmd=split / /,$syscmd;
	#system @cmd;
	my @cmd=grep defined, $syscmd=~m/(?:(?:"(.*[^\\])")|([^ ]*[^ \\]))(?: |$)/g;
	return unless @cmd;
	$_=ReplaceFields($SongID,$_) for @cmd;
	use POSIX ':sys_wait_h';	#for WNOHANG in waitpid
	my $pid=fork;
	if ($pid==0) #child
	{	exec @cmd;
		exit;
	}
	#waitpid $pid,0 if $pid;# && kill(0,$pid);
	while (waitpid(-1, WNOHANG)>0) {}	#reap dead children
}



if ($CmdLine{cmdlist})
{	print "Available commands (for fifo or layouts) :\n";
	for my $cmd (sort keys %Command)
	{	my $print="$cmd : $Command{$cmd}[1]";
		$print.="  (argument : $Command{$cmd}[2])" if $Command{$cmd}[2];
		print "$print\n"
	}
	exit;
}
my $fifofh;
if (defined $FIFOFile)
{	if (-e $FIFOFile) { unlink $FIFOFile unless -p $FIFOFile; }
	else
	{	#system('mknod',$FIFOFile,'p'); #can't use mknod to create fifo on freeBSD
		system 'mkfifo',$FIFOFile;
	}
	if (-p $FIFOFile)
	{	sysopen $fifofh,$FIFOFile,O_NONBLOCK;
		Glib::IO->add_watch(fileno($fifofh),['in','hup'], \&CmdFromFIFO);
	}
}

Glib::set_application_name(PROGRAM_NAME);
Gtk2::Window->set_default_icon_from_file(PIXPATH.'gmusicbrowser.png');

Edittag_mode(@ARGV) if $CmdLine{tagedit};

#-------------INIT-------------
our $Play_package;
my (%Packs,$PlayNext_package);
require 'gmusicbrowser_123.pm';
eval {require 'gmusicbrowser_gstreamer-0.10.pm';};
if ($@)
{	if ($@=~/^gst0.08/) {require 'gmusicbrowser_gstreamer.pm';}
      else { die $@; }
}
require 'gmusicbrowser_win32.pm';
require 'gmusicbrowser_server.pm';
for my $p (qw/Play_Server Play_GST Play_123 Play_Win/) { $Packs{$p}=1 if $p->init; }


&ReadSavedTags;
$Options{version}=VERSION;

$Play_package=$Options{AudioOut};
$Play_package='Play_Server' if $CmdLine{server};
$Play_package='Play_GST' if $CmdLine{gst};
$Play_package=undef unless $Play_package && $Packs{$Play_package};
unless ($Play_package)
{	for (qw/Play_123 Play_GST Play_Win Play_Server/)
	{	next unless $Packs{$_};
		$Play_package=$_;
		last;
	}
}
$PlayNext_package=$Play_package;

IdleCheck() if $Options{StartCheck} && !$CmdLine{nocheck};
IdleScan()  if $Options{StartScan}  && !$CmdLine{noscan};
$Options{Icecast_port}=$CmdLine{port} if $CmdLine{port};

Select(	($SelectedFilter ? $SelectedFilter : ''),
	$Options{'Sort'},
	(defined $SongID ? 'keep' : 'first'),
	( ($CmdLine{play} && !$PlayTime)? 'play' : undef),
	$ListMode
      );
#SkipTo($PlayTime) if $PlayTime; #gstreamer (how I use it) needs the mainloop running to skip, so this is done after the main window is created

Player::InitLayouts;
&LoadPlugins;

our $Tooltips=Gtk2::Tooltips->new;
$MainWindow=Player->new($CmdLine{layout}||$Options{Layout});
SkipTo($PlayTime) if $PlayTime; #done only now because of gstreamer

CreateTrayIcon();

run_command(undef,$CmdLine{runcmd});

#--------------------------------------------------------------
Gtk2->main;
exit;

sub Edittag_mode
{	my @dirs=@_;
	IdleScan(@dirs);
	while ($IdleLoop) {undef @LengthEstimated;Gtk2->main_iteration while Gtk2->events_pending;IdleLoop(); }
	my $dialog = Gtk2::Dialog->new( _"Editing tags", undef,'modal',
				'gtk-save' => 'ok',
				'gtk-cancel' => 'none');
	$dialog->signal_connect(destroy => sub {exit});
	$dialog->set_default_size(500, 600);
	my $edittag;
	if (@Library==1)
	{	$edittag=EditTagSimple->new($dialog,$Library[0]);
		$dialog->signal_connect( response => sub
		 {	my ($dialog,$response)=@_;
			$edittag->save if $response eq 'ok';
			exit;
		 });
	}
	elsif (@Library>1)
	{	$edittag=MassTag->new($dialog,@Library);
		$dialog->signal_connect( response => sub
		 {	my ($dialog,$response)=@_;
			if ($response eq 'ok') { $edittag->save( sub {exit} ); }
			else {exit}
		 });
	}
	else {die "No songs found.\n";}
	$dialog->vbox->add($edittag);
	$dialog->show_all;
	Gtk2->main;
}

sub ChangeDisplay
{	my $display=$_[1];
	$display=~s/\.(\d+)$//;
	my $screen=$1 || 0;
	$display=Gtk2::Gdk::Display->open($display);
	return unless $display && $screen < $display->get_n_screens;
	Gtk2::Gdk::DisplayManager->get->set_default_display($display);
	$screen=$display->get_screen($screen);
	for my $win (Gtk2::Window->list_toplevels)
	{	$win->set_screen($screen);
	}
}

sub filename_to_utf8displayname	#replaced by Glib::filename_display_name if available
{	my $utf8name=eval {filename_to_unicode($_[0])};
	if ($@)
	{	$utf8name=$_[0];
		#$utf8name=~s/[\x80-\xff]/?/gs; #doesn't seem to be needed
	}
	return $utf8name;
}

sub find_ancestor
{	my ($widget,$class)=@_;
	until ( $widget->isa($class) )
	{	$widget=$widget->parent;
		last unless $widget;
	}
	return $widget;
}
sub Hpack
{	my @list=@_;
	my $pad=2;
	my $hbox=Gtk2::HBox->new;
	for my $w (@list)
	{	unless (ref $w) { $pad=$w; next; }
		my $wg=$w;
		$wg=Vpack($pad,@$wg) if ref $wg eq 'ARRAY';
		$hbox->pack_start($wg,FALSE,FALSE,$pad);
	}
	return $hbox;
}
sub Vpack
{	my @list=@_;
	my $pad=2;
	my $vbox=Gtk2::VBox->new;
	for my $w (@list)
	{	unless (ref $w) { $pad=$w; next; }
		my $wg=$w;
		$wg=Hpack($pad,@$wg) if ref $wg eq 'ARRAY';
		$vbox->pack_start($wg,FALSE,FALSE,$pad);
	}
	return $vbox;
}

sub GetGenresList
{	no warnings 'uninitialized';
	my %h;
	$h{$_}=undef for map split(/\x00/, $::Songs[$_][::SONG_GENRE]), @::Library;
	return [sort keys %h];
}

sub Quit
{	$Options{SavedPlayTime}=$PlayTime if $Options{RememberPlayTime};
	&Stop if defined $TogPlay;
	@ToScan=@ToAdd=();
	SaveTags();
	unlink $FIFOFile if defined $FIFOFile;
	Gtk2->main_quit;
}

sub CmdFromFIFO
{	while (my $line=<$fifofh>)
	{	chomp $line;
		my ($cmd, $args)=split / /, $line, 2;
		if (exists $Command{$cmd}) { Glib::Timeout->add(0, sub {&{$Command{$cmd}[0]}(undef,$args); 0;}); warn "fifo:received $line\n" if $debug; }
		else {warn "fifo:received unknown command : $cmd\n"}
	}
	if (1) #FIXME replace 1 by gtk+ version check once the gtk+ bug is fixed (http://bugzilla.gnome.org/show_bug.cgi?id=321053)
	{	#work around gtk bug that use 100% cpu after first command : close and reopen fifo
		close $fifofh;
		sysopen $fifofh,$FIFOFile,O_NONBLOCK;

		Glib::IO->add_watch(fileno($fifofh),['in','hup'], \&CmdFromFIFO);
		return 0; #remove previous watcher
	}
	1;
}

sub LoadPlugins
{	my @list;
	for my $dirname ($DATADIR.SLASH.'plugins', $HomeDir.'plugins')
	{	next unless -d $dirname;
		opendir my$dir,$dirname;
		while (my $file=readdir $dir)
		{	next unless $file=~m/\.p[lm]$/;
			push @list,$dirname.SLASH.$file;
		}
		close $dir;
	}
	push @list,@{$CmdLine{plugins}} if $CmdLine{plugins};

	for my $file (@list)
	{	my $result=do $file;
		warn "plugin $file failed : $@\n" unless $result;
	}
	for my $p (keys %Plugins)
	{	$Plugins{$p}->init('startup') if $Options{'PLUGIN_'.$p};
	}
}

sub ChangeVol
{	my $cmd;
	if ($_[0] eq 'mute')
	{  $cmd=$Play_package->GetMute? 'unmute':'mute' ;
	}
	else
	{	$cmd=(ref $_[0])? $_[1]->direction : $_[0];
		if	($Play_package->GetMute)	{$cmd='unmute'}
		elsif	($cmd eq 'up')	{$cmd=$VolInc.'%+'}
		elsif	($cmd eq 'down'){$cmd=$VolInc.'%-'}
	}
	warn "volume $cmd ...\n" if $debug;
	UpdateVol($cmd);
	warn "volume $cmd" if $debug;
}

sub UpdateVol
{	$Play_package->SetVolume($_[0]);
}
sub GetVol
{	$Play_package->GetVolume;
}
sub GetMute
{	$Play_package->GetMute;
}

sub makeVolSlider
{	my $opt=shift || '0,0';
	my ($hide,$horizon)=split ',',$opt;
	my $vol=$Play_package->GetVolume;
	return if $vol==-1;
	my $adj=Gtk2::Adjustment->new($vol, 0, 100, 1, 10, 1);
	my $slider=$horizon? 'Gtk2::HScale' : 'Gtk2::VScale';
	$slider=$slider->new($adj);
	$slider->set_draw_value(FALSE) if $hide;
	$slider->set_digits(0);
	$slider->set_inverted(TRUE);
	$slider->signal_connect(value_changed => sub { UpdateVol($_[0]->get_value.'%'); 1; });
	return $slider
}

sub PopupVol
{	my $slider=makeVolSlider();
	unless ($slider) { ErrorMessage(_"Can't set the volume, that's probably because amixer (packaged in alsa-utils) is not installed."); return }
	$slider->set_size_request(-1, 100);
	my $popup=Gtk2::Window->new('popup');
	$slider->signal_connect(leave_notify_event => sub { $popup->destroy });
	$popup->add($slider);
	$popup->set_modal(TRUE);
	$popup->set_position('mouse');
	$popup->show_all;
	return 0;
}

sub FirstTime
{ %SavedSorts=
  (	_"Path,File"	=> SONG_UPATH.' '.SONG_UFILE,
	_"Date"		=> SONG_DATE,
	_"Title"	=> SONG_TITLE,
	_"Last played"	=> SONG_LASTPLAY,
	_"Artist,Album,Disc,Track"	=> join(' ',SONG_ARTIST,SONG_ALBUM,SONG_DISC,SONG_TRACK),
	_"Artist,Date,Album,Disc,Track"	=> join(' ',SONG_ARTIST,SONG_DATE,SONG_ALBUM,SONG_DISC,SONG_TRACK),
	_"Path,Album,Disc,Track,File"	=> join(' ',SONG_UPATH,SONG_ALBUM,SONG_DISC,SONG_TRACK,SONG_UFILE),
  );

  %SavedWRandoms=
  (	_"by rating"	=> 'r1r0,.1,.2,.3,.4,.5,.6,.7,.8,.9,1',
	_"by play count"=> 'r-1n5',
	_"by lastplay"	=> 'r1l10',
	_"by added"	=> 'r-1a50',
	_"by lastplay & play count"	=> 'r1l10'."\x1D".'-1n5',
	_"by lastplay & bootleg"	=> 'r1l10'."\x1D".'-.5fbootleg',
  );

  #Default filters
  %SavedFilters=
  (	_"never played"		=> SONG_NBPLAY.'<1',
	_"50 Most Played"	=> SONG_NBPLAY.'h50',
	_"50 Last Played"	=> SONG_LASTPLAY.'h50',
	_"50 Last Added"	=> SONG_ADDED.'h50',
	_"Played Today"		=> SONG_LASTPLAY.'>1d',
	_"Added Today"		=> SONG_ADDED.'>1d',
	_"played>4"		=> SONG_NBPLAY.'>4',
	_"not bootleg"		=> SONG_FLAGS.'-fbootleg',
  );

  $_=Filter->new($_) for values %SavedFilters;
  @Flags=split "\x1D",$Options{Flags};
}


sub ReadSavedTags
{	if (-d $SaveFile) {$SaveFile.=SLASH.'tags'}
	unless (-r $SaveFile)
	{	FirstTime(); return;
	}
	::setlocale(::LC_NUMERIC, 'C');
	warn "Reading saved tags in $SaveFile ...\n";
	open SAVETAG,'<:utf8',$SaveFile;
	while (<SAVETAG>)
	{	chomp; last if $_ eq '';
		$Options{$1}=$2 if m/^([^=]+)=(.+)$/;
	}
	my $oldversion=delete $Options{version} || VERSION;
	$Options{PlayedPercent}=.85 if $oldversion==0.923;
	if ($oldversion<0.9464) {delete $Options{$_} for qw/BrowserTotalMode FilterPane0Page FilterPane0min FilterPane1Page FilterPane1min LCols LSort PlayerWinPos SCols Sticky WSBrowser WSEditQueue paned StickyFilters/} #cleanup old options
	if ($oldversion<0.9540)
	{	$Options{Layout}='default player layout' if $Options{Layout} eq 'default';
		$Options{'Layout_default player layout'}=delete $Options{Layout_default};
	}
	#@Library=();
	my $ID=my $oldID=-1; my @newIDs;
	while (<SAVETAG>)
	{	chomp; last if $_ eq '';
		$oldID++;
		next if $_ eq ' ';	#deleted entry
		my @song=split "\x1D";
		unless ($song[SONG_UFILE] && $song[SONG_UPATH] && $song[SONG_ADDED])
		{	warn "skipping invalid song entry : @song\n";
			next;
		}
		$ID++;
		push @Songs,\@song;
		$newIDs[$oldID]=$ID;
		$song[SONG_PATH]=$song[SONG_UPATH];
		$song[SONG_FILE]=$song[SONG_UFILE];
		if ($oldversion<=0.937)
		{	$song[SONG_PATH]=filename_from_unicode($song[SONG_UPATH]);
			$song[SONG_FILE]=filename_from_unicode($song[SONG_UFILE]);
			#_utf8_on($song[SONG_PATH]);
			#_utf8_on($song[SONG_FILE]);
		}
		_utf8_off($song[SONG_PATH]);
		_utf8_off($song[SONG_FILE]);
		$song[SONG_UPATH]=filename_to_utf8displayname($song[SONG_PATH]);
		$song[SONG_UFILE]=filename_to_utf8displayname($song[SONG_FILE]);
		if ($oldversion<0.940 && $oldversion>=0.937 && defined $GetIDFromFile{ $song[SONG_PATH] }{ $song[SONG_FILE] })
		{$Songs[$ID]=undef;next}	#fix duplicate songs due to bug in v0.939 & v0.938
		$GetIDFromFile{ $song[SONG_PATH] }{ $song[SONG_FILE] }=$ID;
		if (my $m=$song[SONG_MISSINGSINCE])
		{	if ($m=~m/^\d+$/) {AddMissing($ID);next}
			elsif ($m eq 'l') {push @LengthEstimated,$ID}
			elsif ($m eq 'r') {push @ToCheck,$ID}
		}
		elsif (!$song[SONG_MODIF]) { push @ToAdd,$ID;next; }
		push @Library,$ID;
		#warn $song[SONG_PATH].SLASH.$song[SONG_FILE].' not found' unless -f $song[SONG_PATH].SLASH.$song[SONG_FILE];
		AddAA($ID); #Fill %Artist and %Album
	}
	while (<SAVETAG>)
	{	chomp; last if $_ eq '';
		my ($key,$p)=split "\x1D";
		next if $p eq '';
		if ($oldversion<=0.937)
		{	$p=filename_from_unicode($p);
			#_utf8_on($p);
		}
		_utf8_off($p);
		$Artist{$key}[AAPIXLIST]=$p;
	}
	while (<SAVETAG>)
	{	chomp; last if $_ eq '';
		my ($key,$p)=split "\x1D";
		next if $p eq '';
		if ($oldversion<=0.937)
		{	$p=filename_from_unicode($p);
			#_utf8_on($p);
		}
		_utf8_off($p);
		#warn $p.' not found' unless -f $p;
		$Album{$key}[AAPIXLIST]=$p;
	}
	%SavedFilters=();
	%SavedSorts=();
	%SavedWRandoms=();
	while (<SAVETAG>)
	{	chomp;# last if $_ eq '';
		my ($key,$val)=split "\x1D",$_,2;
		$key=~s/^(.)//;
		if ($1 eq 'F')
		{	$SavedFilters{$key}=Filter->new($val);
		}
		elsif ($1 eq 'S')
		{	$SavedSorts{$key}=$val;
		}
		elsif ($1 eq 'R')
		{	$SavedWRandoms{$key}=$val;
		}
		elsif ($1 eq 'L')
		{	$SavedLists{$key}=[grep defined,map $newIDs[$_],split / /,$val];
		}
	}
	close SAVETAG;

	@Recent= grep defined,map $newIDs[$_],split / /,delete $Options{RecentIDs};
	if ($oldversion<=0.9428 && exists $Options{LengthEstimatedIDs})
	{ @LengthEstimated=grep defined,map $newIDs[$_],split / /,delete $Options{LengthEstimatedIDs};
	  $Songs[$_][SONG_MISSINGSINCE]='l' for @LengthEstimated;
	}
	if (my $f=delete $Options{LastPlayFilter})
	{	if ($Options{RememberPlayFilter} && $f=~s/^(filter|savedlist|list) //)
		{	if ($1 eq 'filter') {$SelectedFilter=Filter->new($f)}
			elsif ($1 eq 'savedlist') {$ListMode=$f}
			elsif ($1 eq 'list') {$ListMode=[grep defined,map $newIDs[$_],split / /,$f]}
		}
	}
	if ($Options{RememberPlayFilter})
	{	$TogLock=$Options{Lock};
	}
	if ($Options{RememberPlaySong}) { $SongID=$newIDs[delete $Options{SavedSongID}]; }
	if ($Options{RememberPlaySong} && $Options{RememberPlayTime}) { $PlayTime=delete $Options{SavedPlayTime}; }
	_utf8_off($Options{Path});
	@LibraryPath=	split "\x1D",$Options{Path};
	s/\x00+$// for @LibraryPath; #FIXME ugly fix for paths ending with special char and not encoded in utf8, some \x00 are added when read with :utf8
	@Flags=		split "\x1D",$Options{Flags};
	%CustomBoundKeys=%{ make_keybindingshash($Options{CustomKeyBindings}) };
	::setlocale(::LC_NUMERIC, '');
	&launchIdleLoop unless defined $IdleLoop;
	warn "Reading saved tags in $SaveFile ... done\n";
}
sub SaveTags
{	HasChanged('Save');
	warn "Writing tags in $SaveFile ...\n";
	::setlocale(::LC_NUMERIC, 'C');
	my $savedir=$SaveFile;
	$savedir=~s/[^$QSLASH]+$//o;
	mkdir $savedir if $savedir ne '' && !-d $savedir;
	$Options{Lock}=$TogLock;
	if ($ListMode)	{ $Options{LastPlayFilter}=ref $ListMode? 'list '.join(' ',@$ListMode)
								: 'savedlist '.$ListMode;
			}
	elsif ($SelectedFilter) { $Options{LastPlayFilter}='filter '.$SelectedFilter->{string}; }
	$Options{Flags}=join "\x1D",@Flags;
	$Options{Path} =join "\x1D",@LibraryPath;
	_utf8_on($Options{Path});
	$Options{RecentIDs}=join ' ',@Recent;
	$Options{SavedSongID}=$SongID if $Options{RememberPlaySong};

	my $tooold=0;
	my @sessions=split ' ',$Options{'Sessions'};
	unless (@sessions && $DAYNB==$sessions[0])
	{	unshift @sessions,$DAYNB;
		$tooold=pop if @sessions>15;
		$Options{'Sessions'}=join ' ',@sessions;
	}
	for my $key (grep m/^Layout_/, keys %Options)
	{	$key=~s/^Layout_//;
		my $key2='LayoutLastSeen_'.$key;
		if (exists $Player::Layouts{$key}) { delete $Options{$key2}; }
		elsif (!$Options{$key2})	{ $Options{$key2}=$DAYNB; }
		elsif ($Options{$key2}<$tooold)	{ delete $Options{$_} for $key,$key2; }
	}

	open SAVETAG,'>:utf8',$SaveFile.'.new'; #FIXME check ok
	for (sort keys %Options)
	{	print SAVETAG $_.'='.$Options{$_}."\n";
	}
	print SAVETAG "\n";
	no warnings 'uninitialized';
	for my $song (@Songs)
	{	if (!$song || ($$song[SONG_MISSINGSINCE] && $$song[SONG_MISSINGSINCE]=~m/^\d+$/ && $$song[SONG_MISSINGSINCE]<$tooold))
		{ print SAVETAG " \n";next; }
		_utf8_on($$song[SONG_PATH]);
		_utf8_on($$song[SONG_FILE]);
		my $line=join "\x1D",@$song[@SAVEDFIELDS];
		_utf8_off($$song[SONG_PATH]); #FIXME ugly
		_utf8_off($$song[SONG_FILE]); #FIXME
		$line=~s/\n/ /g;	#FIXME
		if ($line eq '') {warn "trying to save empty Song entry" if $debug;next}
		print SAVETAG $line."\n";
	}
	print SAVETAG "\n";
	while ( my ($a,$ref)=each %Artist )
	{	my $p=$ref->[AAPIXLIST];
		next unless defined $p;
		_utf8_on($p);
		next unless @$ref[AALIST];
		print SAVETAG join("\x1D",$a,$p)."\n";
	}
	print SAVETAG "\n";
	while ( my ($a,$ref)=each %Album )
	{	my $p=$ref->[AAPIXLIST];
		next unless defined $p;
		_utf8_on($p);
		next unless @$ref[AALIST];
		print SAVETAG join("\x1D",$a,$p)."\n";
	}
	use warnings 'uninitialized';
	print SAVETAG "\n";
	for my $name (sort keys %SavedFilters)
	{	my $val=$SavedFilters{$name}; next unless defined $val;
		print SAVETAG "F$name\x1D".$val->{string}."\n";
	}
	for my $name (sort keys %SavedSorts)
	{	my $val=$SavedSorts{$name}; next unless defined $val;
		print SAVETAG "S$name\x1D$val\n";
	}
	for my $name (sort keys %SavedWRandoms)
	{	my $val=$SavedWRandoms{$name}; next unless defined $val;
		print SAVETAG "R$name\x1D$val\n";
	}
	for my $name (sort keys %SavedLists)
	{	my $val=$SavedLists{$name}; next unless defined $val;
		print SAVETAG "L$name\x1D".join(' ',@$val)."\n";
	}
	close SAVETAG;
	::setlocale(::LC_NUMERIC, '');
	rename $SaveFile,$SaveFile.'.bak';
	rename $SaveFile.'.new',$SaveFile;
	warn "Writing tags in $SaveFile ... done\n";
}

sub SetWSize
{	my ($win,$wkey)=@_;
	$wkey='WS'.$wkey;
	$win->resize(split ' ',$Options{$wkey}) if $Options{$wkey};
	$win->signal_connect(unrealize => sub
		{ $::Options{$_[1]}=join ' ',$_[0]->get_size; }
		,$wkey);
}

sub Rewind
{	my $sec=$_[1] || return;
	$sec=(defined $PlayTime && $PlayTime>$sec)? $PlayTime-$sec : 0;
	SkipTo($sec);
}
sub Forward
{	my $sec=$_[1] || return;
	$sec+=$PlayTime if defined $PlayTime;
	SkipTo($sec);
}

sub SkipTo
{	return unless defined $SongID;
	my $sec=shift;
	if (defined $PlayingID)
	{	$StartedAt=$sec unless (defined $PlayTime && $PlayingID==$SongID && $PlayedPartial && $sec<$PlayTime);	#don't re-set $::StartedAt if rewinding a song not fully(85%) played
		$Play_package->SkipTo($sec);
		$TogPlay=1;
		HasChanged('Playing');
	}
	else
	{	Play($sec);
	}
}

sub PlayPause
{	if (defined $TogPlay)	{ Pause()}
	else			{ Play() }
}

sub Pause
{	if ($TogPlay)
	{	$Play_package->Pause;
		$TogPlay=0;
	}
	elsif (defined $TogPlay)
	{	$Play_package->Resume;
		$TogPlay=1;
	}
	HasChanged('Playing');
}

sub Play
{	return unless defined $SongID;
	my $sec=shift;
	$sec=undef unless $sec && !ref $sec;
	Stop() if defined $PlayingID;
	$StartedAt=$sec||0;
	$StartTime=time;
	$PlayingID=$SongID;
	my $f=$Songs[$SongID][SONG_PATH].SLASH.$Songs[$SongID][SONG_FILE];
	$Play_package->Play($f,$sec);
	$TogPlay=1;
	UpdateTime(0);
	HasChanged('Playing');
}

sub ErrorPlay
{	my ($error,$critical)=@_;
	$error='Playing error : '.$error;
	warn $error."\n";
	return if $Options{IgnorePlayError} && !$critical;
	my $dialog = Gtk2::MessageDialog->new
		( undef, [qw/modal destroy-with-parent/],
		  'error','close',
		  $error
		);
	if ($critical)
	{ my $button=Gtk2::Button->new('Save tag/settings now');
	  my $l=Gtk2::Label->new('Warning. This error may cause the program to crash, it could be a good time to save tags/settings now');
	  $l->set_line_wrap(1);
	  $button->signal_connect(clicked => sub
		  { $_[0]->hide;$l->set_text('tags/settings saved');SaveTags(); });
	  $dialog->vbox->pack_start($_,0,0,4) for $l,$button;
	}
	$dialog->show_all;
	$dialog->run;
	$dialog->destroy;
	#$dialog->signal_connect( response => sub {$_[0]->destroy});
	Stop();
}

sub end_of_file_faketime
{	UpdateTime($Songs[$SongID][SONG_LENGTH]);
	end_of_file();
}

sub end_of_file
{	$Play_package=$PlayNext_package;
	&Played;
	ResetTime();
	&NextSong;
}

sub Stop
{	warn "stop\n" if $::debug;
	undef $TogPlay;
	$Play_package->Stop;
	$Play_package=$PlayNext_package;
	HasChanged('Playing');
	&Played;
	ResetTime();
}

sub UpdateTime
{	return if $_[0] eq $PlayTime;
	$PlayTime=$_[0];
	my $f=($Songs[$SongID][SONG_LENGTH]<600)? '%01d:%02d' : '%02d:%02d';
	$PlayTimeMMSS=sprintf $f,$PlayTime/60,$PlayTime%60;
	HasChanged('Time');
}

sub ResetTime
{	undef $PlayTime;
	$PlayTimeMMSS='--:--';
	HasChanged('Time');
}

sub Played
{	return unless defined $PlayingID;
	my $ID=$PlayingID;
	undef $PlayingID;
	warn "Played : $ID $StartTime $StartedAt $PlayTime\n" if $debug;
	#add song to recently played list
	@Recent=($ID,grep $_ ne $ID,@Recent);
	pop @Recent if @Recent>40;

	return unless defined $PlayTime;
	$PlayedPartial=$PlayTime-$StartedAt < $Options{PlayedPercent}*$Songs[$ID][SONG_LENGTH];
	return if $PlayedPartial;
	$Songs[$ID][SONG_NBPLAY]++;
	$Songs[$ID][SONG_LASTPLAY]=$StartTime;
	SongChanged($ID,SONG_NBPLAY,SONG_LASTPLAY);
}

sub ClearQueue
{	@Queue=();
	undef $QueueAction;
	HasChanged('Queue');
}

sub ShuffleQueue
{	my @rand;
	push @rand,rand for 0..$#Queue;
	@Queue=map $Queue[$_], sort { $rand[$a] <=> $rand[$b] } 0..$#Queue;
	HasChanged('Queue');
}

sub EnqueueAlbum
{	my $key=$Songs[shift][SONG_ALBUM];
	Enqueue( @{$Album{$key}[AALIST]} );
}
sub EnqueueArtist
{	my @l=split / ?& ?/, $Songs[shift][SONG_ARTIST];
	my %h; $h{$_}=undef for map @{$Artist{$_}[AALIST]}, @l;
	Enqueue( keys %h );
}
sub EnqueueFilter
{	my $l=$_[0]->filter;
	Enqueue(@$l);
}
sub Enqueue
{	my @l=@_;
	SortList(\@l) if @l>1;
	push @Queue,@l;
	# ToggleLock($TogLock) if $TogLock;	#unset lock
	Select(undef,undef,shift @Queue,'play') if $QueueAction eq 'wait' && !$TogPlay;
	HasChanged('Queue','push');
}
sub EnqueueAction
{	$QueueAction=shift;
	if (!$QueueAction) { $QueueAction=undef; }
	elsif ($QueueAction eq 'autofill') { Watch({},'Queue',sub { IdleDo('1_QAutoFill',10,\&QAutoFill,$_[0]); }); }
	HasChanged('Queue','action');
}
sub QAutoFill
{	my $hash=shift;
	if ($hash && $QueueAction ne 'autofill') { UnWatch($hash,'Queue'); return }
	my $nb=$Options{MaxAutoFill}-@Queue;
	return unless $nb>0;
	my $mode=$RandomMode? $Options{Sort} : 'r';
	my $r=Random->new($mode);
	$r->MakeRandomList(\@ListPlay);
	my @IDs=$r->Draw($nb,\@Queue);
	return unless @IDs;
	push @Queue,@IDs;
	HasChanged('Queue','push');
}

sub GetNeighbourSongs
{	my $nb=shift;
	UpdateSort() if $ToDo{'8_updatesort'};
	my $begin=$Position-$nb;
	my $end=$Position+$nb;
	$begin=0 if $begin<0;
	$end=$#ListPlay if $end>$#ListPlay;
	return @ListPlay[$begin..$end];
}

sub PrevSongInPlaylist
{	UpdateSort() if $ToDo{'8_updatesort'};
	if ($Position==0)
	{	return unless $Options{Repeat};
		$Position=$#ListPlay;
	}
	else { $Position-- }
	UpdateSongID();
}
sub NextSongInPlaylist
{	UpdateSort() if $ToDo{'8_updatesort'};
	if ($Position==$#ListPlay)
	{	return unless $Options{Repeat};
		$Position=0;
	}
	else { $Position++ }
	UpdateSongID();
}

sub GetNextSongs
{	my $nb=shift||1;
	my $list=($nb>1)? 1 : 0;
	my @IDs;
	{ if (@Queue)
	  {	unless ($list) { my $ID=shift @Queue; HasChanged('Queue','shift'); return $ID; }
		push @IDs,'Queue';
		if ($nb>@Queue) { push @IDs,@Queue; $nb-=@Queue; }
		else { push @IDs,@Queue[0..$nb-1]; last; }
	  }
	  if ($QueueAction)
	  {	push @IDs,$QueueAction;
		unless ($list || $QueueAction eq 'wait')
		 { undef $QueueAction; HasChanged('Queue','action'); }
		last;
	  }
	  if ($RandomMode)
	  {	push @IDs,'Random' if $list;
		push @IDs,$RandomMode->Draw($nb,(defined $SongID? [$SongID] : undef));
		last;
	  }
	  return undef unless @ListPlay;
	  UpdateSort() if $ToDo{'8_updatesort'};
	  my $pos;
	  $pos=FindPositionSong( $IDs[$#IDs] ) if @IDs;
	  $pos=$Position unless defined $pos;
	  push @IDs,'next' if $list;
	  while ($nb)
	  {	if ( $pos+$nb > $#ListPlay )
		{	push @IDs,@ListPlay[$pos+1..$#ListPlay];
			last unless $Options{Repeat}; #FIXME repeatlock modes
			$nb-=$#ListPlay-$pos;
			$pos=-1;
		}
		else { push @IDs,@ListPlay[$pos+1..$pos+$nb]; last; }
	  }
	}
	return $list ? @IDs : $IDs[0];
}

sub GetPrevSongs
{	my $nb=shift||1;
	my $list=($nb>1)? 1 : 0;
	my @IDs;
	push @IDs,'recent' if $list;
	if ($nb>@Recent) { push @IDs,@Recent; }
	else { push @IDs,@Recent[0..$nb-1]; }
	return $list ? @IDs : $IDs[0];
}

sub PrevSong
{	#my $ID=GetPrevSongs();
	return if @Recent==0 || $Recent[$#Recent]==$SongID;
	my $ID=$Recent[0];
	for (0..$#Recent-1) { if ($SongID==$Recent[$_]) { $ID=$Recent[$_+1]; next;} }
	return unless defined $ID;
	Select(undef,undef,$ID);
}
sub NextSong
{	my $ID=GetNextSongs();
	if (!defined $ID)  { Stop(); return; }
	if ($ID eq 'wait') { Stop(); return; }
	if ($ID eq 'stop') { Stop(); return; }
	if ($ID eq 'quit') { Quit(); }
	if ( $Position<$#ListPlay && $ListPlay[$Position+1]==$ID ) { $Position++; UpdateSongID(); }
	else { Select(undef,undef,$ID); }
}

sub UpdateLock
{	if (defined $ListMode) { Select(undef,undef,undef,undef,$ListMode); }
	else { Select($SelectedFilter); }
}

sub ToggleLock
{	my ($col,$set)=@_;
	if ($set || !$TogLock || $TogLock!=$col)
	{	$TogLock=$col;
		#&ClearQueue;
	}
	else {undef $TogLock}
	&UpdateLock;
	HasChanged('Lock');
}

sub ToggleSort
{	Select(undef,$Options{AltSort});
}

sub AddToPlaylist
{	my $list=$_[0];
	$list=[@::ListPlay,@$list];
	::Select(undef,undef,'trykeep',undef, $list );
}

sub Select_sort { Select(undef,$_[0]);}
sub Select	#Set filter ($filt), sort order($sort) and selected song($song)
{    my ($filt,$sort,$song,$play,$staticlist)=@_;
     warn "Select filt=".($filt||'')." sort=".($sort||'')." song=".($song||'')." play=".($play||'')." staticlist=".($staticlist||'')."\n" if $debug;
     $TogPlay=1 if $play eq 'play';
     if (defined $staticlist)
     {	$filt=undef;
	$ListMode=$staticlist;
	if (!$SavedListsWatcher && !ref $ListMode)	#FIXME should be done in $SFWatch
	{ Watch($SavedListsWatcher={},'SavedLists',sub
		{ return unless $_[1] eq $ListMode;
		  if ($_[2] && $_[2] eq 'push') { my $l=$SavedLists{$ListMode};AddSongToPlaylist($$l[$#$l]); }
		  else { Select(undef,undef,undef,undef,$ListMode); }
		});
	}
	@ListPlay=@{ ref $ListMode ? $ListMode : $SavedLists{$ListMode} || [] };
	$SelectedFilter=undef;
	HasChanged('Filter');
     }
     elsif (defined $filt)
     {	UnWatch($SavedListsWatcher) if $SavedListsWatcher;
	$ListMode=$SavedListsWatcher=undef;
	if (!defined $sort)
	{	if    ($Options{Sort}    eq '')	{$sort=$Options{SavedSort}}
		elsif ($Options{AltSort} eq '') {$Options{AltSort}=$Options{SavedSort}}
	}
     }
     if (defined $sort)
     {	my $old=$Options{Sort};
	$Options{Sort}=$sort;
	if ($sort eq '')
	{	if ($old=~m/[sr]/) {$Options{SavedSort}=$Options{AltSort}}
		elsif ($old ne '') {$Options{SavedSort}=$old}
	}
	if ($sort=~m/^[sr]/ xor $old=~m/^[sr]/)
	{ $Options{AltSort}=$old; }	#save sort mode for quick toggle random/non-random
	if ($sort!~m/^r/)
	{	$RandomMode=undef;
		$SortFields=[map /^-?([0-9]+)/,split / /,$sort];
	}
	else
	{	$RandomMode=Random->new($sort);
		$SortFields=$RandomMode->fields;
	}
	$sort=1;
     }
     #starting here, $sort TRUE means "resort needed", $sort defined means "has been resorted"
     if    (!defined $song)	{ $song='trykeep';		}
     elsif ( $song=~m/^\d+/ )	{ $SongID=$song; $song='keep';	}
     if (defined $filt)
     {	$filt=Filter->new($filt) unless ref $filt eq 'Filter';
	$SelectedFilter=$filt;
	@ListPlay=@{ $filt->filter };
	$sort=1;
	delete $ToDo{'8_updatefilter'};
     }
     elsif ($::ToDo{'8_updatesort'}) { $sort=1;delete $ToDo{'8_updatesort'}; }
     if ( $song eq 'keep' && !defined FindPositionSong($SongID) )
     {	$filt=$SelectedFilter=Filter->new;
	$sort=1; $ListMode=undef;
	warn "reset filter\n" if $debug;
	@ListPlay=@Library;
     }
     elsif ( $song eq 'trykeep' && !defined FindPositionSong($SongID) )
     {  $song='first';
	if ($TogLock)	# try to
	{	my $pat=$Songs[$SongID][$TogLock];
		($SongID)=grep $Songs[$_][$TogLock] eq $pat, @ListPlay;
		$song='' if defined $SongID;
	}
     }
     if ($song eq 'first')
     {	if ($RandomMode) { $RandomMode->MakeRandomList(\@ListPlay); $SongID=GetNextSongs(); }
	else { SortList(\@ListPlay); $SongID=$ListPlay[0]; } #$song='keep';
	$sort=0;
     }
     if ($TogLock && defined $SongID)
     {	my $pat=$Songs[$SongID][$TogLock];
	@ListPlay=grep $Songs[$_][$TogLock] eq $pat, @ListPlay;
	#@ListPlay=$FilterSubs{'~'}($TogLock,$Songs[$SongID][$TogLock],\@ListPlay);
	#$sort=1	# grep conserve order -> shouldn't be needed
	$PlayFilter=$filt=Filter->newadd( TRUE,$SelectedFilter, $TogLock.'e'.$Songs[$SongID][$TogLock] ) unless defined $ListMode;
     }
     else {$PlayFilter=$SelectedFilter}
     if ($sort)
     {	delete $ToDo{'8_updatesort'};
	if ($RandomMode) { $RandomMode->MakeRandomList(\@ListPlay); }
	else		 { SortList(\@ListPlay); }
     }
     $Position=FindPositionSong($SongID);
     if (defined $sort) { HasChanged('Sort');	}
     if (defined $filt) { HasChanged('Filter');	}
     ChangeWatcher($SFWatch, \@ListPlay, $SortFields,
		sub { if ($RandomMode) { $RandomMode->UpdateIDs(@_); }
		      elsif (!$ToDo{'8_updatesort'}) {IdleDo('8_updatesort',5*@ListPlay,\&UpdateSort);}
		    }, #re-sort
		\&RemoveSongFromPlaylist,	#remove song
		\&AddSongToPlaylist,		#add song
		$PlayFilter,
		sub { IdleDo('7_updatefilter',9000,\&UpdatePlayFilter); }, #re-filter
		);
     &UpdateSongID;
}

sub AddSongToPlaylist
{	push @ListPlay,@_;
	if (@ListPlay==@_) {$Position=0;&UpdateSongID;}
	if ($RandomMode) { $RandomMode->AddIDs(@_); IdleDo('8_updatesort',1000,sub {HasChanged('Pos','add');}); }
	elsif (!$ToDo{'8_updatesort'}) { IdleDo('8_updatesort',5*@ListPlay,\&UpdateSort); }
}
sub RemoveSongFromPlaylist
{	if ($RandomMode) { $RandomMode->RmIDs(@_); }
	my %h;
	$h{$_}=undef for @_;
	my $iscurrentsong= exists $h{$SongID};
	#@ListPlay=grep !exists $h{$_}, @ListPlay;
	my $i=@ListPlay;
	while ($i--)
	{	if (exists $h{ $ListPlay[$i] })
		{	splice @ListPlay,$i,1;
			$Position-- if $Position >= $i;
			#delete $h{ $ListPlay[$i] };
			#last unless keys %h;
		}
	}
	if (@ListPlay==0) { UpdateSongID(); }
	elsif ($iscurrentsong)
	{	#$Position=0 if $Position==-1;
		NextSong();
	}
	else { HasChanged('Pos','remove'); }
}

sub UpdateSort
{	delete $ToDo{'8_updatesort'};
	SortList(\@ListPlay);
	$Position=FindPositionSong($SongID);
	HasChanged('Pos','re-sort');
}

sub UpdatePlayFilter
{	my $m=(defined $SongID)? 'keep':'first';
	Select( $SelectedFilter,undef,$m );
}

sub Shuffle
{	if ($Options{Sort} eq 's')
	{	@Shuffle=();
		push @Shuffle,rand for 0..$#Songs;	#re-randomize @Shuffle
	}
	Select(undef,'s'); #sort according to @Shuffle
}

sub SortList	#sort @$listref according to $sort
{	my $time=times;
	my ($listref,$sort)=@_;
	($sort,$listref)=@_ if !ref $listref; #DELME for version <0.9496
	$sort=$Options{Sort} unless defined $sort;
	my $func;
	if ($sort=~m/^r/)
	{	my $r=Random->new($sort);
		$r->MakeRandomList($listref);
		@$listref=$r->Draw;
	}
	elsif ($sort ne '')		# generate custom sort function
	{  if ($sort=~m/s/) { push @Shuffle,rand for (@Shuffle..$#Songs); }
	   my @expr;
	   for my $col ( split / +/,$sort )
	   {	my ($a,$b)=( $col=~s/^-// )? ('b','a') : ('a','b');
		my ($pre,$post,$op);
		if ($col eq 's') { $pre='$Shuffle[$'; $post=']'; $op='<=>'; }
		else
		{ $post="][$col]";
		  my $type=$TagProp[$col][1];
#		  if	($type eq 's')	  { $pre='lc$Songs[$'; $op='cmp'; }
		  if	($type eq 's')	  { $pre='$Songs[$'; $op='cmp'; }
		  elsif ($type=~m/[ndl]/) { $pre='$Songs[$'; $op='<=>'; }
		  #elsif ($type eq 'f')	  { $pre='join " ",sort split /\x00/,$Songs[$';   $op='cmp'; }
		  elsif ($type eq 'f')	  { $pre='$Songs[$'; $op='cmp'; }	#FIXME multiple flags/genres should be sorted
		  else	{ warn "Don't know how to sort $col\n"; next; }
		}
		push @expr, $pre.$a.$post.' '.$op.' '.$pre.$b.$post;
		#example:  $Songs[$ a ][$col]  <=>   $Songs[$ b ][$col]
	   }
	   warn "sort function for '$sort' :\n".'sub {'.join(' || ',@expr).'}'."\n" if $debug;
	   $func=eval 'no warnings;sub {' . join(' || ',@expr) . '}';
	   warn $@ if $@;
	}
	@$listref=sort $func @$listref if $func;
	$time=times-$time; warn "sort ($sort) : $time s\n" if $debug;
}

sub ExplainSort
{	my $sort=$_[0];
	if ($sort eq '') {return 'no order'}
	elsif ($sort=~m/^r/)
	{	for my $name (keys %SavedWRandoms)
		{	return "Weighted Random $name." if $SavedWRandoms{$name} eq $sort;
		}
		return "unnamed Weighted Random"; #describe ?
	}
	my @text;
	for (split / /,$sort)
	{	my $field=s/^-// ? '-' : '';
		$field.=($_ eq 's')? 'Shuffle' : $TagProp[$_][0];
		push @text,$field;
	}
	return join ', ',@text;
}

sub ReReadTags
{	$Songs[$_][SONG_MISSINGSINCE]='r' for @_;
	&IdleCheck; #keep @_
}
sub IdleCheck
{	if (@_) { push @ToCheck,@_; }
	else	{ unshift @ToCheck,@Library; }
	&launchIdleLoop unless defined $IdleLoop;
}
sub IdleScan
{	@_=@LibraryPath unless @_;
	push @ToScan,@_;
	CreateProgressWindow() if @ToScan && !$ProgressWin;
	&launchIdleLoop unless defined $IdleLoop;
}

sub IdleDo
{	my $task_id=shift;
	my $timeout=shift;
	$ToDo{$task_id}=\@_;
	if ($timeout && !defined $TimeOut{$task_id})
	{ $TimeOut{$task_id}=Glib::Timeout->add($timeout,\&DoTask,$task_id); }
	&launchIdleLoop unless defined $IdleLoop;
}
sub DoTask
{	my $task_id=shift;
	delete $TimeOut{$task_id};
	my $aref=delete $ToDo{$task_id};
	if ($aref)
	{ my $sub=shift @$aref;
	  &$sub(@$aref);
	}
	0;
}

sub launchIdleLoop
{	$IdleLoop=Glib::Idle->add(\&IdleLoop);
}

sub IdleLoop
{	if    (@ToCheck){ SongCheck(pop @ToCheck); }
	elsif (@ToAdd)  { SongAdd(); }
	elsif ($FoundCover) {CheckCover();}
	elsif (@ToScan) { ScanFolder(); }
	elsif (%ToDo)	{ DoTask( (sort keys %ToDo)[0] ); }
	elsif (@LengthEstimated) { SongCheck(shift(@LengthEstimated)); } #to replace estimated length/bitrate by real one(for mp3s without VBR header)
	else
	{	warn "IdleLoop End\n" if $debug;
		undef $IdleLoop;
	}
	return $IdleLoop;
}

sub UpdateSongID
{	warn "pos : $Position\n" if $debug;
	if (@ListPlay==0) { $Position=$SongID=undef; Stop(); }
	else
	{	$SongID=$ListPlay[$Position];
		IdleCheck($SongID);
	}
	HasChanged('SongID');
	HasChanged('Pos','song');
	showTraytip(undef,$TrayIcon) if $TrayIcon && $Options{ShowTipOnSongChange};
	if ( defined $SongID && !(defined $PlayingID && $PlayingID==$SongID) )
	{	if 	($TogPlay)		{Play()}
		elsif	(defined $TogPlay)	{Stop()}
	}
}

sub FindPositionSong
{	my $ID=shift;
	return undef unless defined $ID;
	for (0..$#ListPlay) {return $_ if $ListPlay[$_]==$ID}
	return undef;	#not found
}
sub FindFirstInListPlay
{	my $lref=shift;
	my $sort=$Options{Sort};
	my $ID;
	my %h;
	$h{$_}=undef for @$lref;
	my @l=grep exists $h{$_}, @ListPlay;
	if ($sort=~m/^r/)
	{	$lref=\@l if @l;
		my $r=Random->new($sort);
		$r->MakeRandomList($lref);
		($ID)=$r->Draw(1);
		$ID=$lref->[ int(rand(scalar@$lref)) ] unless defined $ID;
	}
	else
	{	@l=@$lref unless @l;
		push @l,$SongID if defined $SongID && !exists $h{$SongID};
		SortList(\@l,$sort);
		return $l[0] unless defined $SongID;
		for my $i (0..$#l-1) {next if $l[$i]!=$SongID; $ID=$l[$i+1];last}
		$ID=$l[0] unless defined $ID;
	}
	return $ID;
}

sub Playlist
{	if ($BrowserWindow)
	{	if ($_[0] && $_[0] eq 'toggle') {$BrowserWindow->close_window}
		else				{$BrowserWindow->present}
	}
	else
	{	$BrowserWindow=Player->new($Options{LayoutB});
		$BrowserWindow->signal_connect(destroy => sub { $BrowserWindow=undef; });
	}
}
sub ContextWindow
{	if ($ContextWindow)
	{	if ($_[0] && $_[0] eq 'toggle') {$ContextWindow->close_window}
		else				{$ContextWindow->present}
	}
	else
	{	$ContextWindow=Player->new('Context');
		$ContextWindow->signal_connect(destroy => sub { $ContextWindow=undef; });
	}
}

sub EditQueue
{	if ($QueueWindow) { $QueueWindow->present; }
	else
	{	$QueueWindow=Player->new('Queue');
		$QueueWindow->signal_connect(destroy => sub { $QueueWindow=undef; });
	}
}

sub WEditList
{	my $name=$_[0];
	my $window=Player->new('EditList');
	$window->{donotsavepos}=1; #FIXME should be a layout option
	$window->{widgets}{EditList}->SetList($name);
}

sub CalcListLength	#if $return, return formated string (0h00m00s)
{	my ($listref,$return)=@_;
	my $size=0; my $sec=0;
	for my $ID (@$listref)
	{	my $ref=$Songs[$ID];
		next unless $ref;
		$size+=$$ref[SONG_SIZE];
		#next unless $$_[SONG_LENGTH];
		$sec+=$$ref[SONG_LENGTH];
	}
	warn 'ListLength: '.scalar @$listref." Songs, $sec sec, $size bytes\n" if $debug;
	if ($return)	#return formated string (0h00m00s)
	{  $size=sprintf '%.0f',$size/1048576; #1024*1024
	   my $m=int($sec/60); $sec=sprintf '%02d',$sec%60;
	   my $h=int($m/60);     $m=sprintf '%02d',$m%60;
	   my $nb=@$listref;
	   my @values=(hours => $h, min =>$m, sec =>$sec, size => $size);
	   if ($return eq 'long')
	   {	my $format= $h? _"{hours} hours {min} min {sec} s ({size} M)" : _"{min} min {sec} s ({size} M)";
		return __("%d Song","%d Songs",$nb) .', '. __x($format, @values);
	   }
	   elsif ($return eq 'queue')
	   {	$h=($h)? $h.'h ' : '';
		return _"Queue empty" if $nb==0;
		my $format= $h? _"{hours}h{min}m{sec}s" : _"{min}m{sec}s";
		return __("%d song in queue","%d songs in queue",$nb) .' ('. __x($format, @values) . ')';
	   }
	   else
	   {	my $format= $h? _"{hours}h {min}m {sec}s ({size}M)" : _"{min}m {sec}s ({size}M)";
		return __x($format, @values);
	   }
	}
	else { return ($sec,$size); } #return numbers
}

# http://www.allmusic.com/cg/amg.dll?p=amg&opt1=1&sql=%s    artist search
# http://www.allmusic.com/cg/amg.dll?p=amg&opt1=2&sql=%s    album search
sub AMGLookup
{	my ($col,$key)=@_;
	my $opt1=	$col==SONG_ARTIST ? 1 :
			$col==SONG_ALBUM  ? 2 : 3;
	my $url='http://www.allmusic.com/cg/amg.dll?p=amg&opt1='.$opt1.'&sql=';
	$key=~s/ /|/g;
	$key=~s/([][\\@;?\/\$&="'<>~:#+,`])/sprintf '%%%x',ord$1/ge;
	if ($^O eq 'MSWin32') {system 'start',$url.$key;return}
	$url=quotemeta $url.$key;	#s/(\W)/\\$1/g;	#escape special chars
	openurl($url);
}

sub Google
{	my $ID=shift;
	my $url='http://google.com/search?q=';
	my @q;
	push @q,$Songs[$ID][$_] for SONG_TITLE,SONG_ARTIST,SONG_ALBUM;
	s/([][\\@;?\/\$&="'<>~: #+,`])/sprintf '%%%x',ord$1/ge for @q;
	if ($^O eq 'MSWin32') {system 'start',$url.join('+',@q);return}
	$url=quotemeta $url.join('+',@q);
	openurl($url);
}
sub openurl
{	my $url=$_[0];
	my $cmd="$browsercmd $url";
	$cmd.=' &' unless $^O eq 'MSWin32';
	system $cmd;
}

sub ChooseAAPicture
{	my ($ID,$col,$key)=@_;
	my $AAref=($col==SONG_ARTIST)? \%Artist : \%Album;
	my $file;
	if (defined $ID) { $file=$::Songs[$ID][SONG_PATH]; }
	else { my %h; $h{$_}++ for map $::Songs[$_][SONG_PATH],@{ $AAref->{$key}[AALIST] }; ($file)=sort { $h{$b} <=> $h{$a} } keys %h; }	#FIXME should try to find common parent folder
	$file=ChoosePix($file,_"Choose picture for ".$key,$AAref->{$key}[AAPIXLIST]);
	return unless defined $file;
	$AAref->{$key}[AAPIXLIST]=$file;
	HasChanged('AAPicture',$key);
}

sub ChooseSongsTitle		#Songs with the same title
{	my $ID=$_[0];
	my $title=$Songs[$ID][SONG_TITLE];
	return 0 if $title eq '';
	my @list=@{ Filter->new(SONG_TITLE.'~'.$Songs[$ID][SONG_TITLE])->filter; };
	return 0 if @list<2 || @list>100;	#probably a problem if it finds >100 matching songs, and making a menu with a huge number of items is slow
	@list=grep $_!=$ID,@list;
	SortList(\@list,SONG_ARTIST.' '.SONG_ALBUM);
	return ChooseSongs( __x( _"by {artist} from {album}", artist => "<b>%a</b>", album => "%l") ,@list);
}

sub ChooseSongsFromA
{	my $album=shift;
	return unless defined $album;
	my $list=$Album{ $album }[AALIST];
	SortList($list,SONG_DISC.' '.SONG_TRACK.' '.SONG_UFILE);
	my $menu = ChooseSongs('%n %t', @$list);
	$menu->show_all;
	my $picsize=$menu->size_request->height;
	$picsize=220 if $picsize>220;
	if ( my $img=NewScaledImageFromFile($Album{ $album }[AAPIXLIST],$picsize) )
	{	my $item=Gtk2::MenuItem->new;
		$item->add($img);
		my $col=$menu->{nbcols};
		$menu->attach($item, $col, $col+1, 0, scalar @$list);
		$item->show_all;
	}
	if (defined wantarray)	{return $menu}
	$menu->popup(undef,undef,\&menupos,undef,$LEvent->button,$LEvent->time);
}

sub ChooseSongs
{	my ($format,@IDs)=@_;
	$format||= __x( _"{song} by {artist}", song => "<b>%t</b>", artist => "%a");
	my $menu = Gtk2::Menu->new;
	my $activate_callback=sub
	 { if ($_[0]->{middle}) { Enqueue($_[1]); }
	   else { Select(undef,undef,$_[1]); }
	 };
	my $click_callback=sub
	 { if	($_[1]->button == 2) { $_[0]->{middle}=1 }
#	   elsif($_[1]->button == 3)
#	   {	$LEvent=$_[1];
#		my $submenu=PopupContextMenu(\@SongCMenu,{IDs=> [$_[2]]});
#		$submenu->signal_connect( selection_done => sub {$menu->popdown});
#		$submenu->show_all;
#		$submenu->popup(undef,undef,undef,undef,$LEvent->button,$LEvent->time);
#		return 1;
#	   }
	   return 0;
	 };

	my $cols= $menu->{nbcols}= (@IDs<40)? 1 : (@IDs<80)? 2 : 3;
	my $rows=int(@IDs/$cols);

	my $row=0; my $col=0;
	for my $ID (@IDs)
	{   my $label=Gtk2::Label->new;
	    my $item=Gtk2::CheckMenuItem->new;
	    $item->set_draw_as_radio(1);
	    $item->add($label);
	    $menu->attach($item, $col, $col+1, $row, $row+1); if (++$row>$rows) {$row=0;$col++;}
	    #$menu->append($item);
	    if ($ID=~m/^\d+$/)
	    {	$label->set_alignment(0,.5); #left-aligned
		$label->set_markup( ReplaceFieldsAndEsc($ID,$format) );
		$item->set_active(1) if $ID==$SongID;
		$item->signal_connect (activate => $activate_callback, $ID);
		$item->signal_connect (button_release_event => $click_callback, $ID);
		#set_drag($item, source => [::DRAG_ID,sub {$ID}]);
	    }
	    else
	    {	$label->set_markup("<b>$ID</b>");
	    }
	}
	if (defined wantarray)	{return $menu}
	my $ev=$LEvent;
	$menu->show_all;
	$menu->popup(undef,undef,\&menupos,undef,$ev->button,$ev->time);
}

sub menupos	# function to position popupmenu below clicked widget
{	my $h=$_[0]->size_request->height;		# height of menu to position
	my $ymax=$LEvent->get_screen->get_height;	# height of the screen
	my ($x,$y)=$LEvent->window->get_origin;		# position of the clicked widget on the screen
	my $dy=($LEvent->window->get_geometry)[3];	# height of the clicked widget
	if ($dy+$y+$h > $ymax)  { $y-=$h; $y=0 if $y<0 }	# display above the widget
	else			{ $y+=$dy; }			# display below the widget
	return $x,$y;
}

sub PopupAA
{	my ($col,$key,$callback,$format)=@_;
	return undef unless @Library;
	$format||="%a";
	my $href=($col==SONG_ARTIST)? \%Artist : \%Album;

####make list of albums/artists
	my @keys;
	if (defined $key)
	{	if (ref $key) {@keys=@$key;}
		elsif ($col==SONG_ALBUM)
		{ my %alb;
		  for my $artist (split / ?& ?/,$key)
		  {	push @{$alb{$_}},$artist for keys %{ $Artist{$artist}[AAXREF] };  }
		  #{	$alb{$_}=undef for keys %{ $Artist{$artist}[AAXREF] };  }
		  #@keys=keys %alb;
		  my %art_keys;
		  while (my($album,$list)=each %alb)
		  {	my $artist=join ' & ',@$list;
			push @{$art_keys{$artist}},$album;
		  }
		  if (1==keys %art_keys)
		  {	@keys=@{ $art_keys{ (keys %art_keys)[0] } };
		  }
		  else	#multiple artists -> create a submenu for each artist
		  {	my $menu=Gtk2::Menu->new;
			for my $artist (keys %art_keys)
			{	my $item=Gtk2::MenuItem->new($artist);
				$item->set_submenu(PopupAA(SONG_ALBUM,$art_keys{$artist}));
				$menu->append($item);
			}
			$menu->show_all;
			if (defined wantarray) {return $menu}
			$menu->popup(undef,undef,\&menupos,undef,$LEvent->button,$LEvent->time);
			return;
		  }
		}
		else
		{ @keys=@{$Album{$key}[AAXREF]}; }
	}
	else { @keys=grep @{$href->{$_}[AALIST]}, keys %$href; }

	my @keys=sort { uc$a cmp uc$b } @keys;

#### callbacks
	my $rmbcallback;
	unless ($callback)
	{  $callback=sub		#jump to first song
	   {	return if $_[0]->get_submenu;
		my $key=$_[1];
		my $ID=FindFirstInListPlay( $href->{$key}[AALIST] );
		Select(undef,undef,$ID);
	   };
	   $rmbcallback=($col==SONG_ARTIST)?
	   	sub	#Arists rmb cb
		{	(my$item,$LEvent,my$key)=@_;
			return 0 unless $LEvent->button==3;
			my $submenu=PopupAA(SONG_ALBUM,$key);
			$item->set_submenu($submenu);
			1;
		}:
	   	sub	#Albums rmb cb
		{	(my$item,$LEvent,my$key)=@_;
			return 0 unless $LEvent->button==3;
			my $submenu=ChooseSongsFromA($key);
			$item->set_submenu($submenu);
			1;
		};
	}


	my $max=($LEvent->get_screen->get_height)*.8;
	#my $minsize=Gtk2::ImageMenuItem->new('')->size_request->height;
	my @todo;	#hold images not yet loaded because not cached

	my $createAAMenu=sub
	{	my ($start,$end)=@_;
		my $nb=$end-$start+1;
		my $size=32;	$size=64 if 64*$nb < $max;
		my $row=0; my $rows=($nb<21)? 1 : ($nb<50)? 2 : 3; $rows=int($nb/$rows); my $colnb=0;
	#	my $size=int($max/$rows);	#my $size=int($max/$nb);
	#	if ($size<$minsize) {$size=$minsize} elsif ($size>100) {$size=100}
		my $menu = Gtk2::Menu->new;
		for my $i ($start..$end)
		{	my $key=$keys[$i];
			my $item=Gtk2::ImageMenuItem->new;
			my $label=Gtk2::Label->new; $label->set_alignment(0,.5);
			$label->set_markup( ReplaceAAFields($key,$format,$col,1) );
			#$label->set_markup( ReplaceAAFields($key,"<b>%a</b>%Y\n<small>%s <small>%l</small></small>",$col,1) );
			$item->add($label);
			$item->signal_connect(activate => $callback,$key);
			$item->signal_connect(button_press_event => $rmbcallback,$key) if $rmbcallback;
			#$menu->append($item);
			$menu->attach($item, $colnb, $colnb+1, $row, $row+1); if (++$row>$rows) {$row=0;$colnb++;}
			if (my $f=$$href{$key}[AAPIXLIST])
			{	my $img=AAPicture::newimg($col,$key,$size,\@todo);
				$item->set_image($img);
				#push @todo,$item,$f,$size;
			}
		}
		return $menu;
	}; #end of createAAMenu

	my $menu=Breakdown_List(\@keys,5,20,35,$createAAMenu);
	return undef unless $menu;
	$menu->show_all;

	if (@todo)
	{ Glib::Idle->add(sub
		{	my $img=shift @todo;
			return 0 unless $img;
			AAPicture::setimg($img);
			1;
		});
	  $menu->signal_connect(destroy => sub {undef @todo})
	}

	if (defined wantarray) {return $menu}
	$menu->popup(undef,undef,\&menupos,undef,$LEvent->button,$LEvent->time);
}

sub Breakdown_List
{	my ($keys,$min,$opt,$max,$makemenu)=@_;

	if ($#$keys<=$max) { return $makemenu ? &$makemenu(0,$#$keys) : [0,$#$keys] }

	my @bounds;
	for my $start (0..$#$keys)
	{	my $name1= $start==0 ?  '' : lc $keys->[$start-1];
		my $name2= lc $keys->[$start];
		my $name3= $start==$#$keys ?  '' : lc $keys->[$start+1];
		my ($c1,$c3); my $pos=0;
		until (defined $c1 && defined $c3)
		{	my $l2=substr $name2,$pos,1;
			unless (defined $c1)
			{	my $l1=substr $name1,$pos,1;
				$c1=substr $name2,0,$pos+1 unless defined $l1 &&  $l1 eq $l2;
			}
			unless (defined $c3)
			{	my $l3=substr $name3,$pos,1;
				$c3=substr $name2,0,$pos+1 unless defined $l3 &&  $l3 eq $l2;
			}
			$pos++;
		}
		push @bounds,[$c1,$c3];
	}

	my @chunk;
	my @toobig=(1)x@bounds;
	my $len=1;
	{	my $c=0;
		for my $pos (0..$#bounds)
		{	if (length $bounds[$pos][0]<=$len) {$c=0} else {$c++}
			$chunk[$pos]=$c if $toobig[$pos];
		}
		$c=0;
		for my $pos (reverse 0..$#bounds)
		{	if (length $bounds[$pos][1]<=$len) {$c=0} else {$c++}
			if ($toobig[$pos])
			{	$chunk[$pos]+=$c+1;
				$toobig[$pos]=0 unless $chunk[$pos]>$max;
			}
		}
#		for my $pos (0..$#bounds)	#DEBUG
#		{	print "(pos=$pos) $len|| $bounds[$pos][0] $bounds[$pos][1] $chunk[$pos]\n";
#		}
		$len++;
		redo if grep $_, @toobig;
	}
	my @breakpoints=(0); my @length=(0);
	my $pos=0;
	while ($pos<@chunk)
	{	my $size=$chunk[$pos];
		$pos+=$size;
		push @breakpoints,$pos;
		push @length,length $bounds[$pos][0];
		#print "$#length : ".(length $bounds[$pos][0])." ($pos)\n";	#DEBUG
	}
#	push @breakpoints,$#$keys+1; push @length,1;

	my $start=0; my @list;
	while ($start<$#breakpoints)
	{	my $best; my $bestpos;
		for (my $i=$start+1; $i<=$#breakpoints; $i++)
		{	my $nb=$breakpoints[$i]-$breakpoints[$start];
			my $nbafter=$#$keys-$breakpoints[$i]+1;
			next if $nb<$min && $i<$#breakpoints;
			my $score=$length[$i]*100+abs($nb-$opt)+ ($nbafter==0 ? -10 : $nbafter<8 ? 8-$nbafter : 0);
#warn "$start-$i ($breakpoints[$start]-$breakpoints[$i]): $nb  length=$length[$i]	score=$score  nbafter=$nbafter\n";	#DEBUG
			if (!defined $best || $best>$score)
			 {$best=$score; $bestpos=$i;}
			last if $nb>$max && $nbafter>$min;
		}
#warn " best: $start-$bestpos ($breakpoints[$start]-$breakpoints[$bestpos]): score=$best\n";	#DEBUG
		push @list,$breakpoints[$bestpos]; $start=$bestpos;
	}
#	for my $i (0..$#$keys)	#DEBUG
#	{	my $b=grep $i==$_, @breakpoints;
#		$b= $b? '->' : ' ';
#		my $b2=grep $i==$_, @list;
#		$b2= $b2? '=>' : ' ';
#		warn "$i\t$b\t$b2\t$bounds[$i][0]\t$bounds[$i][1]\t$keys->[$i]\n";
#	}
	@breakpoints=@list;

	my @menus; my $start=0;
	for my $end (@breakpoints)
	{	my $c1=$bounds[$start][0];
		my $c2=$bounds[$end-1][1];
		for my $i (0..length($c1)-1)
		{	my $c2i=substr $c2,$i,1;
			if ($c2i eq '') { $c2.=$c2i= substr $keys->[$end-1],$i,1; }
			last if substr($c1,$i,1) ne $c2i;
		}
		#warn "$c1-$c2\n";
		push @menus,[$start,$end-1,$c1,$c2];
		$start=$end;
	}

	return @menus unless $makemenu;
	my $menu;
	if (@menus>1)
	{	$menu=Gtk2::Menu->new;
		for my $ref (@menus)
		{	my ($start,$end,$c1,$c2)=@$ref;
			$c1=ucfirst$c1; $c2=ucfirst$c2;
			$c1.='-'.$c2 if $c2 ne $c1;
			my $item=Gtk2::MenuItem->new($c1);
			my $submenu= &$makemenu($start,$end);
			$item->set_submenu($submenu);
			$menu->append($item);
		}
	}
	elsif (@menus==1) { $menu= &$makemenu(0,$#$keys); }
	else {return undef}

	return $menu;
}

sub PixLoader_callback
{	my ($loader,$w,$h,$max)=@_;
	$loader->{w}=$w;
	$loader->{h}=$h;
	if ($max!~s/^-// or $w>$max or $h>$max)
	{	my $r=$w/$h;
		if ($r>1) {$h=int(($w=$max)/$r);}
		else	  {$w=int(($h=$max)*$r);}
		$loader->set_size($w,$h);
	}
}
sub LoadPixData
{	my ($pixdata,$size)=($_[0],$_[1]);
	my $loader=Gtk2::Gdk::PixbufLoader->new;
	$loader->signal_connect(size_prepared => \&::PixLoader_callback,$size) if $size;
	eval { $loader->write($pixdata); };
	eval { $loader->close; };
	$loader=undef if $@;
	warn "$@\n" if $debug;
	return $loader;
}

sub PixBufFromFile
{	my ($file,$size)=($_[0],$_[1]);
	return unless $file;
	unless (-r $file) {warn "$file not found\n" unless $_[1]; return undef;}

	my $loader=Gtk2::Gdk::PixbufLoader->new;
	$loader->signal_connect(size_prepared => \&::PixLoader_callback,$size) if $size;
	if ($file=~m/\.mp3/i)
	{	my $data=ReadTag::PixFromMP3($file);
		eval { $loader->write($data) } if defined $data;
	}
	else	#eval{Gtk2::Gdk::Pixbuf->new_from_file(filename_to_unicode($file))};
		# work around Gtk2::Gdk::Pixbuf->new_from_file which wants utf8 filename
	{	open my$fh,'<',$file;
		my $buf; eval {$loader->write($buf) while read $fh,$buf,1024*50;};
		close $fh;
	}
	eval {$loader->close;};
	return $@ ? undef : $loader->get_pixbuf;
}

sub NewScaledImageFromFile
{	my ($pix,$w,$q)=@_;	# $pix=file or pixbuf , $w=size, $q true for HQ
	return undef unless $pix;
	my $h=$w;
	unless (ref $pix)
	{ $pix=PixBufFromFile($pix);
	  return undef unless $pix;
	}
	my $ratio=$pix->get_width / $pix->get_height;
	if    ($ratio>1) {$h=int($w/$ratio);}
	elsif ($ratio<1) {$w=int($h*$ratio);}
	$q=($q)? 'bilinear' : 'nearest';
	return Gtk2::Image->new_from_pixbuf( $pix->scale_simple($w, $h, $q) );
}

sub ScaleImageFromFile
{	my ($img,$w,$file,$nowarn)=@_;
	$img->{pixbuf}=PixBufFromFile($file,$nowarn);
	ScaleImage($img,$w);
}

sub ScaleImage
{	my ($img,$w)=@_;
	my $pix=$img->{pixbuf};
	if (!$pix || $w<16) { $img->set_from_pixbuf(undef); return; }
	my $h=$w;
	my $ratio=$pix->get_width / $pix->get_height;
	if    ($ratio>1) {$h=int($w/$ratio);}
	elsif ($ratio<1) {$w=int($h*$ratio);}
	$img->set_from_pixbuf( $pix->scale_simple($w, $h, 'bilinear') );
}

sub pixbox_button_press_cb	# zoom picture when clicked
{	my ($eventbox,$event,$button)=@_;
	return 0 if $button && $event->button != $button;
	my $image;
	if ($eventbox->{pixdata})
	{	my $loader=::LoadPixData($eventbox->{pixdata},300);
		$image=Gtk2::Image->new_from_pixbuf($loader->get_pixbuf) if $loader;
	}
	elsif (my $pixbuf=$eventbox->child->{pixbuf})
	 { $image=::NewScaledImageFromFile($pixbuf,300,1); }
	return 1 unless $image;
	my $menu=Gtk2::Menu->new;
	my $item=Gtk2::MenuItem->new;
	$item->add($image);
	$menu->append($item);
	$menu->show_all;
	$menu->popup(undef,undef,undef,undef,$event->button,$event->time);
	1;
}

sub PopupContextMenu
{	my ($mref,$args)=@_;
	my $menu_callback=sub
		 {	my $sub=$_[1];
			&$sub( $args );
		 };
	my $mode=$args->{mode} || '^$';
	$mode=qr/$mode/;
	my $count;
	my $menu=Gtk2::Menu->new;
	for my $m (@$mref)
	{   	next if $m->{ignore};
		next if $m->{isdefined}	&& !defined $args->{ $m->{isdefined} };
		next if $m->{mode}	&& $m->{mode}	!~m/$mode/;
		next if $m->{notmode}	&& $m->{notmode}=~m/$mode/;;
		next if $m->{empty}	&& (  $args->{ $m->{empty} }	&& @{ $args->{ $m->{empty}   } }!=0 );
		next if $m->{notempty}	&& ( !$args->{ $m->{notempty} }	|| @{ $args->{ $m->{notempty}} }==0 );
		next if $m->{onlyone}	&& ( !$args->{ $m->{onlyone}  }	|| @{ $args->{ $m->{onlyone} } }!=1 );
		next if $m->{onlymany}	&& ( !$args->{ $m->{onlymany} }	|| @{ $args->{ $m->{onlymany}} }<2  );
		next if $m->{test}	&& !&{ $m->{test} }($args);

		my $label=$m->{label};
		$label=&$label($args) if ref $label;
		my $item;
		if ($m->{stockicon})
		{	$item=Gtk2::ImageMenuItem->new($label);
			$item->set_image( Gtk2::Image->new_from_stock($m->{stockicon},'menu') );
		}
		elsif ($m->{check})
		{	$item=Gtk2::CheckMenuItem->new($label);
			$item->set_active(1) if &{ $m->{check} }($args);
		}
		else	{ $item=Gtk2::MenuItem->new($label); }
		if (my $submenu=$m->{submenu})
		{	$submenu=&$submenu($args) if ref $submenu eq 'CODE';
			$submenu=PopupContextMenu($submenu,$args) if ref $submenu eq 'ARRAY';
			next unless $submenu;
			$item->set_submenu($submenu);
		}
		else
		{	$item->signal_connect (activate => $menu_callback, $m->{code} );
		}
		$count++;
		$menu->append($item);
	}
	if (defined wantarray) {return $menu}
	return unless $count;
	$menu->show_all;
	$menu->popup(undef,undef,undef,undef,$LEvent->button,$LEvent->time);
}

sub set_drag
{	my ($widget,%params)=@_;
	if (my $dragsrc=$params{source})
	{	my $n=$dragsrc->[0];
		$widget->drag_source_set(
			['button1-mask'],['copy','move'],
			map [ $DRAGTYPES[$_][0], [] , $_ ], $n,
				keys %{$DRAGTYPES[$n][1]} );
		$widget->{dragsrc}=$dragsrc;
		$widget->signal_connect(drag_data_get => \&drag_data_get_cb);
		$widget->signal_connect(drag_begin => \&drag_begin_cb);
		$widget->signal_connect(drag_end => \&drag_end_cb);
	}
	if (my $dragdest=$params{dest})
	{	$widget->drag_dest_set(
			'all',['copy','move'],
			map [ $DRAGTYPES[$_][0], ($_==DRAG_ID ? 'same-app' : []) , $_ ],
				@$dragdest[0..$#$dragdest-1] );
		$widget->{dragdest}=$dragdest->[$#$dragdest];
		$widget->signal_connect(drag_data_received => \&drag_data_received_cb);
		$widget->signal_connect(drag_leave => \&drag_leave_cb);
		$widget->signal_connect(drag_motion => $params{motion}) if $params{motion}; $widget->{drag_motion_cb}=$params{motion};
	}
}

sub drag_begin_cb	#create drag icon
{	my ($self,$context)=@_;# warn "drag_begin_cb @_";
	$self->signal_stop_emission_by_name('drag_begin');
	$self->{drag_is_source}=1;
	my ($srcinfo,$sub)=@{ $self->{dragsrc} };
	my @values=&$sub($self);
	$context->{data}=\@values;
	my $plaintext;
	{	$sub=$DRAGTYPES[$srcinfo][1]{&DRAG_MARKUP};
		last if $sub;
		$plaintext=1;
		$sub=$DRAGTYPES[$srcinfo][1]{&DRAG_STRING};
		last if $sub;
		$sub=sub { join "\n",@_ };
	}
	my $text=&$sub(@values);
	###### create pixbuf from text
	return if !defined $text || $text eq '';
	my $layout=Gtk2::Pango::Layout->new( $self->create_pango_context );
	if ($plaintext) { $layout->set_text($text);   }
	else		{ $layout->set_markup($text); }
	my $PAD=3;
	my ($w,$h)=$layout->get_pixel_size; $w+=$PAD*2; $h+=$PAD*2;
	my $pixmap = Gtk2::Gdk::Pixmap->new(undef,$w,$h, $self->window->get_depth);
	$pixmap->draw_rectangle($self->style->bg_gc($self->state),TRUE,0,0,$w,$h);
	$pixmap->draw_rectangle($self->style->fg_gc($self->state),FALSE,0,0,$w-1,$h-1);
	$pixmap->draw_layout(   $self->style->fg_gc($self->state), $PAD, $PAD, $layout);
	my $pixbuf = Gtk2::Gdk::Pixbuf->get_from_drawable($pixmap, undef, 0, 0, 0, 0, $w,$h);
	$context->set_icon_pixbuf($pixbuf,$w/2,$h);
	######
	&{$self->{drag_begin_cb}}($self,$context) if $self->{drag_begin_cb};
}
sub drag_end_cb
{	shift->{drag_is_source}=undef;
}
sub drag_leave_cb
{	my ($self,$context)=@_;
	delete $self->{scroll};
	delete $self->{context};
}

sub drag_data_get_cb
{	my ($self,$context,$data,$destinfo,$time)=@_; #warn "drag_data_get_cb @_";
	my ($srcinfo,$sub)=@{ $self->{dragsrc} };
	my @values=@{ $context->{data} };#my @values=&$sub($self); return unless @values;
	if ($destinfo != $srcinfo)
	{	my $convsub=$DRAGTYPES[$srcinfo][1]{$destinfo};
		@values=$convsub?  &$convsub(@values)  :  ();
	}
	$data->set($data->target,8, join("\x0d\x0a",@values) ) if @values;
}
sub drag_data_received_cb
{	my ($self,$context,$x,$y,$data,$info,$time)=@_;# warn "drag_data_received_cb @_";
	my $sub=$self->{dragdest};
	my $ret=my$del=0;
	if ($data->length >=0 && $data->format==8)
	{	my @values=split "\x0d\x0a",$data->data;
		unshift @values,$context->{dest} if $context->{dest} && $context->{dest}[0]==$self;
		&$sub($self, $::DRAGTYPES{$data->target->name} , @values);
		$ret=1;#$del=1;
	}
	$context->finish($ret,$del,$time);
}

sub drag_checkscrolling	#check if need scrolling
{	my ($self,$context,$y)=@_;
	my $yend=$self->get_visible_rect->height;
	if	($y<40)		{$self->{scroll}=-1}
	elsif	($y>$yend-10)	{$self->{scroll}=1}
	else { delete $self->{scroll};delete $self->{context}; }
	if ($self->{scroll})
	{	$self->{scrolling}||=Glib::Timeout->add(200, \&drag_scrolling_cb,$self);
		$self->{context}||=$context;
	}
}
sub drag_scrolling_cb
{	my $self=$_[0];
	if (my $s=$self->{scroll})
	{	my ($align,$path)=($s<0)? (.1, $self->get_path_at_pos(0,0))
					: (.9, $self->get_path_at_pos(0,$self->get_visible_rect->height));
		$self->scroll_to_cell($path,undef,::TRUE,$align) if $path;
		&{ $self->{drag_motion_cb} } ($self,$self->{context}, ($self->window->get_pointer)[1,2], 0 ) if $self->{drag_motion_cb};
		return 1;
	}
	else
	{	delete $self->{scrolling};
		return 0;
	}
}

sub CreateDir
{	my ($format,$ID,$win,$abortmsg)=@_;
	my $path='';
	for my $dir (split /$QSLASH/,$format)
	{	$dir=ReplaceFields($ID,$dir) if defined $ID;
		$dir=~s/$ILLEGALCHAR//g;
		next if $dir eq '';
		$path.=SLASH.$dir;
		next if -d $path;
		until (mkdir $path)
		{	#if (-f $path) { ErrorMessage("Can't create folder '$path' :\na file with that name exists"); return undef }
			my $ret=Retry_Dialog( __x( _"Can't create Folder '{path}' : \n{error}", path => $path, error=> $!) ,$win,$abortmsg);
			return undef unless $ret eq 'yes';
		}
	}
	return $path;
}

sub CopyMoveFiles
{	my ($mode,$newdir,@IDs)=@_;
	my $format;
	if ($mode=~s/WithFormat$//) { $format=$newdir; $newdir=undef; }
	my ($sub,$msg,$errormsg,$abortmsg)=
	   ($mode eq 'Move') ?
		(\&move,_"Choose directory to move files to",_"Move failed",_"abort move") :
		(\&copy,_"Choose directory to copy files to",_"Copy failed",_"abort copy") ;
	$abortmsg=undef unless @IDs>1;
	my $action=($mode eq 'Move') ?	__("Moving file", "Moving %d files", scalar@IDs) :
					__("Copying file","Copying %d files",scalar@IDs) ;
	$newdir||=ChooseDir($msg,$Songs[$IDs[0]][SONG_PATH].SLASH) unless defined $format;
	return unless defined $newdir;
	#_utf8_on($newdir);

	my $win=Gtk2::Window->new('toplevel');
	$win->set_border_width(3);
	my $label=Gtk2::Label->new($action);
	my $progressbar=Gtk2::ProgressBar->new;
	my $Bcancel=Gtk2::Button->new_from_stock('gtk-cancel');
	my $cancel;
	my $cancelsub=sub {$cancel=1};
	$win->signal_connect( destroy => $cancelsub);
	$Bcancel->signal_connect( clicked => $cancelsub);
	my $vbox=Gtk2::VBox->new(FALSE, 2);
	$vbox->pack_start($_, FALSE, TRUE, 3) for $label,$progressbar,$Bcancel;
	$win->add($vbox);
	$win->show_all;
	my $done=0;

	my $Unewdir;
	$Unewdir=filename_to_utf8displayname($newdir) if defined $newdir;

	my $owrite_all;
COPYNEXTID:for my $ID (@IDs)
	{	$progressbar->set_fraction($done/@IDs);
		Gtk2->main_iteration while Gtk2->events_pending;
		$done++;
		$abortmsg=undef if $done==@IDs;
		my $aref=$Songs[$ID];
		my $file  =$$aref[SONG_FILE];
		my $olddir=$$aref[SONG_PATH];
		my $old=$olddir.SLASH.$file;
		if ($format)
		{	$Unewdir=CreateDir($format,$ID);
			next unless defined $Unewdir;
			$newdir=filename_from_unicode($Unewdir);
		}
		my $new=$newdir.SLASH.$file;
		my $res=1;
		if (-f $new)
		{	my $ow=$owrite_all;
			$ow||=OverwriteDialog($win,$new,(@IDs>1));
			$owrite_all=$ow if $ow=~m/all$/;
			next if $ow=~m/^no/;
		}
		until (&$sub($old,$new))
		{	$res=Retry_Dialog("$errormsg :\n'$old'\n -> '$new'\n$!",$win,$abortmsg);
			if ($res eq 'abort') { last COPYNEXTID }
			last unless $res eq 'yes';
		}
		if ($res && $mode eq 'Move')
		{	$GetIDFromFile{$newdir}{$file} = delete $GetIDFromFile{$olddir}{$file};
			$$aref[SONG_PATH]=$newdir;
			$$aref[SONG_UPATH]=$Unewdir;
			SongChanged($ID,SONG_PATH,SONG_UPATH);
			delete $GetIDFromFile{$olddir} unless keys %{ $GetIDFromFile{$olddir} };
		}
		last if $cancel;
	}
	$win->destroy;
}

sub ChooseDir
{	my ($msg,$path,$extrawidget) = @_;
	my $dialog=Gtk2::FileChooserDialog->new($msg,undef,'select-folder',
					'gtk-ok' => 'ok',
					'gtk-cancel' => 'none',
					);
	$dialog->set_current_folder($path);
	$dialog->set_extra_widget($extrawidget) if $extrawidget;

	if ($dialog->run eq 'ok')
	{   $path=$dialog->get_filename;
	    eval { $path=filename_from_unicode($path); };
	    $path=undef unless -d $path;
	}
	else {$path=undef}
	$dialog->destroy;
	return $path;
}
sub ChooseDir_old
{	my ($msg,$path) = @_;
	my $DirSelector=Gtk2::FileSelection->new($msg);
	$DirSelector->file_list->set_sensitive(FALSE);
	$DirSelector->set_transient_for($LEventW);
	$DirSelector->set_filename(filename_to_utf8displayname($path)) if -d $path;
	if ($DirSelector->run eq 'ok')
	{   $path=filename_from_unicode($DirSelector->get_filename);
	    $path=undef unless -d $path;
	}
	else {$path=undef}
	$DirSelector->destroy;
	return $path;
}

sub ChoosePix
{	my ($path,$text,$file)=@_;
	$text||=_"Choose Picture";
	my $dialog=Gtk2::FileChooserDialog->new($text,undef,'open',
					_"no picture" => 'reject',
					'gtk-ok' => 'ok',
					'gtk-cancel' => 'none');

	my $filter = Gtk2::FileFilter->new;
	#$filter->add_mime_type('image/'.$_) for qw/jpeg gif png bmp/;
	$filter->add_mime_type('image/*');
	$filter->add_pattern('*.mp3');
	$filter->set_name(_"Pictures & mp3 files");
	$dialog->add_filter($filter);
	$filter = Gtk2::FileFilter->new;
	$filter->add_mime_type('image/*');
	$filter->set_name(_"Pictures files");
	$dialog->add_filter($filter);
	$filter = Gtk2::FileFilter->new;
	#$filter->add_mime_type('*');
	$filter->add_pattern('*');
	$filter->set_name(_"All files");
	$dialog->add_filter($filter);

	my $preview=Gtk2::VBox->new;
	my $label=Gtk2::Label->new;
	my $image=Gtk2::Image->new;
	$preview->pack_start($_,FALSE,FALSE,2) for $image,$label;
	$dialog->set_preview_widget($preview);
	#$dialog->set_use_preview_label(FALSE);
	my $update_preview=sub
		{ my ($dialog,$file)=@_;
		  unless ($file)
		  {	$file= eval {$dialog->get_preview_filename};
			$file=$dialog->get_filename if $@; #for some reason get_preview_filename doesn't work with bad utf8 whereas get_filename works. don't know if there is any difference
		  }
		  return unless $file;
		  eval{ $file=filename_from_unicode($file); };
		  ScaleImageFromFile($image,150,$file);
		  my $p=$image->{pixbuf};
		  if ($p) { $label->set_text($p->get_width.' x '.$p->get_height); }
		  #else { $label->set_text('no picture'); }
		  $dialog->set_preview_widget_active($p);
		};
	$dialog->signal_connect(selection_changed => $update_preview);

	$preview->show_all;
	if ($file && -f $file)	{ $dialog->set_filename($file); &$update_preview($dialog,$file); }
	#else			{ $dialog->set_current_folder($path); }
	else			{ $dialog->set_filename($path.SLASH.'*.jpg'); }

	my $response=$dialog->run;
	my $ret;
	if ($response eq 'ok')
	{	$ret=$dialog->get_filename;
		eval { $ret=filename_from_unicode($ret); };
		unless (-r $ret) { warn "can't read $ret\n"; $ret=undef; }
	}
	elsif ($response eq 'reject') {$ret='0'}
	else {$ret=undef}
	$dialog->destroy;
	return $ret;
}

sub ChoosePix_old
{	my ($path,$text)=@_;
	my $PixSelector=Gtk2::FileSelection->new($text||'Choose Picture');
	$PixSelector->set_transient_for($LEventW);
	$PixSelector->add_button(_"no picture",'reject'); #FIXME add before ok and cancel buttons
	my $flist=$PixSelector->file_list;
	my $dialog_hbox=$flist->parent->parent->parent; #FIXME
	my $previewbox=Gtk2::VBox->new(FALSE,2);
	my $frame=Gtk2::Frame->new('Preview');
	my $eventbox=Gtk2::EventBox->new;
	my $img=Gtk2::Image->new;
	$eventbox->add($img);
	$frame->add($eventbox);
	$eventbox->signal_connect(button_press_event => \&pixbox_button_press_cb);
	$frame->set_size_request(155,155);
	my $label=Gtk2::Label->new;
	$PixSelector->set_filename(filename_to_utf8displayname($path.SLASH)) if $path &&  -d $path;
	$previewbox->pack_start($_,FALSE,FALSE,2) for $frame,$label;
	$PixSelector->selection_entry->signal_connect(changed => sub
		{	my ($file)=$PixSelector->get_selections;
			$file=filename_from_unicode($file);
			ScaleImageFromFile($img,150,$file,'nowarn');
			my $p=$img->{pixbuf};
			my $text=$p? $p->get_width.' x '.$p->get_height  : '';
			$label->set_text($text);
			$img->show_all;
		});
	$previewbox->show_all;
	$dialog_hbox->pack_start($previewbox,FALSE,FALSE,2);
	$PixSelector->complete ('*.jpg');
	my $response = $PixSelector->run;
	my $ret;
	if ($response eq 'ok')
	{	$ret=filename_from_unicode($PixSelector->get_filename);
		#$ret=$PixSelector->get_filename;
		unless (-r $ret) { warn "can't read $ret\n"; $ret=undef; }
	}
	elsif ($response eq 'reject') {$ret='0'}
	else {$ret=undef}
	$PixSelector->destroy;
	return $ret;
}

sub ChooseSaveFile
{	my ($window,$msg,$path,$file,$widget) = @_;
	my $dialog=Gtk2::FileChooserDialog->new($msg,$window,'save',
					'gtk-ok' => 'ok',
					'gtk-cancel' => 'none',
					);
	#$dialog->set_current_folder($path) if defined $path;
	$dialog->set_filename($path.SLASH.'*') if defined $path;
	$dialog->set_current_name(filename_to_utf8displayname($file)) if defined $file;
	$dialog->set_extra_widget($widget) if $widget;

	if ($dialog->run eq 'ok')
	{   $file=$dialog->get_filename;
	}
	else {$file=undef}
	$dialog->destroy;
	if ($file && -f $file)
	{	my $dialog = Gtk2::MessageDialog->new
		( $window,
		  [qw/modal destroy-with-parent/],
		  'warning','yes-no',
		  __x( _"'{file}' exists. Overwrite ?", file => $file )
		);
		$dialog->show_all;
		if ($dialog->run ne 'yes') {$file=undef;}
		$dialog->destroy;
	}
	return $file;
}

sub OverwriteDialog
{	my ($window,$file,$multiple)=@_;
	my $dialog = Gtk2::MessageDialog->new
	( $window,
	  [qw/modal destroy-with-parent/],
	  'warning','yes-no',
	  __x( _"'{file}' exists. Overwrite ?", file => $file )
	);
	if ($multiple)
	{	$dialog->add_button(_"yes to all",'1');
		$dialog->add_button(_"no to all",'2');
	}
	$dialog->show_all;
	my $ret=$dialog->run;
	$dialog->destroy;
	$ret=2 unless $ret;
	$ret=	($ret eq '1')	? 'yesall':
		($ret eq '2')	? 'noall' :
		($ret)		? $ret	  :
		'no';
	return $ret;
}

sub Retry_Dialog
{	my ($err,$window,$abortmsg)=@_;
	my $dialog = Gtk2::MessageDialog->new
	( $window,
	  [qw/modal destroy-with-parent/],
	  'error','yes-no',
	  "$err\n "._("retry ?")
	);
	$dialog->add_button($abortmsg, '1') if $abortmsg;
	$dialog->show_all;
	my $ret=$dialog->run;
	$dialog->destroy;
	$ret=	($ret eq '1')	? 'abort':
		($ret)		? $ret	  :
		'abort';
	return $ret;
}

sub ErrorMessage
{	my ($err,$window)=@_;
	warn "$err\n";
	my $dialog = Gtk2::MessageDialog->new
	( $window,
	  [qw/modal destroy-with-parent/],
	  'error','close',
	  $err
	);
	$dialog->show_all;
	$dialog->run;
	$dialog->destroy;
}

sub EditLyrics
{	my $ID=$_[0];
	if (exists $Editing{'L'.$ID}) { $Editing{'L'.$ID}->present; return; }
	my $lyrics=ReadTag::GetLyrics($ID);
	$lyrics='' unless defined $lyrics;
	$Editing{'L'.$ID}=
	  EditLyricsDialog(undef,$lyrics,"Lyrics for ".$Songs[$ID][SONG_UPATH].SLASH.$Songs[$ID][SONG_UFILE],sub
	   {	delete $Editing{'L'.$ID};
		ReadTag::WriteLyrics($ID,$_[0]) if defined $_[0];
	   });
}

sub EditLyricsDialog
{	my ($window,$init,$text,$sub)=@_;
	my $dialog = Gtk2::Dialog->new ($text||_"Edit Lyrics", $window,'destroy-with-parent');
	my $bsave=$dialog->add_button('gtk-save' => 'ok');
		  $dialog->add_button('gtk-cancel' => 'none');
	$dialog->set_default_response ('ok');
	my $textview=Gtk2::TextView->new;
	my $buffer=$textview->get_buffer;
	$buffer->set_text($init);
	$buffer->signal_connect( changed => sub { $bsave->set_sensitive( $buffer->get_text($buffer->get_bounds,1) ne $init); });
	$bsave->set_sensitive(0);

	my $sw=Gtk2::ScrolledWindow->new(undef,undef);
	$sw->set_shadow_type('etched-in');
	$sw->set_policy('automatic','automatic');
	$sw->add_with_viewport($textview);
	$dialog->vbox->add($sw);
	SetWSize($dialog,'Lyrics');
	$dialog->show_all;
	$dialog->signal_connect( response => sub
		{	my $response=$_[1];
			my $lyrics;
			$lyrics=$buffer->get_text( $buffer->get_bounds, 1) if $response eq 'ok';
			$dialog->destroy;
			&$sub($lyrics) if $sub;
		});
	return $dialog;
}

sub DeleteFiles
{	my $IDs=$_[0];
	my $text=(@$IDs==1)? $Songs[$IDs->[0]][SONG_UFILE] : @$IDs.' files';
	my $dialog = Gtk2::MessageDialog->new
		( undef,
		  'modal',
		  'warning','ok-cancel',
		  "About to delete $text\nAre you sure ?"
		);
	$dialog->show_all;
	if ('ok' eq $dialog->run)
	{ my $abortmsg;
	  $abortmsg='Abort' if @$IDs>1;
	  for my $ID (@$IDs)
	  {	my $f=$Songs[$ID][SONG_PATH].SLASH.$Songs[$ID][SONG_FILE];
		my $res=1;
		until (unlink $f)
		{	$res=::Retry_Dialog("Failed to delete '$f'\n$!",undef,$abortmsg);
			last unless $res eq 'yes';
		}
		if ($res) { IdleCheck($ID); }
		elsif (defined $res)	{last}
	  }
	}
	$dialog->destroy;
}

sub filenamefromformat
{	my ($ID,$format)=@_;
	my $s=ReplaceFields( $ID, $format );
	$s=~s/$ILLEGALCHAR//g;
	$s=~s/ +$//;
	return $s.( $Songs[$ID][SONG_UFILE]=~m/(\.[^\.]+$)/ )[0]; #add extension
}

sub DialogMassRename
{	my @IDs=do {my %h; grep !$h{$_}++, @_ }; #remove duplicates IDs in @_ => @IDs
	my $dialog = Gtk2::Dialog->new
			(_"Mass Renaming", undef,
			 [qw/destroy-with-parent/],
			 'gtk-ok'	=> 'ok',
			 'gtk-cancel'	=> 'none',
		 	);
	$dialog->set_border_width(4);
	$dialog->set_default_response ('ok');
	my $table=MakeReplaceTable('talydno');
	$dialog->vbox->add(Gtk2::Label->new("Rename files based on these fields :\n"));	#FIXME explain
	$dialog->vbox->pack_start($table,TRUE,TRUE,5);
	my $combo=Gtk2::ComboBoxEntry->new_text;
	$combo->child->set_activates_default(TRUE);
	my $label=Gtk2::Label->new;
	$combo->signal_connect(changed => sub
		{	my $s=filenamefromformat( $_[1], $_[0]->child->get_text );
			$label->set_text('ex : '.$s);
		},$IDs[0]);
	###
#	my $notebook=Gtk2::Notebook->new;
#	my $store=Gtk2::ListStore->new('Glib::String','Glib::String','Glib::Boolean');
#	my $treeview1=Gtk2::TreeView->new($store);
#	my $treeview2=Gtk2::TreeView->new($store);
#	$notebook->append_page($treeview1,'before');
#	$notebook->append_page($treeview2,'after');
#	my $sw=Gtk2::ScrolledWindow->new(undef,undef);
#	$sw->set_shadow_type('etched-in');
#	$sw->set_policy('never','automatic');
#	$sw->add_with_viewport($notebook);
#	$dialog->vbox->pack_end($sw,TRUE,TRUE,5);
	###
	$combo->append_text($_) for ('%a - %l - %n - %t','%l - %n - %t','%n-%t','cd%d-%n-%t');
	$combo->set_active(0);
	$dialog->vbox->pack_start($combo,FALSE,FALSE,5);
	$dialog->vbox->pack_start($label,FALSE,FALSE,5);

	$dialog->show_all;
	$dialog->signal_connect( response => sub
	 {	my ($dialog,$response)=@_;
		if ($response eq 'ok')
		{ my $format=$combo->child->get_text;
	   	  if ($format)
		  {  for my $ID (@IDs)
		     {	my $s=filenamefromformat( $ID, $format );
			my $ret=RenameFile($ID,$s,$dialog,'Abort mass-renaming');
			last if $ret eq 'abort';
		      }
		  }
		}
		$dialog->destroy;
	 });
}

sub RenameFile
{	my ($ID,$newutf8,$window,$abortmsg)=@_;
	$newutf8=~s/$ILLEGALCHAR//g;
	my $new=filename_from_unicode($newutf8);
	my $old=$Songs[$ID][SONG_FILE];
	my $dir=$Songs[$ID][SONG_PATH];
	until ($new ne '' && ( rename $dir.SLASH.$old, $dir.SLASH.$new ))
	{	my $res=::Retry_Dialog("Rename failed :\n'$old'\n -> '$new'\n$!",$window,$abortmsg);
		return $res unless $res eq 'yes';
	}
	$Songs[$ID][SONG_FILE]=$new;
	$Songs[$ID][SONG_UFILE]=$newutf8;
	$GetIDFromFile{$dir}{$new}=delete $GetIDFromFile{$dir}{$old};
	SongChanged($ID,SONG_UFILE,SONG_FILE);
}

sub DialogRename
{	my $ID=$_[0];
	my $dialog = Gtk2::Dialog->new (_"Rename File", undef, [],
				'gtk-ok'	=> 'ok',
				'gtk-cancel'	=> 'none');
	$dialog->set_default_response ('ok');
	my $table=Gtk2::Table->new(4,2);
	my $row=0;
	for my $col (SONG_TITLE,SONG_ARTIST,SONG_ALBUM,SONG_DISC,SONG_TRACK)
	{	next if ($_==SONG_DISC || $_==SONG_TRACK) && !$Songs[$ID][$_];
		my $lab1=Gtk2::Label->new;
		my $lab2=Gtk2::Label->new($Songs[$ID][$col]);
		$lab1->set_markup('<b>'.$TagProp[$col][0].' :</b>');
		$lab1->set_padding(5,0);
		$lab1->set_alignment(1,.5);
		$lab2->set_alignment(0,.5);
		$lab2->set_line_wrap(1);
		$lab2->set_selectable(TRUE);
		$table->attach_defaults($lab1,0,1,$row,$row+1);
		$table->attach_defaults($lab2,1,2,$row,$row+1);
		$row++;
	}
	my $entry=Gtk2::Entry->new;
	$entry->set_activates_default(TRUE);
	$entry->set_text($Songs[$ID][SONG_UFILE]);
	$dialog->vbox->add($table);
	$dialog->vbox->add($entry);
	SetWSize($dialog,'Rename');

	$dialog->show_all;
	$dialog->signal_connect( response => sub
	 {	my ($dialog,$response)=@_;
		if ($response eq 'ok')
		{	my $name=$entry->get_text;
			RenameFile($ID,$name,$dialog) if $name;
		}
		$dialog->destroy;
	 });
}

sub AddToListMenu
{	return undef unless keys %SavedLists;
	my $IDs=$_[0]{IDs};
	my $menusub=sub {my $key=$_[1]; push @{$SavedLists{$key}},@$IDs; HasChanged('SavedLists',$key,'push'); };

	my @keys=sort { uc$a cmp uc$b } keys %SavedLists;
	my $makemenu=sub
	{	my ($start,$end)=@_;
		my $menu=Gtk2::Menu->new;
		for my $i ($start..$end)
		{	my $l=$keys[$i];
			my $item=Gtk2::MenuItem->new($l);
			$item->signal_connect(activate => $menusub,$l);
			$menu->append($item);
		}
		return $menu;
	};
	my $menu=Breakdown_List(\@keys,5,20,35,$makemenu);
	return $menu;
}

sub FlagEditMenu
{	return undef unless @Flags;
	my $IDs=$_[0]{IDs};
	my $menusub_unset=sub
	 {	my $f=$_[1];
		SetFlags($IDs,undef,[$f]);
	 };
	my $menusub_set=sub
	 {	my $f=$_[1];
		SetFlags($IDs,[$f],undef);
	 };
	my @keys=sort { uc$a cmp uc$b } @::Flags;
	my $makemenu=sub
	{	my ($start,$end)=@_;
		my $menu=Gtk2::Menu->new;
		for my $i ($start..$end)
		{	my $f=$keys[$i];
			my $item=Gtk2::CheckMenuItem->new_with_label($f);
			my $state=grep $Songs[$_][SONG_FLAGS]=~m/(?:^|\x00)\Q$f\E(?:$|\x00)/ , @$IDs;
			my $sub=$menusub_set;
			if ($state==@$IDs) { $item->set_active($state==@$IDs); $sub=$menusub_unset; }
			elsif ($state>0)  { $item->set_inconsistent(1); }
			$item->signal_connect(activate => $sub,$f);
			$menu->append($item);
		}
		return $menu;
	};
	my $menu=Breakdown_List(\@keys,5,20,35,$makemenu);
	return $menu;
}

sub filterAA
{	my ($col,$key)=@{$_[0]}{qw/col key/};
	my $cmd=($col==SONG_ARTIST)? '~' : 'e';
	Select($col.$cmd.$key);
}
sub FilterOnAA
{	my ($widget,$col,$key,$filternb)=@{$_[0]}{qw/self col key filternb/};
	$filternb=1 unless defined $filternb;
	my $cmd=($col==SONG_ARTIST)? '~' : 'e';
	$widget->get_toplevel->SetFilter($filternb,$col.$cmd.$key);
}
sub SearchSame
{	my $col=$_[0];
	my ($widget,$IDs,$filternb)=@{$_[1]}{qw/self IDs filternb/};
	$filternb=1 unless defined $filternb;
	my $cmd= ($col==SONG_TITLE || $col==SONG_ARTIST)? '~' : 'e';
	my $filter=Filter->newadd(FALSE,map($col.$cmd.$Songs[$_][$col], @$IDs));
	$widget->get_toplevel->SetFilter($filternb,$filter);
}

sub SongsSubMenuTitle
{	my $aaref= ($_[0]{col}==SONG_ARTIST)? \%Artist : \%Album;
	my $nb=@{ $aaref->{$_[0]{key}}[AALIST] };
	return __("%d Song","%d Songs",$nb);
}
sub SongsSubMenu
{	my %args=%{$_[0]};
	$args{mode}='S';
	my $aaref= ($args{col}==SONG_ARTIST)? \%Artist : \%Album;
	$args{IDs}=\@{ $aaref->{$args{key}}[AALIST] };
	return PopupContextMenu(\@SongCMenu,\%args);
}

sub ArtistContextMenu
{	my ($widget,$artist,$mode)=@_;
	my @mparams=(self=>$widget,col=>::SONG_ARTIST,mode=>$mode);
	my @l=split / ?& ?/,$artist;
	if (@l==1) { PopupContextMenu(\@cMenuAA,{@mparams,key=>$artist}); return; }
	my $menu = Gtk2::Menu->new;
	for my $ar (@l)
	{	my $item=Gtk2::MenuItem->new($ar);
		my $submenu=::PopupContextMenu(\@cMenuAA,{@mparams,key=>$ar});
		$item->set_submenu($submenu);
		$menu->append($item);
	}
	$menu->show_all;
	$menu->popup(undef,undef,undef,undef,$LEvent->button,$LEvent->time);
}

sub EditFlags
{	my @IDs=@_;
	my $vbox=Gtk2::VBox->new;
	my $table=Gtk2::Table->new(int(@Flags/3),3,FALSE);
	my %checks;
	my $changed;
	my $row=0; my $col=0;
	my $addflag=sub
	 {	my $flag=$_[0];
		my $check=Gtk2::CheckButton->new_with_label($flag);
		my $state=grep $Songs[$_][SONG_FLAGS]=~m/(?:^|\x00)\Q$flag\E(?:$|\x00)/ , @IDs;
		if ($state==@IDs) { $check->set_active(1); }
		elsif ($state>0)  { $check->set_inconsistent(1); }
		$check->signal_connect( toggled => sub	{  $_[0]->set_inconsistent(0); $changed=1 });
		$checks{$flag}=$check;
		if ($col==3) {$col=0; $row++;}
		$table->attach($check,$col,$col+1,$row,$row+1,['fill','expand'],'shrink',1,1);
		$col++;
	 };
	for my $flag (sort @Flags)
	{	&$addflag($flag);
	}
	$vbox->add($table);
	my $entry=Gtk2::Entry->new;
	my $addnew=sub
	 {	my $flag=$entry->get_text;
		return unless $flag;
		$entry->set_text('');
		&$addflag($flag) unless exists $checks{$flag};
		$checks{$flag}->set_active(1);
		$checks{$flag}->show_all;
	 };
	my $button=NewIconButton('gtk-add',_"Add new flag",$addnew);
	$entry->signal_connect( activate => $addnew );
	$vbox->pack_end( Hpack($entry,$button), FALSE, FALSE, 2 );

	$vbox->{save}=sub
	{	return unless $changed;
		no warnings;
		my @toremove; my @toadd;
		while (my ($flag,$check)=each %checks)
		{	next if $check->get_inconsistent;
			if ($check->get_active) { push @toadd,$flag }
			else			{ push @toremove,$flag }
			push @Flags,$flag unless (grep $_ eq $flag, @Flags);
		}
		SetFlags(\@IDs,\@toadd,\@toremove);
		#for my $sub (@ChangedFlags) {&$sub}
		SongsChanged(SONG_FLAGS,\@IDs);
	};
	return $vbox;
}

sub DialogSongsProp
{	my @IDs=@_;
	my $dialog = Gtk2::Dialog->new (_"Edit Multiple Songs Properties", undef,
				'destroy-with-parent',
				'gtk-save' => 'ok',
				'gtk-cancel' => 'none');
	$dialog->set_default_response ('ok');
	my $notebook = Gtk2::Notebook->new;
	$notebook->set_tab_border(4);
	$dialog->vbox->add($notebook);

	my $edittag=MassTag->new($dialog,@IDs);
	my $editflags=EditFlags(@IDs);
	my $rating=SongRating(@IDs);
	$notebook->append_page( $edittag ,	Gtk2::Label->new(_"Tag"));
	$notebook->append_page( $editflags,	Gtk2::Label->new(_"Flags"));
	$notebook->append_page( $rating,	Gtk2::Label->new(_"Rating"));

	SetWSize($dialog,'MassTag');
	$dialog->show_all;

	$dialog->signal_connect( response => sub
		{	#warn "MassTagging response : @_\n" if $debug;
			my ($dialog,$response)=@_;
			if ($response eq 'ok')
			{ $dialog->action_area->set_sensitive(FALSE);
			  &{ $editflags->{save} };
			  &{ $rating->{save} };
			  $edittag->save( sub {$dialog->destroy;} ); #the closure will be called when tagging finished #FIXME not clean
			}
			else { $dialog->destroy; }
			#delete $Editing{$ID};
		});
}

sub DialogSongProp
{	my $ID=$_[0];
	if (exists $Editing{$ID}) { $Editing{$ID}->present; return; }
	my $dialog = Gtk2::Dialog->new (_"Song Properties", undef, [],
				'gtk-save' => 'ok',
				'gtk-cancel' => 'none');
	$dialog->set_default_response ('ok');
	$Editing{$ID}=$dialog;
	my $notebook = Gtk2::Notebook->new;
	$notebook->set_tab_border(4);
	$dialog->vbox->add($notebook);

	my $edittag=EditTagSimple->new($dialog,$ID);
	my $editflags=EditFlags($ID);
	my $rating=SongRating($ID);
	my $songinfo=SongInfo($ID);
	$notebook->append_page( $edittag,	Gtk2::Label->new(_"Tag"));
	$notebook->append_page( $editflags,	Gtk2::Label->new(_"Flags"));
	$notebook->append_page( $rating,	Gtk2::Label->new(_"Rating"));
	$notebook->append_page( $songinfo,	Gtk2::Label->new(_"Info"));

	SetWSize($dialog,'SongInfo');
	$dialog->show_all;

	$dialog->signal_connect( response => sub
	{	warn "EditTag response : @_\n" if $debug;
		my ($dialog,$response)=@_;
		$songinfo->destroy;
		if ($response eq 'ok')
		{	&{ $editflags->{save} };
			&{ $rating->{save} };
			$edittag->save;
			IdleCheck($ID);
		}
		delete $Editing{$ID};
		$dialog->destroy;
	});
}

sub SongInfo
{	my $ID = shift;
	my $table=Gtk2::Table->new(8,2);
	my $row=0;
	for my $col (@TAGSELECTION)
	{	my $lab1=Gtk2::Label->new;
		my $lab2=$table->{$col}=Gtk2::Label->new;
		$lab1->set_markup('<b>'.$TagProp[$col][0].' :</b>');
		$lab1->set_padding(5,0);
		$lab1->set_alignment(1,.5);
		$lab2->set_alignment(0,.5);
		$lab2->set_line_wrap(1);
		$lab2->set_selectable(TRUE);
		$table->attach_defaults($lab1,0,1,$row,$row+1);
		$table->attach_defaults($lab2,1,2,$row,$row+1);
		$row++;
	}
	my $fillsub=sub
	{ for my $col (@TAGSELECTION)
	  {	my $t=$TagProp[$col][1];
		my $val=$Songs[$ID][$col];
		if    ($t eq 'd') { $val=$val ? scalar localtime $val : 'never' }
		elsif ($t eq 'l') { $val=sprintf '%d:%02d',$val/60,$val%60 }
		elsif ($t eq 'f') { $val=~s/\x00/, /g; }
		$table->{$col}->set_text($val);
	  }
	};
	my $watcher=AddWatcher([$ID],\@TAGSELECTION,$fillsub);
	$table->signal_connect( destroy => sub {RemoveWatcher($watcher);} );
	&$fillsub();
	return $table;
}

sub SongRating
{	my @IDs=@_;
	my %h; $h{ $Songs[$_][SONG_RATING] }++ for @IDs;
	my $val=(sort { $h{$b} <=> $h{$a} } keys %h)[0];

	my $adj=Gtk2::Adjustment->new(0,0,100,10,20,100);
	my $spin=Gtk2::SpinButton->new($adj,10,0);
	my $check=Gtk2::CheckButton->new(_"use default");
	my $busy; my $stars; my $modif;
	my $updatesub=sub
	{	my $v=$_[0];
		$v='' unless defined $v;
		$busy=1;
		$check->set_active($v eq '');
		$stars->set($v);
		$v=$Options{DefaultRating} if $v eq '';
		$adj->set_value($v);
		$busy=0;
		$modif=1;
	};
	$stars=Stars->new($val,$updatesub);

	my $vbox=Vpack( $check,[$spin,$stars] );

	&$updatesub($val);
	$adj->signal_connect(value_changed => sub{ return if $busy; &$updatesub($_[0]->get_value) });
	$check->signal_connect(toggled	   => sub{ return if $busy; &$updatesub( ($_[0]->get_active ? '' : $Options{DefaultRating}) ) });

	$modif=0;
	$vbox->{save}=sub
	{	return unless $modif; #do nothing if rating not modified
		my $val=$stars->get;
		$Songs[$_][SONG_RATING]=$val for @IDs;
		::SongsChanged(SONG_RATING,\@IDs);
	};

	return $vbox;
}

sub UpdateDefaultRating
{	my @l=grep !defined $Songs[$_][SONG_RATING] || $Songs[$_][SONG_RATING] eq '',@Library;
	SongsChanged(SONG_RATING,\@l);
}

sub AddWatcher
{	my $n=1;
	$n++ while defined $SongsWatchers[$n];
	if (@_) { ChangeWatcher($n,@_); }
	else { $SongsWatchers[$n]=0; }
	return $n;
}
sub ChangeWatcher
{	warn "ChangeWatcher @_\n" if $debug;
	my $n=shift;
	#Remove watcher if it exist
	for my $aref (grep defined,@Watched,@WatchedFilt)
	{	@$aref=grep $_!=$n,@$aref;
	}
	unless (@_) { $SongsWatchers[$n]=0; return; }
	my ($listref,$cols,$update,$remove,$add,$filter,$fqueue)=@_;
	#Add watcher with new properties
	push @{$Watched[$_]},$n for ( ref$cols ? @$cols : $cols );
	if ($filter)
	{	my ($greponly,@fields)=$filter->info;
		push @{ $WatchedFilt[$_] },$n for @fields;
		warn "filter fields : '@fields'\n" if $debug;
		$fqueue=undef if $greponly;
		$filter=undef if $filter->is_empty;
	}
	$SongsWatchers[$n]=[$listref,$update,$remove,$add,$filter,$fqueue];
}
sub RemoveWatcher
{	warn "RemoveWatcher @_\n" if $debug;
	my $n=$_[0];
	ChangeWatcher($n);
	delete $SongsWatchers[$n];
}

# old watchers notes :
	#watchers:
	#	one field			#flags/dir lists, filterpane, queue
	#	few/many fields			#AABox, Random, listview, add/removeAA, skin
	#
	#	incremental sub with old value	#removeAA, AABox, flags/dir lists, filterpane
	#	incremental sub			#addAA, listview
	#	big sub	(with sort)		#SF
	#	short sub multi-IDs		#queue
	#
	#	all IDs				#random, add/removeAA, filterpane
	#	list of IDs with filter		#listview, AABox, SF, filterpane
	#	list of IDs			#queue
	#	one ID				#skin, (listview->AAbox) ??
	#changes:
	#	one field, all IDs		#remove flag
	#	one field, many IDs		#rename file/folder, edit flags/rating
	#	few/many fields, few/one IDs	#checksong, played
	#	all fields, few/one IDs		#add/delete/remove song
	#-------------------------------------------------------------------------------------
	#listref :	undef : all
	#		ref : [@IDs]
	#filter :	undef : none/don't watch
	#		defined : extract cols to watch
	#			  extract cmd -> determine incremental/not
	#cols :		[@cols]
	#$sub_chg,$sub_rmv,$sub_add : undef -> don't care
	#				incremental / not
	#
	#
	#
sub SongsChanged	# ($field,@IDs)
{	warn "SongsChanged @_\n" if $debug;
	my ($col,$IDs)=@_;
	$Filter::CachedList=undef;
	if ($TogLock && $TogLock==$col && grep($SongID==$_,@$IDs)) {&UpdateLock}	#update lock
	my @wake;
	$wake[$_]=1 for @{$Watched[$col]};
	$wake[$_]|=2 for @{$WatchedFilt[$col]};

	for my $n (grep $wake[$_],0..$#wake)
	{	my %ID;
		$ID{$_}=0 for @$IDs;
		my ($listref,$update,$remove,$add,$filter,$fqueue)=@{ $SongsWatchers[$n] };
		if ($fqueue && $wake[$n]&2) {&$fqueue(keys %ID); next; }
		if ($listref)
		{	for (@$listref) { $ID{$_}=1 if exists $ID{$_}; }
		}
		else { $_=1 for values %ID; }		#$listref is undef -> every song is in the list
		if ($wake[$n]==1) { &$update( grep $ID{$_},keys %ID ); next }
		$ID{$_}|=2 for @{ &{$filter->{'sub'}}([keys %ID]) };
		my @f;
		while (my($ID,$f)=each %ID) { push @{$f[ $f ]},$ID; }
		&$update( @{$f[3]} ) if $f[3] && $wake[$n]&1 && $update;
		&$remove( @{$f[1]} ) if $f[1] && $remove;
		&$add   ( @{$f[2]} ) if $f[2] && $add;
	}
}
sub SongChanged		# ($ID,@fields)
{	warn "SongChanged @_\n" if $debug; my $time=times;		#DEBUG
	$Filter::CachedList=undef;
	my $ID=shift;
	if ($ID==$SongID && $TogLock && grep($TogLock==$_,@_)) {&UpdateLock}	#update lock
	my @wake; #warn "@$_" for @Watched;
	$wake[$_]=1 for map @{$Watched[$_]},@_;
	$wake[$_]|=2 for map @{$WatchedFilt[$_]},@_;
	for my $n (grep $wake[$_],0..$#wake)
	{	#warn "SongChanged $n $wake[$n] @{$SongsWatchers[$n]}\n";
		my ($listref,$update,$remove,$add,$filter,$fqueue)=@{ $SongsWatchers[$n] };
		if ($fqueue && $wake[$n]&2) {&$fqueue($ID) if $fqueue; warn "$n -> queue\n" if $debug; next; }
		my $found;
		if ($listref)
		{	for (@$listref) {next unless $_==$ID;$found=1;last}
		}
		else {$found=1;}		#$listref is undef -> every song is in the list
		if ($wake[$n]==1) {&$update($ID) if $found && $update; warn "$n -> update\n" if ($found && $debug);next;}
		$found|=2 if @{ &{$filter->{'sub'}}([$ID]) }>0;
		if    ($found==3) { &$update($ID) if $wake[$n]&1 && $update; warn "$n -> update\n" if $debug; }
		elsif ($found==1) { &$remove($ID) if $remove; warn "$n -> remove\n" if $debug; }
		elsif ($found==2) { &$add($ID)    if $add; warn "$n -> add\n" if $debug; }
	}
	$time=times-$time; warn "song changed : $time s\n" if $debug;	#DEBUG
}
sub SongAdd		#only called from IdleLoop
{	my $time=times;		#DEBUG
	$Filter::CachedList=undef;
	my $ID=shift @ToAdd;
	my $aref=$Songs[$ID];
	RemoveMissing($ID) if $$aref[SONG_MISSINGSINCE] && $$aref[SONG_MISSINGSINCE]=~m/^\d+$/;
	my $file=$$aref[SONG_PATH].SLASH.$$aref[SONG_FILE];
	my ($size,$timestamp)=(stat $file)[7,9];
	unless ($$aref[SONG_MODIF] && $$aref[SONG_MODIF]==$timestamp && $$aref[SONG_LENGTH])
		#don't read tag if song already known and not modified
	{	my $checkmissing= !$$aref[SONG_MODIF] && $MissingCount;
		$$aref[SONG_MODIF]=$timestamp;
		$$aref[SONG_SIZE]=$size;
		warn "Reading Tag for [$ID] $file\n";
		my $read_result=ReadTag::Read($aref,$file,1);
		unless ($read_result) {$Songs[$ID]=undef;return}
		if ($checkmissing && CheckMissing($aref) && $$aref[SONG_LENGTH]) #check if new song is a missing song
		 {$read_result=1} #No need to check length if found missing song because the length was copied from old record
		if ($read_result==2) { push @LengthEstimated,$ID; $Songs[$ID][SONG_MISSINGSINCE]='l'; }
	}
	push @Library,$ID;
	AddAA($ID);
	for my $watcher (@SongsWatchers)
	{	next unless $watcher;
		my ($listref,undef,undef,$add,$filter,$fqueue)=@$watcher;
		next unless $add;
		#my @ret=@{ &{$filter->{'sub'}}([$ID]) } if $filter;	#DEBUG
		#warn "Addind $ID (".$::Songs[$ID][::SONG_ALBUM].") ".($filter->explain)." [@ret] =".scalar(@ret) if $filter;	#DEBUG
		if ($filter)
		{	if ($fqueue) { &$fqueue($ID); next; }
			elsif ( @{ &{$filter->{'sub'}}([$ID]) }==0 ) {next}
		}
		warn "$watcher -> add\n" if $debug;
		&$add($ID);
	}
	$ProgressNBSongs++;
	$time=times-$time; warn "song add : $time s\n" if $debug;	#DEBUG
}
sub SongsRemove
{	my $IDs=$_[0];
	$Filter::CachedList=undef;
	my %ID;
	$ID{$_}=undef for @$IDs;
	for my $watcher (@SongsWatchers)
	{	next unless $watcher;
		my ($listref,undef,$remove,undef,$filter)=@$watcher;
		next unless $remove;
		if ($listref)
		{	&$remove( grep(exists $ID{$_},@$listref) );
		}
		else { &$remove(@$IDs); }	#$listref is undef -> every song is in the list
	}
	for my $ID (@$IDs) { RemoveAA($ID, $Songs[$ID] ) }
	my $i=@Library;
	while ($i--)
	{	splice @Library,$i,1 if exists $ID{$Library[$i]};
	}

	$i=my $qsize=@Queue;
	while ($i--)
	{	splice @Queue,$i,1 if exists $ID{$Queue[$i]};
	}
	HasChanged('Queue') if $qsize!=@Queue;

	@Recent=	 grep !exists $ID{$_}, @Recent;
#	@LengthEstimated=grep !exists $ID{$_}, @LengthEstimated;
	@$_=grep !exists $ID{$_},@$_ for values %SavedLists;

	for my $ID (@$IDs)
	{ AddMissing($ID,1);
	}
}
sub SongRemove
{	#my $time=times;		#DEBUG
	$Filter::CachedList=undef;
	my $ID=shift;
	for my $watcher (@SongsWatchers)
	{	next unless $watcher;
		my ($listref,undef,$remove,undef,$filter)=@$watcher;	#remove unused
		next unless $remove;
		my $found;
		if ($listref)
		{	for (@$listref) {next unless $_==$ID;$found=1;last}
			next unless $found;
		}
		&$remove($ID);
	}
	RemoveAA($ID, $Songs[$ID] );
	for (0..$#Library)
	{	if ($ID==$Library[$_]) {splice @Library,$_,1; last;}
	}

	my $qchanged;
	for (0..$#Queue)
	{	if ($ID==$Queue[$_]) {splice @Queue,$_,1; $qchanged=1;}
	}
	HasChanged('Queue') if $qchanged;

	@Recent=	 grep $_ ne $ID, @Recent;
#	@LengthEstimated=grep $_ ne $ID, @LengthEstimated;
	@$_=grep $_ ne $ID,@$_ for values %SavedLists;

	AddMissing($ID,1);
	#$Songs[$ID]=undef;
	#$time=times-$time; warn "song remove : $time s\n" if $debug;	#DEBUG
}
sub SongCheck
{	return if $CmdLine{demo};
	my ($ID,$forcefullscan)=@_;
	my $aref=$Songs[$ID];
	$forcefullscan=2 if $aref->[SONG_MISSINGSINCE];
	return -1 unless $aref;
	my $file=$$aref[SONG_PATH].SLASH.$$aref[SONG_FILE];
	if (-r $file)
	{	my ($size,$timestamp)=(stat $file)[7,9];
		return if $$aref[SONG_MODIF]==$timestamp && !$forcefullscan;
		my $checklength=$forcefullscan || ( $$aref[SONG_SIZE]!=$size );
		my @old=@$aref;
		$$aref[SONG_MODIF]=$timestamp;
		$$aref[SONG_SIZE]=$size;
		warn "Reading Tag for [$ID] $file\n";
		my $read_result=ReadTag::Read($aref,$file,$checklength);
		if ($checklength && $read_result==2) { push @LengthEstimated,$ID; $aref->[SONG_MISSINGSINCE]='l'; }
		else {$aref->[SONG_MISSINGSINCE]=undef}
		my @changed=grep $$aref[$_] ne $old[$_],0..SONGLAST;
		for my $field (@changed)
		{	next unless $field==SONG_ALBUM || $field==SONG_ARTIST || $field==SONG_LENGTH || $field==SONG_DATE;
			RemoveAA($ID,\@old);
			AddAA($ID);
			last;
		}
		SongChanged($ID, @changed );
	}
	else	#file not found/readable
	{	warn "can't read file '$file'\n";
		for ($ID,"L$ID") { $Editing{$_}->destroy if exists $Editing{$_};}
		SongRemove($ID);
		#$Songs[$ID]=undef;
		#delete $GetIDFromFile{$file};
		#AddMissing($ID,1);
	}
}

sub AddAA
{	my $ID=$_[0];
	my ($album,$artist,$l,$y)=@{ $Songs[$ID] }[SONG_ALBUM,SONG_ARTIST,SONG_LENGTH,SONG_DATE];
	#warn "adding $ID to $album $artist\n" if $debug;
	push @{$Album{$album}[AALIST]},$ID;
	for my $art (split / ?& ?/,$artist)
	{	push @{$Artist{$art}[AALIST]},$ID;
		$Artist{$art}[AALENGTH]+=$l;
		$Artist{$art}[AAXREF]{$album}++;
		$Album{$album}[AAXREF]{$art}++;
		$ToUpdateYAr{$art}=undef if $y;
	}
	$Album{$album}[AALENGTH]+=$l;
	if ($y)
	{	$ToUpdateYAl{$album}=undef;
		::IdleDo('2_UAAYear',500, \&UpdateAAYear);
	}
	#if (!$Album{$album}[AAPIXLIST] && $Album{$album}[AAPIXLIST] ne '0')
	#{	my $f=$Songs[$ID][SONG_PATH].SLASH.'cover.jpg';
	#	if (-r $f)
	#	{ $Album{$album}[AAPIXLIST]=$f; HasChanged('AAPicture',$album); }
	#}
}

sub RemoveAA
{	my ($ID,$oldref)=($_[0],$_[1]);
	my ($album,$artist,$l,$y)=@$oldref[SONG_ALBUM,SONG_ARTIST,SONG_LENGTH,SONG_DATE];
	warn "removing $ID from $album $artist\n" if $debug;
	my @listrefs=( \@{$Album{$album}[AALIST]} );
	$Album{$album}[AALENGTH]-=$l;
	for my $art (split / ?& ?/,$artist)
	{	$Artist{$art}[AALENGTH]-=$l;
		push @listrefs,\@{$Artist{$art}[AALIST]};
		unless (--$Artist{$art}[AAXREF]{$album}) {delete $Artist{$art}[AAXREF]{$album};}
		unless (--$Album{$album}[AAXREF]{$art})  {delete $Album{$album}[AAXREF]{$art};}
		$ToUpdateYAr{$art}=undef if $y;
	}
	if ($y)
	{	$ToUpdateYAl{$album}=undef;
		::IdleDo('2_UAAYear',500, \&UpdateAAYear);
	}
	for my $l (@listrefs) { @$l=grep $_!=$ID,@$l; }
}

sub UpdateAAYear
{	for my $refs ([\%Artist,\%ToUpdateYAr],[\%Album,\%ToUpdateYAl])
	{	my ($href,$lref)=@$refs;
		for my $aa (keys %$lref)
		{	my @y=sort { $a <=> $b } grep $_,map $Songs[$_][SONG_DATE], @{ $href->{$aa}[AALIST] };
			my $year='';
			if (@y)
			{	$year=$y[0];
				my $max=pop @y;
				$year.=' - '.$max if $year!=$max; #add last year if !=
			}
			$href->{$aa}[AAYEAR]=$year;
		}
		%$lref=();
	}
}

sub AddMissing
{	my $ID=$_[0];
	if ($_[1]) { my $ref=\$Songs[$ID][SONG_MISSINGSINCE]; if ($$ref && $$ref=~m/l/) {$Songs[$ID][SONG_LENGTH]=''} $$ref=$DAYNB; }
	push @{ $MissingSTAAT{ join("\x1D",map $Songs[$ID][$_],@STAAT) } },$ID;
	$MissingCount++;
}
sub RemoveMissing
{	my $ID=$_[0];
	my $staat=join "\x1D",map $Songs[$ID][$_],@STAAT;
	my $aref=$MissingSTAAT{$staat};
	if (!$aref)	{warn "unregistered missing song";return}
	elsif (@$aref>1){ @$aref=grep $_ != $ID, @$aref; }
	else		{ delete $MissingSTAAT{$staat}; }
	$Songs[$ID][SONG_MISSINGSINCE]=undef;
	$MissingCount--;
}
sub CheckMissing
{	my $songref=$_[0];
	for my $oldID (@{$MissingSTAAT{ join("\x1D",map $songref->[$_],@STAAT) }})
	{	my $m;
		for my $f (SONG_FILE,SONG_PATH)
		{	$m++ if $songref->[$f] eq $Songs[$oldID][$f];
		}
		next unless $m;	#must have the same path or the same filename
		# Found -> remove old ID, copy non-written song fields to new ID
		my $olddir =$Songs[$oldID][SONG_PATH];
		my $oldfile=$Songs[$oldID][SONG_FILE];
		warn "Found missing song, formerly '".$olddir.SLASH.$oldfile."'\n";# if $debug;
		RemoveMissing($oldID);
		$songref->[$_]=$Songs[$oldID][$_] for SONG_ADDED,SONG_LASTPLAY,SONG_NBPLAY,SONG_RATING,SONG_FLAGS,SONG_LENGTH; #SONG_LENGTH is copied to avoid the need to check length for mp3 without VBR header
		if (keys %{ $GetIDFromFile{$olddir} } ==1)
		{	delete $GetIDFromFile{$olddir};
		}
		else { delete $GetIDFromFile{$olddir}{$oldfile} }
		$Songs[$oldID]=undef;
		return 1;
	}
	return undef;
}

sub ScanFolder
{	my $dir=shift @ToScan;
	warn "Scanning $dir\n" if $debug;
	$ProgressNBFolders++;
	my (@pictures,$album,$morethan1);
	unless ($ScanRegex)
	{	my @list= $Options{ScanPlayOnly} ? $PlayNext_package->supported_formats : qw/mp3 ogg flac mpc/;
		my $s=join '|',@list;
		$ScanRegex=qr/\.(?:$s)$/i;
	}
	my @files;
	if (-d $dir)
	{	opendir my$DIRH,$dir;
		@files=readdir $DIRH;
		closedir $DIRH;
	}
	elsif (-f $dir && $dir=~s/$QSLASH([^$QSLASH]+)$//)
	{	@files=($1);
	}
	my $utf8dir=filename_to_utf8displayname($dir);
	for my $file (@files)
	{	next if $file=~m/^\./;		# skip . .. and hidden files/folders
		my $path_file=$dir.SLASH.$file;
		#warn "slash is utf8\n" if utf8::is_utf8(SLASH);
		#if (-d $path_file) { push @ToScan,$path_file; next; }
		if (-d $path_file)
		{	if (-l $path_file)
			{	my $real=readlink $path_file;
				next if exists $FollowedDirs{$real};
				$FollowedDirs{$real}=undef;
			}
			else
			{	next if exists $FollowedDirs{$path_file};
				$FollowedDirs{$path_file}=undef;
			}
			push @ToScan,$path_file;
			next;
		}
		if ($file=~m/(?:^|\W)(?:cover|front|folder|thumb|thumbnail)(?:\W.*)?\.(?:jpg|png|gif)$/i) { push @pictures,$file;next; }
		next unless $file=~$ScanRegex;
		my $rID=\$GetIDFromFile{$dir}{$file};
		if (defined $$rID)
		{	unless ($Songs[$$rID][SONG_MISSINGSINCE] && $Songs[$$rID][SONG_MISSINGSINCE]=~m/^\d+$/)
			{	if (!$morethan1 && defined (my $alb=$Songs[$$rID][SONG_ALBUM]))
				{ if (defined $album) { $morethan1=1 if $album ne $alb }
				  else { $album=$alb; }
				}
				next;
			}
		}
		else
		{	my @song;#=('')xSONGLAST;	#FIXME only to avoid undef warnings
			#$song[SONG_NBPLAY]=0;
			@song[SONG_UFILE,SONG_UPATH,SONG_ADDED,SONG_FILE,SONG_PATH]=(filename_to_utf8displayname($file),$utf8dir,time,$file,$dir);
			#_utf8_on($song[SONG_FILE]); # lie to perl so it doesn't get upgraded
			#_utf8_on($song[SONG_PATH]); #  to utf8 when saving with '>:utf8'
			push @Songs,\@song;
			$$rID=$#Songs;
		}
		push @ToAdd,$$rID;
	}
	undef %FollowedDirs unless @ToScan;
	if (@pictures && !$morethan1)
	{ #FIXME try to guess best file if @pictures>1
	  if (defined $album && !@ToAdd)
	  {	if (!$Album{$album}[AAPIXLIST] && $Album{$album}[AAPIXLIST] ne '0')
		{	$Album{$album}[AAPIXLIST]=$dir.SLASH.$pictures[0];
			HasChanged('AAPicture',$album);
		}
	  }
	  elsif (!defined $album && @ToAdd)
	  {	$FoundCover=[$dir,$pictures[0],@ToAdd];
	  }
	}
}
sub CheckCover
{	my ($dir,$picture,@IDs)=@$FoundCover;
	$FoundCover=undef;
	my %albums;
	$albums{ $Songs[$_][SONG_ALBUM]||return }=undef for @IDs;
	if (keys %albums==1)
	{	(my $alb)=keys %albums;
		if ($alb && $alb!~m/^<Unknown>/ && !$Album{$alb}[AAPIXLIST] && $Album{$alb}[AAPIXLIST] ne '0')
		{	$Album{$alb}[AAPIXLIST]=$dir.SLASH.$picture;
			HasChanged('AAPicture',$alb);# warn "found cover for $alb\n";
		}
	}
}

sub AboutDialog
{	my $dialog=Gtk2::AboutDialog->new;
	$dialog->set_version(VERSION);
	$dialog->set_copyright('(c) 2005-2006 Quentin Sculo');	#
	#$dialog->set_comments();
	$dialog->set_license("Released under the GNU General Public Licence\n(http://www.gnu.org/copyleft/gpl.html)");
	$dialog->set_website('http://squentin.free.fr/gmusicbrowser/gmusicbrowser.html');
	$dialog->set_authors('Quentin Sculo <squentin@free.fr>');
	$dialog->set_url_hook(sub { warn "@_";openurl($_[1]); }); #FIXME doesn't seem to work
	$dialog->show_all;
}

sub PrefDialog
{	if ($OptionsDialog) { $OptionsDialog->present; return; }
	$OptionsDialog=my $dialog = Gtk2::Dialog->new (_"Settings", undef,[],
				'gtk-about' => 1,
				'gtk-close' => 'close');
	$dialog->set_default_response ('close');
	#$dialog->action_area->pack_end(NewIconButton('gtk-about',_"about",\&AboutDialog),FALSE,FALSE,2);
	SetWSize($dialog,'Pref');

	my $notebook = Gtk2::Notebook->new;
	$notebook->append_page( PrefLibrary()	,Gtk2::Label->new(_"Library"));
	$notebook->append_page( PrefFlags()	,Gtk2::Label->new(_"Flags"));
	$notebook->append_page( PrefAudio()	,Gtk2::Label->new(_"Audio"));
	$notebook->append_page( PrefMisc()	,Gtk2::Label->new(_"Misc."));
	$notebook->append_page( PrefPlugins()	,Gtk2::Label->new(_"Plugins"));
	$notebook->append_page( PrefKeys()	,Gtk2::Label->new(_"Keys"));
	$notebook->append_page( PrefTags()	,Gtk2::Label->new(_"Tags"));

	$dialog->vbox->pack_start($notebook,TRUE,TRUE,4);

	$dialog->signal_connect( response => sub
		{	if ($_[1] eq '1') {AboutDialog();return};
			$OptionsDialog=undef;
			$_[0]->destroy;
		});
	$dialog->show_all;
	#$dialog->set_position('center-always');
}

sub PrefKeys
{	my $vbox=Gtk2::VBox->new;
	my $store=Gtk2::ListStore->new(('Glib::String')x2);
	my $treeview=Gtk2::TreeView->new($store);
	$treeview->append_column( Gtk2::TreeViewColumn->new_with_attributes
	 ( _"Key",Gtk2::CellRendererText->new,text => 0
	 ));
	$treeview->append_column( Gtk2::TreeViewColumn->new_with_attributes
	 ( _"Command",Gtk2::CellRendererText->new,text => 1
	 ));
	my $sw=Gtk2::ScrolledWindow->new(undef,undef);
	$sw->set_shadow_type('etched-in');
	$sw->set_policy('never','automatic');
	$sw->add($treeview);
	$vbox->add($sw);

	my $refresh_sub=sub
	 {	$store->clear;
		my %list=  $Options{CustomKeyBindings}=~m/$re_spaces_unlessinbrackets/g;
		for my $key (sort keys %list)
		{	my ($cmd,$arg)=  $list{$key}=~m/^(\w+)(?:\((.*)\))?$/;
			$cmd=$Command{$cmd}[1];
			$cmd.="($arg)" if defined $arg;
			$store->set($store->append,0,$key,1,$cmd);
		}
		%CustomBoundKeys=%{ make_keybindingshash($Options{CustomKeyBindings}) };
	 };

	my $refresh_sensitive;
	my $key_entry=Gtk2::Entry->new;
	$Tooltips->set_tip($key_entry,_"Press a key or a key combination");
	$key_entry->set_editable(FALSE);
	$key_entry->signal_connect(key_press_event => sub
	 {	my $event=$_[1];
		my $keyval=$event->keyval;
		my $keyname;
		for (keys %Gtk2::Gdk::Keysyms) {$keyname=$_ if $Gtk2::Gdk::Keysyms{$_}==$keyval};
		my $mod;
		$mod.='c' if $event->state >= 'control-mask';
		$mod.='a' if $event->state >= 'mod1-mask';
		$mod.='s' if $event->state >= 'shift-mask';
		if (defined $keyname && !grep($_ eq $keyname,qw/Shift_L Control_L Alt_L Super_L ISO_Level3_Shift Multi_key Menu Control_R Shift_R/))
		{	$keyname=$mod.'-'.$keyname if $mod;
			$_[0]->set_text($keyname);
			&$refresh_sensitive;
		}
		return 1;
	 });

	my $combochanged;
	my $entry_extra=Gtk2::EventBox->new;
	my $combo=TextCombo->new( {map {$_ => $Command{$_}[1]}
		       			sort {$Command{$a}[1] cmp $Command{$b}[1]}
					keys %Command
				  });
	$combochanged=sub
	 {	my $cmd=$combo->get_value;
		my $child=$entry_extra->child;
		$entry_extra->remove($child) if $child;
		if ($Command{$cmd}[2])
		{	$Tooltips->set_tip($entry_extra,$Command{$cmd}[2]);
			$child= (ref $Command{$cmd}[3] eq 'CODE')? &{ $Command{$cmd}[3] }  : Gtk2::Entry->new;
			$child->signal_connect(changed => $refresh_sensitive);
			$entry_extra->add( $child );
			$entry_extra->parent->show_all;
		}
		else
		{	$entry_extra->parent->hide;
		}
		&$refresh_sensitive;
	 };
	$combo->signal_connect( changed => $combochanged );

	my $butadd= ::NewIconButton('gtk-add',_"Add shorcut key",sub
	 {	my $cmd=$combo->get_value;
		return unless defined $cmd;
		my $key=$key_entry->get_text;
		return if $key eq '';
		if (my $child=$entry_extra->child)
		{	my $extra= (ref $child eq 'Gtk2::Entry')? $child->get_text : $child->get_value;
			$cmd.="($extra)" if $extra ne '';
		}
		my %list=  $Options{CustomKeyBindings}=~m/$re_spaces_unlessinbrackets/g;
		$list{$key}=$cmd;
		$Options{CustomKeyBindings}=join ' ',%list;
		&$refresh_sub;
	 });
	my $butrm=  ::NewIconButton('gtk-remove',_"Remove",sub
	 {	my $iter=$treeview->get_selection->get_selected;
		my $key=$store->get($iter,0);
		my %list=  $Options{CustomKeyBindings}=~m/$re_spaces_unlessinbrackets/g;
		delete $list{$key};
		$Options{CustomKeyBindings}=join ' ',%list;
		&$refresh_sub;
	 });

	$treeview->get_selection->signal_connect(changed => sub
	 {	$butrm->set_sensitive( $_[0]->count_selected_rows );
	 });
	$_->set_sensitive(FALSE) for $butadd,$butrm;
	$refresh_sensitive=sub
	 {	my $ok=0;
		{	last if $key_entry->get_text eq '';
			my $cmd=$combo->get_value;
			last unless defined $cmd;
			if ($Command{$cmd}[2])	{ my $re=$Command{$cmd}[3]; last if $re && ref($re) ne 'CODE' && $entry_extra->child->get_text!~m/$re/; }
			$ok=1;
		}
		$butadd->set_sensitive( $ok );
	 };


	 $vbox->pack_start(
	   Vpack([	[ 0, Gtk2::Label->new(_"Key") , $key_entry ],
			[ 0, Gtk2::Label->new(_"Command") , $combo ],
			[ 0, Gtk2::Label->new(_"Arguments") , $entry_extra ],
		  ],[$butadd,$butrm]
		),FALSE,FALSE,2);
	#$entry_extra->parent->set_no_show_all(TRUE); $_->show for $entry_extra->parent->get_children;
	&$refresh_sub;
	&$combochanged;

	return $vbox;
}

sub PrefPlugins
{	my $vbox=Gtk2::VBox->new;
	my $store=Gtk2::ListStore->new('Glib::String','Glib::String','Glib::Boolean');
	my $treeview=Gtk2::TreeView->new($store);
	$treeview->set_headers_visible(FALSE);
	my $renderer = Gtk2::CellRendererToggle->new;
	my $plug_box;
	$renderer->signal_connect(toggled => sub
	 {	#my ($cell, $path_str) = @_;
		my $path = Gtk2::TreePath->new($_[1]);
		my $iter = $store->get_iter($path);
		my $plugin=$store->get($iter, 0);
		my $key='PLUGIN_'.$plugin;
		if ($Options{$key})	{$Plugins{$plugin}->end}
		else			{$Plugins{$plugin}->init}
		$Options{$key} ^= 1;
		$store->set ($iter, 2, $Options{$key});
		$plug_box->set_sensitive($Options{$key}) if $plug_box;
	 });
	$treeview->append_column
	 ( Gtk2::TreeViewColumn->new_with_attributes('on',$renderer,active => 2)
	 );
	$treeview->append_column( Gtk2::TreeViewColumn->new_with_attributes
	 ( 'plugin name',Gtk2::CellRendererText->new,text => 1
	 ));
	$store->set($store->append,0,$_,1,$Plugins{$_}->name,2,$Options{'PLUGIN_'.$_}) for sort {lc$Plugins{$a}->name cmp lc$Plugins{$b}->name} keys %Plugins;
	my $hbox=Gtk2::HBox->new(FALSE, 2);
	my $plugin;
	$treeview->signal_connect(cursor_changed => sub
	 {	my $path=($treeview->get_cursor)[0];
		my $old=$plugin;
		$plugin=$store->get( $store->get_iter($path), 0);
		return if $plugin eq $old;
		if ($plug_box) { $hbox->remove($plug_box); }
		$plug_box=$Plugins{$plugin}->prefbox;
		$hbox->add($plug_box);
		$plug_box->set_sensitive(0) unless $Options{'PLUGIN_'.$plugin};
		$plug_box->show_all;
	 });

	my $sw=Gtk2::ScrolledWindow->new(undef,undef);
	$sw->set_shadow_type('etched-in');
	$sw->set_policy('never','automatic');
	$sw->add($treeview);
	$hbox->pack_start($sw,FALSE,FALSE,2);
	$vbox->add($hbox);
	return $vbox;
}
sub LogView
{	my $store=shift;
	my $treeview=Gtk2::TreeView->new($store);
	$treeview->append_column( Gtk2::TreeViewColumn->new_with_attributes
	 ( 'log',Gtk2::CellRendererText->new,text => 0
	 ));
	$treeview->set_headers_visible(FALSE);
	my $sw=Gtk2::ScrolledWindow->new(undef,undef);
	$sw->set_shadow_type('etched-in');
	$sw->set_policy('automatic','automatic');
	$sw->add($treeview);
	return $sw;
}
sub SetDefaultOptions
{	my $prefix='PLUGIN_'.shift().'_';
	while (my ($key,$val)=splice @_,0,2)
	{	$Options{$prefix.$key}=$val unless defined $Options{$prefix.$key};
	}
}

sub PrefAudio
{	my $vbox=Gtk2::VBox->new(FALSE, 2);
	my $sg1=Gtk2::SizeGroup->new('horizontal');
	my $sg2=Gtk2::SizeGroup->new('horizontal');
	my ($radio1,$radio2,$radio3)=NewPrefRadio('AudioOut',sub
		{	$PlayNext_package=$Options{AudioOut};
			$Play_package=$PlayNext_package unless defined $PlayTime;
			$ScanRegex=undef;
		},
		'mpg321/ogg123/flac123' => 'Play_123',
		'gstreamer'	=> 'Play_GST',
		_"icecast server"=> 'Play_Server'
		);

	#123
	my $vbox1=Gtk2::VBox->new (FALSE, 2);
	my $hbox1=NewPrefCombo(Device => [qw/default oss alsa esd arts sun/],_"output device :",undef,$sg1,$sg2);
	my $list1=join ' ',sort Play_123->supported_formats;
	my $label1=Gtk2::Label->new;
	$label1->set_markup('<small>'._("supports : ").$list1.'</small>') if $list1;
	my $label1b=Gtk2::Label->new; $label1b->set_markup('<small>'._('advanced options').'</small>');
	my $Badv123=Gtk2::Button->new; $Badv123->add($label1b);	$Badv123->set_relief('none');
	$Badv123->signal_connect(clicked => \&Play_123::AdvancedOptions);
	my $hbox1b=Gtk2::HBox->new(FALSE, 2);
	$hbox1b->pack_start($_,TRUE,TRUE,4) for $label1,$Badv123;
	$vbox1->pack_start($_,FALSE,FALSE,2) for $radio1,$hbox1,$hbox1b;

	#gstreamer
	my $vbox2=Gtk2::VBox->new (FALSE, 2);
	my $hbox2=NewPrefCombo(gst_sink => [sort Play_GST->supported_sinks],_"output device :",undef,$sg1,$sg2);
	my $list2=join ' ',sort Play_GST->supported_formats;
	my $label2=Gtk2::Label->new;
	$label2->set_markup('<small>'._("supports : ").$list2.'</small>') if $list2;
	$vbox2->pack_start($_,FALSE,FALSE,2) for $radio2,$hbox2,$label2;

	#icecast
	my $vbox3=Gtk2::VBox->new (FALSE, 2);
	my $hbox3=NewPrefEntry('Icecast_port',_"port");
	$vbox3->pack_start($_,FALSE,FALSE,2) for $radio3,$hbox3;

	$vbox1->set_sensitive($Packs{Play_123});
	$vbox2->set_sensitive($Packs{Play_GST});
	$vbox3->set_sensitive($Packs{Play_Server});

	$vbox->pack_start($_,FALSE,FALSE,2) for $vbox1,Gtk2::HSeparator->new,
						$vbox2,Gtk2::HSeparator->new,
						$vbox3,Gtk2::HSeparator->new,
						NewPrefCheckButton(IgnorePlayError => _"Ignore playback errors",undef,_"Skip to next song if an error occurs");
	return $vbox;
}

sub PrefMisc
{	my $vbox=Gtk2::VBox->new (FALSE, 2);

	#Tray
	my $check2=NewPrefCheckButton(CloseToTray => _"Close to tray");
	my $check3=NewPrefCheckButton(ShowTipOnSongChange => _"Show tray tip on song change");
	$_->set_sensitive($Options{UseTray} && $Gtk2TrayIcon) for $check2,$check3;;
	my $check1=NewPrefCheckButton(UseTray => _"show tray icon",sub {$_->set_sensitive($Options{UseTray}) for $check2,$check3; &CreateTrayIcon; });
	$check1->set_sensitive($Gtk2TrayIcon);

	#layouts
	my $sg1=Gtk2::SizeGroup->new('horizontal');
	my $sg2=Gtk2::SizeGroup->new('horizontal');
	my $layoutT=NewPrefCombo(LayoutT=> {map {$_ => _ ($_)} grep $Player::Layouts{$_}{Type}=~/T/,keys %Player::Layouts},_"Tray tip window layout :",undef,$sg1,$sg2);
	my $layout =NewPrefCombo(Layout => {map {$_ => _ ($_)} grep $Player::Layouts{$_}{Type}=~/G/,keys %Player::Layouts},_"Player window layout :",\&set_layout,$sg1,$sg2);
	my $layoutB=NewPrefCombo(LayoutB=> {map {$_ => _ ($_)} grep $Player::Layouts{$_}{Type}=~/B/,keys %Player::Layouts},_"Browser window layout :",undef,$sg1,$sg2);

	#Default rating
	my $DefRating=NewPrefSpinButton('DefaultRating',sub
		{ IdleDo('0_DefaultRating',500,\&UpdateDefaultRating);
		},10,0,0,100,10,20,_"Default rating :",undef,$sg1);

	my $check4=NewPrefCheckButton(RememberPlayFilter => _"Remember last Filter/Playlist between sessions");
	my $check6=NewPrefCheckButton( RememberPlayTime  => _"Remember playing position between sessions");
	my $check5=NewPrefCheckButton( RememberPlaySong  => _"Remember playing song between sessions",sub {$check6->set_sensitive($Options{RememberPlaySong})});
	$check6->set_sensitive($Options{RememberPlaySong});

	$vbox->pack_start($_,FALSE,FALSE,1) for $check4,$check5,$check6,$check1,$check2,$check3,$layoutT,$layout,$layoutB,$DefRating;
	return $vbox;
}

sub set_layout
{	my $old=$MainWindow;
	$old->SaveOptions;
	$MainWindow=Player->new( $Options{Layout} );
	$old->destroy;
}

sub PrefTags
{	my $vbox=Gtk2::VBox->new (FALSE, 2);
	my $checkv4=NewPrefCheckButton('TAG_write_id3v2.4',_"write ID3v2.4 tags",undef,_"Use ID3v2.4 instead of ID3v2.3, ID3v2.3 are probably better supported by other softwares");
	my $checklatin1=NewPrefCheckButton(TAG_use_latin1_if_possible => _"use latin1 encoding if possible in id3v2 tags",undef,_"the default is utf16 for ID3v2.3 and utf8 for ID3v2.4");
	my $check_unsync=NewPrefCheckButton(TAG_no_desync => _"do not unsynchronise id3v2 tags",undef,_"itunes doesn't support unsynchronised tags last time I checked, mostly affect tags with pictures");
	my $id3v1encoding=NewPrefCombo(TAG_id3v1_encoding => \@Encodings,_"Encoding used for id3v1 tags :");
	$vbox->pack_start($_,FALSE,FALSE,1) for $checkv4,$checklatin1,$check_unsync,$id3v1encoding;
	return $vbox;
}

sub AskRenameFolder
{	my $parent=shift; #parent is in utf8
	$parent=~s/([^$QSLASH]+)$//o;
	my $old=$1;
	my $askdiag=Gtk2::Dialog->new(_"Rename folder", undef,
			[qw/modal destroy-with-parent/],
			'gtk-ok' => 'ok',
			'gtk-cancel' => 'none');
	$askdiag->set_default_response('ok');
	$askdiag->set_border_width(3);
	my $entry=Gtk2::Entry->new;
	$entry->set_text($old);
	$askdiag->vbox->pack_start( Gtk2::Label->new(_"Rename this folder to :") ,FALSE,FALSE,1);
	$askdiag->vbox->pack_start($entry,FALSE,FALSE,1);
	$askdiag->show_all;
	{	last unless $askdiag->run eq 'ok';
		my $new=$entry->get_text;
		last if $new eq '';
		last if $old eq $new;
		last if $new=~m/$QSLASH/;	#FIXME allow moving folder
		$old=filename_from_unicode($parent.$old.SLASH);
		$new=filename_from_unicode($parent.$new.SLASH);
		-d $parent.$new and warn "$new already exists\n" and last;
		rename $old,$new
			or warn "rename $old to $new failed\n" and last;
		UpdateFolderNames($old,$new);
	}
	$askdiag->destroy;
}

sub MoveFolder#FIXME implement
{	my $parent=shift;
	$parent=~s/([^$QSLASH]+)$//o;
	my $folder=$1;
	my $new=ChooseDir(_"Move folder to",$parent);
	return unless $new;
	my $old=$parent.$folder.SLASH;
	$new.=SLASH.$folder.SLASH;
#	if ( move(filename_from_unicode($old),filename_from_unicode($new)) )
	if (0) #FIXME implement move folders
	{	UpdateFolderNames($old,$new);
	}
}

sub UpdateFolderNames
{	my ($oldpath,$newpath)=@_;
	my @renamed;
	$oldpath=qr/^\Q$oldpath\E/;
	for my $ID (0..$#Songs)
	{	my $aref=$Songs[$ID];
		next unless defined $aref;
		my $old=$$aref[SONG_PATH];
		my $new=$old.SLASH;
		#_utf8_off($new);
		next unless $new=~s/$oldpath/$newpath/;
		my $file=$$aref[SONG_FILE];
		chop $new; #remove SLASH
		#_utf8_on($new);
		$GetIDFromFile{$new}{$file}=delete $GetIDFromFile{$old}{$file};
		delete $GetIDFromFile{$old} unless keys %{ $GetIDFromFile{$old} };
		$$aref[SONG_PATH]=$new;
		$$aref[SONG_UPATH]=filename_to_utf8displayname($new);
		push @renamed,$ID;
	}

	#rename pic files in %Artist %Album
	for my $ref (values %Artist, values %Album)
	{	#_utf8_off($$ref[AAPIXLIST]);
		$$ref[AAPIXLIST]=~s/$oldpath/$newpath/;
		#_utf8_on($$ref[AAPIXLIST]);
	}
	SongsChanged(SONG_PATH,\@renamed);
	SongsChanged(SONG_UPATH,\@renamed);
}

sub PrefLibrary
{	my $store=Gtk2::ListStore->new('Glib::String');
	my $treeview=Gtk2::TreeView->new($store);
	$treeview->set_headers_visible(FALSE);
	$treeview->append_column( Gtk2::TreeViewColumn->new_with_attributes
		( _"Folders to search for new songs",Gtk2::CellRendererText->new,'text',0)
		);
	$store->set($store->append,0,filename_to_utf8displayname($_)) for @LibraryPath;

	my $addbut=NewIconButton('gtk-add',_"add folder");
	my $rmdbut=NewIconButton('gtk-remove',_"remove");

	my $selection=$treeview->get_selection;
	$selection->signal_connect( changed => sub
		{	my $sel=$_[0]->count_selected_rows;
			$rmdbut->set_sensitive($sel);
		});
	$rmdbut->set_sensitive(FALSE);

	$addbut->signal_connect( clicked => sub
	{	my $dir=ChooseDir(_"Choose folder to add",$CurrentDir.SLASH);
		return unless $dir;
		return if (grep $dir eq $_,@LibraryPath);
		$dir=~s/$QSLASH$//o unless $dir eq SLASH || $dir=~m/^\w:.$/;
		$store->set($store->append,0,filename_to_utf8displayname($dir));
		push @LibraryPath,$dir;
		IdleScan($dir);
	});
	$rmdbut->signal_connect( clicked => sub
	{	my $iter=$selection->get_selected;
		return unless defined $iter;
		my $i=$store->get_path($iter)->to_string;
		$store->remove($iter);
		splice @LibraryPath,$i,1;
	});

	my $sw = Gtk2::ScrolledWindow->new (undef, undef);
	$sw->set_shadow_type ('etched-in');
	$sw->set_policy ('automatic', 'automatic');
	$sw->add($treeview);

	my $Cscanall=NewPrefCheckButton('ScanPlayOnly',_"Do not add songs that can't be played",sub {$ScanRegex=undef});
	my $CScan=NewPrefCheckButton('StartScan',_"Search for new songs on startup");
	my $CCheck=NewPrefCheckButton('StartCheck',_"Check for updated/deleted songs on startup");
	my $BScan= NewIconButton('gtk-refresh',_"scan now", sub { IdleScan();	});
	my $BCheck=NewIconButton('gtk-refresh',_"check now",sub { IdleCheck();	});
	my $label=Gtk2::Label->new(_"Folders to search for new songs");
	my $table=Gtk2::Table->new(2,2,FALSE);
	$table->attach_defaults($CScan, 0,1,1,2);
	$table->attach_defaults($BScan, 1,2,1,2);
	$table->attach_defaults($CCheck,0,1,0,1);
	$table->attach_defaults($BCheck,1,2,0,1);
	$table->attach_defaults($Cscanall, 0,2,2,3);

	my $vbox=Gtk2::VBox->new(FALSE, 2);
	my $hbox=Gtk2::HBox->new(FALSE, 2);
	$vbox->pack_start($label,FALSE,FALSE,1);
	$vbox->add($sw);
	$vbox->pack_start($_,FALSE,FALSE,1) for $hbox,$table;
	$hbox->pack_start($_,FALSE,FALSE,1) for $addbut,$rmdbut;
	return $vbox;
}

sub SetFlags
{	my ($IDs,$toadd,$torm)=@_;
	no warnings;
	for my $ID (@$IDs)
	{	my %h;
		$h{$_}=undef for @$toadd,split /\x00/,$Songs[$ID][SONG_FLAGS];
		delete $h{$_} for @$torm;
		$Songs[$ID][SONG_FLAGS]=join "\x00",sort keys %h;
	}
	SongsChanged(SONG_FLAGS,$IDs);
}

sub PrefFlags
{	my $vbox=Gtk2::VBox->new(FALSE,2);
	my $store=Gtk2::ListStore->new('Glib::String','Glib::Int');
	my $treeview=Gtk2::TreeView->new($store);
	my $renderer=Gtk2::CellRendererText->new;
	$renderer->set(editable => TRUE);
	$renderer -> signal_connect(edited => sub
	    {	my ($cell, $pathstr, $new) = @_;
		$new=~s/\x00//g;
		return if ($new eq '') || (grep $_ eq $new, @Flags);
		my $iter=$store->get_iter_from_string($pathstr);
		my ($old,$nb)=$store->get_value($iter);
		return if $new eq $old;
		$store->set($iter,0,$new);
		@Flags=grep $_ ne $old, @Flags;
		push @Flags,$new;
		return unless $nb;
		my $pat=qr/(?:^|\x00)\Q$old\E(?:$|\x00)/;
		my @l=grep $Songs[$_][SONG_FLAGS]=~m/$pat/, 0..$#Songs;
		SetFlags(\@l,[$new],[$old]);
	    });
	$treeview->append_column( Gtk2::TreeViewColumn->new_with_attributes
		( _"Flag name",$renderer,'text',0)
		);
	$treeview->append_column( Gtk2::TreeViewColumn->new_with_attributes
		( _"# songs",Gtk2::CellRendererText->new,'text',1)
		);
	my $fillsub=sub
	    {	delete $ToDo{'9_Flags'};
		$store->clear;
		my %set; no warnings;
		for my $ref (@Songs)
		{	$set{$_}++ for split /\x00/,$$ref[SONG_FLAGS];
		}
		for my $f (sort @Flags)
		{	$store->set($store->append,0,$f,1,$set{$f}||0);
		}
	    };
	my $watcher;
	$vbox->signal_connect(realize => sub
		{  warn "realize @_\n" if $debug;
		   my $sub=sub { IdleDo('9_Flags',3000,$fillsub); };
		    $watcher=AddWatcher(undef,SONG_FLAGS,$sub);
		   &$fillsub;
		 });
	$vbox->signal_connect(unrealize => sub
		{  warn "unrealize @_\n" if $debug;
		   delete $ToDo{'9_Flags'};
		   RemoveWatcher($watcher);
		});

	my $delbut=NewIconButton('gtk-remove',_"remove flag");
	$delbut->set_sensitive(FALSE);
	$treeview->get_selection->signal_connect( changed => sub
		{ $delbut->set_sensitive($_[0]->count_selected_rows); });
	$delbut->signal_connect( clicked => sub
		{	my ($row)=$treeview->get_selection->get_selected_rows;
			return unless defined $row;
			my $iter=$store->get_iter($row);
			my ($flag,$nb)=$store->get_value($iter);
			if ($nb)
			{	my $dialog = Gtk2::MessageDialog->new
					( undef, #FIXME
					  [qw/modal destroy-with-parent/],
					  'warning','ok-cancel',
					  __("This flag is set for %d song.","This flag is set for %d songs.",$nb)."\n".
					  __x("Are you sure you want to delete the '{flag}' flag ?", flag => $flag)
					);
				$dialog->show_all;
				if ($dialog->run ne 'ok') {$dialog->destroy;return;}
				my $pat=qr/(?:^|\x00)\Q$flag\E(?:$|\x00)/;
				my @l=grep $Songs[$_][SONG_FLAGS]=~m/$pat/, 0..$#Songs;
				SetFlags(\@l,undef,[$flag]);
				$dialog->destroy;
			}
			$store->remove($iter);
			@Flags=grep $_ ne $flag, @Flags;
		});
	my $addbut=NewIconButton('gtk-add',_"add flag");
	$addbut->signal_connect( clicked => sub
		{	my $iter=$store->append;
			$store->set($iter,0,'',1,0);
			$treeview->set_cursor ($store->get_path($iter), $treeview->get_column(0), TRUE);
		});

	my $sw=Gtk2::ScrolledWindow->new(undef, undef);
	$sw->set_shadow_type('etched-in');
	$sw->set_policy('never', 'automatic');
	$sw->add($treeview);
	$vbox->add($sw);
	my $hbox=Gtk2::HBox->new (FALSE, 2);
	$hbox->pack_start($_,FALSE,FALSE,2) for $addbut,$delbut;
	$vbox->pack_start($hbox,FALSE,FALSE,2);

	return $vbox;
}

sub NewPrefRadio
{	my ($key,$sub,@text_val)=@_;
	my $init=$Options{$key};
	$init='' unless defined $init;
	my $cb=sub
		{	return unless $_[0]->get_active;
			$Options{$key}=$_[1];
			&$sub if $sub;
		};
	my $radio; my @radios;
	while (defined (my $text=shift @text_val))
	{	my $val=shift @text_val;
		push @radios, $radio=Gtk2::RadioButton->new($radio,$text);
		$radio->set_active(1) if $val eq $init;
		$radio->signal_connect(toggled => $cb,$val);
	}
	return @radios;
}
sub NewPrefCheckButton
{	my ($key,$text,$sub,$tip)=@_;
	my $check=Gtk2::CheckButton->new($text);
	$check->set_active(1) if $Options{$key};
	$check->signal_connect( toggled => sub
	{	$Options{ $_[1] }=($_[0]->get_active)? 1 : 0;
		&$sub if $sub;
	},$key);
	$Tooltips->set_tip($check,$tip) if defined $tip;
	return $check;
}
sub NewPrefEntry
{	my ($key,$text,$sub,$sizeg1,$sizeg2,$hide)=@_;
	my $label=Gtk2::Label->new($text);
	my $entry=Gtk2::Entry->new;
	my $hbox=Gtk2::HBox->new;
	$hbox->pack_start($_,TRUE,TRUE,2) for $label,$entry;
	$sizeg1->add_widget($label) if $sizeg1;
	$sizeg2->add_widget($entry) if $sizeg2;
	$label->set_alignment(0,.5);

	$entry->set_visibility(0) if $hide;
	$entry->set_text($Options{$key}) if defined $Options{$key};
	$entry->signal_connect( changed => sub
	{	$Options{ $_[1] }=$_[0]->get_text;
		&$sub if $sub;
	},$key);
	return $hbox;
}
sub NewPrefFileEntry
{	my ($key,$text,$folder,$sub)=@_;
	my $label=Gtk2::Label->new($text);
	my $entry=Gtk2::Entry->new;
	my $button=NewIconButton('gtk-open');
	my $hbox=Gtk2::HBox->new;
	$hbox->pack_start($_,TRUE,TRUE,2) for $label,$entry,$button;
	$label->set_alignment(0,.5);

	$entry->set_text(filename_to_utf8displayname($Options{$key})) if defined $Options{$key};
	my $busy;
	$entry->signal_connect( changed => sub
	{	return if $busy;
		$Options{ $_[1] }=filename_from_unicode( $_[0]->get_text );
		&$sub if $sub;
	},$key);
	$button->signal_connect( clicked => sub
	{	my $file= $folder? ::ChooseDir($text,$Options{$key}) : undef;
		return unless $file;
		$Options{$key}=$file;
		$busy=1; $entry->set_text(filename_to_utf8displayname($file)); $busy=undef;
		&$sub if $sub;
	});
	return $hbox;
}
sub NewPrefSpinButton
{	my ($key,$sub,$climb_rate,$digits,$min,$max,$stepinc,$pageinc,$text1,$text2,$sg1,$sg2)=@_;
	$text1=Gtk2::Label->new($text1) if defined $text1;
	$text2=Gtk2::Label->new($text2) if defined $text2;
	my $adj=Gtk2::Adjustment->new($Options{$key},$min,$max,$stepinc,$pageinc,undef);
	my $spin=Gtk2::SpinButton->new($adj,$climb_rate,$digits);
	$adj->signal_connect(value_changed => sub
	 {	$::Options{ $_[1] }=$_[0]->get_value;
		&$sub if $sub;
	 },$key);
	if ($sg1 && $text1) { $sg1->add_widget($text1); $text1->set_alignment(0,.5); }
	if ($sg2) { $sg2->add_widget($spin); }
 	if ($text1 or $text2)
	{	my $hbox=Gtk2::HBox->new;
		$hbox->pack_start($_,FALSE,FALSE,2) for grep $_, $text1,$spin,$text2;
		return $hbox;
	}
	return $spin;
}

sub NewPrefCombo
{	my ($key,$list,$text,$sub,$sizeg1,$sizeg2)=@_;
	my $combo=Gtk2::ComboBox->new_text;
	my $names=$list;
	if (ref $list eq 'HASH')
	{	my $h=$list;
		$list=[]; $names=[];
		for my $key (sort {$h->{$a} cmp $h->{$b}} keys %$h)
		{	push @$list,$key;
			push @$names,$h->{$key}
		}
	}
	my $found;
	for my $i (0..$#$list)
	{	$combo->append_text( $names->[$i] );
		$found=$i if defined $Options{$key} && $list->[$i] eq $Options{$key};
	}
	$combo->set_active($found) if defined $found;
	$combo->signal_connect(changed => sub
		{	$Options{$key}= $list->[ $_[0]->get_active ];
			&$sub if $sub;
		});
	return $combo unless defined $text;
	my $label=Gtk2::Label->new($text);
	my $hbox=Gtk2::HBox->new;
	$hbox->pack_start($_,TRUE,TRUE,2) for $label,$combo;
	$sizeg1->add_widget($label) if $sizeg1;
	$sizeg2->add_widget($combo) if $sizeg2;
	$label->set_alignment(0,.5);
	return $hbox;
}

sub NewIconButton
{	my ($icon,$text,$coderef,$style)=@_;
	my $but=Gtk2::Button->new;
	$but->set_relief($style) if $style;
	my $widget=Gtk2::Image->new_from_stock($icon,'menu');
	if ($text)
	{	my $box=Gtk2::HBox->new(FALSE, 4);
		$box->pack_start($_, FALSE, FALSE, 2)
			for $widget,Gtk2::Label->new($text);
		$widget=$box;
	}
	$but->add($widget);
	$but->signal_connect(clicked => $coderef) if $coderef;
	return $but;
}

sub EditWeightedRandom
{	my ($sort,$name,$sub)=@_;
	my $dialog=EditSFR->new($LEventW,'WRandom',$sort,$name);
	return $dialog->Result($sub);
}
sub EditSortOrder
{	my ($sort,$name,$sub)=@_;
	my $dialog=EditSFR->new($LEventW,'Sort',$sort,$name);
	return $dialog->Result($sub);
}
sub EditFilter
{	my ($widget,$filter,$name,$sub)=@_;
	my $dialog=EditSFR->new($widget,'Filter',$filter,$name);
	$sub||='' unless wantarray;#FIXME
	return $dialog->Result($sub);
}

sub SaveWRandom
{	my ($name,$val,$newname)=@_;
	if ($newname)		{$SavedWRandoms{$newname}=delete $SavedWRandoms{$name};}
	elsif (defined $val)	{$SavedWRandoms{$name}=$val;}
	else			{delete $SavedWRandoms{$name};}
	HasChanged('SavedWRandoms');
}
sub SaveSort
{	my ($name,$val,$newname)=@_;
	if ($newname)		{$SavedSorts{$newname}=delete $SavedSorts{$name};}
	elsif (defined $val)	{$SavedSorts{$name}=$val;}
	else			{delete $SavedSorts{$name};}
	HasChanged('SavedSorts');
}
sub SaveFilter
{	my ($name,$val,$newname)=@_;
	if ($newname)		{$SavedFilters{$newname}=delete $SavedFilters{$name};}
	elsif (defined $val)	{$SavedFilters{$name}=$val;}
	else			{delete $SavedFilters{$name};}
	HasChanged('SavedFilters');
}
sub SaveList
{	my ($name,$val,$newname)=@_;
	if ($newname)		{$SavedLists{$newname}=delete $SavedLists{$name}; HasChanged('SavedLists',$name,'renamedto',$newname); $name=$newname; }
	elsif (defined $val)	{$SavedLists{$name}=$val;}
	else			{delete $SavedLists{$name};}
	HasChanged('SavedLists',$name);
}

sub Watch
{	my ($object,$key,$sub)=@_;
	warn "watch $key $object\n" if $debug;
	push @{$EventWatchers{$key}},$object;
	$object->{'Update'.$key}=$sub;
	$object->signal_connect(destroy => \&UnWatch,$key) unless ref $object eq 'HASH' || !$object->isa('Glib::Object');
}
sub UnWatch
{	my ($object,$key)=@_;
	warn "unwatch $key $object\n" if $debug;
	@{$EventWatchers{$key}}=grep $_ ne $object, @{$EventWatchers{$key}};
}

sub HasChanged
{	my ($key,@args)=@_;
	warn "HasChanged $key -> updating @{$EventWatchers{$key}}\n" if $debug && $EventWatchers{$key};
	for my $r ( @{$EventWatchers{$key}} ) { &{$r->{'Update'.$key}}($r,@args) };
}


sub CreateProgressWindow
{	$ProgressWin = Gtk2::Window->new('toplevel');
	#$ProgressWin->set_title(PROGRAM_NAME);
	$ProgressWin->set_border_width(3);

	$ProgressNBSongs=$ProgressNBFolders=0;
	my $lengthcheck_max=0;

	my $check_max=0;
	my $Checklabel=Gtk2::Label->new(_"Checking existing songs ...");
	my $CheckProgress=Gtk2::ProgressBar->new;
	my $Checkstop=NewIconButton('gtk-stop',_"Stop checking",sub { @ToCheck=(); });
	my $CheckVB=Vpack($Checklabel,$CheckProgress,$Checkstop);
	$CheckVB->set_no_show_all(1);

	my $label1=Gtk2::Label->new(_"Scanning ...");
	my $label2=Gtk2::Label->new;
	my $label3=Gtk2::Label->new;
	my $Bstop=NewIconButton('gtk-stop',_"Stop scanning",sub { @ToScan=();undef %FollowedDirs; });
	my $progressbar=Gtk2::ProgressBar->new;
	my $handle=Glib::Timeout->add(500,sub
		{	if (@ToCheck)
			{	unless ($check_max)
				{	$CheckVB->set_no_show_all(0);
					$CheckVB->show_all;
				}
				$check_max=@ToCheck if @ToCheck>$check_max;
				my $checked=$check_max-@ToCheck;
				$CheckProgress->set_fraction( $checked/$check_max );
				$CheckProgress->set_text( "$checked / $check_max" );
			}
			elsif ($check_max) { $CheckVB->hide; $check_max=0; $ProgressWin->resize(1,1); }

			my $fraction;
			if (@ToScan || @ToAdd || @LengthEstimated)
			{	$label2->set_label( __("Scanned %d folder, ","Scanned %d folders, ", $ProgressNBFolders)
							.__("%d song added.","%d songs added.", $ProgressNBSongs)  );
			}
			if (@ToScan || @ToAdd)
			{	$label3->set_label( __("%d folder left","%d folders left", scalar@ToScan) );
				#$progressbar->set_text(@ToScan.' folders '.@ToAdd.' songs');
				$fraction=@ToScan + $ProgressNBFolders;
				$fraction=$fraction?	$ProgressNBFolders/$fraction : 1;
				#$progressbar->pulse;
				if ($lengthcheck_max)
				{	$Bstop->show;
					$lengthcheck_max=0;
				}
			}
			elsif (@LengthEstimated)
			{	if (@LengthEstimated > $lengthcheck_max)
				{	$lengthcheck_max=@LengthEstimated;
					$Bstop->hide;
					$label3->set_label( __( "Checking length/bitrate of %d mp3 file without VBR header...",
								"Checking length/bitrate of %d mp3 files without VBR header...", $lengthcheck_max)  );
				}
				$fraction=($lengthcheck_max-@LengthEstimated)/$lengthcheck_max;
			}
			else
			{	$ProgressWin->destroy;
				undef $ProgressWin;
				return 0;
			}
			$progressbar->set_fraction($fraction);
			return 1;
		});
	$ProgressWin->signal_connect (delete_event => sub
		{	Glib::Source->remove($handle);
			$ProgressWin->destroy;
			$ProgressWin=undef;
		});

	my $vbox = Gtk2::VBox->new(FALSE, 2);
	$ProgressWin->add($vbox);
	$vbox->pack_start($_, FALSE, TRUE, 3) for $CheckVB,$label1,$label2,$label3,$progressbar,$Bstop;
	$ProgressWin->show_all;
}

sub WindowListMenu
{	my $menu=Gtk2::Menu->new;
	my $cb=
	 sub {	my $w=$_[1];
		unless ($w->visible)
		{ $w->show;
		  $w->move(split / /,$w->{SavedPosition});
		}
		$w->present;
	     };

	for my $w (grep exists $_->{layout}, Gtk2::Window->list_toplevels)
	{	my $item=Gtk2::MenuItem->new(_( $w->{layout} ));
		$item->signal_connect( activate => $cb, $w );
		$menu->append($item);
	}
	return $menu;
}

sub CreateTrayIcon
{	if ($TrayIcon)
	{	return if $Options{UseTray};
		$TrayIcon->destroy;
		$TrayIcon=undef;
		return;
	}
	elsif (!$Options{UseTray} || !$Gtk2TrayIcon)
	 {return}
	$TrayIcon= Gtk2::TrayIcon->new(PROGRAM_NAME);
	my $eventbox=Gtk2::EventBox->new;
	my $img=Gtk2::Image->new_from_file(PIXPATH.'trayicon.png');
	#my $img=NewScaledImageFromFile(PIXPATH.'gmusicbrowser.png',22,1);
#	my $pixbuf=Gtk2::Gdk::Pixbuf->new_from_file(PIXPATH.'gmusicbrowser.png');
#	my $img=Gtk2::Image->new; my $size=0;
#	$img->signal_connect(size_allocate => sub
#		{ my $h=$_[1]->height; my $w=$_[1]->width;
#		  my $s=($h<$w)? $h : $w; $s=48 if $s>48;
#		  return if $size==$s; $size=$s;
#		  $_[0]->set_from_pixbuf( $pixbuf->scale_simple($s, $s,'bilinear') );
#		});
	$eventbox->add($img);
	$TrayIcon->add($eventbox);
	$eventbox->signal_connect(scroll_event => \&::ChangeVol);
	$eventbox->signal_connect(button_press_event => sub
		{	$LEvent=$_[1];
			my $b=$LEvent->button;
			if	($b==3) { &TrayMenuPopup }
			elsif	($b==2) { &PlayPause}
			else		{ &ShowHide }
			1;
		});

	$eventbox->signal_connect(enter_notify_event => \&showTraytip);
	$eventbox->signal_connect(leave_notify_event => \&destroyTraytip);

	$TrayIcon->show_all;
	#Watch($eventbox,'SongID', \&UpdateTrayTip);
	#&UpdateTrayTip($eventbox);
}
sub TrayMenuPopup
{	my @TrayMenu=
 (	{ label=> _"Play", code => \&PlayPause,	test => sub {!defined $TogPlay},stockicon => 'gtk-media-play' },
	{ label=> _"Pause",code => \&PlayPause,	test => sub {defined $TogPlay},	stockicon => 'gtk-media-pause' },
	{ label=> _"Stop", code => \&Stop,	stockicon => 'gtk-media-stop' },
	{ label=> _"Next", code => \&NextSong,	stockicon => 'gtk-media-next' },
	{ label=> _"Recently played", submenu => sub { my $m=::ChooseSongs(undef,::GetPrevSongs(5)); }, stockicon => 'gtk-media-previous' },
	{ label=> sub {$TogLock==SONG_ARTIST? _"Unlock Artist" : _"Lock Artist"}, code => sub {ToggleLock(SONG_ARTIST);} },
	{ label=> sub {$TogLock==SONG_ALBUM? _"Unlock Album" : _"Lock Album"}, code => sub {ToggleLock(SONG_ALBUM);} },
	{ label=> _"Windows",	submenu => \&WindowListMenu, },
	{ label=> sub {$MainWindow->visible ? _"Hide": _"Show"}, code => \&ShowHide },
	{ label=> _"Quit", code => \&Quit,	stockicon => 'gtk-quit' },
 );
	&destroyTraytipNow;
	$NoTrayTip=1;
 	my $m=PopupContextMenu(\@TrayMenu,{});
	$m->signal_connect( selection_done => sub {$NoTrayTip=undef});
	$m->show_all;
	$m->popup(undef,undef,\&::menupos,undef,$LEvent->button,$LEvent->time);
}
sub showTraytip
{	return 0 if $NoTrayTip;
	if ($TrayTipWin)
	{	Glib::Source->remove($TrayTipWin->{timeout}) if $TrayTipWin->{timeout};
		$TrayTipWin->{timeout}=undef;
	}
	else
	{	my $event=$_[1];
		$TrayTipWin=Player->new($Options{LayoutT},'popup');
		$TrayTipWin->child->show_all;	#needed to get the true size of the window
		$TrayTipWin->child->realize;	#needed for the cover widget, which needs to know how much size it gets to decide how much it asks :( #FIXME
		$TrayTipWin->move( windowpos($TrayTipWin,$event) );
		$TrayTipWin->show;
		$TrayTipWin->signal_connect(leave_notify_event => sub
			{ &destroyTraytip if $_[1]->detail ne 'inferior';0; });
		$TrayTipWin->signal_connect(enter_notify_event => \&showTraytip);
		#Glib::Timeout->add( 300,sub { return 0 unless $TrayTipWin && $TrayTipWin->window; &destroyTraytip unless ($TrayTipWin->window->get_pointer)[1]; });
	}
	$TrayTipWin->{timeout}=Glib::Timeout->add( 3000,\&destroyTraytipNow) unless $_[0]; #when called directly by UpdateSongID()
	0;
}
sub destroyTraytip
{	return 0 if !$TrayTipWin || $TrayTipWin->{timeout};
	$TrayTipWin->{timeout}=Glib::Timeout->add( 300,\&destroyTraytipNow);
	0;
}
sub destroyTraytipNow
{	$TrayTipWin->destroy if $TrayTipWin;
	$TrayTipWin=undef;
	0;
}

sub windowpos	# function to position window next to clicked widget ($event can be a widget)
{	my ($win,$event)=@_;
	my $h=$win->size_request->height;		# height of window to position
	my $w=$win->size_request->width;		# width of window to position
	my $xmax=$event->get_screen->get_width;		# width of the screen
	my $ymax=$event->get_screen->get_height;	# height of the screen
	my ($x,$y)=$event->window->get_origin;		# position of the clicked widget on the screen
	my ($dx,$dy)=($event->window->get_geometry)[2,3];#width,height of the clicked widget
	my $xcenter=1;
	$xcenter=0 if $dx>$xmax;
	if ($x+$w/2 < $xmax && $x-$w/2 >0){ $x-=int($w/2); }# centered
	elsif ($dx+$x+$w > $xmax)	{ $x=$xmax-$w; $x=0 if $x<0 }	# right side
	else				{ $x=0; }			# left side
	if (!$xcenter && $y+$h/2 < $ymax && $y-$h/2 >0){ $y-=int($h/2) }# y center
	elsif ($dy+$y+$h > $ymax)  { $y-=$h; $y=0 if $y<0 }		# display above the widget
	else			   { $y+=$dy; }				# display below the widget
	return $x,$y;
}

sub UpdateTrayTip #not used
{	$Tooltips->set_tip($_[0],$Songs[$SongID][SONG_TITLE]."\nby ".$Songs[$SongID][SONG_ARTIST]."\nfrom ".$Songs[$SongID][SONG_ALBUM]);
}

sub ShowHide
{	if ($MainWindow->visible)
	{	for my $w ($MainWindow,$BrowserWindow,$ContextWindow)
		{	next unless $w;
			$w->{SavedPosition}=join ' ',$w->get_position;
			$w->hide;
		}
	}
	else
	{	for my $w ($MainWindow,$BrowserWindow,$ContextWindow)
		{	next unless $w;
			$w->show;
			$w->move(split / /,$w->{SavedPosition}); #FIXME should be before show, but doesn't work
		}
	}
}

package EditSFR;
use Gtk2;
use base 'Gtk2::Dialog';

my %refs;

INIT
{ %refs=
  (	Filter	=> [_"Edit filter",	\%::SavedFilters,	'SavedFilters',	\&::SaveFilter,	_"saved filters",
	  _"name of the new filter",	_"save filter as",	_"delete selected filter"	],
	Sort	=> [_"Edit sort mode",  \%::SavedSorts,		'SavedSorts',	\&::SaveSort,	_"saved sort modes",
	  _"name of the new sort mode",	_"save sort mode as",	_"delete selected sort mode"	],
	WRandom	=> [_"Edit random mode",\%::SavedWRandoms,	'SavedWRandoms',\&::SaveWRandom,_"saved random modes",
	  _"name of the new random mode", _"save random mode as", _"delete selected random mode"],
  );
}

sub new
{	my ($class,$window,$type,$init,$name) = @_;
	$window=$window->get_toplevel if $window;
	my $typedata=$refs{$type};
	my $self = bless Gtk2::Dialog->new( $typedata->[0], $window,[qw/destroy-with-parent/]), $class;
	$self->add_button('gtk-cancel' => 'none');
	if (defined $name && $name ne '')
	{	my $button=::NewIconButton('gtk-save', ::__x( _"save as '{name}'", name => $name) );
		$button->can_default(::TRUE);
		$self->add_action_widget( $button,'ok' );
		$self->{save_name}=$name;
	}
	else
	{	$self->{save_name_entry}=Gtk2::Entry->new if defined $name; # eq ''
		$self->add_button('gtk-ok' => 'ok');
	}
	$self->set_default_response('ok');
	$self->set_border_width(3);

	@$self{qw/hash update save/}=@$typedata[1,2,3];
	::Watch($self,$self->{update},\&Fill);

	if (defined $name)
	{	if ($name eq '')
		{	$name=0;
			$name++ while exists $self->{hash}{_"noname".$name};
			$name=_"noname".$name;
		}
		else { $init=$self->{hash}{$name} unless defined $init; }
	}

	my $store=Gtk2::ListStore->new('Glib::String');
	my $treeview=Gtk2::TreeView->new($store);
#	$treeview->set_headers_visible(::FALSE);
	my $renderer=Gtk2::CellRendererText->new;
	$renderer->signal_connect(edited => \&name_edited_cb,$self);
	$renderer->set(editable => 1);
	$treeview->append_column( Gtk2::TreeViewColumn->new_with_attributes
		( $typedata->[4],$renderer,'text',0)
		);
	my $sw=Gtk2::ScrolledWindow->new (undef, undef);
	$sw->set_shadow_type ('etched-in');
	$sw->set_policy ('automatic', 'automatic');
	my $butrm=::NewIconButton('gtk-remove',_"remove");
	$treeview->get_selection->signal_connect( changed => sub
		{	my $sel=$_[0]->count_selected_rows;
			$butrm->set_sensitive($sel);
		});
	$butrm->set_sensitive(::FALSE);
	$butrm->signal_connect(clicked => \&Remove_cb,$self);
	my $butsave=::NewIconButton('gtk-save');
	my $NameEntry=Gtk2::Entry->new;
	$NameEntry->signal_connect(changed => sub { $butsave->set_sensitive(length $_[0]->get_text); });
	$butsave->signal_connect(  clicked  => sub {$self->Save});
	$NameEntry->signal_connect(activate => sub {$self->Save});
	$butsave->set_sensitive(0);
	$NameEntry->set_text($name) if defined $name;
	$::Tooltips->set_tip($NameEntry,$typedata->[5] );
	$::Tooltips->set_tip($butsave,	$typedata->[6] );
	$::Tooltips->set_tip($butrm,	$typedata->[7]);
	my $savebox=Gtk2::HBox->new;
	my $vbox=Gtk2::VBox->new;
	my $hbox=Gtk2::HBox->new;
	$savebox->pack_start($NameEntry, ::FALSE,::FALSE,0);
	$savebox->pack_start($butsave, ::FALSE,::FALSE,0);
	$sw->add($treeview);
	$vbox->pack_start($savebox, ::FALSE,::FALSE, 2);
	$vbox->add($sw);
	$vbox->pack_start($butrm,::FALSE,::FALSE, 2);
	$hbox->pack_start($vbox, ::FALSE,::FALSE, 2);
	$self->vbox->add($hbox);

	$self->{entry}=$NameEntry;
	$self->{store}=$store;
	$self->{treeview}=$treeview;
	$self->Fill;
	my $package='Edit'.$type;
	my $editobject=$package->new($self,$init);
	$hbox->add($editobject);
	if ($self->{save_name_entry}) { $editobject->pack_start(::Hpack(Gtk2::Label->new('Save as : '),$self->{save_name_entry}), ::FALSE,::FALSE, 2); $self->{save_name_entry}->set_text($name); }
	$self->{editobject}=$editobject;

	::SetWSize($self,'Edit'.$type);
	$self->show_all;

	$treeview->get_selection->unselect_all;
	$treeview->signal_connect(cursor_changed => \&cursor_changed_cb,$self);

	return $self;
}

sub name_edited_cb
{	my ($cell, $path_string, $newname,$self) = @_;
	my $store=$self->{store};
	my $iter=$store->get_iter( Gtk2::TreePath->new($path_string) );
	my $name=$store->get($iter,0);
	#$self->{busy}=1;
	&{ $self->{save} }($name,undef,$newname);
	#$self->{busy}=undef;
	#$store->set($iter, 0, $newname);
}

sub Remove_cb
{	my $self=$_[1];
	my $path=($self->{treeview}->get_cursor)[0]||return;
	my $store=$self->{store};
	my $name=$store->get( $store->get_iter($path) ,0);
	&{ $self->{save} }($name,undef);
}

sub Save
{	my $self=shift;
	my $name=$self->{entry}->get_text;
	return unless $name;
	my $result=$self->{editobject}->Result;
	&{ $self->{save} }($name, $result);
}

sub Fill
{	my $self=shift;
	return if $self->{busy};
	my $store=$self->{store};
	$store->clear;
	$store->set($store->append,0,$_) for sort keys %{ $self->{hash} };
}

sub cursor_changed_cb
{	my ($treeview,$self)=@_;
	my $store=$self->{store};
	my ($path)=$treeview->get_cursor;
	return unless $path;
	my $name=$store->get( $store->get_iter($path) ,0);
	$self->{entry}->set_text($name);
	$self->{editobject}->Set( $self->{hash}{$name} );
}

sub Result
{	my ($self,$sub)=@_;
	if (defined $sub)
	{	$self->add_button('gtk-apply','apply') if $sub;
		$self->signal_connect( response =>sub
		 {	my $ans=$_[1];
			if ($ans eq 'ok' || $ans eq 'apply')
			{	my $result=$self->{editobject}->Result;
				$self->{save_name}=$self->{save_name_entry}->get_text if $self->{save_name_entry};
				&{ $self->{save} }($self->{save_name}, $result) if $ans eq 'ok' && defined $self->{save_name} && $self->{save_name} ne '';
				&$sub($result) if $sub;
				return if $ans eq 'apply';
			}
			$self->destroy;
		 });
		return;
	}
	my $result;
	if ('ok' eq $self->run) #FIXME stop using this, always supply a $sub
	{	$result=$self->{editobject}->Result;
	}
	$self->destroy;
	return $result;
}


package EditFilter;
use Gtk2;
use base 'Gtk2::VBox';
use constant
{  TRUE  => 1, FALSE => 0,
   C_NAME => 0,	C_POS => 1, C_VAL1 => 2, C_VAL2	=> 3,
};

sub new
{	my ($class,$dialog,$init) = @_;
	my $self = bless Gtk2::VBox->new, $class;

	my $store=Gtk2::TreeStore->new(('Glib::String')x4);
	$self->{treeview}=
	my $treeview=Gtk2::TreeView->new($store);
	$treeview->set_reorderable(TRUE);
	$treeview->append_column( Gtk2::TreeViewColumn->new_with_attributes(
		filters => Gtk2::CellRendererText->new,
		text => C_NAME) );
	my $sw = Gtk2::ScrolledWindow->new (undef, undef);
	$sw->set_shadow_type('etched-in');
	$sw->set_policy('never','automatic');

	my $butadd= ::NewIconButton('gtk-add',_"add");
	my $butadd2=::NewIconButton('gtk-add',_"add nested");
	my $butrm=  ::NewIconButton('gtk-remove',_"remove");
	$butadd->signal_connect( clicked => \&Add_cb,$self);
	$butadd2->signal_connect(clicked => \&Add_cb,$self);
	$butrm->signal_connect(  clicked => \&Rm_cb, $self);
	$butrm->set_sensitive(FALSE);
	$butadd->{filter}=::SONG_TITLE.'s';
	$butadd2->{filter}="(\x1D".::SONG_TITLE."s\x1D)";

	$treeview->get_selection->signal_connect( changed => sub
		{	my $sel=$_[0]->count_selected_rows;
			$butrm->set_sensitive($sel);
		});
	$self->{fbox}=
	my $fbox=Gtk2::EventBox->new;
	my $bbox=Gtk2::HButtonBox->new;
	$bbox->add($_) for $butadd,$butadd2,$butrm;
	$sw->add($treeview);
	$self->add($sw);
	$self->pack_start($fbox, FALSE, FALSE, 1);
	$self->pack_start($bbox, FALSE, FALSE, 1);

	$treeview->signal_connect(cursor_changed => \&cursor_changed_cb, $self);

	::set_drag($treeview,
	source=>[::DRAG_FILTER,sub
		{	my $treeview=$_[0];
			my $f=$self->Result( ($treeview->get_cursor)[0] );
			return $f->{string} if $f;
		}],
	dest =>	[::DRAG_FILTER,sub
		{	my ($treeview,$type,$dest,$filter)=@_;
			#$self->signal_stop_emission_by_name('drag_data_received');
			return if $treeview->{drag_is_source} && !$store->iter_has_child($store->get_iter_first);
			my (undef,$path,$pos)=@$dest;
			#warn "-------- $filter,$path,$pos";
			my $rowref_todel;
			$rowref_todel=Gtk2::TreeRowReference->new($treeview->get_model,($treeview->get_cursor)[0]) if $treeview->{drag_is_source};
			$self->Set($filter,$path,$pos);
			if ($rowref_todel)
			{	my $path=$rowref_todel->valid	?
					$rowref_todel->get_path	: $pos=~m/after$/ ?
					Gtk2::TreePath->new_from_indices(0,0)	  :
					Gtk2::TreePath->new_from_indices(0,1);
				$self->Remove_path($path);
			}
		}],
	motion => sub
		{	my ($treeview,$context,$x,$y,$time)=@_;# warn "drag_motion_cb @_";
			my $store=$treeview->get_model;
			my ($path,$pos)=$treeview->get_dest_row_at_pos($x,$y);
			$path||=Gtk2::TreePath->new_first;
			$pos||='after';

			if ($treeview->{drag_is_source})
			{	my $sourcepath=($treeview->get_cursor)[0];
				if ($sourcepath->is_ancestor($path) || !$sourcepath->compare($path))
				{	$treeview->set_drag_dest_row(undef,$pos);
					$context->status('default', 0);
					return 0;
				}
			}

			my $iter=$store->get_iter($path);
			if (!$store->iter_has_child($iter))				{ $pos=~s/^into-or-//; }
			elsif ($pos!~m/^into-or-/ && !$store->iter_parent($iter))	{ $pos='into-or-'.$pos; }
			#warn "$pos, ".$self->Result($path,$pos)->{string};
			$context->{dest}=[$treeview,$path,$pos];
			$treeview->set_drag_dest_row($path,$pos);
			$context->status(($treeview->{drag_is_source} ? 'move' : 'copy'),0);
			return 1;
		});
	$self->Set($init);

	return $self;
}

sub Add_cb
{	my ($button,$self)=@_;
	my $path=($self->{treeview}->get_cursor)[0];
	$path||=Gtk2::TreePath->new_first;
	$self->Set( $button->{filter}, $path);
}
sub Rm_cb
{	my ($button,$self)=@_;
	my $treeview=$self->{treeview};
	my ($path)=$treeview->get_cursor;
	return unless $path;
	my $oldpath=$self->Remove_path($path);
	$oldpath->prev or $oldpath->up;
	$oldpath=Gtk2::TreePath->new_first unless $oldpath->get_depth;
	$treeview->set_cursor($oldpath);
}
sub Remove_path
{	my ($self,$path)=@_;
	my $store=$self->{treeview}->get_model;
	my $iter=$store->get_iter($path);
	my $parent=$store->iter_parent($iter);
	while ($parent && $store->iter_n_children($parent)<2)
	{	my $p=$store->iter_parent($parent);
		$iter=$parent;
		$parent=$p;
	}
	my $oldpath=$store->get_path($iter);
	$store->remove($iter);
	# recreate a default entry if no more entry :
	$self->Set(::SONG_TITLE.'s') unless $store->get_iter_first;
	return $oldpath;
}

sub cursor_changed_cb
{	my ($treeview,$self)=@_;
	my $fbox=$self->{fbox};
	my $store=$treeview->get_model;
	my ($path,$co)=$_[0]->get_cursor;
	return unless $path;
	#warn "row : ",$path->to_string," / col : $co\n";
	$fbox->remove($fbox->child) if $fbox->child;
	my $iter=$store->get_iter($path);
	my $box;
	if ($store->iter_has_child($iter))
	{	$box=Gtk2::HBox->new;
		my $state=$store->get($iter,C_POS);
		my $group;
		for my $ao ('&','|')
		{	my $name=($ao eq '&')? _"All of :":_"Any of :";
			my $b=Gtk2::RadioButton->new($group,$name);
			$group=$b unless $group;
			$b->set_active(1) if $ao eq $state;
			$b->signal_connect( toggled => sub
			{	return unless $_[0]->get_active;
				$store->set($iter,C_NAME,$name,C_POS,$ao);
			});
			$box->add($b);
		}
	}
	else
	{	$box=FilterBox->new
		(	undef,
			sub
			{	warn "filter : @_\n" if $::debug;
				my ($pos,@vals)=@_;
				$store->set($iter,
					C_NAME,FilterBox::posval2desc($pos,@vals),
					C_POS,$pos,C_VAL1,$vals[0], C_VAL2,$vals[1]);
			},
			$store->get($iter,C_POS,C_VAL1,C_VAL2)
		);
	}
	$fbox->add($box);
	$fbox->show_all;
}

sub Set
{	my ($self,$filter,$startpath,$startpos)=@_;
	$filter=$filter->{string} if ref $filter;
#	$filter=::SONG_TITLE.'s' unless $filter;
	my $treeview=$self->{treeview};
	my $store=$treeview->get_model;

	my $iter;
	if ($startpath)
	{	$iter=$store->get_iter($startpath);
		my $parent=$store->iter_parent($iter);
		if (!$parent && !$store->iter_has_child($iter)) #add a root
		{	$parent=$store->prepend(undef);
			$store->set($parent,C_NAME,_"All of :",C_POS,'&');
			my $new=$store->append($parent);
			$store->set($new,$_,$store->get($iter,$_)) for C_NAME,C_POS,C_VAL1,C_VAL2;
			$store->remove($iter);
			$iter=$parent;
			$startpos='into-or-'.$startpos if $startpos;
		}
		elsif (!$startpos && !$store->iter_has_child($iter))
		{	$iter=$parent;
		}
	}
	else { $store->clear }

	my $firstnewpath;
	my $createrowsub=sub
	 {	my $iter=shift;
		if ($startpos)
		{	my @args= $startpos=~m/^into/ ? ($iter,undef) : (undef,$iter);
			$iter=	($startpos=~m/^into/ xor $startpos=~m/after$/)
				? $store->insert_after(@args) : $store->insert_before(@args);
			$startpos=undef;
		}
		else	{$iter=$store->append($iter);}
		$firstnewpath||=$store->get_path($iter);
		return $iter;
	 };

	for my $f (split /\x1D/,$filter)
	{	if ($f eq ')')
		{	$iter=$store->iter_parent($iter);
		}
		elsif ($f=~m/^\(/)	# '(|' or '(&'
		{	$iter=&$createrowsub($iter);
			my ($ao,$text)=($f eq '(|')? ('|',_"Any of :") : ('&',_"All of :");
			$store->set($iter,C_NAME,$text,C_POS,$ao);
		}
		else
		{ next if $f eq '';
		  my ($pos,@vals)=FilterBox::filter2posval($f);
		  unless ($pos) { warn "Invalid filter : $f\n"; next; }
		  my $leaf=&$createrowsub($iter);
		  $firstnewpath=$store->get_path($leaf) unless $firstnewpath;
		  $store->set($leaf, C_NAME,FilterBox::posval2desc($pos,@vals),	C_POS,$pos, C_VAL1,$vals[0], C_VAL2,$vals[1]);
		}
	}
	unless ($store->get_iter_first)	#default filter if no/invalid filter
	{	my ($pos,@vals)=FilterBox::filter2posval(::SONG_TITLE.'s');
		$store->set($store->append(undef), C_NAME,FilterBox::posval2desc($pos,@vals),	C_POS,$pos, C_VAL1,$vals[0], C_VAL2,$vals[1]);
	}

	$firstnewpath||=Gtk2::TreePath->new_first;
	my $path_string=$firstnewpath->to_string;
	if ($firstnewpath->get_depth>1)	{ $firstnewpath->up; $treeview->expand_row($firstnewpath,TRUE); }
	else	{ $treeview->expand_all }
	$treeview->set_cursor( Gtk2::TreePath->new($path_string) );
}

sub Result
{	my ($self,$startpath)=@_;
	my $store=$self->{treeview}->get_model;
	my $filter='';
	my $depth=0;
	my $next=$startpath? $store->get_iter($startpath) : $store->get_iter_first;
	while (my $iter=$next)
	{	my ($pos,@vals)=$store->get($iter,C_POS,C_VAL1,C_VAL2);
		if ( $next=$store->iter_children($iter) )
		{	$filter.="($pos\x1D" unless $store->iter_n_children($iter)<2;
			$depth++;
		}
		else
		{	$filter.=FilterBox::posval2filter($pos,@vals)."\x1D";
			last unless $depth;
			$next=$store->iter_next($iter);
		}
		until ($next)
		{	last unless $depth and $iter=$store->iter_parent($iter);
			$filter.=")\x1D"  unless $store->iter_n_children($iter)<2;
			$depth--;
			last unless $depth;
			$next=$store->iter_next($iter);
		}
	}
	warn "filter= $filter\n" if $::debug;
	return Filter->new($filter);
}

package EditSort;
use Gtk2;
use base 'Gtk2::VBox';
use constant { TRUE  => 1, FALSE => 0, };
sub new
{	my ($class,$dialog,$init) = @_;
	$init=undef if $init=~m/^[rs]/;
	my $self = bless Gtk2::VBox->new, $class;

	$self->{store1}=	my $store1=Gtk2::ListStore->new(('Glib::String')x2);
	$self->{store2}=	my $store2=Gtk2::ListStore->new(('Glib::String')x3);
	$self->{treeview1}=	my $treeview1=Gtk2::TreeView->new($store1);
	$self->{treeview2}=	my $treeview2=Gtk2::TreeView->new($store2);
	$treeview2->set_reorderable(TRUE);
	$treeview2->append_column
		( Gtk2::TreeViewColumn->new_with_attributes
		  ('Order',Gtk2::CellRendererPixbuf->new,'stock-id',2)
		);
	my $butadd=	::NewIconButton('gtk-add',	_"add",		sub {$self->Add_selected});
	my $butrm=	::NewIconButton('gtk-remove',	_"remove",	sub {$self->Del_selected});
	my $butclear=	::NewIconButton('gtk-clear',	_"clear",	sub { $self->Set(''); });
	my $butup=	::NewIconButton('gtk-go-up',	undef,		sub { $self->Move_Selected(1,0); });
	my $butdown=	::NewIconButton('gtk-go-down',	undef,		sub { $self->Move_Selected(0,0); });
	$self->{butadd}=$butadd;
	$self->{butrm}=$butrm;
	$self->{butup}=$butup;
	$self->{butdown}=$butdown;

	my $size_group=Gtk2::SizeGroup->new('horizontal');
	$size_group->add_widget($_) for $butadd,$butrm,$butclear;

	$treeview1->get_selection->signal_connect (changed => sub{$self->Buttons_update;});
	$treeview2->get_selection->signal_connect (changed => sub{$self->Buttons_update;});
	$treeview1->signal_connect (row_activated => sub {$self->Add_selected});
	$treeview2->signal_connect (row_activated => sub {$self->Del_selected});
	$treeview2->signal_connect (cursor_changed => \&cursor_changed2_cb,$self);

	#my $hbox=Gtk2::HBox->new (FALSE, 8);
	my $table=Gtk2::Table->new (2, 4, FALSE);
	my $col=0;
	for ([_"Available",$treeview1,$butadd],[_"Sort order",$treeview2,$butrm,$butclear])
	{	my ($text,$tv,@buts)=@$_;
		#my $vbox=Gtk2::VBox->new (FALSE, 1);
		my $lab=Gtk2::Label->new;
		$lab->set_markup("<b>$text</b>");
		$tv->set_headers_visible(FALSE);
		$tv->append_column( Gtk2::TreeViewColumn->new_with_attributes($text,Gtk2::CellRendererText->new,'text',1) );
		my $sw = Gtk2::ScrolledWindow->new (undef, undef);
		$sw->set_shadow_type('etched-in');
		$sw->set_policy('never','automatic');
		$sw->set_size_request(-1,200);
		$sw->add($tv);
		my $row=0;
		$table->attach($lab,$col,$col+1,$row++,$row,'fill','shrink',1,1);
		$table->attach($sw,$col,$col+1,$row++,$row,'fill','fill',1,1);
		$table->attach($_,$col,$col+1,$row++,$row,'expand','shrink',1,1) for @buts;
		$col++;
	}

	my $vbox=Gtk2::VBox->new (FALSE, 4);
	$vbox->pack_start($_,FALSE,TRUE,1) for $butup,$butdown;
	$table->attach($vbox,$col,$col+1,1,2,'shrink','expand',1,1);
	$self->pack_start($table,TRUE,TRUE,1);

	$self->Set($init);
	return $self;
}

sub Set
{	my ($self,$list)=@_;
	my $store2=$self->{store2};
	$store2->clear;
	$self->{nb2}=0;	#nb of rows in $store2;
	my %cols;
	for my $n (split ' ',$list)
	{   my $o=($n=~s/^-//)? 'gtk-sort-descending' : 'gtk-sort-ascending';
	    my $text=($n eq 's')? _"Shuffle" : $TagProp[$n][0];
	    $store2->set($store2->append,0,$n,1,$text,2,$o);
	    $self->{nb2}++;
	    $cols{$n}=1;
	}

	my $store1=$self->{store1};
	$store1->clear;
	$store1->set($store1->append,0,'s',1,_"Shuffle");
	for my $n (grep $TagProp[$_][1]=~m/[dnsl]/, 0..$#TagProp)
	{   next if $cols{$n};
	    $store1->set($store1->append,0,$n,1,$TagProp[$n][0]);
	}
	$self->Buttons_update;
}

sub cursor_changed2_cb
{	my ($treeview2,$self)=@_;
	my ($path,$col)=$treeview2->get_cursor;
	return unless $path;
	return unless $col eq $treeview2->get_column(0);
	my $store2=$self->{store2};
	my $iter=$store2->get_iter($path);
	my $o=$store2->get_value($iter,2);
	$o=($o eq 'gtk-sort-ascending')? 'gtk-sort-descending' : 'gtk-sort-ascending';
	$store2->set($iter,2,$o);
}

sub Add_selected
{	my $self=shift;
	my $path=($self->{treeview1}->get_cursor)[0]||return;
	my $store1=$self->{store1};
	my $store2=$self->{store2};
	my $iter=$store1->get_iter($path);
	return unless $iter;
	my ($n,$v)=$store1->get_value($iter,0,1);
	$store1->remove($iter);
	$store2->set($store2->append,0,$n,1,$v,2,'gtk-sort-ascending');
	$self->{nb2}++;
	$self->Buttons_update;
}
sub Del_selected
{	my $self=shift;
	my $path=($self->{treeview2}->get_cursor)[0]||return;
	my $store1=$self->{store1};
	my $store2=$self->{store2};
	my $iter=$store2->get_iter($path);
	my ($n,$v)=$store2->get_value($iter,0,1);
	$store2->remove($iter);
	$self->{nb2}--;
	$store1->set($store1->append,0,$n,1,$v);	#FIXME should be inserted in correct order
	$self->Buttons_update;
}
sub Move_Selected
{	my ($self,$up,$max)=@_;
	my $path=($self->{treeview2}->get_cursor)[0]||return;
	my $store2=$self->{store2};
	my $iter=$store2->get_iter($path);
	if ($max)
	{	if ($up) { $store2->move_after($iter,undef); }
		else	 { $store2->move_before($iter,undef);}
		return;
	}
	my $row=$path->to_string;
	if ($up) {$row--} else {$row++}
	my $iter2=$store2->get_iter_from_string($row)||return;
	$store2->swap ($iter,$iter2);
	$self->Buttons_update;
};


sub Buttons_update	#update sensitive state of buttons
{	my $self=shift;
	$self->{butadd}->set_sensitive( $self->{treeview1}->get_selection->count_selected_rows );
	my ($sel)=$self->{treeview2}->get_selection->get_selected_rows;
	if ($sel)
	{	my $row=$sel->to_string;
		$self->{butup}	->set_sensitive($row>0);
		$self->{butdown}->set_sensitive($row<$self->{nb2}-1);
		$self->{butrm}	->set_sensitive(1);
	}
	else { $self->{$_}->set_sensitive(0) for qw/butrm butup butdown/; }
}

sub Result
{	my $self=shift;
	my $store=$self->{store2};
	my $order='';
	my $iter=$store->get_iter_first;
	while ($iter)
	{	my ($n,$o)=$store->get($iter,0,2);
		$order.='-' if $o eq 'gtk-sort-descending';
		$order.=$n;
		$order.=' ' if $iter=$store->iter_next($iter);
	}
	return $order;
}

package EditWRandom;
use Gtk2;
use base 'Gtk2::VBox';
use constant
{ TRUE  => 1, FALSE => 0,
  NBCOLS	=> 20,
  COLWIDTH	=> 15,
  HHEIGHT	=> 100,
  HWIDTH	=> 20*15,

  SCORE_FIELDS	=> 0, SCORE_DESCR	=> 1, SCORE_UNIT	=> 2,
  SCORE_ROUND	=> 3, SCORE_DEFAULT	=> 4, SCORE_VALUE	=> 5,
};
sub new
{	my ($class,$dialog,$init) = @_;
	my $self = bless Gtk2::VBox->new, $class;

	my $table=Gtk2::Table->new (1, 4, FALSE);
	my $sw=Gtk2::ScrolledWindow->new(undef, undef);
	$sw->set_shadow_type('etched-in');
	$sw->set_policy('never', 'automatic');
	$sw->add_with_viewport($table);
	$self->add($sw);

	my $addlist=TextCombo->new({map {$_ => $Random::ScoreTypes{$_}[SCORE_DESCR]} keys %Random::ScoreTypes}, (keys %Random::ScoreTypes)[0] );
	my $addbut=::NewIconButton('gtk-add',_"add");
	my $addhbox=Gtk2::HBox->new(FALSE, 8);
	$addhbox->pack_start($_,FALSE,FALSE,0) for Gtk2::Label->new(_"Add rule : "), $addlist, $addbut;

	my $histogram=Gtk2::Image->new;
	my $eventbox=Gtk2::EventBox->new;
	my $histoframe=Gtk2::Frame->new;
	my $histoAl=Gtk2::Alignment->new(.5,.5,0,0);
	$eventbox->add($histogram);
	$histoframe->add($eventbox);
	$histoAl->add($histoframe);

	my $LabEx=$self->{example_label}=Gtk2::Label->new;
	$self->pack_start($_,FALSE,FALSE,2) for $addhbox,$histoAl,$LabEx;

	$::Tooltips->set_tip($eventbox,'');
	$eventbox->signal_connect(enter_notify_event => sub
		{	$_[0]->{timeout}=Glib::Timeout->add(500,\&UpdateTip_timeout,$histogram);0;
		});
	$eventbox->signal_connect(leave_notify_event => sub { Glib::Source->remove( $_[0]->{timeout} );0; });

	$addbut->signal_connect( clicked => sub
		{	my $type=$addlist->get_value;
			$self->AddRow( $Random::ScoreTypes{$type}[SCORE_DEFAULT] );
		});
	::Watch($self,'SongID',\&UpdateID);
	::Watch($self,'Filter',\&UpdateFilter);
	$self->{watcher}=::AddWatcher();
	$self->signal_connect( destroy => \&RemoveWatchers );

	$histogram->{eventbox}=$eventbox;
	$self->{histogram}=$histogram;
	$self->{table}=$table;
	$self->Set($init);

	return $self;
}

sub RemoveWatchers
{	my $self=shift;
	return unless defined $self->{watcher};
	delete $ToDo{'2_WRandom'.$self};
	::RemoveWatcher($self->{watcher});
	$self->{watcher}=undef;
}

sub Set
{	my ($self,$sort)=@_;
	$sort=~s/^r//;
	my $table=$self->{table};
	$table->remove($_) for $table->get_children;
	$self->{frames}=[];
	$self->{row}=0;
	return unless $sort;
	$self->AddRow($_) for split /\x1D/,$sort;
}

sub Redraw
{	my $self=shift;
	my $histogram=$self->{histogram};
	$histogram->{col}=undef;
	my $r=Random->new( $self->get_string );
	my ($tab)=($histogram->{tab},$self->{sum})=$r->MakeTab(NBCOLS);
	my $pixmap=Gtk2::Gdk::Pixmap->new ($histogram->window,HWIDTH,HHEIGHT,-1);
	$pixmap->draw_rectangle ($histogram->style->bg_gc($histogram->state),TRUE,0,0,HWIDTH,HHEIGHT);
	my $max=(sort { $b <=> $a } @$tab)[0] ||10;
	my $gc = $histogram->style->fg_gc($histogram->state);
	for my $x (0..NBCOLS-1)
	{	my $y=int(HHEIGHT*$$tab[$x]/$max);
		warn "$x : $y\n" if $debug;
		$pixmap->draw_rectangle($gc,TRUE,COLWIDTH*$x,HHEIGHT-$y,COLWIDTH,$y);
	}
	$histogram->set_from_pixmap ($pixmap,undef);
	$histogram->show_all;

	my $sub=sub { ::IdleDo('2_WRandom'.$self,500,\&Redraw, $self); };
	::ChangeWatcher( $self->{watcher}, undef, $r->fields, $sub, $sub, $sub, $PlayFilter, $sub);
	$self->UpdateID; #update examples

	0;
}

sub UpdateTip_timeout
{	my $histogram=shift;
	my ($x,$y)=$histogram->get_pointer;#warn "$x,$y\n";
	return 0 if $x<0;
	my $col=int($x/COLWIDTH);
	return 1 if $histogram->{col} && $histogram->{col}==$col;
	$histogram->{col}=$col;
	my $nb=$histogram->{tab}[$col]||0;
	my $range=sprintf '%.2f - %.2f',$col/NBCOLS,($col+1)/NBCOLS;
	#my $sum=$histogram->get_ancestor('Gtk2::VBox')->{sum};
	#my $prob='between '.join ' and ',map $_? '1 chance in '.sprintf('%.0f',$sum/$_) : 'no chance', $col/NBCOLS,($col+1)/NBCOLS;
	$::Tooltips->set_tip($histogram->{eventbox}, "$range : ".::__('%d song','%d songs',$nb) );
	1;
}

sub AddRow
{	my ($self,$params)=@_;
	my $table=$self->{table};
	my $row=$self->{row}++;
	my $deleted;
	my ($inverse,$weight,$type,$extra)=$params=~m/^(-?)([0-9.]+)([a-z])(.*)$/;
	return unless $type;
	my $frame=Gtk2::Frame->new( $Random::ScoreTypes{$type}[SCORE_DESCR] );
	push @{$self->{frames}},$frame;
	$frame->{params}=$params;
	my $exlabel=$frame->{label}=Gtk2::Label->new;
	$frame->{unit}=$Random::ScoreTypes{$type}[SCORE_UNIT];
	$frame->{round}=$Random::ScoreTypes{$type}[SCORE_ROUND];
	my $delbut=Gtk2::Button->new;
	$delbut->set_relief('none');
	$delbut->signal_connect( clicked => sub
		{ $frame->{params}=undef;
		  $table->remove($_) for $delbut,$frame;
		  ::IdleDo('2_WRandom'.$self,500,\&Redraw, $self);
		});
	$delbut->add(Gtk2::Image->new_from_stock('gtk-remove','menu'));
	$::Tooltips->set_tip($delbut,_"Remove this rule");
	$table->attach($delbut,0,1,$row,$row+1,'shrink','shrink',1,1);
	$table->attach($frame,1,2,$row,$row+1,['fill','expand'],'shrink',2,4);
	$delbut->show_all;
	my $fvbox=Gtk2::VBox->new;
	my $fhbox=Gtk2::HBox->new;
	my $fadj=Gtk2::Adjustment->new ($weight, 0, 1, .01, .05, 0);
	my $fscale=Gtk2::HScale->new($fadj);
	$fscale->set_digits(2);
	my $fcheck=Gtk2::CheckButton->new(_"inverse");
	$fcheck->set_active($inverse);
	$fvbox->pack_start($fhbox, FALSE, FALSE, 1);
	$fhbox->pack_start($_, FALSE, FALSE, 2)
	 for $fcheck, Gtk2::VSeparator->new, Gtk2::Label->new(_"weight :");
	$fhbox->pack_start($fscale, TRUE, TRUE, 1);
	$frame->add($fvbox);
	my $extrasub;
	my $updatesub=sub
	{	::setlocale(::LC_NUMERIC, 'C');
		$inverse=$fcheck->get_active;
		$weight=$fadj->get_value;
		$extra=&$extrasub;
		$frame->{params}=($inverse? '-' : '').$weight.$type.$extra;
		::setlocale(::LC_NUMERIC, '');
		_frame_example($frame);
		::IdleDo('2_WRandom'.$self,500, \&Redraw, $self);
	};
	my $hbox=Gtk2::HBox->new;
	$fvbox->add($hbox);
	$hbox->pack_end($exlabel, FALSE, FALSE, 1);

	if ($type eq 'f')
	{	$::Tooltips->set_tip($fcheck,"ON less probable if flag is set\nOFF more probable if flag is set");
		my $flaglist=TextCombo->new([sort @Flags],$extra,$updatesub);
		$extrasub=sub { $flaglist->get_value; };
		$hbox->pack_start($flaglist, FALSE, FALSE, 1);
	}
	elsif ($type eq 'g')
	{	$::Tooltips->set_tip($fcheck,_"ON less probable if genre is set\nOFF more probable if genre is set");
		my $genrelist=TextCombo->new( ::GetGenresList ,$extra,$updatesub);
		$extrasub=sub { $genrelist->get_value; };
		$hbox->pack_start($genrelist, FALSE, FALSE, 1);
	}
	elsif ($type eq 'r')
	{	$exlabel->parent->remove($exlabel);	#remove example to place it in the table
		$::Tooltips->set_tip($fcheck,_"ON -> smaller means more probable\nOFF -> bigger means more probable");
		my @l=split /,/,$extra;
		@l=(0,.1,.2,.3,.4,.5,.6,.7,.8,.9,1) unless @l==11;
		my @adjs;
		my $table=Gtk2::Table->new(3,4,FALSE);
		my $col=0; my $row=0;
		for my $r (0..10)
		{	my $label=Gtk2::Label->new($r*10);
			my $adj=Gtk2::Adjustment->new($l[$r], 0, 1, .01, .1, .1);
			my $spin=Gtk2::SpinButton->new($adj, 2, 2);
			$table->attach_defaults($label,$col,$col+1,$row,$row+1);
			$table->attach_defaults($spin,$col+1,$col+2,$row,$row+1);
			$row++;
			if ($row>2) {$col+=2; $row=0;}
			push @adjs,$adj;
			$spin->signal_connect( value_changed => $updatesub );
		}
		$extrasub=sub { join ',',map $_->get_value, @adjs; };
		$exlabel->set_alignment(1,.5);
		$table->attach_defaults($exlabel,0,$col+2,$row+1,$row+2);
		$hbox->pack_start($table, TRUE, TRUE, 1);
	}
	else
	{	$::Tooltips->set_tip($fcheck,_"ON -> smaller means more probable\nOFF -> bigger means more probable");
		my $halflife=$extra;
		my $adj=Gtk2::Adjustment->new ($halflife, 0.1, 10000, 1, 10, 10);
		my $spin=Gtk2::SpinButton->new($adj, 5, 1);
		$hbox->pack_start($_, FALSE, FALSE, 0)
		  for Gtk2::Label->new(_"half-life : "),$spin,
		      Gtk2::Label->new($frame->{unit});
		$extrasub=sub { $adj->get_value; };
		$spin->signal_connect( value_changed => $updatesub );
	}
	&$updatesub;
	$fadj->signal_connect( value_changed => $updatesub );
	$fcheck->signal_connect( toggled => $updatesub );
	$frame->show_all;
}

sub _frame_example
{	my $frame=shift;
	my $p=$frame->{params};
	return unless $p;
	$frame->{label}->set_markup( '<small><i>'._("ex : ").::PangoEsc(Random->MakeExample($p,$::SongID)).'</i></small>' ) if defined $::SongID;
}

sub UpdateID
{	my $self=shift;
	for my $frame (@{$self->{frames}})
	{	_frame_example($frame);
	}
	my $r=Random->new( $self->get_string );
	return unless defined $::SongID;
	my $s=$r->CalcScore($::SongID);
	my $v=sprintf '%.3f', $s;
	my $prob;
	if ($s)
	{ $prob=$self->{sum}/$s;
	  $prob= ::__x( _"1 chance in {probability}", probability => sprintf($prob>=10? '%.0f' : '%.1f', $prob) );
	}
	else {$prob=_"0 chance"}
	$self->{example_label}->set_markup( '<small><i>'.::__x( _"example (selected song) : {score}  ({chances})", score =>$v, chances => $prob). "</i></small>" );
}

sub UpdateFilter
{	my $self=shift;
	::IdleDo('2_WRandom'.$self,500,\&Redraw, $self);
}

sub get_string
{	join "\x1D",grep defined,map $_->{params}, @{$_[0]->{frames}};
}

sub Result
{	my $self=shift;
	my $sort='r'.$self->get_string;
	$sort=undef if $sort eq 'r';
	return $sort;
}

package Stars;
use Gtk2;
use base 'Gtk2::EventBox';

my (@pixbufs,$width);
use constant NBSTARS => 5;

INIT
{	@pixbufs=map Gtk2::Gdk::Pixbuf->new_from_file(::PIXPATH.'stars'.$_.'.png'),(0..NBSTARS);
	$width=$pixbufs[0]->get_width/NBSTARS;
}

sub new
{	my ($class,$nb,$sub) = @_;
	my $self = bless Gtk2::EventBox->new, $class;
	$self->{callback}=$sub || sub { $self->set(@_); };
	my $image=$self->{image}=Gtk2::Image->new;
	$self->add($image);
	$self->set($nb);
	$self->signal_connect(button_press_event => \&click);
	return $self;
}

sub set
{	my ($self,$nb)=@_;
	$self->{nb}=$nb;
	$nb=$Options{DefaultRating} if !defined $nb || $nb eq '';
	$::Tooltips->set_tip($self,_("song rating")." : $nb %");
	$nb=sprintf '%d',$nb*NBSTARS/100;
	$self->{image}->set_from_pixbuf($pixbufs[$nb]);
}
sub get { shift->{nb}; }

sub click
{	my ($self,$event)=@_;
	goto \&popup if $event->button == 3;
	my ($x)=$event->coords;
	my $nb=1+int($x/$width);
	$nb*=100/NBSTARS;
	&{ $self->{callback} }($nb);
	return 1;
}

sub popup
{	(my $self,$::LEvent)=@_;
	my $sub=
	my $menu=Gtk2::Menu->new;
	my $sub=sub { &{$self->{callback}}($_[1]); };
	for my $nb (0,10,20,30,40,50,60,70,80,90,100,'')
	{	my $item=Gtk2::CheckMenuItem->new( ($nb eq '' ? _"default" : $nb) );
		$item->set_draw_as_radio(1);
		$item->set_active(1) if $self->{nb} eq $nb;
		$item->signal_connect(activate => $sub, $nb);
		$menu->append($item);
	}
	$menu->show_all;
	$menu->popup(undef, undef, \&::menupos, undef, $::LEvent->button, $::LEvent->time);
}

package FilterBox;
use Gtk2;
use base 'Gtk2::HBox';
use strict;
use warnings;

my (@FLIST,%TYPEREGEX,%ENTRYTYPE);
my %PosRe;

INIT
{ %TYPEREGEX=
  (	s => '[^\x1D]*',
	r => '[^\x1D]*',
	n => '[0-9]*',
	d => '(\d\d\d\d)-(\d\d?)-(\d\d?)',
	a => '[-0-9]*[smhdwMy]',	#see %DATEUNITS
	b => '[-0-9]*[bkm]',		#see %SIZEUNITS
	l => '.+'
  );
  %ENTRYTYPE=
  (	s => 'FilterEntryString',
	r => 'FilterEntryString',
	n => 'FilterEntryNumber',
	d => 'FilterEntryDate',
	a => 'FilterEntryDateAgo',
	b => 'FilterEntrySize',
	f => 'FilterEntryFlags',
	g => 'FilterEntryGenres',
	l => 'FilterEntryLists',
  );
  @FLIST=
  (	_"Title",
	[	_"contains %s",		::SONG_TITLE.'s%s',
		_"doesn't contains %s",	'-'.::SONG_TITLE.'s%s',
		_"is %s",		::SONG_TITLE.'~%s',
		_"is not %s",		'-'.::SONG_TITLE.'~%s',
		_"match regexp %r",	::SONG_TITLE.'m%r',
		_"doesn't match regexp %r",'-'.::SONG_TITLE.'m%r',
	],
	_"Artist",
	[	_"contains %s",		::SONG_ARTIST.'s%s',
		_"doesn't contains %s",	'-'.::SONG_ARTIST.'s%s',
		_"is %s",		::SONG_ARTIST.'~%s',
		_"is not %s",		'-'.::SONG_ARTIST.'~%s',
		_"match regexp %r",	::SONG_ARTIST.'m%r',
		_"doesn't match regexp %r",'-'.::SONG_ARTIST.'m%r',
	],
	_"Album",
	[	_"contains %s",		::SONG_ALBUM.'s%s',
		_"doesn't contains %s",	'-'.::SONG_ALBUM.'s%s',
		_"is %s",		::SONG_ALBUM.'e%s',
		_"is not %s",		'-'.::SONG_ALBUM.'e%s',
		_"match regexp %r",	::SONG_ALBUM.'m%r',
		_"doesn't match regexp %r",'-'.::SONG_ALBUM.'m%r',
	],
	_"Year",
	[	_"is %n",	::SONG_DATE.'e%n',
		_"isn't %n",	'-'.::SONG_DATE.'e%n',
		_"is before %n",::SONG_DATE.'<%n',
		_"is after %n",	::SONG_DATE.'>%n',
	],
	_"Track",
	[	_"is %n",		::SONG_TRACK.'e%n',
		_"is not %n",		'-'.::SONG_TRACK.'e%n',
		_"is more than %n",	::SONG_TRACK.'>%n',
		_"is less than %n",	::SONG_TRACK.'<%n',
	],
	_"Disc",
	[	_"is %n",		::SONG_DISC.'e%n',
		_"is not %n",		'-'.::SONG_DISC.'e%n',
		_"is more than %n",	::SONG_DISC.'>%n',
		_"is less than %n",	::SONG_DISC.'<%n',
	],
	_"Rating",
	[	_"is %n( %)",		::SONG_RATING.'e%n',
		_"is not %n( %)",	'-'.::SONG_RATING.'e%n',
		_"is more than %n( %)",	::SONG_RATING.'>%n',
		_"is less than %n( %)",	::SONG_RATING.'<%n',
		_"is between %n( and )%n( %)",::SONG_RATING.'b%n %n',
	],
	_"Length",
	[	_"is more than %n( s)",		::SONG_LENGTH.'>%n',
		_"is less than %n( s)",		::SONG_LENGTH.'<%n',
		_"is between %n( and )%n( s)",	::SONG_LENGTH.'b%n %n',
	],
	_"Size",
	[	_"is more than %b",		::SONG_SIZE.'>%b',
		_"is less than %b",		::SONG_SIZE.'<%b',
		_"is between %b( and )%b",	::SONG_SIZE.'b%b %b',
	],
	_"played",
	[	_"more than %n( times)",	::SONG_NBPLAY.'>%n',
		_"less than %n( times)",	::SONG_NBPLAY.'<%n',
		_"exactly %n( times)",		::SONG_NBPLAY.'e%n',
		_"exactly not %n( times)",	'-'.::SONG_NBPLAY.'e%n',
		_"between %n( and )%n",		::SONG_NBPLAY.'b%n %n',
	],
	_"last played",
	[	_"less than %a( ago)",	::SONG_LASTPLAY.'>%a',
		_"more than %a( ago)",	::SONG_LASTPLAY.'<%a',
		_"before %d",		::SONG_LASTPLAY.'<%d',
		_"after %d",		::SONG_LASTPLAY.'>%d',
		_"between %d( and )%d",	::SONG_LASTPLAY.'b%d %d',
		#_"on %d",		::SONG_LASTPLAY.'o%d',
	],
	_"modified",
	[	_"less than %a( ago)",	::SONG_MODIF.'>%a',
		_"more than %a( ago)",	::SONG_MODIF.'<%a',
		_"before %d",		::SONG_MODIF.'<%d',
		_"after %d",		::SONG_MODIF.'>%d',
		_"between %d( and )%d",	::SONG_MODIF.'b%d %d',
		#_"on %d",		::SONG_MODIF.'o%d',
	],
	_"added",
	[	_"less than %a( ago)",	::SONG_ADDED.'>%a',
		_"more than %a( ago)",	::SONG_ADDED.'<%a',
		_"before %d",		::SONG_ADDED.'<%d',
		_"after %d",		::SONG_ADDED.'>%d',
		_"between %d( and )%d",	::SONG_ADDED.'b%d %d',
		#_"on %d",		::SONG_ADDED.'o%d',
	],
	_"The [most/less]",
	[	_"%n most played",	::SONG_NBPLAY.'h%n',
		_"%n less played",	::SONG_NBPLAY.'t%n',
		_"%n last played",	::SONG_LASTPLAY.'h%n',
		_"%n not played for the longuest time",	::SONG_LASTPLAY.'t%n', #FIXME description
		_"%n longest",		::SONG_LENGTH.'h%n',
		_"%n shortest",		::SONG_LENGTH.'t%n',
		_"%n last added",	::SONG_ADDED.'h%n',
		_"%n first added",	::SONG_ADDED.'t%n',
		_"All but the [most/less]%n",
		[	_"most played",		'-'.::SONG_NBPLAY.'h%n',
			_"less played",		'-'.::SONG_NBPLAY.'t%n',
			_"last played",		'-'.::SONG_LASTPLAY.'h%n',
			_"not played for the longuest time",	'-'.::SONG_LASTPLAY.'t%n',
			_"longest",		'-'.::SONG_LENGTH.'h%n',
			_"shortest",		'-'.::SONG_LENGTH.'t%n',
			_"last added",		'-'.::SONG_ADDED.'h%n',
			_"first added",		'-'.::SONG_ADDED.'t%n',
		],
	],
	_"Genre",
	[	_"is %g",		::SONG_GENRE.'f%s',
		_"isn_'t %g",		'-'.::SONG_GENRE.'f%s',
		_"contains %s",		::SONG_GENRE.'s%s',
		_"doesn't contains %s",	'-'.::SONG_GENRE.'s%s',
	],
	_"Flag",
	[	_"%f is set",		::SONG_FLAGS.'f%s',
		_"%f isn't set",	'-'.::SONG_FLAGS.'f%s',
		_"contains %s",		::SONG_FLAGS.'s%s',
		_"doesn't contains %s",	'-'.::SONG_FLAGS.'s%s',
	],
	_"Filename",
	[	_"contains %s",		::SONG_UFILE.'s%s',
		_"doesn't contains %s",	'-'.::SONG_UFILE.'s%s',
		_"is %s",		::SONG_UFILE.'e%s',
		_"is not %s",		'-'.::SONG_UFILE.'e%s',
		_"match regexp %r",	::SONG_UFILE.'m%r',
		_"doesn't match regexp %r",'-'.::SONG_UFILE.'m%r',
	],
	_"Folder",
	[	_"contains %s",		::SONG_UPATH.'s%s',
		_"doesn't contains %s",	'-'.::SONG_UPATH.'s%s',
		_"is %s",		::SONG_UPATH.'e%s',
		_"is not %s",		'-'.::SONG_UPATH.'e%s',
		_"is in %s",		::SONG_UPATH.'i%s',
		_"is not in %s",	'-'.::SONG_UPATH.'i%s',
		_"match regexp %r",	::SONG_UPATH.'m%r',
		_"doesn't match regexp %r",'-'.::SONG_UPATH.'m%r',
	],
	_"Comment",
	[	_"contains %s",		::SONG_COMMENT.'s%s',
		_"doesn't contains %s",	'-'.::SONG_COMMENT.'s%s',
		_"is %s",		::SONG_COMMENT.'e%s',
		_"is not %s",		'-'.::SONG_COMMENT.'e%s',
		_"match regexp %r",	::SONG_COMMENT.'m%r',
		_"doesn't match regexp %r",'-'.::SONG_COMMENT.'m%r',
	],
	_"Version",
	[	_"contains %s",		::SONG_VERSION.'s%s',
		_"doesn't contains %s",	'-'.::SONG_VERSION.'s%s',
		_"is %s",		::SONG_VERSION.'e%s',
		_"is not %s",		'-'.::SONG_VERSION.'e%s',
		_"match regexp %r",	::SONG_VERSION.'m%r',
		_"doesn't match regexp %r",'-'.::SONG_VERSION.'m%r',
	],
	_"is in list %l",	'l%l',
	_"is not in list %l",	'-l%l',
	_"file format",
	[	_"is",
		[	_"a mp3 file",	::SONG_FORMAT.'m^mp3',
			_"an ogg file",	::SONG_FORMAT.'m^ogg',
			_"a flac file",	::SONG_FORMAT.'m^flac',
			_"a musepack file",::SONG_FORMAT.'m^mpc',
			_"mono",	::SONG_CHANNELS.'e1',
			_"stereo",	::SONG_CHANNELS.'e2',
		],
		_"is not",
		[	_"a mp3 file",	'-'.::SONG_FORMAT.'m^mp3',
			_"an ogg file",	'-'.::SONG_FORMAT.'m^ogg',
			_"a musepack file",'-'.::SONG_FORMAT.'m^mpc',
			_"mono",	'-'.::SONG_CHANNELS.'e1',
			_"stereo",	'-'.::SONG_CHANNELS.'e2',
		],
		_"bitrate",
		[	_"is %n(kbps)",		::SONG_BITRATE.'e%n',
			_"isn't %n(kbps)",	'-'.::SONG_BITRATE.'e%n',
			_"is more than %n(kbps)",::SONG_BITRATE.'>%n',
			_"is less than %n(kbps)",::SONG_BITRATE.'<%n',
		],
		_"sampling rate",
		[	_"is %n(Hz)",		::SONG_SAMPRATE.'e%n',
			_"isn't %n(Hz)",	'-'.::SONG_SAMPRATE.'e%n',
			_"is more than %n(Hz)",	::SONG_SAMPRATE.'>%n',
			_"is less than %n(Hz)",	::SONG_SAMPRATE.'<%n',
			_"is not 44.1kHz",	'-'.::SONG_SAMPRATE.'e44100',
		],
	],
  );
  my @todo=(\@FLIST);
  my @todopos=('');
  while (my $ref=shift @todo)
  {	my $pos=shift @todopos;
	for (my $i=0; $ref->[$i]; $i+=2)
	{ if (ref $ref->[$i+1] eq 'ARRAY')
	  {	push @todo,$ref->[$i+1];
		push @todopos,$pos.$i.' ';
	  }
	  else	#put in %PosRe a regex to find the value(s) and pos
	  {	my $f=$ref->[$i+1];
		my ($colcmd,$val)=$f=~m/^(-?\d*[A-Za-z<>!~])(.*)$/;
		$val=~s/(\W)/\\$1/g;
		$val=~s/\\%([a-z])/'('.$TYPEREGEX{$1}.')'/ge;
		push @{ $PosRe{$colcmd} }, [qr/^$val$/, $pos.$i.' '];
	  }
	}
  }
}

sub new
{	my ($class,$activatesub,$changesub,$pos,@vals)=@_;#$init,$ao)=@_;
	my $self = bless Gtk2::HBox->new, $class;
#	$self->{init}=shift @$init;
#	$self->{ao}=$ao;
#	$self->{more}=$init;
#	my $menu=$self->make_submenu(\@FLIST);
#	Selected_cb($self->{item},$self);

	$self->makemenus;
#	my ($pos,@vals)=filter2posval(shift @$init);
	$self->Set($pos,@vals);
	$self->{activatesub}=$activatesub;
	$self->{changesub}=$changesub;
	return $self;
}

sub addtomainmenu
{	my ($self,$label,$sub)=@_;
	my $item=Gtk2::MenuItem->new($label);
	$self->{menu}->append($item);
	$item->signal_connect(activate => $sub);
}

sub filter2posval
{	my $f=shift;
	my ($colcmd,$val)=$f=~m/^(-?\d*[A-Za-z<>!~])(.*)$/;
	my $aref=$PosRe{$colcmd};
	for my $aref (@{ $PosRe{$colcmd} })
	{	my ($re,$pos)=@$aref;
		if (my @vals=$val=~m/$re/)
		{	return $pos,@vals;
		}
	}
	return undef;	#not found
}

sub posval2filter
{	my ($pos,@vals)=@_;
	my $ref=\@FLIST;
	for my $i (split / /,$pos)
	{	$ref=$ref->[$i+1];
	}
	my $filter=$ref;
	$filter=~s/%[a-z]/shift @vals/ge;
	return $filter;
}

sub posval2desc
{	my ($pos,@vals)=@_;
	my $string;
	my $ref=\@FLIST;
	for my $i (split / /,$pos)
	{	$string.=$ref->[$i].' ';
		$ref=$ref->[$i+1];
	}
	chop $string;
	$string=~s/\[[^]]*\]//g;	#remove what is between []
	$string=~tr/()//d;		#keep what is between ()
	my $desc;
	for my $s (split /(%[a-z])/,$string)
	{	if ($s=~m/^%[a-z]/)
		{	my $v=shift @vals;
			if	($s eq '%a') { $v=~s/([smhdwMy])$/' '.$::DATEUNITS{$1}[1]/e }
			elsif	($s eq '%b') { $v=~s/([bkm])$/    ' '.$::SIZEUNITS{$1}[1]/e }
			$desc.=$v;
		}
		else {$desc.=$s;}
	}
	#$string=~s/%[a-z]/shift @vals/ge;
	return $desc;
}

sub makemenus
{	my $self=shift;
	my %items;
	my @todo=(\@FLIST);
	my @todopos=('');
	while (my $ref=shift @todo)
	{	my $pos=shift @todopos;
		$self->{'menu'.$pos}=my $menu=Gtk2::Menu->new;
		if ($items{$pos}) { $items{$pos}->set_submenu($menu); }
		for (my $i=0; $ref->[$i]; $i+=2)
		{	my $name=$ref->[$i];
			$name=~s/\([^)]*\)//g;	#remove what is between ()
			$name=~s/ ?%[a-z]//g; $name=~s/^ +//;
			$name=~tr/[]//d;	#keep what is between []
			my $item=Gtk2::MenuItem->new($name);
			$item->{'pos'}=$pos.$i.' ';
			$menu->append($item);
			if (ref $ref->[$i+1] eq 'ARRAY')
			{	push @todo,$ref->[$i+1];
				push @todopos,$pos.$i.' ';
				$items{$pos.$i.' '}=$item;
			}
			else
			{	$item->signal_connect(activate => \&Selected_cb,$self);
			}
		}
	}
}

sub Selected_cb
{	my ($item,$self)=@_;
	$self->Set($item->{'pos'});
}

sub Get
{	my $self=shift;
	my @vals;
	push @vals,$_->Get for @{ $self->{entry} };
	return $self->{'pos'},@vals;
}

sub Set
{	my ($self,$pos,@vals)=@_;
	$self->{'pos'}=$pos;
	if ($self->{entry} && !@vals)
	{	for my $entry ( @{$self->{entry}} )
		{ push @vals,$entry->Get };
	}
	$self->{entry}=[];
	$self->remove($_) for $self->get_children;
	my $menu='menu';
	my $ref=\@FLIST;
	for my $i (split / /,$pos)
	{	my $string=$ref->[$i];
		$string=~s/\[[^]]*\]//g;
		$string=~tr/()//d;
		my $first=1;
		for my $s (split /(%[a-z])/,$string)
		{	my $widget;
			if ($s=~m/^%([a-z])$/)
			{	$widget=$ENTRYTYPE{$1}->new(sub {$self->activate},sub {$self->changed},shift @vals);
				push @{ $self->{entry} },$widget;
				#$widget->Set( shift @vals ) if @vals;
			}
			elsif ($first && $s ne '')
			{	$widget=Gtk2::Button->new($s);
				$widget->set_relief('none');
				$widget->signal_connect( button_press_event => \&button_press_cb,$self->{$menu});
		  		$first=undef;
			}
			else
			{	$widget=Gtk2::Label->new($s);
			}
			$self->pack_start($widget,::FALSE,::FALSE, 0);
		}
		$menu.=$i.' ';
		$ref=$ref->[$i+1];
	}
	$self->show_all;
	$self->changed;
}

sub button_press_cb
{	my (undef,$ev,$menu)=@_;
	$::LEvent=$ev;
	$menu->show_all;
	$menu->popup(undef, undef, \&::menupos, undef, $ev->button, $ev->time);
}

sub changed
{	my $self=shift;
	return unless $self->{changesub};
	&{ $self->{changesub} } ( $self->Get );
}
sub activate
{	my $self=shift;
	return unless $self->{activatesub};
	&{ $self->{activatesub} } ( $self->Get );
}


package FilterEntryString;
use strict;
use warnings;
use Gtk2;
use base 'Gtk2::Entry';

sub new
{	my ($class,$activatesub,$changesub,$init) = @_;
	my $self = bless Gtk2::Entry->new, $class;
	$self->set_text($init) if defined $init;
	$self->signal_connect(changed => $changesub) if $changesub;
	$self->signal_connect(activate => $activatesub) if $activatesub;
	return $self;
}
sub Get { $_[0]->get_text; }
sub Set { $_[0]->set_text($_[1]); }

package FilterEntryNumber;
use strict;
use warnings;
use Gtk2;
use base 'Gtk2::SpinButton';

sub new
{	my ($class,$activatesub,$changesub,$init) = @_;
	my $self = bless Gtk2::SpinButton->new( Gtk2::Adjustment->new($init||0, 0, 99999, 1, 10, 1) ,10,0  ), $class;
	$self->set_numeric(::TRUE);
	$self->signal_connect(value_changed => $changesub) if $changesub;
	$self->signal_connect(activate => $activatesub) if $activatesub;
	return $self;
}
sub Get { $_[0]->get_adjustment->get_value; }
sub Set { $_[0]->get_adjustment->set_value($_[1]) if $_[1]=~m/^\d+$/; }

package FilterEntryDate;
use strict;
use warnings;
use Gtk2;
use base 'Gtk2::Button';

sub new
{	my ($class,$activatesub,$changesub,$init) = @_;
	my $self = bless Gtk2::Button->new(_"Choose Date"), $class;
	unless ($init && $init=~m/(\d\d\d\d)-(\d\d?)-(\d\d?)/)
	{	my ($y,$m,$d)=(localtime)[5,4,3]; $y+=1900; $m++;
		$init=sprintf '%04d-%02d-%02d',$y,$m,$d;
	}
	$self->{date}=$init;
	$self->set_label($init);
	$self->signal_connect (clicked => sub
	{	#if ($self->{popup}) { $self->{popup}->destroy; $self->{popup}=undef; return; }
		my $popup=Gtk2::Window->new('popup');
		$popup->set_modal(::TRUE);
		#$self->{popup}=$popup;
		my $cal=Gtk2::Calendar->new;
		if ($self->{date}=~m/(\d\d\d\d)-(\d\d?)-(\d\d?)/)
		{ $cal->select_month($2-1,$1);
		  $cal->select_day($3);
		}
		$cal->signal_connect(day_selected_double_click => sub
			{	my ($y,$m,$d)=$cal->get_date;
				$m++;
				$self->{date}=sprintf '%04d-%02d-%02d',$y,$m,$d;
				$self->set_label($self->{date});
				$popup->destroy;
				$self->{popup}=undef;
				&$changesub if $changesub;
				&$activatesub if $activatesub;
			});
		$popup->add($cal);

		#calculate position
		$cal->show_all;
		my $xmax=$self->get_screen->get_width;
		my $ymax=$self->get_screen->get_height;
		my $w=$popup->size_request->width;
		my $h=$popup->size_request->height;
		my ($x,$y)=$self->window->get_origin;
		my $dy=($self->window->get_geometry)[3];
		$x+=($self->window->get_pointer)[1];
		$x-=$w/2;
		if ($dy+$y+$h > $ymax)  { $y-=$h; $y=0 if $y<0 }
		else			{ $y+=$dy; }
		if	($x<0)		{$x=0}
		elsif	($x+$w>$xmax)	{$x=$xmax-$w}
		#move window to the calculated position
		$popup->move($x,$y);

		$popup->show_all;
	});
	return $self;
}
sub Get { $_[0]->{date} }
sub Set { $_[0]->{date}=$_[1]; $_[0]->set_label($_[1]); }

package FilterEntryDateAgo;
use strict;
use warnings;
use Gtk2;
use base 'Gtk2::HBox';

sub new
{	my ($class,$activatesub,$changesub,$init) = @_;
	my $self = bless Gtk2::HBox->new, $class;
	my ($val,$unit);
	($val,$unit)=($init=~m/^(.*)([smhdwMy])$/) if $init;
	$val=1 unless defined $val;
	$unit='d' unless defined $unit;
	my $spin=Gtk2::SpinButton->new( Gtk2::Adjustment->new($val||0, 0, 99999, 1, 10, 1) ,10,0  );
	$spin->set_numeric(::TRUE);
	my $combo=Gtk2::OptionMenu->new;
	$self->pack_start($spin, ::FALSE,::FALSE, 0);
	$self->pack_start($combo,::FALSE,::FALSE, 0);
	$spin->signal_connect(value_changed => $changesub) if $changesub;
	$spin->signal_connect(activate => $activatesub) if $activatesub;
	$self->{adj}=$spin->get_adjustment;
	my $menuCombo=Gtk2::Menu->new;
	my $h;my $n=0;
	for my $u (sort { $::DATEUNITS{$a}[0] <=> $::DATEUNITS{$b}[0] } keys %::DATEUNITS)
	{	$h=$n if ($unit eq $u);
		my $item=Gtk2::MenuItem->new( $::DATEUNITS{$u}[1] );
		$item->signal_connect(activate => sub
			{	$self->{u}=$_[1];
				&$changesub if $changesub;
			},$u );
		$menuCombo->append($item);
		$n++;
	}
	$combo->set_menu($menuCombo);
	$combo->set_history($h);
	$self->{u}=$unit;
	return $self;
}
sub Get
{	my $self=shift;
	return ($self->{adj}->get_value) . $self->{u};
}
#sub Set { $_[0]->get_adjustment->set_value($_[1]) if $_[1]=~m/^\d+$/; }

package FilterEntrySize;	#FIXME almost identical with FilterEntryDateAgo -> should be merged
use strict;
use warnings;
use Gtk2;
use base 'Gtk2::HBox';

sub new
{	my ($class,$activatesub,$changesub,$init) = @_;
	my $self = bless Gtk2::HBox->new, $class;
	my ($val,$unit);
	($val,$unit)=($init=~m/^(.*)([bkm])$/) if $init;
	$val=1 unless defined $val;
	$unit='m' unless defined $unit;
	my $spin=Gtk2::SpinButton->new( Gtk2::Adjustment->new($val||0, 0, 99999999, 1, 10, 1) ,10,0  );
	$spin->set_numeric(::TRUE);
	my $combo=Gtk2::OptionMenu->new;
	$self->pack_start($spin, ::FALSE,::FALSE, 0);
	$self->pack_start($combo,::FALSE,::FALSE, 0);
	$spin->signal_connect(value_changed => $changesub) if $changesub;
	$spin->signal_connect(activate => $activatesub) if $activatesub;
	$self->{adj}=$spin->get_adjustment;
	my $menuCombo=Gtk2::Menu->new;
	my $h;my $n=0;
	for my $u (sort { $::SIZEUNITS{$a}[0] <=> $::SIZEUNITS{$b}[0] } keys %::SIZEUNITS)
	{	$h=$n if ($unit eq $u);
		my $item=Gtk2::MenuItem->new( $::SIZEUNITS{$u}[1] );
		$item->signal_connect(activate => sub
			{	$self->{u}=$_[1];
				&$changesub if $changesub;
			},$u );
		$menuCombo->append($item);
		$n++;
	}
	$combo->set_menu($menuCombo);
	$combo->set_history($h);
	$self->{u}=$unit;
	return $self;
}
sub Get
{	my $self=shift;
	return ($self->{adj}->get_value) . $self->{u};
}


package FilterEntryLists;
use strict;
use warnings;
use Gtk2;
use base 'Gtk2::OptionMenu';

sub new
{	my ($class,$activatesub,$changesub,$init) = @_;
	my $self = bless Gtk2::OptionMenu->new, $class;
	my $menuCombo=Gtk2::Menu->new;
	my $menusub=sub
	{	$self->{val}=$_[1];
		&$changesub if $changesub;
		&$activatesub if $activatesub;
	};
	my %hash; my $n=0;
	for my $f (sort keys %SavedLists)
	{	$hash{$f}=$n;
		my $item=Gtk2::MenuItem->new($f);
		$item->signal_connect(activate => $menusub,$f );
		$menuCombo->append($item);
		$n++;
	}
	$self->set_menu($menuCombo);
	$self->set_history($hash{$init});
	$self->{hash}=\%hash;
	$self->{val}=$init;
	return $self;
}
sub Get { $_[0]->{val}; }
sub Set
{	my ($self,$val)=@_;
	return unless defined $val && defined $self->{hash}{$val};
	$self->set_history( $self->{hash}{$val} );
	$self->{val}=$val;
}

package FilterEntryFlags;
use strict;
use warnings;
use Gtk2;
use base 'Gtk2::OptionMenu';

sub new
{	my ($class,$activatesub,$changesub,$init) = @_;
	my $self = bless Gtk2::OptionMenu->new, $class;
	my $menuCombo=Gtk2::Menu->new;
	my $menusub=sub
	{	$self->{val}=$_[1];
		&$changesub if $changesub;
		&$activatesub if $activatesub;
	};
	my %hash; my $n=0;
	for my $f (sort @Flags)
	{	$hash{$f}=$n;
		my $item=Gtk2::MenuItem->new($f);
		$item->signal_connect(activate => $menusub,$f );
		$menuCombo->append($item);
		$n++;
	}
	$self->set_menu($menuCombo);
	$init=$Flags[0] unless defined $init && defined $hash{$init};
	$self->set_history($hash{$init});
	$self->{hash}=\%hash;
	$self->{val}=$init;
	return $self;
}
sub Get { $_[0]->{val}; }
sub Set
{	my ($self,$val)=@_;
	return unless defined $val && defined $self->{hash}{$val};
	$self->set_history( $self->{hash}{$val} );
	$self->{val}=$val;
}

package FilterEntryGenres;
use strict;
use warnings;
use Gtk2;
use base 'Gtk2::OptionMenu';

sub new
{	my ($class,$activatesub,$changesub,$init) = @_;
	my $self = bless Gtk2::OptionMenu->new, $class;
	my $menuCombo=Gtk2::Menu->new;
	my $menusub=sub
	{	$self->{val}=$_[1];
		&$changesub if $changesub;
		&$activatesub if $activatesub;
	};
	my $genrelist=::GetGenresList;
	my $found;
	for my $i (0..$#$genrelist)
	{	my $f=$genrelist->[$i];
		my $item=Gtk2::MenuItem->new($f);
		$item->signal_connect(activate => $menusub,$f );
		$menuCombo->append($item);
		$found=$i if defined $init && $init eq $f;
	}
	$self->set_menu($menuCombo);
	$self->set_history($found) if defined $found;
	$self->{val}=$init;
	return $self;
}
sub Get { $_[0]->{val}; }

package Filter;
use strict;
use warnings;

#our %cmd_name;	# names of filter commands
my %GrepSubs;
my %NGrepSubs;

my $CachedString; our $CachedList;

INIT
{
  %GrepSubs=
  ( m => sub	#regex
	{ my ($n,$pat,$inv)=@_;
	  $pat=~s#'#\\'#g;
	  $inv=$inv ? '!' : '=';
	  return '$::Songs[$_]'."[$n]$inv~m'$pat'";
	},
    i => sub	#is in folder or sub-folders
    	{ my ($n,$pat,$inv)=@_;
	  $inv=$inv ? '!' : '=';
	  return '$::Songs[$_]'."[$n]$inv~m/^\Q$pat\E".'(?:\\'.::SLASH.'|$)/';
	},
#  w => sub	# word
#  	{ my ($n,$pat)=@_;
#	  $pat=~s/(\W)/\\$1/g;
#	  return '$::Songs[$_]['.$n.']=~m/\b'.$pat.'\b/';
#	},
  f => sub	# flag is set
  	{ my ($n,$pat,$inv)=@_;
	  $pat=~s/(\W)/\\$1/g;
	  $inv=$inv ? '!' : '=';
	  return '$::Songs[$_]['.$n.']'.$inv.'~m/(?:^|\x00)'.$pat.'(?:$|\x00)/';
	},
  s => sub	# substring
	{ my ($n,$pat,$inv)=@_;
	  $pat=lc$pat; $pat=~s#'#\\'#g;
	  $inv=$inv ? '=' : '!';
	  return "index (lc\$::Songs[\$_][$n],'$pat')$inv=-1";
	},
  e => sub	# equal
	{ my ($n,$pat,$inv)=@_; $pat=~s#'#\\'#g;
	  $inv=$inv ? 'ne' : 'eq';
	  return '$::Songs[$_]['.$n."] $inv '$pat'";
	},
  '~' => sub	    #smart equal
	{ my ($n,$pat,$inv)=@_;
	  $pat=~s/(\W)/\\$1/g;
	  $inv=$inv ? '!' : '=';
	  if ($n==::SONG_TITLE)
	  { #$pat=~s/[e]/[e]/g;	#FIXME, probably too slow anyway
	    #$pat=~s/[a]/[a]/g;
	    #$pat=~s/[o]/[o]/g;
	    $pat=~s/\\'/ ?. ?/g;		#for 's == is ...
	    $pat=~s/\Bing\b/in[g']/g;
	    $pat=~s/\\ is\b/(?:'s|\\ is)/ig;
	    $pat=~s/\\ (?:and|\\&|et)\\ /\\ (?:and|\\&|et)\\ /ig;
	    $pat=~s/\\[-,.]/.?/g;
	    $pat=~s/ ?\\\?/ ?\\?/g;
	    return '' if $pat eq '';
	    $pat='m#(?:^|/) *'.$pat.' *(?:[/\(\[]|$)#i';
	  }
	  elsif ($n==::SONG_ARTIST)
	  { $pat='m/(?:^|& *)'.$pat.'(?: *&|$)/';
	  }
	  else { $pat='m/^'.$pat.'$/i'; }	#FIXME use index() ?
	  return '$::Songs[$_]['.$n.']'.$inv.'~'.$pat;
	},
  '<' => sub
	{ my ($n,$pat,$inv)=@_;
	  $inv=$inv ? '>=' : '<';
	  if ($TagProp[$n][1] eq 'd') { $pat=::ConvertTime($pat); }
	  elsif ($n==::SONG_SIZE && $pat=~m/^(\d+)([bkm])$/) { $pat=$1*$::SIZEUNITS{$2}[0] }
	  return '$::Songs[$_]['.$n.'] '.$inv.' '.$pat;
	},
  '>' => sub
	{ my ($n,$pat,$inv)=@_;
	  $inv=$inv ? '<=' : '>';
	  if ($TagProp[$n][1] eq 'd') { $pat=::ConvertTime($pat); }
	  elsif ($n==::SONG_SIZE && $pat=~m/^(\d+)([bkm])$/) { $pat=$1*$::SIZEUNITS{$2}[0] }
	  return '$::Songs[$_]['.$n.'] '.$inv.' '.$pat;
	},
  b => sub
	{ my ($n,$pat,$inv)=@_;
	  my $inv1=$inv ? '>' : '<=';
	  my $inv2=$inv ? '<' : '>=';
	  if ($TagProp[$n][1] eq 'd') { $pat=::ConvertTime($pat); }
	  elsif ($n==::SONG_SIZE && $pat=~m/^(\d+)([bkm]) (\d+)([bkm])$/) { $pat=$1*$::SIZEUNITS{$2}[0].' '.$3*$::SIZEUNITS{$4}[0] }
	  my ($start,$end)=$pat=~m/(\d+)\D+(\d+)/;
	  return '$::Songs[$_]['.$n.'] '.$inv1.' '.$start.' && $::Songs[$_]['.$n.'] '.$inv2.' '.$end;
	},
  c => sub {$_[1]},
  );
  %NGrepSubs=
  (	t => sub
	     {	my ($n,$pat,$lref,$assign,$inv)=@_;
		$inv=$inv ? "$pat..(($pat>@\$tmp)? $pat : \$#\$tmp)"
			  :    "0..(($pat>@\$tmp)? \$#\$tmp : ".($pat-1).')';
		return "\$tmp=$lref;::SortList(\$tmp,$n);$assign @\$tmp[$inv];";
	     },
	h => sub
	     {	my ($n,$pat,$lref,$assign,$inv)=@_;
		$inv=$inv ? "$pat..(($pat>@\$tmp)? $pat : \$#\$tmp)"
			  :    "0..(($pat>@\$tmp)? \$#\$tmp : ".($pat-1).')';
		return "\$tmp=$lref;::SortList(\$tmp,-$n);$assign @\$tmp[$inv];";
	     },
	a => sub #should probably use 'e' for albums and '~' for artists
	     {	my ($n,$pat,$lref,$assign,$inv)=@_;
		$inv=$inv ? '!' : '';
		my $r;
		if    ($n==::SONG_ARTIST){ $r='$::Artist'; }
		elsif ($n==::SONG_ALBUM) { $r='$::Album'; }
		$pat=~s#'#\\'#g;
		return '$tmp={}; $$tmp{$_}=undef for @{'.$r."{'$pat'}[".::AALIST.']};'
			.$assign.'grep '.$inv.'exists $$tmp{$_},@{'.$lref.'};';
	     },
	l => sub	#is in a saved lists
	     {	my ($n,$pat,$lref,$assign,$inv)=@_;
		$inv=$inv ? '!' : '';
		return '$tmp={}; $$tmp{$_}=undef for @{$::SavedLists'."{'$pat'}};"
			.$assign.'grep '.$inv.'exists $$tmp{$_},@{'.$lref.'};';
	     },
  );
}

#Filter object contains :
# - string :	string notation of the filter
# - sub    :	ref to a sub which takes a ref to an array of IDs as argument and returns a ref to the filtered array
# - greponly :	set to 1 if the sub dosn't need to filter the whole list each times -> ID can be tested individualy
# - fields :	ref to a list of the columns used by the filter

sub new
{	my ($class,$string) = @_;
	my $self=bless {}, $class;
	if	(!defined $string)	  {$string='';}
	elsif	($string=~m/^-?[0-9]+~/) { ($string)=_smart_simplify($string); }
	$self->{string}=$string;
	return $self;
}

sub newadd
{	my ($class,$and,@filters)=@_;
	my $self=bless {}, $class;
	my %sel;

	my ($ao,$re)=$and? ( '&', qr/^\(\&\x1D(.*)\)\x1D$/)
			 : ( '|', qr/^\(\|\x1D(.*)\)\x1D$/);
	my @strings;
	for my $f (@filters)
	{	$f='' unless defined $f;
		my $string=(ref $f)? $f->{string} : $f;
		unless ($string)
		{	next if $and;			# all and ... = ...
			return $self->{string}='';	# all or  ... = all
		}
		if ($string=~s/$re/$1/)			# a & (b & c) => a & b & c
		{	my $d=0; my $str='';
			for (split /\x1D/,$string)
			{	if    (m/^\(/)	{$d++}
				elsif (m/^\)/)	{$d--}
				elsif (!$d)	{push @strings,$_;next}
				$str.=$_."\x1D";
				unless ($d) {push @strings,$str; $str='';}
			}
		}
		else
		{	push @strings,( ($string=~m/^-?[0-9]+~/)
					? _smart_simplify($string,!$and)
					: $string
				      );
		}
	}
	my %exist;
	my $sum=''; my $count=0;
	for my $s (@strings)
	{	$s.="\x1D" unless $s=~m/\x1D$/;
		next if $exist{$s}++;		#remove duplicate filters
		$sum.=$s; $count++;
	}
	$sum="($ao\x1D$sum)\x1D" if $count>1;

	$self->{string}=$sum;
	warn "Filter->newadd=".$self->{string}."\n" if $::debug;
	return $self;
}

sub are_equal #FIXME could try harder
{	my ($f1,$f2)=($_[0],$_[1]);
	$f1=defined $f1 ? ref $f1 ? $f1->{string} : $f1 : '';
	$f2=defined $f2 ? ref $f2 ? $f2->{string} : $f2 : '';
	#$f1=$f1->{string} if ref $f1;
	#$f2=$f2->{string} if ref $f2;
	return $f1 eq $f2;
}

sub _smart_simplify
{	(local $_,my $returnlist)=($_[0],$_[1]);
	return $_ unless s/^(-)?([0-9]+)~//;
	my $inv=$1||''; my $col=$2;
	my @pats;
	if ($col==::SONG_TITLE)	# for medleys (songs separated by '/')
	{   s/(?<=.) *[\(\[].*//;	#remove '(...' unless '(' is at the begining of the string
	    @pats=split / *\/ */,$_;
	}
	elsif ($col==::SONG_ARTIST) # for multiple artists (separated by '&')
	{   @pats=split / *& */,$_;
	}
	else {@pats=($_)}
	if ($returnlist || @pats==1)
	{	return map $inv.$col.'~'.$_ , @pats;
	}
	else
	{	return "(|\x1D".join('',map($inv.$col.'~'.$_."\x1D", @pats)).")\x1D";
	}
}

sub invert
{	my $self=shift;
	$self->{'sub'}=undef;
	warn 'before invert : '.$self->{string} if $::debug;
	my @filter=split /\x1D/,$self->{string};
	for (@filter)
	{	s/^\(\&$/(|/ && next;
		s/^\(\|$/(&/ && next;
		next if $_ eq ')';
		$_='-'.$_ unless s/^-//;
	}
	$self->{string}=join "\x1D",@filter;
	warn 'after invert  : '.$self->{string} if $::debug;
}

sub filter
{	my $self=shift;
	#my $time=times;								#DEBUG
	my $sub=$self->{'sub'} || $self->makesub;
	my $listref=\@Library;
	if ($CachedList && $CachedString eq $self->{string}) {return $CachedList } #else {warn "not cached : ".$self->{string}."\n"}
	my $r=&$sub($listref);
	#$time=times-$time; warn "filter $time s ( ".$self->{string}." )\n" if $debug; 	#DEBUG
	if ($listref eq \@Library) { $CachedString=$self->{string}; $CachedList=$r }
	return $r;
}

sub info
{	my $self=shift;
	$self->makesub unless $self->{'sub'};
	return $self->{greponly},@{ $self->{fields} };
}

sub makesub
{	my $self=shift;
	my $filter=$self->{string};
	warn "makesub filter=$filter\n" if $::debug;
	$self->{fields}=[];
	if ($filter eq '') { return $self->{'sub'}=sub {$_[0]}; }
	#$filter="(\x1D$filter)\x1D" if $filter=~m/^[|&]/;
	my @filter=split /\x1D/,$filter;
	#warn "$_\n" for @filter;

	###########	# optimisation for some special cases
	my @hashes;
	if (@filter>4)
	{ my $d=0; my (@or,@val,@ilist);
	    for my $i (0..$#filter)
	    {	local $_=$filter[$i];
		if    (m/^\(/)	  { $d++; $or[$d]=($_ eq '(|')? 1 : 0; }
		elsif ($_ eq ')')
		{	my $vd=delete $val[$d];
			my $ilist=delete $ilist[$d];
			while (my ($icc,$h)=each %$vd)
			{	next unless (keys %$h)>2;
				my ($inv,$col,$cmd)=$icc=~m/^(-?)(\d+)([ef~])$/;
				next if $cmd eq '~' && $col!=::SONG_ARTIST;
				my $l=$ilist->{$icc};
				my $first=$l->[0]; my $last=$l->[$#$l];
				if ( $last-$first==$#$l
					&& $filter[$first-1] && $filter[$first-1]=~m/^\(/
					&& $filter[$last+1] eq ')'
				   )
				 {push @$l,$first-1,$last+1}
				$filter[$_]=undef for @$l;
				push @hashes,$h;
				my $code;
				if ($cmd eq 'e')
				{ $code=($inv ? '!':'').'exists $hashes['.$#hashes.']{$Songs[$_]['.$col.']}'; }
				elsif ($cmd eq 'f' || $cmd eq '~')
				{ my $sep=($cmd eq '~')? ' ?& ?' : '\x00';
				  $code='do { my $r;exists($hashes['.$#hashes.']{$_}) and $r=1 and last for split /'.$sep.'/,$Songs[$_]['.$col.'];'.$inv.'$r;}';
				}
				$filter[$first]=$col.'c'.$code;
			}
			$d--;
		}
		elsif (($or[$d] && m/^(\d+[ef~])(.*)$/) || (!$or[$d]&& m/^(-\d+[ef~])(.*)$/))
		{ $val[$d]{$1}{$2}=undef; push @{$ilist[$d]{$1}},$i; }
	    }
	    @filter=grep defined,@filter if @hashes; #warn "$_\n" for @filter;
	}
	###########

	my $func;
	if ( ! grep m/^-?\d*[tThHaAlL]/, @filter)
	{	############################### grep filter
		$self->{greponly}=1;
		$func='';
		my $op=' && ';
		my @ops;
		my $first=1;
		for (@filter)
		{  if (m/^\(/)
		   {	$func.=$op unless $first;
			$func.='(';
			push @ops,$op;
			$op=($_ eq '(|')? ' || ' : ' && ';
			$first=1;
		   }
		   elsif ($_ eq ')') { $func.=')'; $op=pop @ops; }
		   else
		   {	my ($inv,$col,$cmd,$pat)=m/^(-?)(\d*)([A-Za-z<>!~])(.*)$/;
			push @{ $self->{fields} },$col unless $col eq '';
			$func.=$op unless $first;
			$func.=&{$GrepSubs{$cmd}}($col,$pat,$inv);
			$first=0;
		   }
		}
		$func='[ grep {'.$func.'} @{$_[0]} ];';
	}
	else
	{	############################### non-grep filter
	  { my $d=0; my $c=0;
	    for (@filter)
	    {	if    (m/^\(/)	  {$d++}
		elsif ($_ eq ')') {$d--}
		elsif ($d==0)	  {$c++}
	    }
	    @filter=('(',@filter,')') if $c;
	  }
	  my $d=0;
	  $func='my @hash; my @list=($_[0]); my $tmp;';
	  my @out=('@{$_[0]}'); my @in; my @outref;
	  my $listref='$_[0]';
	  for my $f (@filter)
	  {	if ($f=~m/^[\(\)]/)
		{	if ($f ne ')') #$f begins with '('
			{  $d++;
			   $func.='@{$list['.$d.']}=@{$list['.($d-1).']};';
			   if ($f eq '(|')
			   {	$func.=		    '$hash['.$d.']={};';
				$out[$d]=    'keys %{$hash['.$d.']}';
				$outref[$d]='[keys %{$hash['.$d.']}]';
				$in[$d]=	    '$hash['.$d.']{$_}=undef for ';
			   }
			   else	# $f eq '(&' or '('
			   {	$outref[$d]='$list['.$d.']';
				$out[$d]= '@{$list['.$d.']}';
				$in[$d]=  '@{$list['.$d.']}=';
			   }
			   $listref='$list['.$d.']';
			}
			else # $f eq ')'
			{	$d--; if ($d<0) { warn "invalid filter\n"; return undef; }
				$func.=($d==0)? 'return '.$outref[1].';'
					      :   $in[$d].$out[$d+1].';';
			}
		}
		else
		{	my ($inv,$col,$cmd,$pat)=$f=~m/^(-?)(\d*)([A-Za-z<>!~])(.*)$/;
			push @{ $self->{fields} },$col unless $col eq '';
			unless ($cmd) { warn "Invalid filter : $col $cmd $pat\n"; next; }
			$func.= (exists $GrepSubs{$cmd})
				? $in[$d].'grep '.&{$GrepSubs{$cmd}}($col,$pat,$inv).',@{'.$listref.'};'
				: &{$NGrepSubs{$cmd}}($col,$pat,$listref,$in[$d],$inv);
		}
	  }
	}
	warn "filter=$filter \$sub=eval sub{ $func }\n" if $::debug;
	my $sub=eval "no warnings; sub {$func}";
	if ($@) { warn "filter error : $@"; $sub=sub {$_[0]}; }; #return empty filter if compilation error
	return $self->{'sub'}=$sub;
}

sub is_empty
{	my $f=$_[0];
	return 1 unless defined $f;
	$f=$f->{string} if ref $f;
	return 1 if $f eq '';
}

sub explain	# return a string describing the filter
{	my $self=shift;
	return $self->{desc} if $self->{desc};
	my $filter=$self->{string};
	return _"All" if $filter eq '';
	my $text=''; my $depth=0;
	for my $f (split /\x1D/,$filter)
	{   if ($f=~m/^\(/)		# '(|' or '(&'
	    {	$text.=' 'x$depth++;
		$text.=($f eq '(|')? _"Any of :\n" : _"All of :\n";
	    }
	    elsif ($f eq ')') { $depth--; }
	    else
	    {   next if $f eq '';
		my ($pos,@vals)=FilterBox::filter2posval($f);
		next unless $pos;
		$text.=' 'x$depth;
		$text.=FilterBox::posval2desc($pos,@vals)."\n";
	    }
	}
	chop $text;	#remove last "\n"
	return $self->{desc}=$text;
}

package Random;
use strict;
use warnings;

use constant
{ SCORE_FIELDS	=> 0, SCORE_DESCR	=> 1, SCORE_UNIT	=> 2,
  SCORE_ROUND	=> 3, SCORE_DEFAULT	=> 4, SCORE_VALUE	=> 5,
};
our %ScoreTypes;


INIT
{
  %ScoreTypes=
 (	f => [::SONG_FLAGS,_"Flag is set", '','%s','.5f',sub {'($ref->['.::SONG_FLAGS.']=~m/\Q'.$_[0].'\E/)? 1 : 0'}],
	g => [::SONG_GENRE,_"Genre is set",'','%s','.5g',sub {'($ref->['.::SONG_GENRE.']=~m/\Q'.$_[0].'\E/)? 1 : 0'}],
	l => [::SONG_LASTPLAY,_"Number of days since last played",_"days",'%.1f','-1l10',
		'do { my $t=(time-( $ref->['.::SONG_LASTPLAY.'] ||0 ))/86400; ($t<0)? 0 : $t}'],
		#'(time-( $ref->['.::SONG_LASTPLAY.'] ||0 ))/86400'
	a => [::SONG_ADDED,_"Number of days since added",_"days",'%.1f','1a50',
		'do { my $t=(time-( $ref->['.::SONG_ADDED.'] ||0 ))/86400; ($t<0)? 0 : $t}'],
	n => [::SONG_NBPLAY,_"Number of times played",_"times",'%d','1n5',
		'($ref->['.::SONG_NBPLAY.'] ||0)'],
	r => [::SONG_RATING,_"Rating",'%%','%d','1r0_.1_.2_.3_.4_.5_.6_.7_.8_.9_1',
		'do {my $v=$ref->['.::SONG_RATING.']; (defined $v && $v ne "")? $v : $Options{DefaultRating} }'
	     ],
 );
}

sub new
{	my ($class,$string)=@_;
	my $self=bless {}, $class;
	$string=~s/^r//;
	$self->{string}=$string;
	return $self;
}

sub fields
{	my $self=shift;
	my %fields;
	for my $s ( split /\x1D/, $self->{string} )
	{	my ($type)=$s=~m/^-?[0-9.]+([a-z])/;
		next unless $type;
		$fields{ $ScoreTypes{$type}[SCORE_FIELDS] }=undef;
	}
	return [keys %fields];
}

sub make
{	my $self=shift;
	return $self->{score} if $self->{score};
	my @scores;
	::setlocale(::LC_NUMERIC, 'C');
	for my $s ( split /\x1D/, $self->{string} )
	{	my ($inverse,$weight,$type,$extra)=$s=~m/^(-?)([0-9.]+)([a-z])(.*)/;
		next unless $type;
		my $score=$ScoreTypes{$type}[SCORE_VALUE];
		if ($type eq 'f' || $type eq 'g')
		{	$score=&$score($extra);
		}
		elsif ($type eq 'r')
		{	my @l=split /,/,$extra;
			next unless @l==11;
			$score='('.$extra.')[int('.$score.'/10)]';
		}
		else
		{	$inverse=!$inverse;
			if (my $halflife=$extra)
			{	my $lambda=log(2)/$halflife;
				$score="exp(-$lambda*$score)";
			}
			else {$score='0';}
		}
		$inverse=($inverse)? '1-':'';
		$score=(1-$weight).'+'.$weight.'*('.$inverse.$score.')';
		push @scores,$score;
	}
	unless (@scores) { @scores=(1); }
	$self->{score}='('.join(')*(',@scores).')';
	::setlocale(::LC_NUMERIC, '');
	return $self->{score}
}

sub MakeRandomList
{	my ($self,$lref)=@_;
	$self->{lref}=$lref;
	if ($self->{AddToList})
	{	${ $self->{Sum} }=0;
		@{ $self->{Slist} }=();
	}
	else
	{	my $Sum=0; my @Score;
		$self->{Sum}=\$Sum;
		$self->{Slist}=\@Score;
		my $score=$self->make;
		my $func='no warnings;sub { for my $ID (@{$_[0]}) { my $ref=$::Songs[$ID]; $Sum+=$Score[$ID]='.$score.';} }';
		$func=eval $func;
		if ($@) { warn "Error in eval '$func' :\n$@"; $Sum+=@{$_[0]}; $Score[$_]=1 for @{$_[0]}; }
		$self->{AddToList}=$func;
		$self->{RmFromList}=sub { for my $ID (@{$_[0]}) { $Sum-=$Score[$ID]; $Score[$ID]=undef; } };
	}
	&{$self->{AddToList}}($lref);
}
sub AddIDs
{	my $self=shift;
	&{ $self->{AddToList} }(\@_);
}
sub RmIDs
{	my $self=shift;
	&{ $self->{RmFromList} }(\@_);
}
sub UpdateIDs
{	my $self=shift;
	&{ $self->{RmFromList} }(\@_);
	&{ $self->{AddToList} }(\@_);
}
sub Draw_old	#FIXME too slow if drawing a LOT of songs
{	my ($self,$nb,$no_list)=@_;
	$no_list||=[];
	my $sum=${ $self->{Sum} };
	my @scores=@{ $self->{Slist} };
	my $lref=$self->{lref};
	unless ($nb)
	{	if (defined $nb) {return ()}
		else { $nb=@$lref; }
	}
	#@$no_list may contains duplicates IDs -> hash,
	#and IDs may not be in @$lref -> check if $scores[$ID] defined to know if in @$lref
	{ my %no;
	  for (grep defined $scores[$_],@$no_list)
	  	{$sum-=$scores[$_];$scores[$_]=0;$no{$_}=undef;}
	  my $nb_no=keys %no;
	  $nb=@$lref-$nb_no if $nb>@$lref-$nb_no;
	}
	my @drawn;
	NEXTDRAW:while ($nb>0)
	{	last unless $sum>0;
		my $r=rand $sum;
		($r-=$scores[$_])>0 or do { $nb--; push @drawn,$_;$sum-=$scores[$_];$scores[$_]=0; next NEXTDRAW; } for @$lref;
#		for my $i (@$lref)
#		{	next if ($r-=$scores[$i])>0;
#			$nb--;
#			push @drawn,$i;
#			$sum-=$scores[$i];
#			$scores[$i]=0;
#			next NEXTDRAW;
#		}
		last;
	}

	if ($nb)	#if still need more -> select at random (no weights) FIXME too complex
	{	my %drawn; $drawn{$_}=undef for @drawn,@$no_list;
		my @undrawn=grep !exists $drawn{$_},@$lref;
		my @rand; push @rand,rand for @undrawn;
		push @drawn,map( $undrawn[$_], (sort { $rand[$a] <=> $rand[$b] } 0..$#undrawn)[0..$nb-1] );
	}
	return @drawn;
}

sub Draw
{	my ($self,$nb,$no_list)=@_;
	$no_list||=[];
	my $sum=${ $self->{Sum} };
	my @scores=@{ $self->{Slist} };
	my $lref=$self->{lref};
	unless ($nb)
	{	if (defined $nb) {return ()}
		else { $nb=@$lref; }
	}
	#@$no_list may contains duplicates IDs -> hash,
	#and IDs may not be in @$lref -> check if $scores[$ID] defined to know if in @$lref
	{ my %no;
	  for (grep defined $scores[$_],@$no_list)
	  	{$sum-=$scores[$_];$scores[$_]=0;$no{$_}=undef;}
	  my $nb_no=keys %no;
	  $nb=@$lref-$nb_no if $nb>@$lref-$nb_no;
	}
	my @drawn;
	my $time=times;
	my (@chunknb,@chunksum);
	if ($nb>1)
	{	my $chunk=0; my $count;
		my $size=int(@$lref/60); $size=15 if $size<15;
		for my $id (@$lref)
		{	$chunksum[$chunk]+=$scores[$id];
			$chunknb[$chunk]++;
			$count||=$size;
			$chunk++ unless --$count;
		}
	}
	else { $chunksum[0]=$sum; $chunknb[0]=@$lref; }
	warn "\@chunknb=@chunknb\n" if $::debug;
	warn "\@chunksum=@chunksum\n" if $::debug;
	NEXTDRAW:while ($nb>0)
	{	last unless $sum>0;
		my $r=rand $sum; my $savedr=$r;
		my $chunk=0;
		my $start=0;
		until ($chunksum[$chunk]>$r)
		{	$start+=$chunknb[$chunk];
			$r-=$chunksum[$chunk++];
			#warn "no more chunks : savedr=$savedr r=$r chunk=$chunk" if $chunk>$#chunksum;
			last NEXTDRAW if $chunk>$#chunksum;#FIXME rounding error
		}
		for my $i ($start..$start+$chunknb[$chunk]-1)
		{	next if ($r-=$scores[$lref->[$i]])>0;
			$nb--;
			my $id=$lref->[$i];
			push @drawn,$id;
			$sum-=$scores[$id];
			$chunksum[$chunk]-=$scores[$id];
			$scores[$id]=0;
			next NEXTDRAW;
		}
		#warn $r; warn $nb;
		last;
	}
	warn "drawing took ".(times-$time)." s\n" if $::debug;

	if ($nb)	#if still need more -> select at random (no weights) FIXME too complex
	{	my %drawn; $drawn{$_}=undef for @drawn,@$no_list;
		my @undrawn=grep !exists $drawn{$_},@$lref;
		my @rand; push @rand,rand for @undrawn;
		push @drawn,map( $undrawn[$_], (sort { $rand[$a] <=> $rand[$b] } 0..$#undrawn)[0..$nb-1] );
	}
	return @drawn;
}

sub MakeTab
{	my ($self,$nbcols)=@_;
	my $score=$self->make;
	my $func='no warnings;my $sum;my @tab; for my $ID (@::ListPlay) { my $ref=$::Songs[$ID]; $sum+=my $s='.$score.'; $tab[int(.5+'.($nbcols-1).'*$s)]++;}; return \@tab,$sum;';
	my ($tab,$sum)=eval $func;
	if ($@)
	{	warn "Error in eval '$func' :\n$@";
		$tab=[(0)x$nbcols]; $sum=@::ListPlay;
	}
	return $tab,$sum;
}

sub CalcScore
{	my ($self,$ID)=@_;
	my $score=$self->make;
	eval 'no warnings; my $ref=$::Songs[$ID]; '.$score;
}

sub MakeExample
{	my ($class,$string,$ID)=@_;
	::setlocale(::LC_NUMERIC, 'C');
	my ($inverse,$weight,$type,$extra)=$string=~m/^(-?)([0-9.]+)([a-z])(.*)/;
	return 'error' unless $type;
	my $round=$ScoreTypes{$type}[SCORE_ROUND];
	my $unit= $ScoreTypes{$type}[SCORE_UNIT];
	my $value=$ScoreTypes{$type}[SCORE_VALUE];
	my $score;
	if ($type eq 'f' || $type eq 'g')
	{	$score=&$value($extra);
		$value="($score)? '"._("true")."' : '"._("false")."'";
	}
	elsif ($type eq 'r')
	{	my @l=split /,/,$extra;
		return 'error' unless @l==11;
		$score='('.$extra.')[int('.$value.'/10)]';
	}
	else
	{	$inverse=!$inverse;
		if (my $halflife=$extra)
		{	my $lambda=log(2)/$halflife;
			$score="exp(-$lambda*$value)";
		}
		else {$score='0';}
	}
	$inverse=($inverse)? '1-':'';
	$score=(1-$weight).'+'.$weight.'*('.$inverse.$score.')';
	::setlocale(::LC_NUMERIC, '');
	my $func='no warnings;my $ref=$::Songs[$ID]; return (('.$value.'),('.$score.'));';
	my ($v,$s)=eval $func;
	return 'error' if $@;
	return sprintf("$round $unit -> %.2f",$v,$s);
}


package AAPicture;
use strict;
use warnings;

my %Cache;
my $watcher;

INIT
{	$watcher={};
	::Watch($watcher,'AAPicture',\&AAPicture_Changed);
}

sub newimg
{	my ($col,$key,$size,$todoref)=@_;
	my $p= pixbuf($col,$key,$size);
	return Gtk2::Image->new_from_pixbuf($p) if $p;

	my $img=Gtk2::Image->new;
	push @$todoref,$img;
	$img->{params}=[$col,$key,$size];
	$img->set_size_request($size,$size);
	return $img;
}
sub setimg
{	my $img=$_[0];
	my $p=pixbuf( @{delete $img->{params}},1 );
	$img->set_from_pixbuf($p);
}

sub pixbuf
{	my ($col,$key,$size,$now)=@_;
	my ($href,$aa)= $col==::SONG_ARTIST ? (\%::Artist,'a') : (\%::Album,'b');
	my $file= $href->{$key}[::AAPIXLIST];
	return undef unless $file;
	$key=$size.$aa.$key;
	if (exists $Cache{$key})
	{	my $p=$Cache{$key};
		$p->{lastuse}=time;
		return $p;
	}
	return 0 unless $now;
	return load($file,$size,$key);
}

sub load
{	my ($file,$s,$key)=@_;
	my $pixbuf=::PixBufFromFile($file,$s);
	return undef unless $pixbuf;
#	my $ratio=$pixbuf->get_width / $pixbuf->get_height;
#	my $ph=my $pw=$s;
#	if    ($ratio>1) {$ph=int($pw/$ratio);}
#	elsif ($ratio<1) {$pw=int($ph*$ratio);}
#	$Cache{$key}=$pixbuf=$pixbuf->scale_simple($pw, $ph, 'bilinear'); #or 'nearest'
	$Cache{$key}=$pixbuf;
	$pixbuf->{lastuse}=time;
	::IdleDo('9_AAPicPurge',undef,\&purge) if keys %Cache >120;
	return $pixbuf;
}

sub purge
{	my $nb= keys(%Cache)-100; #warn "purging $nb cached AApixbufs\n";
	delete $Cache{$_} for (sort {$Cache{$a}->{lastuse} <=> $Cache{$b}->{lastuse}} keys %Cache)[0..$nb];
}

sub AAPicture_Changed
{	my $key=$_[1];
	my $re=qr/^\d+[ab]$key/;
	delete $Cache{$_} for grep m/$re/, keys %Cache;
}

package TextCombo;
use strict;
use warnings;

use base 'Gtk2::ComboBox';

sub new
{	my ($class,$list,$init,$sub) = @_;
	my $self = bless Gtk2::ComboBox->new_text, $class;
	my $names=$list;
	if (ref $list eq 'HASH')
	{	my $h=$list;
		$list=[]; $names=[];
		for my $key (sort {$h->{$a} cmp $h->{$b}} keys %$h)
		{	push @$list,$key;
			push @$names,$h->{$key}
		}
	}
	my $found;
	for my $i (0..$#$list)
	{	$self->append_text( $names->[$i] );
		$found=$i if defined $init && $list->[$i] eq $init;
	}
	$found=0 unless defined $found;
	$self->set_active($found);
	$self->{list}=$list;
	$self->signal_connect( changed => $sub ) if $sub;
	return $self;
}

sub get_value
{	my $self=shift;
	return $self->{list}[ $self->get_active ];
}
