Permalink
Fetching contributors…
Cannot retrieve contributors at this time
8291 lines (7738 sloc) 277 KB
# Copyright (C) 2005-2015 Quentin Sculo <squentin@free.fr>
#
# This file is part of Gmusicbrowser.
# Gmusicbrowser is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3, as
# published by the Free Software Foundation.
use strict;
use warnings;
package Browser;
use constant { TRUE => 1, FALSE => 0, };
our @MenuPlaying=
( { label => _"Follow playing song", code => sub { $_[0]{songlist}->FollowSong if $_[0]{songlist}->{follow}; }, toggleoption => 'songlist/follow' },
{ label => _"Filter on playing Album", code => sub { ::SetFilter($_[0]{songlist}, Songs::MakeFilterFromID('album',$::SongID) ) if defined $::SongID; }},
{ label => _"Filter on playing Artist", code => sub { ::SetFilter($_[0]{songlist}, Songs::MakeFilterFromID('artists',$::SongID) )if defined $::SongID; }},
{ label => _"Filter on playing Song", code => sub { ::SetFilter($_[0]{songlist}, Songs::MakeFilterFromID('title',$::SongID) ) if defined $::SongID; }},
{ label => _"Use the playing filter", code => sub { ::SetFilter($_[0]{songlist}, $::PlayFilter ); }, test => sub {::GetSonglist($_[0]{songlist})->{mode} ne 'playlist'}}, #FIXME if queue use queue, if $ListMode use list
{ label => _"Recent albums", submenu => sub { my $sl=$_[0]{songlist};my @gid= ::uniq( Songs::Map_to_gid('album',$::Recent) ); $#gid=19 if $#gid>19; my $m=::PopupAA('album',nosort=>1,nominor=>1,widget => $_[0]{self}, list=>\@gid, cb=>sub { ::SetFilter($sl, $_[0]{filter}); }); return $m; } },
{ label => _"Recent artists", submenu => sub { my $sl=$_[0]{songlist};my @gid= ::uniq( Songs::Map_to_gid('artist',$::Recent) ); $#gid=19 if $#gid>19; my $m=::PopupAA('artists',nosort=>1,nominor=>1,widget => $_[0]{self}, list=>\@gid, cb=>sub { ::SetFilter($sl, $_[0]{filter}); }); return $m; } },
{ label => _"Recent songs", submenu_use_markup => 1, submenu_ordered_hash => 1, submenu_reverse=>1,
submenu => sub { my @ids=@$::Recent; $#ids=19 if $#ids>19; return [map {$_, ::ReplaceFieldsAndEsc($_, ::__x( _"{song} by {artist}", song => "<b>%S</b>%V", artist => "%a"))} @ids]; },
code => sub { ::SetFilter($_[0]{songlist}, Songs::MakeFilterFromID('title',$_[1]) ); }, },
);
sub makeFilterBox
{ my $box=Gtk2::HBox->new;
my $FilterWdgt=GMB::FilterBox->new
( sub { my $filt=shift; ::SetFilter($box,$filt); },
undef,
'title:si:',
_"Edit filter..." => sub
{ ::EditFilter($box,::GetFilter($box),undef,sub {::SetFilter($box,$_[0]) if defined $_[0]});
});
my $okbutton=::NewIconButton('gtk-apply',undef,sub {$FilterWdgt->activate},'none');
$okbutton->set_tooltip_text(_"apply filter");
$box->pack_start($FilterWdgt, FALSE, FALSE, 0);
$box->pack_start($okbutton, FALSE, FALSE, 0);
return $box;
}
sub makeLockToggle
{ my $opt=$_[0];
my $toggle=Gtk2::ToggleButton->new;
$toggle->set_relief( $opt->{relief} ) if $opt->{relief};
$toggle->add(Gtk2::Image->new_from_stock('gmb-lock','menu'));
#$toggle->set_active(1) if $self->{Filter0};
$toggle->signal_connect( clicked =>sub
{ my $self=$_[0];
return if $self->{busy};
my $f=::GetFilter($self,0);
my $empty=Filter::is_empty($f);
if ($empty) { ::SetFilter($self,::GetFilter($self),0); }
else { ::SetFilter($self,undef,0); }
});
$toggle->signal_connect (button_press_event => sub
{ my ($self,$event)=@_;
return 0 unless $event->button==3;
::SetFilter($self,::GetFilter($self),0);
1;
});
::set_drag($toggle, dest => [::DRAG_FILTER,sub {::SetFilter($_[0],$_[2],0);}]);
::WatchFilter($toggle,$opt->{group},sub
{ my ($self,undef,undef,$group)=@_;
my $filter=$::Filters{$group}[0+1]; #filter for level 0
my $empty=Filter::is_empty($filter);
$self->{busy}=1;
$self->set_active(!$empty);
$self->{busy}=0;
my $desc=($empty? _("No locked filter") : _("Locked on :\n").$filter->explain);
$self->set_tooltip_text($desc);
});
return $toggle;
}
sub make_sort_menu
{ my $selfitem=$_[0];
my $songlist= $selfitem->isa('SongList::Common') ? $selfitem : ::GetSonglist($selfitem);
my $menu= ($selfitem->can('get_submenu') && $selfitem->get_submenu) || Gtk2::Menu->new;
my $menusub=sub { $songlist->Sort($_[1]) };
for my $name (sort keys %{$::Options{SavedSorts}})
{ my $sort=$::Options{SavedSorts}{$name};
my $item = Gtk2::CheckMenuItem->new_with_label($name);
$item->set_draw_as_radio(1);
$item->set_active(1) if $songlist->{sort} eq $sort;
$item->signal_connect (activate => $menusub,$sort );
$menu->append($item);
}
my $itemEditSort=Gtk2::ImageMenuItem->new(_"Custom...");
$itemEditSort->set_image( Gtk2::Image->new_from_stock('gtk-preferences','menu') );
$itemEditSort->signal_connect (activate => sub
{ my $sort=::EditSortOrder($selfitem,$songlist->{sort});
$songlist->Sort($sort) if $sort;
});
$menu->append($itemEditSort);
return $menu;
}
sub fill_history_menu
{ my $selfitem=$_[0];
my $menu= $selfitem->get_submenu || Gtk2::Menu->new;
my $mclicksub=sub { $_[0]{middle}=1 if $_[1]->button == 2; return 0; };
my $menusub=sub
{ my $f=($_[0]{middle})? Filter->newadd(FALSE, ::GetFilter($selfitem,1),$_[1]) : $_[1];
::SetFilter($selfitem,$f);
};
for my $f (@{ $::Options{RecentFilters} })
{ my $item = Gtk2::MenuItem->new_with_label( $f->explain );
$item->signal_connect(activate => $menusub,$f);
$item->signal_connect(button_release_event => $mclicksub,$f);
$menu->append($item);
}
return $menu;
}
package LabelTotal;
use base 'Gtk2::Bin';
our %Modes=
( list => {label=> _"Listed songs", setup => \&list_Set, update=>\&list_Update, delay=> 1000, },
filter => {label=> _"Filter", setup => \&filter_Set, update=>\&filter_Update, delay=> 1500, },
library => {label=> _"Library", setup => \&library_Set, update=>\&library_Update, delay=> 4000, },
selected => {label=> _"Selected songs", setup => \&selected_Set,update=>\&selected_Update, delay=> 500, },
);
our @default_options=
( button =>1, format => 'long', relief=> 'none', mode=> 'list',
);
sub new
{ my ($class,$opt) = @_;
%$opt=( @default_options, %$opt );
my $self;
if ($opt->{button})
{ $self=Gtk2::Button->new;
$self->set_relief($opt->{relief});
}
else { $self=Gtk2::EventBox->new; }
bless $self,$class;
$self->{$_}= $opt->{$_} for qw/size format group noheader/;
$self->add(Gtk2::Label->new);
$self->signal_connect( destroy => \&Remove);
$self->signal_connect( button_press_event => \&button_press_event_cb);
::Watch($self, SongsChanged => \&SongsChanged_cb);
$self->Set_mode($opt->{mode});
return $self;
}
sub Set_mode
{ my ($self,$mode)=@_;
$self->Remove;
$self->{mode}=$mode;
$Modes{ $self->{mode} }{setup}->($self);
$self->QueueUpdateFast;
}
sub Remove
{ my $self=shift;
delete $::ToDo{'9_Total'.$self};
::UnWatchFilter($self,$self->{group});
::UnWatch($self,'Selection_'.$self->{group});
::UnWatch($self,$_) for qw/SongArray SongsAdded SongsHidden SongsRemoved/;
}
sub button_press_event_cb
{ my ($self,$event)=@_;
my $menu=Gtk2::Menu->new;
for my $mode (sort {$Modes{$a}{label} cmp $Modes{$b}{label}} keys %Modes)
{ my $item = Gtk2::CheckMenuItem->new( $Modes{$mode}{label} );
$item->set_draw_as_radio(1);
$item->set_active($mode eq $self->{mode});
$item->signal_connect( activate => sub { $self->Set_mode($mode) } );
$menu->append($item);
}
::PopupMenu($menu);
}
sub QueueUpdateFast
{ my $self=shift;
$self->{needupdate}=2;
::IdleDo('9_Total'.$self, 10, \&Update, $self);
}
sub QueueUpdateSlow
{ my $self=shift;
return if $self->{needupdate};
$self->{needupdate}=1;
my $maxdelay= $Modes{ $self->{mode} }{delay};
::IdleDo('9_Total'.$self, $maxdelay, \&Update, $self);
}
sub Update
{ my $self=shift;
delete $::ToDo{'9_Total'.$self};
my ($text,$array,$tip)= $Modes{ $self->{mode} }{update}->($self);
$text='' if $self->{noheader};
if (!$array) { $tip=$text=_"error"; }
else { $text.= ::CalcListLength($array,$self->{format}); }
my $format= $self->{size} ? '<span size="'.$self->{size}.'">%s</span>' : '%s';
$self->child->set_markup_with_format($format,$text);
$self->set_tooltip_text($tip);
$self->{needupdate}=0;
}
sub SongsChanged_cb
{ my ($self,$IDs,$fields)=@_;
return if $self->{needupdate};
my $needupdate= $fields && (grep $_ eq 'length' || $_ eq 'size', @$fields);
if (!$needupdate && $self->{mode} eq 'filter')
{ my $filter=::GetFilter($self);
$needupdate=$filter->changes_may_affect($IDs,$fields);
}
#if in list mode, could check : return if $IDs && !$songarray->AreIn($IDs)
$self->QueueUpdateSlow if $needupdate;
}
### filter functions
sub filter_Set
{ my $self=shift;
::WatchFilter($self,$self->{group}, \&QueueUpdateFast);
::Watch($self, SongsAdded => \&SongsChanged_cb);
::Watch($self, SongsRemoved => \&SongsChanged_cb);
::Watch($self, SongsHidden => \&SongsChanged_cb);
}
sub filter_Update
{ my $self=shift;
my $filter=::GetFilter($self);
my $array=$filter->filter;
return _("Filter : "), $array, $filter->explain;
}
### list functions
sub list_Set
{ my $self=shift;
::Watch($self, SongArray =>\&list_SongArray_changed);
}
sub list_SongArray_changed
{ my ($self,$array,$action)=@_;
return if $self->{needupdate};
my $array0=::GetSongArray($self) || return;
return unless $array0==$array;
return if grep $action eq $_, qw/mode sort move up down/;
$self->QueueUpdateFast;
}
sub list_Update
{ my $self=shift;
my $array=::GetSongArray($self) || return;
return _("Listed : "), $array, ::__n('%d song','%d songs',scalar@$array);
}
### selected functions
sub selected_Set
{ my $self=shift;
::Watch($self,'Selection_'.$self->{group}, \&QueueUpdateFast);
}
sub selected_Update
{ my $self=shift;
my $songlist=::GetSonglist($self);
return unless $songlist;
my @list=$songlist->GetSelectedIDs;
return _('Selected : '), \@list, ::__n('%d song selected','%d songs selected',scalar@list);
}
### library functions
sub library_Set
{ my $self=shift;
::Watch($self, SongsAdded =>\&QueueUpdateSlow);
::Watch($self, SongsRemoved =>\&QueueUpdateSlow);
::Watch($self, SongsHidden =>\&QueueUpdateSlow);
}
sub library_Update
{ my $tip= ::__n('%d song in the library','%d songs in the library',scalar@$::Library);
return _('Library : '), $::Library, $tip;
}
package EditListButtons;
use Glib qw(TRUE FALSE);
use base 'Gtk2::Box';
sub new
{ my ($class,$opt)=@_;
my $self= ($opt->{orientation}||'') eq 'vertical' ? Gtk2::VBox->new : Gtk2::HBox->new;
bless $self, $class;
$self->{group}=$opt->{group};
$self->{bshuffle}=::NewIconButton('gmb-shuffle',($opt->{small} ? '' : _"Shuffle"),sub {::GetSongArray($self)->Shuffle});
$self->{brm}= ::NewIconButton('gtk-remove', ($opt->{small} ? '' : _"Remove"),sub {::GetSonglist($self)->RemoveSelected});
$self->{bclear}=::NewIconButton('gtk-clear', ($opt->{small} ? '' : _"Clear"),sub {::GetSonglist($self)->Empty} );
$self->{bup}= ::NewIconButton('gtk-go-up', undef, sub {::GetSonglist($self)->MoveUpDown(1)});
$self->{bdown}= ::NewIconButton('gtk-go-down', undef, sub {::GetSonglist($self)->MoveUpDown(0)});
$self->{btop}= ::NewIconButton('gtk-goto-top', undef, sub {::GetSonglist($self)->MoveUpDown(1,1)});
$self->{bbot}= ::NewIconButton('gtk-goto-bottom', undef, sub {::GetSonglist($self)->MoveUpDown(0,1)});
$self->{brm}->set_tooltip_text(_"Remove selected songs");
$self->{bclear}->set_tooltip_text(_"Remove all songs");
if (my $r=$opt->{relief}) { $self->{$_}->set_relief($r) for qw/brm bclear bup bdown btop bbot bshuffle/; }
$self->pack_start($self->{$_},FALSE,FALSE,2) for qw/btop bup bdown bbot brm bclear bshuffle/;
::Watch($self,'Selection_'.$self->{group}, \&SelectionChanged);
::Watch($self,SongArray=> \&ListChanged);
$self->{PostInit}= sub { $self->SelectionChanged; $self->ListChanged; };
return $self;
}
sub ListChanged
{ my ($self,$array)=@_;
my $songlist=::GetSonglist($self);
my $watchedarray= $songlist && $songlist->{array};
return if !$watchedarray || ($array && $watchedarray!=$array);
$self->{bclear}->set_sensitive(@$watchedarray>0);
$self->{bshuffle}->set_sensitive(@$watchedarray>1);
$self->set_sensitive( !$songlist->{autoupdate} );
$self->set_visible( !$songlist->{autoupdate} );
}
sub SelectionChanged
{ my ($self)=@_;
my $rows;
my $songlist=::GetSonglist($self);
if ($songlist)
{ $rows=$songlist->GetSelectedRows;
}
if ($rows && @$rows)
{ $self->{brm}->set_sensitive(1);
my $i=0;
$i++ while $i<@$rows && $rows->[$i]==$i;
$self->{$_}->set_sensitive($i!=@$rows) for qw/btop bup/;
$i=$#$rows;
my $array=$songlist->{array};
$i-- while $i>-1 && $rows->[$i]==$#$array-$#$rows+$i;
$self->{$_}->set_sensitive($i!=-1) for qw/bbot bdown/;
}
else
{ $self->{$_}->set_sensitive(0) for qw/btop bbot brm bup bdown/;
}
}
package QueueActions;
use Glib qw(TRUE FALSE);
use base 'Gtk2::Box';
sub new
{ my $class=$_[0];
my $self=bless Gtk2::HBox->new, $class;
my $action_store=Gtk2::ListStore->new(('Glib::String')x3);
$self->{queuecombo}=
my $combo=Gtk2::ComboBox->new($action_store);
my $renderer=Gtk2::CellRendererPixbuf->new;
$combo->pack_start($renderer,FALSE);
$combo->add_attribute($renderer, stock_id => 0);
$renderer=Gtk2::CellRendererText->new;
$combo->pack_start($renderer,TRUE);
$combo->add_attribute($renderer, text => 1);
$combo->signal_connect(changed => sub
{ return if $self->{busy};
my $iter=$_[0]->get_active_iter;
my $action=$_[0]->get_model->get_value($iter,2);
::EnqueueAction($action);
});
$self->{eventcombo}=Gtk2::EventBox->new;
$self->{eventcombo}->add($combo);
$self->{spin}=::NewPrefSpinButton('MaxAutoFill', 1,50, step=>1, page=>5, cb=>sub
{ return if $self->{busy};
::HasChanged('QueueAction','maxautofill');
});
$self->{spin}->set_no_show_all(1);
$self->pack_start($self->{$_},FALSE,FALSE,2) for qw/eventcombo spin/;
::Watch($self, QueueAction => \&Update);
::Watch($self, QueueActionList => \&Fill);
$self->Fill;
return $self;
}
sub Fill
{ my $self=shift;
my $store= $self->{queuecombo}->get_model;
$self->{busy}=1;
$store->clear;
delete $self->{actionindex};
my $i=0;
for my $action (::List_QueueActions(0))
{ $store->set($store->append, 0,$::QActions{$action}{icon}, 1,$::QActions{$action}{short} ,2, $action );
$self->{actionindex}{$action}=$i++;
}
$self->Update;
}
sub Update
{ my $self=$_[0];
$self->{busy}=1;
my $action=$::QueueAction;
$self->{queuecombo}->set_active( $self->{actionindex}{$action} );
$self->{eventcombo}->set_tooltip_text( $::QActions{$action}{long} );
$self->{spin}->set_visible($::QActions{$action}{autofill});
$self->{spin}->set_value($::Options{MaxAutoFill});
delete $self->{busy};
}
package SongList::Common; #common functions for SongList and SongTree
our %Register;
our $EditList; #list that will be used in 'editlist' mode, used only for editing a list in a separate window
our @DefaultOptions=
( 'sort' => 'path album:i disc track file',
hideif => '',
colwidth=> '',
autoupdate=>1,
);
our %Markup_Empty=
( Q => _"Queue empty",
L => _"List empty",
A => _"Playlist empty",
B => _"No songs found",
S => _"No songs found",
);
sub new
{ my $opt=$_[1];
my $package= $opt->{songtree} ? 'SongTree' : $opt->{songlist} ? 'SongList' : 'SongList';
$package->new($opt);
}
sub CommonInit
{ my ($self,$opt)=@_;
%$opt=( @DefaultOptions, %$opt );
$self->{$_}=$opt->{$_} for qw/mode group follow sort hideif hidewidget shrinkonhide markup_empty markup_library_empty autoupdate/,grep(m/^activate\d?$/, keys %$opt);
$self->{mode}||='';
my $type= $self->{type}=
$self->{mode} eq 'playlist' ? 'A' :
$self->{mode} eq 'editlist' ? 'L' :
$opt->{type} || 'B';
$self->{mode}='playlist' if $type eq 'A';
#default double-click action :
$self->{activate} ||= $type eq 'L' ? 'playlist' :
$type eq 'Q' ? 'remove_and_play' :
'play';
$self->{activate2}||='queue' unless $type eq 'Q'; #default to 'queue' songs when double middle-click
$self->{markup_empty}= $Markup_Empty{$type} unless defined $self->{markup_empty};
$self->{markup_library_empty}= _"Library empty.\n\nUse the settings dialog to add music."
unless defined $self->{markup_library_empty} or $type=~m/[QL]/;
::WatchFilter($self,$self->{group}, \&SetFilter ) if $type!~m/[QL]/;
$self->{need_init}=1;
$self->signal_connect_after(show => sub
{ my $self=$_[0];
return unless delete $self->{need_init};
if ($self->{type}=~m/[QLA]/)
{ $self->SongArray_changed_cb($self->{array},'replace');
}
else { ::InitFilter($self); }
});
$self->signal_connect_after('map' => sub { $_[0]->FollowSong }) unless $self->{type}=~m/[QL]/;
$self->{colwidth}= { split / +/, $opt->{colwidth} };
my $songarray=$opt->{songarray};
if ($type eq 'A')
{ #$songarray= SongArray->new_copy($::ListPlay);
$self->{array}=$songarray=$::ListPlay;
$self->{sort}= $::RandomMode ? $::Options{Sort_LastOrdered} : $::Options{Sort};
$self->UpdatePlayListFilter;
::Watch($self,Filter=> \&UpdatePlayListFilter);
$self->{follow}=1 if !defined $self->{follow}; #default to follow current song on new playlists
}
elsif ($type eq 'L')
{ if (defined $EditList) { $songarray=$EditList; $EditList=undef; } #special case for editing a list via ::WEditList
unless (defined $songarray && $songarray ne '') #create a new list if none specified
{ $songarray='list000';
$songarray++ while $::Options{SavedLists}{$songarray};
}
}
elsif ($type eq 'Q') { $songarray=$::Queue; }
elsif ($type eq 'B' || $type eq 'S') { $songarray=SongArray::AutoUpdate->new($self->{autoupdate},$self->{sort}); }
if ($songarray && !ref $songarray) #if not a ref, treat it as the name of a saved list
{ ::SaveList($songarray,[]) unless $::Options{SavedLists}{$songarray}; #create new list if doesn't exists
$songarray=$::Options{SavedLists}{$songarray};
}
$self->{follow}=0 if !defined $self->{follow};
delete $self->{autoupdate} unless $songarray && $songarray->isa('SongArray::AutoUpdate');
$self->{array}= $songarray || SongArray->new;
$self->RegisterGroup($self->{group});
$self->{SaveOptions}=\&CommonSave;
}
sub RegisterGroup
{ my ($self,$group)=@_;
$Register{ $group }=$self;
::weaken($Register{ $group }); #or use a destroy cb ?
}
sub UpdatePlayListFilter
{ my $self=shift;
$self->{ignoreSetFilter}=1;
::SetFilter($self,$::PlayFilter,0);
$self->{ignoreSetFilter}=0;
}
sub CommonSave
{ my $self=shift;
my $opt= $self->SaveOptions;
$opt->{$_}= $self->{$_} for qw/sort rowtip/;
$opt->{autoupdate}=$self->{autoupdate} if exists $self->{autoupdate};
$opt->{follow}= ! !$self->{follow};
#save options as default for new SongTree/SongList of same type
my $name= $self->isa('SongTree') ? 'songtree_' : 'songlist_';
$name= $name.$self->{name}; $name=~s/\d+$//;
$::Options{"DefaultOptions_$name"}={%$opt};
if ($self->{type} eq 'L' && defined(my $n= $self->{array}->GetName)) { $opt->{type}='L'; $opt->{songarray}=$n; }
return $opt;
}
sub Sort
{ my ($self,$sort)=@_;
$self->{array}->Sort($sort);
}
sub SetFilter
{ my ($self,$filter)=@_;# ::red($self->{type},' ',($self->{filter} || 'no'), ' ',$filter);::callstack();
if ($self->{hideif} eq 'nofilter')
{ $self->Hide($filter->is_empty);
return if $filter->is_empty;
}
$self->{filter}=$filter;
return if $self->{ignoreSetFilter};
$self->{array}->SetSortAndFilter($self->{sort},$filter);
}
sub Empty
{ my $self=shift;
$self->{array}->Replace;
}
sub GetSelectedIDs
{ my $self=shift;
my $rows=$self->GetSelectedRows;
my $array=$self->{array};
return map $array->[$_], @$rows;
}
sub PlaySelected ##
{ my $self=$_[0];
my @IDs=$self->GetSelectedIDs;
::Select(song=>'first',play=>1,staticlist => \@IDs ) if @IDs;
}
sub EnqueueSelected##
{ my $self=$_[0];
my @IDs=$self->GetSelectedIDs;
::Enqueue(@IDs) if @IDs;
}
sub RemoveSelected
{ my $self=shift;
return if $self->{autoupdate}; #can't remove selection from an always-filtered list
my $songarray=$self->{array};
$songarray->Remove($self->GetSelectedRows);
}
sub PopupContextMenu
{ my $self=shift;
#return unless @{$self->{array}}; #no context menu for empty lists
my @IDs=$self->GetSelectedIDs;
my %args=(self => $self, mode => $self->{type}, IDs => \@IDs, listIDs => $self->{array});
$args{allowremove}=1 unless $self->{autoupdate};
::PopupContextMenu(\@::SongCMenu,\%args);
}
sub MoveUpDown
{ my ($self,$up,$max)=@_;
my $songarray=$self->{array};
my $rows=$self->GetSelectedRows;
if ($max)
{ if ($up){ $songarray->Top($rows); }
else { $songarray->Bottom($rows); }
$self->Scroll_to_TopEnd(!$up);
}
else
{ if ($up){ $songarray->Up($rows) }
else { $songarray->Down($rows) }
}
}
sub Hide
{ my ($self,$hide)=@_;
my $name=$self->{hidewidget} || $self->{name};
my $toplevel=::get_layout_widget($self);
unless ($toplevel)
{ $self->{need_hide}=$name if $hide;
return;
}
if ($hide) { $toplevel->Hide($name,$self->{shrinkonhide}) }
else { $toplevel->Show($name,$self->{shrinkonhide}) }
}
sub Activate
{ my ($self,$button)=@_;
my $row= $self->GetCurrentRow;
return unless defined $row;
my $songarray=$self->{array};
my $ID=$songarray->[$row];
my $activate=$self->{'activate'.$button} || $self->{activate};
my $aftercmd;
$aftercmd=$1 if $activate=~s/&(.*)$//;
if ($activate eq 'playlist') { ::Select( staticlist=>[@$songarray], position=>$row, play=>1); }
elsif ($activate eq 'filter_and_play'){ ::Select(filter=>$self->{filter}, song=>$ID, play=>1); }
elsif ($activate eq 'filter_sort_and_play'){ ::Select(sort=>$self->{sort}, filter=>$self->{filter}, song=>$ID, play=>1); }
elsif ($activate eq 'remove_and_play')
{ $songarray->Remove([$row]);
::Select(song=>$ID,play=>1);
}
elsif ($activate eq 'remove') { $songarray->Remove([$row]); }
elsif ($activate eq 'properties') { ::DialogSongProp($ID); }
elsif ($activate eq 'play')
{ if ($self->{type} eq 'A') { ::Select(position=>$row,play=>1); }
else { ::Select(song=>$ID,play=>1); }
}
else { ::DoActionForList($activate,[$ID]); }
::run_command($self,$aftercmd) if $aftercmd;
}
# functions for dynamic titles
sub DynamicTitle
{ my ($self,$format)=@_;
return $format unless $format=~m/%n/;
my $label=Gtk2::Label->new;
$label->{format}=$format;
::weaken( $label->{songarray}=$self->{array} );
::Watch($label,SongArray=> \&UpdateDynamicTitle);
UpdateDynamicTitle($label);
return $label;
}
sub UpdateDynamicTitle
{ my ($label,$array)=@_;
return if $array && $array != $label->{songarray};
my $format=$label->{format};
my $nb= @{ $label->{songarray} };
$format=~s/%(.)/$1 eq 'n' ? $nb : $1/eg;
$label->set_text($format);
}
# functions for SavedLists, ie type=L
sub MakeTitleLabel
{ my $self=shift;
my $name=$self->{array}->GetName;
my $label=Gtk2::Label->new($name);
::weaken( $label->{songlist}=$self );
::Watch($label,SavedLists=> \&UpdateTitleLabel);
return $label;
}
sub UpdateTitleLabel
{ my ($label,$list,$action,$newname)=@_;
return unless $action && $action eq 'renamedto';
my $self=$label->{songlist};
my $old=$label->get_text;
my $new=$self->{array}->GetName;
return if $old eq $new;
$label->set_text($new);
}
sub RenameTitleLabel
{ my ($label,$newname)=@_;
my $self=$label->{songlist};
my $oldname=$self->{array}->GetName;
return if $newname eq '' || exists $::Options{SavedLists}{$newname};
::SaveList($oldname,$self->{array},$newname);
}
sub DeleteList
{ my $self=shift;
my $name=$self->{array}->GetName;
::SaveList($name,undef) if defined $name;
}
sub DrawEmpty
{ my ($self,$window,$window_size,$offset)=@_;
return unless $window;
$offset||=0;
$window_size||=$window;
my $type=$self->{type};
my $markup= scalar @$::Library ? undef : $self->{markup_library_empty};
$markup ||= $self->{markup_empty};
if ($markup)
{ $markup=~s#(?:\\n|<br>)#\n#g;
my ($width,$height)=$window_size->get_size;
my $layout= Gtk2::Pango::Layout->new( $self->create_pango_context );
$width-=2*5;
$layout->set_width( Gtk2::Pango->scale * $width );
$layout->set_wrap('word-char');
$layout->set_alignment('center');
my $style= $self->style;
my $font= $style->font_desc;
$font->set_size( 2 * $font->get_size );
$layout->set_font_description($font);
$layout->set_markup( "\n".$markup );
my $gc=$style->text_aa_gc($self->state);
$window->draw_layout($gc, $offset+5,5, $layout);
}
}
sub SetRowTip
{ my ($self,$tip)=@_;
$tip= "<b><big>%t</big></b>\\nby <b>%a</b>\\nfrom <b>%l</b>" if $tip && $tip eq '1'; #for rowtip=1, deprecated
$self->{rowtip}=$tip||'';
return unless *Gtk2::Widget::set_has_tooltip{CODE}; # since gtk+ 2.12, Gtk2 1.160
$self->set_has_tooltip(!!$tip);
}
sub EditRowTip
{ my $self=shift;
if ($self->{rowtip_edit}) { $self->{rowtip_edit}->force_present; return; }
my $dialog = Gtk2::Dialog->new(_"Edit row tip", $self->get_toplevel,
[qw/destroy-with-parent/],
'gtk-apply' => 'apply',
'gtk-ok' => 'ok',
'gtk-cancel' => 'none',
);
::weaken( $self->{rowtip_edit}=$dialog );
::SetWSize($dialog,'RowTip');
$dialog->set_default_response('ok');
my $combo=Gtk2::ComboBoxEntry->new_text;
my $hist= $::Options{RowTip_history} ||=[ _("Play count").' : $playcount\\n'._("Last played").' : $lastplay',
'<b>$title</b>\\n'._('<i>by</i> %a\\n<i>from</i> %l'),
'$title\\n$album\\n$artist\\n<small>$comment</small>',
'$comment',
];
$combo->append_text($_) for @$hist;
my $entry=$combo->child;
$entry->set_text($self->{rowtip});
$entry->set_activates_default(::TRUE);
my $preview= Label::Preview->new(event => 'CurSong', wrap=>1, entry=>$entry, noescape=>1,
format=>'<small><i>'._("example :")."\n\n</i></small>%s",
preview => sub { defined $::SongID ? ::ReplaceFieldsAndEsc($::SongID,$_[0]) : $_[0]; },
);
$preview->set_alignment(0,.5);
$dialog->vbox->pack_start($_,::FALSE,::FALSE,4) for $combo,$preview;
$dialog->show_all;
$dialog->signal_connect( response => sub
{ my ($dialog,$response)=@_;
my $tip=$entry->get_text;
if ($response eq 'ok' || $response eq 'apply')
{ ::PrefSaveHistory(RowTip_history=>$tip) if $tip;
$self->SetRowTip($tip);
}
$dialog->destroy unless $response eq 'apply';
});
}
package SongList;
use Glib qw(TRUE FALSE);
use Gtk2::Pango; #for PANGO_WEIGHT_BOLD, PANGO_WEIGHT_NORMAL
use base 'Gtk2::ScrolledWindow';
our @ISA;
our %SLC_Prop;
INIT
{ unshift @ISA, 'SongList::Common';
%SLC_Prop=
( #PlaycountBG => #TEST
# { value => sub { Songs::Get($_[2],'playcount') ? 'grey' : '#ffffff'; },
# attrib => 'cell-background', type => 'Glib::String',
# #can't be updated via a event key, so not updated on its own for now, but will be updated if a playcount row is present
# },
# italicrow & boldrow are special 'playrow', can't be updated via a event key, a redraw is made when CurSong changed if $self->{playrow}
italicrow =>
{ value => sub
{ defined $::SongID && $_[2]==$::SongID && (!$_[0]{is_playlist} || !defined $::Position || $::Position==$_[1]) ?
'italic' : 'normal';
},
attrib => 'style', type => 'Gtk2::Pango::Style',
},
boldrow =>
{ value => sub
{ defined $::SongID && $_[2]==$::SongID && (!$_[0]{is_playlist} || !defined $::Position || $::Position==$_[1]) ?
PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL;
},
attrib => 'weight', type => 'Glib::Uint',
},
right_aligned_folder=>
{ menu => _("Folder (right-aligned)"), title => _("Folder"),
value => sub { Songs::Display($_[2],'path'); },
attrib => 'text', type => 'Glib::String', depend => 'path',
sort => 'path', width => 200,
init => { ellipsize=>'start', },
},
titleaa =>
{ menu => _('Title - Artist - Album'), title => _('Song'),
value => sub { ::ReplaceFieldsAndEsc($_[2],"<b>%t</b>%V\n<small><i>%a</i> - %l</small>"); },
attrib => 'markup', type => 'Glib::String', depend => 'title version artist album',
sort => 'title:i', noncomp => 'boldrow', width => 200,
},
playandqueue =>
{ menu => _('Playing and queue icons'), title => '', width => 20,
value => sub { ::Get_PPSQ_Icon($_[2], !(defined $::SongID && $_[2]==$::SongID && (!$_[0]{is_playlist} || !defined $::Position || $::Position==$_[1]))); },
class => 'Gtk2::CellRendererPixbuf', attrib => 'stock-id',
type => 'Glib::String', noncomp => 'boldrow italicrow',
event => 'Playing Queue CurSong',
},
playandqueueandtrack =>
{ menu => _('Play, queue or track'), title => '#', width => 20,
value => sub { my $ID=$_[2]; ::Get_PPSQ_Icon($ID, !(defined $::SongID && $ID==$::SongID && (!$_[0]{is_playlist} || !defined $::Position || $::Position==$_[1])),'text') || Songs::Display($ID,'track'); },
type => 'Glib::String', attrib => 'markup', yalign => '0.5',
event => 'Playing Queue CurSong', sort => 'track',
depend=> 'track',
},
icolabel =>
{ menu => _("Labels' icons"), title => '', value => sub { $_[2] },
class => 'CellRendererIconList',attrib => 'ID', type => 'Glib::Uint',
depend => 'label', sort => 'label:i', noncomp => 'boldrow italicrow',
event => 'Icons', width => 50,
init => {field => 'label'},
},
albumpic =>
{ title => _("Album picture"), width => 100,
value => sub { CellRendererSongsAA::get_value('album',$_[0]{array},$_[1]); },
class => 'CellRendererSongsAA', attrib => 'ref', type => 'Glib::Scalar',
depend => 'album', sort => 'album:i', noncomp => 'boldrow italicrow',
init => {aa => 'album'},
event => 'Picture_album',
},
artistpic =>
{ title => _("Artist picture"),
value => sub { CellRendererSongsAA::get_value('first_artist',$_[0]{array},$_[1]); },
class => 'CellRendererSongsAA', attrib => 'ref', type => 'Glib::Scalar',
depend => 'artist', sort => 'artist:i', noncomp => 'boldrow italicrow',
init => {aa => 'first_artist', markup => '<b>%a</b>'}, event => 'Picture_artist',
},
stars =>
{ title => _("Rating"), menu => _("Rating (picture)"),
value => sub { Songs::Stars( Songs::Get($_[2],'rating'),'rating'); },
class => 'Gtk2::CellRendererPixbuf', attrib => 'pixbuf',
type => 'Gtk2::Gdk::Pixbuf', noncomp => 'boldrow italicrow',
depend => 'rating', sort => 'rating',
},
rownumber=>
{ menu => _("Row number"), title => '#', width => 50,
value => sub { $_[1]+1 },
type => 'Glib::String', attrib => 'text', init => { xalign => 1, },
},
);
%{$SLC_Prop{albumpicinfo}}=%{$SLC_Prop{albumpic}};
$SLC_Prop{albumpicinfo}{title}=_"Album picture & info";
$SLC_Prop{albumpicinfo}{init}={aa => 'album', markup => "<b>%a</b>%Y\n<small>%s <small>%l</small></small>"};
}
our @ColumnMenu=
( { label => _"_Sort by", submenu => sub { Browser::make_sort_menu($_[0]{self}) }, },
{ label => _"_Insert column", submenu => sub
{ my %names=map {my $l=$SLC_Prop{$_}{menu} || $SLC_Prop{$_}{title}; defined $l ? ($_,$l) : ()} keys %SLC_Prop;
delete $names{$_->{colid}} for $_[0]{self}->child->get_columns;
return \%names;
}, submenu_reverse =>1,
code => sub { $_[0]{self}->ToggleColumn($_[1],$_[0]{pos}); }, stockicon => 'gtk-add'
},
{ label => sub { _('_Remove this column').' ('. ($SLC_Prop{$_[0]{pos}}{menu} || $SLC_Prop{$_[0]{pos}}{title}).')' },
code => sub { $_[0]{self}->ToggleColumn($_[0]{pos},$_[0]{pos}); }, stockicon => 'gtk-remove'
},
{ label => _("Edit row tip").'...', code => sub { $_[0]{self}->EditRowTip; },
},
{ label => _"Keep list filtered and sorted", code => sub { $_[0]{self}{array}->SetAutoUpdate( $_[0]{self}{autoupdate} ); },
toggleoption => 'self/autoupdate', mode => 'B',
},
{ label => _"Follow playing song", code => sub { $_[0]{self}->FollowSong if $_[0]{self}{follow}; },
toggleoption => 'self/follow',
},
{ label => _"Go to playing song", code => sub { $_[0]{self}->FollowSong; }, },
);
our @DefaultOptions=
( cols => 'playandqueue title artist album year length track file lastplay playcount rating',
playrow => 'boldrow',
headers => 'on',
no_typeahead => 0,
);
sub init_textcolumns #FIXME support calling it multiple times => remove columns for removed fields, update added columns ?
{
for my $key (Songs::ColumnsKeys())
{ $SLC_Prop{$key}=
{ title => Songs::FieldName($key), value => sub { Songs::Display($_[2],$key)},
type => 'Glib::String', attrib => 'text',
sort => Songs::SortField($key), width => Songs::FieldWidth($key),
depend => join(' ',Songs::Depends($key)),
};
$SLC_Prop{$key}{init}{xalign}=1 if Songs::ColumnAlign($key);
}
}
sub new
{ my ($class,$opt) = @_;
my $self = bless Gtk2::ScrolledWindow->new, $class;
$self->set_shadow_type('etched-in');
$self->set_policy('automatic','automatic');
::set_biscrolling($self);
#use default options for this songlist type
my $name= 'songlist_'.$opt->{name}; $name=~s/\d+$//;
my $default= $::Options{"DefaultOptions_$name"} || {};
%$opt=( @DefaultOptions, %$default, %$opt );
$self->CommonInit($opt);
$self->{$_}=$opt->{$_} for qw/songypad playrow/;
my $store=SongStore->new; $store->{array}=$self->{array}; $store->{size}=@{$self->{array}};
$store->{is_playlist}= $self->{mode} eq 'playlist';
my $tv=Gtk2::TreeView->new($store);
$self->add($tv);
$self->{store}=$store;
::set_drag($tv,
source =>[::DRAG_ID,sub { my $tv=$_[0]; return ::DRAG_ID,$tv->parent->GetSelectedIDs; }],
dest =>[::DRAG_ID,::DRAG_FILE,\&drag_received_cb],
motion => \&drag_motion_cb,
);
$tv->signal_connect(drag_data_delete => sub { $_[0]->signal_stop_emission_by_name('drag_data_delete'); }); #ignored
$tv->set_rules_hint(TRUE);
$tv->set_headers_clickable(TRUE);
$tv->set_headers_visible(FALSE) if $opt->{headers} eq 'off';
$tv->set('fixed-height-mode' => TRUE);
$tv->set_enable_search(!$opt->{no_typeahead});
$tv->set_search_equal_func(\&SongStore::search_equal_func);
$tv->signal_connect(key_release_event => sub
{ my ($tv,$event)=@_;
if (Gtk2::Gdk->keyval_name( $event->keyval ) eq 'Delete')
{ $tv->parent->RemoveSelected;
return 1;
}
return 0;
});
MultiTreeView::init($tv,__PACKAGE__);
$tv->signal_connect(cursor_changed => \&cursor_changed_cb);
$tv->signal_connect(row_activated => \&row_activated_cb);
$tv->get_selection->signal_connect(changed => \&sel_changed_cb);
$tv->get_selection->set_mode('multiple');
$tv->signal_connect(query_tooltip=> \&query_tooltip_cb) if *Gtk2::Widget::set_has_tooltip{CODE}; # requires gtk+ 2.12, Gtk2 1.160
$self->SetRowTip($opt->{rowtip});
# used to draw text when treeview empty
$tv->signal_connect(expose_event=> \&expose_cb);
$tv->get_hadjustment->signal_connect_swapped(changed=> sub { my $tv=shift; $tv->queue_draw unless $tv->get_model->iter_n_children },$tv);
$self->AddColumn($_) for split / +/,$opt->{cols};
$self->AddColumn('title') unless $tv->get_columns; #make sure there is at least one column
::Watch($self, SongArray => \&SongArray_changed_cb);
::Watch($self, SongsChanged => \&SongsChanged_cb);
::Watch($self, CurSongID => \&CurSongChanged);
$self->{DefaultFocus}=$tv;
return $self;
}
sub SaveOptions
{ my $self=shift;
my %opt;
my $tv=$self->child;
#save displayed cols
$opt{cols}=join ' ',(map $_->{colid},$tv->get_columns);
#save their width
my %width;
$width{$_}=$self->{colwidth}{$_} for keys %{$self->{colwidth}};
$width{ $_->{colid} }=$_->get_width for $tv->get_columns;
$opt{colwidth}= join ' ',map "$_ $width{$_}", sort keys %width;
return \%opt;
}
sub AddColumn
{ my ($self,$colid,$pos)=@_;
my $prop=$SLC_Prop{$colid};
unless ($prop) {warn "Ignoring unknown column $colid\n"; return undef}
my $renderer= ( $prop->{class} || 'Gtk2::CellRendererText' )->new;
if (my $init=$prop->{init})
{ $renderer->set(%$init);
}
$renderer->set(ypad => $self->{songypad}) if defined $self->{songypad};
my $colnb=SongStore::get_column_number($colid);
my $attrib=$prop->{attrib};
my @attributes=($prop->{title},$renderer,$attrib,$colnb);
if (my $playrow=$self->{playrow})
{ if (my $noncomp=$prop->{noncomp}) { $playrow=undef if (grep $_ eq $playrow, split / /,$noncomp); }
push @attributes,$SLC_Prop{$playrow}{attrib},SongStore::get_column_number($playrow) if $playrow;
#$playrow='PlaycountBG'; #TEST
#push @attributes,$SLC_Prop{$playrow}{attrib},SongStore::get_column_number($playrow); #TEST
}
my $column = Gtk2::TreeViewColumn->new_with_attributes(@attributes);
#$renderer->set_fixed_height_from_font(1);
$column->{colid}=$colid;
$column->set_sizing('fixed');
$column->set_resizable(TRUE);
$column->set_min_width(0);
$column->set_fixed_width( $self->{colwidth}{$colid} || $prop->{width} || 100 );
$column->set_clickable(TRUE);
$column->set_reorderable(TRUE);
$column->signal_connect(clicked => sub
{ my $self=::find_ancestor($_[0]->get_widget,__PACKAGE__);
my $s=$_[1];
$s='-'.$s if $self->{sort} eq $s;
$self->Sort($s);
},$prop->{sort}) if defined $prop->{sort};
my $tv=$self->child;
if (defined $pos) { $tv->insert_column($column, $pos); }
else { $tv->append_column($column); }
#################################### connect col selection menu to right-click on column
my $label=Gtk2::Label->new($prop->{title});
$column->set_widget($label);
$label->show;
my $button_press_sub=sub
{ my $event=$_[1];
return 0 unless $event->button == 3;
my $self=::find_ancestor($_[0],__PACKAGE__);
$self->SelectColumns($_[2]); # $_[2]=$colid
1;
};
if (my $event=$prop->{event})
{ ::Watch($label,$_,sub { my $self=::find_ancestor($_[0],__PACKAGE__); $self->queue_draw if $self; }) for split / /,$event; # could queue_draw only column
}
my $button=$label->get_ancestor('Gtk2::Button'); #column button
$button->signal_connect(button_press_event => $button_press_sub,$colid) if $button;
return $column;
}
sub UpdateSortIndicator
{ my $self=$_[0];
my $tv=$self->child;
$_->set_sort_indicator(FALSE) for grep $_->get_sort_indicator, $tv->get_columns;
return if $self->{no_sort_indicator};
if ($self->{sort}=~m/^(-)?([^ ]+)$/)
{ my $order=($1)? 'descending' : 'ascending';
my @cols=grep( ($SLC_Prop{$_->{colid}}{sort}||'') eq $2, $tv->get_columns);
for my $col (@cols)
{ $col->set_sort_indicator(TRUE);
$col->set_sort_order($order);
}
}
}
sub SelectColumns
{ my ($self,$pos)=@_;
::PopupContextMenu( \@ColumnMenu, {self=>$self, 'pos' => $pos, mode=>$self->{type}, } );
}
sub ToggleColumn
{ my ($self,$colid,$colpos)=@_;
my $tv=$self->child;
my $position;
my $n=0;
for my $column ($tv->get_columns)
{ if ($column->{colid} eq $colid)
{ $self->{colwidth}{$colid}= $column->get_width;
$tv->remove_column($column);
undef $position;
last;
}
$n++;
$position=$n if $column->{colid} eq $colpos;
}
$self->AddColumn($colid,$position) if defined $position;
$self->AddColumn('title') unless $tv->get_columns; #if removed the last column
$self->{cols_to_watch}=undef; #to force update list of columns to watch
}
sub set_has_tooltip { $_[0]->child->set_has_tooltip($_[1]) }
sub expose_cb
{ my ($tv,$event)=@_;
my $self=$tv->parent;
unless ($tv->get_model->iter_n_children && $event->window != $tv->window)
{ $tv->get_bin_window->clear;
# draw empty text when no songs
$self->DrawEmpty($tv->get_bin_window,$tv->window, $tv->get_hadjustment->value);
}
return 0;
}
sub query_tooltip_cb
{ my ($tv, $x, $y, $keyb, $tooltip)=@_;
return 0 if $keyb;
my ($path, $column)=$tv->get_path_at_pos($tv->convert_widget_to_bin_window_coords($x,$y));
return 0 unless $path;
my ($row)=$path->get_indices;
my $self=::find_ancestor($tv,__PACKAGE__);
my $ID=$self->{array}[$row];
return unless defined $ID;
my $markup= ::ReplaceFieldsAndEsc($ID,$self->{rowtip});
$tooltip->set_markup($markup);
$tv->set_tooltip_row($tooltip,$path);
1;
}
sub GetCurrentRow
{ my $self=shift;
my $tv=$self->child;
my ($path)= $tv->get_cursor;
return unless $path;
my $row=$path->to_string;
return $row;
}
sub GetSelectedRows
{ my $self=shift;
return [map $_->to_string, $self->child->get_selection->get_selected_rows];
}
sub drag_received_cb
{ my ($tv,$type,$dest,@IDs)=@_;
$tv->signal_stop_emission_by_name('drag_data_received'); #override the default 'drag_data_received' handler on GtkTreeView
my $self=$tv->parent;
my $songarray=$self->{array};
my (undef,$path,$pos)=@$dest;
my $row=$path? ($path->get_indices)[0] : scalar@{$self->{array}};
$row++ if $path && $pos && $pos eq 'after';
if ($tv->{drag_is_source})
{ $songarray->Move($row,$self->GetSelectedRows);
return;
}
if ($type==::DRAG_FILE) #convert filenames to IDs
{ @IDs=::FolderToIDs(1,0,map ::decode_url($_), @IDs);
return unless @IDs;
}
$songarray->Insert($row,\@IDs);
}
sub drag_motion_cb
{ my ($tv,$context,$x,$y,$time)=@_;# warn "drag_motion_cb @_";
my $self=$tv->parent;
if ($self->{autoupdate}) { $context->status('default',$time); return } # refuse any drop if autoupdate is on
::drag_checkscrolling($tv,$context,$y);
return if $x<0 || $y<0;
my ($path,$pos)=$tv->get_dest_row_at_pos($x,$y);
if ($path)
{ $pos= ($pos=~m/after$/)? 'after' : 'before';
}
else #cursor is in an empty (no rows) zone #FIXME also happens when above or below treeview
{ my $n=$tv->get_model->iter_n_children;
$path=Gtk2::TreePath->new_from_indices($n-1) if $n; #at the end
$pos='after';
}
$context->{dest}=[$tv,$path,$pos];
$tv->set_drag_dest_row($path,$pos);
$context->status(($tv->{drag_is_source} ? 'move' : 'copy'),$time);
return 1;
}
sub sel_changed_cb
{ my $treesel=$_[0];
my $group=$treesel->get_tree_view->parent->{group};
::IdleDo('1_Changed'.$group,10, \&::HasChanged, 'Selection_'.$group); #delay it, because it can be called A LOT when, for example, removing 10000 selected rows
}
sub cursor_changed_cb
{ my $tv=$_[0];
my ($path)= $tv->get_cursor;
return unless $path;
my $self=$tv->parent;
my $ID=$self->{array}[ $path->to_string ];
::HasChangedSelID($self->{group},$ID);
}
sub row_activated_cb
{ my ($tv,$path,$column)=@_;
my $self=$tv->parent;
$self->Activate(1);
}
sub ResetModel
{ my $self=$_[0];
my $tv=$self->child;
$tv->set_model(undef);
$self->{store}{size}=@{$self->{array}};
$tv->set_model($self->{store});
$self->UpdateSortIndicator;
my $ID=::GetSelID($self);
my $songarray=$self->{array};
if (defined $ID && $songarray->IsIn($ID)) #scroll to last selected ID if in the list
{ my $row= ::first { $songarray->[$_]==$ID } 0..$#$songarray;
$row=Gtk2::TreePath->new($row);
$tv->get_selection->select_path($row);
$tv->scroll_to_cell($row,undef,::TRUE,0,0);
}
else
{ $self->Scroll_to_TopEnd();
$self->FollowSong if $self->{follow};
}
}
sub Scroll_to_TopEnd
{ my ($self,$end)=@_;
my $songarray=$self->{array};
return unless @$songarray;
my $row= $end ? $#$songarray : 0;
$row=Gtk2::TreePath->new($row);
$self->child->scroll_to_cell($row,undef,::TRUE,0,0);
}
sub CurSongChanged
{ my $self=$_[0];
$self->queue_draw if $self->{playrow};
$self->FollowSong if $self->{follow};
}
sub SongsChanged_cb
{ my ($self,$IDs,$fields)=@_;
my $usedfields= $self->{cols_to_watch}||= do
{ my $tv=$self->child;
my %h;
for my $col ($tv->get_columns)
{ if (my $d= $SLC_Prop{ $col->{colid} }{depend})
{ $h{$_}=undef for split / /,$d;
}
}
[keys %h];
};
return unless ::OneInCommon($fields,$usedfields);
if ($IDs)
{ my $changed=$self->{array}->AreIn($IDs);
return unless @$changed;
#call UpdateID(@$changed) ? update individual rows or just redraw everything ?
}
$self->child->queue_draw;
}
sub SongArray_changed_cb
{ my ($self,$array,$action,@extra)=@_;
#if ($self->{mode} eq 'playlist' && $array==$::ListPlay)
#{ $self->{array}->Mirror($array,$action,@extra);
#}
return unless $self->{array}==$array;
warn "SongArray_changed $action,@extra\n" if $::debug;
my $tv=$self->child;
my $store=$tv->get_model;
my $treesel=$tv->get_selection;
my @selected=map $_->to_string, $treesel->get_selected_rows;
my $updateselection;
if ($action eq 'sort')
{ my ($sort,$oldarray)=@extra;
$self->{'sort'}=$sort;
my @order;
$order[ $array->[$_] ]=$_ for reverse 0..$#$array; #reverse so that in case of duplicates ID, $order[$ID] is the first row with this $ID
my @IDs= map $oldarray->[$_], @selected;
@selected= map $order[$_]++, @IDs; # $order->[$ID]++ so that in case of duplicates ID, the next row (with same $ID) are used
$self->ResetModel;
#$self->UpdateSortIndicator; #not needed : already called by $self->ResetModel
$updateselection=1;
}
elsif ($action eq 'update') #should only happen when in filter mode, so no duplicates IDs
{ my $oldarray=$extra[0];
my @selectedID;
$selectedID[$oldarray->[$_]]=1 for @selected;
@selected=grep $selectedID[$array->[$_]], 0..$#$array;
# lie to the model, just tell it that some rows were removed/inserted and refresh
# if it cause a problem, just use $self->ResetModel; instead
my $diff= @$array - @$oldarray;
if ($diff>0) { $store->rowinsert(scalar @$oldarray,$diff); }
elsif ($diff<0) { $store->rowremove([$#$array+1..$#$oldarray]); }
$self->queue_draw;
$updateselection=1;
}
elsif ($action eq 'insert')
{ my ($destrow,$IDs)=@extra;
#$_>=$destrow and $_+=@$IDs for @selected; #not needed as the treemodel will update the selection
$store->rowinsert($destrow,scalar @$IDs);
}
elsif ($action eq 'move')
{ my (undef,$rows,$destrow)=@extra;
my $i= my $j= my $delta=0;
if (@selected)
{ for my $row (0..$selected[-1])
{ if ($row==$destrow+$delta) {$delta-=@$rows}
if ($i<=$#$rows && $row==$rows->[$i]) #row moved
{ if ($selected[$j]==$rows->[$i]) { $selected[$j]=$destrow+$i; $j++; } #row moved and selected
$delta++; $i++;
}
elsif ($row==$selected[$j]) #row selected
{ $selected[$j]-=$delta; $j++; }
}
$updateselection=1;
}
$self->queue_draw;
#$store->rowremove($rows);
#$store->rowinsert($destrow,scalar @$rows);
}
elsif ($action eq 'up')
{ my $rows=$extra[0];
my $i=0;
for my $row (@$rows)
{ $i++ while $i<=$#selected && $selected[$i]<$row-1;
last if $i>$#selected;
if ($selected[$i]==$row-1) { $selected[$i]++ unless $i<=$#selected && $selected[$i+1]==$row;$updateselection=1; }
elsif ($selected[$i]==$row) { $selected[$i]--;$updateselection=1; $i++ }
}
$self->queue_draw;
}
elsif ($action eq 'down')
{ my $rows=$extra[0];
my $i=$#selected;
for my $row (reverse @$rows)
{ $i-- while $i>=0 && $selected[$i]>$row+1;
last if $i<0;
if ($selected[$i]==$row+1) { $selected[$i]-- unless $i>=0 && $selected[$i-1]==$row;$updateselection=1; }
elsif ($selected[$i]==$row) { $selected[$i]++;$updateselection=1; $i-- }
}
$self->queue_draw;
}
elsif ($action eq 'remove')
{ my $rows=$extra[0];
$store->rowremove($rows);
$self->ResetModel if @$array==0; #don't know why, but when the list is not empty and adding/removing columns that result in a different row height; after removing all the rows, and then inserting a row, the row height is reset to the previous height. Doing a reset model when the list is empty solves this.
}
elsif ($action eq 'mode' || $action eq 'proxychange') {return} #the list itself hasn't changed
else #'replace' or unknown action
{ $self->ResetModel; #FIXME if replace : check if a filter is in $extra[0]
#$treesel->unselect_all;
}
$self->SetSelection(\@selected) if $updateselection;
$self->Hide(!scalar @$array) if $self->{hideif} eq 'empty';
}
sub FollowSong
{ my $self=$_[0];
my $tv=$self->child;
#$tv->get_selection->unselect_all;
my $songarray=$self->{array};
return unless defined $::SongID;
my $rowplaying;
if ($self->{mode} eq 'playlist') { $rowplaying=$::Position; } #$::Position may be undef even if song is in list (random mode), in that case fallback to the usual case below
$rowplaying= ::first { $songarray->[$_]==$::SongID } 0..$#$songarray unless defined $rowplaying && $rowplaying>=0;
if (defined $rowplaying)
{ my $path=Gtk2::TreePath->new($rowplaying);
my $visible;
my $win = $tv->get_bin_window;
if ($win) #check if row is visible -> no need to scroll_to_cell
{ #maybe should use gtk_tree_view_get_visible_range (requires gtk 2.8)
my $first=$tv->get_path_at_pos(0,0);
my $last=$tv->get_path_at_pos(0,($win->get_size)[1] - 1);
if ((!$first || $first->to_string < $rowplaying) && (!$last || $rowplaying < $last->to_string))
{
$visible=1;
}
}
$tv->scroll_to_cell($path,undef,TRUE,.5,.5) unless $visible;
$tv->set_cursor($path);
}
elsif (defined $::SongID) #Set the song ID even if the song isn't in the list
{ ::HasChangedSelID($self->{group},$::SongID); }
}
sub SetSelection
{ my ($self,$select)=@_;
my $treesel=$self->child->get_selection;
$treesel->unselect_all;
$treesel->select_path( Gtk2::TreePath->new($_) ) for @$select;
}
#sub UpdateID #DELME ? update individual rows or just redraw everything ?
#{ my $self=$_[0];
# my $array=$self->{array};
# my $store=$self->child->get_model;
# my %updated;
# warn "update ID @_\n" if $::debug;
# $updated{$_}=undef for @_;
# my $row=@$array;
# while ($row-->0) #FIXME maybe only check displayed rows
# { my $ID=$$array[$row];
# next unless exists $updated{$ID};
# $store->rowchanged($row);
# #delete $updated{$ID};
# #last unless (keys %updated);
# }
#}
################################################################################
package SongStore;
use Glib qw(TRUE FALSE);
my (%Columns,@Value,@Type);
use Glib::Object::Subclass
Glib::Object::,
interfaces => [Gtk2::TreeModel::],
;
sub get_column_number
{ my $colid=$_[0];
my $colnb=$Columns{$colid};
unless (defined $colnb)
{ push @Value, $SongList::SLC_Prop{$colid}{value};
push @Type, $SongList::SLC_Prop{$colid}{type};
$colnb= $Columns{$colid}= $#Value;
}
return $colnb;
}
sub INIT_INSTANCE {
my $self = $_[0];
# int to check whether an iter belongs to our model
$self->{stamp} = $self+0;#sprintf '%d', rand (1<<31);
}
#sub FINALIZE_INSTANCE
#{ #my $self = $_[0];
# # free all records and free all memory used by the list
#}
sub GET_FLAGS { [qw/list-only iters-persist/] }
sub GET_N_COLUMNS { $#Value }
sub GET_COLUMN_TYPE { $Type[ $_[1] ]; }
sub GET_ITER
{ my $self=$_[0]; my $path=$_[1];
die "no path" unless $path;
# we do not allow children
# depth 1 = top level; a list only has top level nodes and no children
# my $depth = $path->get_depth;
# die "depth != 1" unless $depth == 1;
my $n=$path->get_indices; #return only one value because it's a list
#warn "GET_ITER $n\n";
return undef if $n >= $self->{size} || $n < 0;
#my $ID = $self->{array}[$n];
#die "no ID" unless defined $ID;
#return iter :
return [ $self->{stamp}, $n, $self->{array} , undef ];
}
sub GET_PATH
{ my ($self, $iter) = @_; #warn "GET_PATH\n";
die "no iter" unless $iter;
my $path = Gtk2::TreePath->new;
$path->append_index ($iter->[1]);
return $path;
}
sub GET_VALUE
{ my $row=$_[1][1]; #warn "GET_VALUE\n";
$Value[$_[2]]( $_[0], $row, $_[1][2][$row] ); #args : self, row, ID
}
sub ITER_NEXT
{ #my ($self, $iter) = @_;
my $self=$_[0];
# return undef unless $_[1];
my $n=$_[1]->[1]; #$iter->[1]
#warn "GET_NEXT $n\n";
return undef unless ++$n < $self->{size};
return [ $self->{stamp}, $n, $self->{array}, undef ];
}
sub ITER_CHILDREN
{ my ($self, $parent) = @_; #warn "GET_CHILDREN\n";
# this is a list, nodes have no children
return undef if $parent;
# parent == NULL is a special case; we need to return the first top-level row
# No rows => no first row
return undef unless $self->{size};
# Set iter to first item in list
return [ $self->{stamp}, 0, $self->{array}, undef ];
}
sub ITER_HAS_CHILD { FALSE }
sub ITER_N_CHILDREN
{ my ($self, $iter) = @_; #warn "ITER_N_CHILDREN\n";
# special case: if iter == NULL, return number of top-level rows
return ( $iter? 0 : $self->{size} );
}
sub ITER_NTH_CHILD
{ #my ($self, $parent, $n) = @_; #warn "ITER_NTH_CHILD\n";
# a list has only top-level rows
return undef if $_[1]; #$parent;
my $self=$_[0]; my $n=$_[2];
# special case: if parent == NULL, set iter to n-th top-level row
return undef if $n >= $self->{size};
return [ $self->{stamp}, $n, $self->{array}, undef ];
}
sub ITER_PARENT { FALSE }
sub search_equal_func
{ #my ($self,$col,$string,$iter)=@_;
my $iter= $_[3]->to_arrayref($_[0]{stamp});
my $ID= $iter->[2][ $iter->[1] ];
my $string=uc $_[2];
#my $r; for (qw/title album artist/) { $r=index uc(Songs::Display($ID,$_)), $string; last if $r==0 } return $r;
index uc(Songs::Display($ID,'title')), $string;
}
sub rowremove
{ my ($self,$rows)=@_;
for my $row (reverse @$rows)
{ $self->row_deleted( Gtk2::TreePath->new($row) );
$self->{size}--;
}
}
sub rowinsert
{ my ($self,$row,$number)=@_;
for (1..$number)
{ $self->{size}++;
$self->row_inserted( Gtk2::TreePath->new($row), $self->get_iter_from_string($row) );
$row++;
}
}
#sub rowchanged #not used anymore
#{ my $self=$_[0]; my $row=$_[1];
# my $iter=$self->get_iter_from_string($row);
# return unless $iter;
# $self->row_changed( $self->get_path($iter), $iter);
#}
package MultiTreeView;
#for common functions needed to support correct multi-rows drag and drop in treeviews
sub init
{ my ($tv,$selfpkg)=@_;
$tv->{selfpkg}=$selfpkg;
$tv->{drag_begin_cb}=\&drag_begin_cb;
$tv->signal_connect(button_press_event=> \&button_press_cb);
$tv->signal_connect(button_release_event=> \&button_release_cb);
}
sub drag_begin_cb
{ my ($tv,$context)=@_;# warn "drag_begin @_";
$tv->{pressed}=undef;
}
sub button_press_cb
{ my ($tv,$event)=@_;
return 0 if $event->window!=$tv->get_bin_window; #ignore click outside the bin_window (for example the column headers)
my $self=::find_ancestor($tv, $tv->{selfpkg} );
my $but=$event->button;
my $sel=$tv->get_selection;
if ($but!=1 && $event->type eq '2button-press')
{ $self->Activate($but);
return 1;
}
my $ctrl_shift= $event->get_state * ['shift-mask', 'control-mask'];
if ($but==1) # do not clear multi-row selection if button press on a selected row (to allow dragging selected rows)
{{ last if $ctrl_shift; #don't interfere with default if control or shift is pressed
last unless $sel->count_selected_rows > 1;
my $path=$tv->get_path_at_pos($event->get_coords);
last unless $path && $sel->path_is_selected($path);
$tv->{pressed}=1;
return 1;
}}
if ($but==3)
{ my $path=$tv->get_path_at_pos($event->get_coords);
if ($path && !$sel->path_is_selected($path))
{ $sel->unselect_all unless $ctrl_shift;
#$sel->select_path($path);
$tv->set_cursor($path);
}
$self->PopupContextMenu;
return 1;
}
return 0; #let the event propagate
}
sub button_release_cb #clear selection and select current row only if the press event was on a selected row and there was no dragging
{ my ($tv,$event)=@_;
return 0 unless $event->button==1 && $tv->{pressed};
$tv->{pressed}=undef;
my $path=$tv->get_path_at_pos($event->get_coords);
return 0 unless $path;
my $sel=$tv->get_selection;
$sel->unselect_all;
$sel->select_path($path);
return 1;
}
package FilterPane;
use base 'Gtk2::Box';
use constant { TRUE => 1, FALSE => 0, };
our %Pages=
( filter => [SavedTree => 'F', 'i', _"Filter" ],
list => [SavedTree => 'L', 'i', _"List" ],
savedtree=>[SavedTree => 'FL', 'i', _"Saved" ],
folder => [FolderList => 'path', 'n', _"Folder" ],
filesys => [Filesystem => '', '',_"Filesystem"],
);
our @MenuMarkupOptions=
( "%a",
"<b>%a</b>%Y\n<small>%s <small>%l</small></small>",
"<b>%a</b>%Y\n<small>%b</small>",
"<b>%a</b>%Y\n<small>%b</small>\n<small>%s <small>%l</small></small>",
"<b>%y %a</b>",
);
my @picsize_menu=
( _("no pictures") => 0,
_("automatic size") => -1,
_("small size") => 16,
_("medium size") => 32,
_("big size") => 64,
);
my @mpicsize_menu=
( _("small size") => 32,
_("medium size") => 64,
_("big size") => 96,
_("huge size") => 128,
);
my @cloudstats_menu=
( _("number of songs") => 'count',
_("rating average") => 'rating:average',
_("play count average") => 'playcount:average',
_("skip count average") => 'skipcount:average',
),
my %sort_menu=
( year => _("year"),
year2=> _("year (highest)"),
alpha=> _("alphabetical"),
songs=> _("number of songs in filter"),
'length'=> _("length of songs"),
);
my %sort_menu_album=
( %sort_menu,
artist => _("artist")
);
my @sort_menu_append=
( {separator=>1},
{ label=> _"reverse order", check=> sub { $_[0]{self}{'sort'}[$_[0]{depth}]=~m/^-/ },
code=> sub { my $self=$_[0]{self}; $self->{'sort'}[$_[0]{depth}]=~s/^(-)?/$1 ? "" : "-"/e; $self->SetOption; }
},
);
our @MenuPageOptions;
my @MenuSubGroup=
( { label => sub {_("Set subgroup").' '.$_[0]{depth}}, submenu => sub { return {0 => _"None",map {$_=>Songs::FieldName($_)} Songs::FilterListFields()}; },
first_key=> "0", submenu_reverse => 1,
code => sub { $_[0]{self}->SetField($_[1],$_[0]{depth}) },
check => sub { $_[0]{self}{field}[$_[0]{depth}] ||0 },
},
{ label => sub {_("Options for subgroup").' '.$_[0]{depth}}, submenu => \@MenuPageOptions,
test => sub { $_[0]{depth} <= $_[0]{self}{depth} },
},
);
@MenuPageOptions=
( { label => _"show pictures", code => sub { my $self=$_[0]{self}; $self->{lpicsize}[$_[0]{depth}]=$_[1]; $self->SetOption; }, mode => 'LS',
submenu => \@picsize_menu, submenu_ordered_hash => 1, check => sub {$_[0]{self}{lpicsize}[$_[0]{depth}]},
test => sub { Songs::FilterListProp($_[0]{subfield},'picture'); }, },
{ label => _"text format", code => sub { my $self=$_[0]{self}; $self->{lmarkup}[$_[0]{depth}]= $_[1]; $self->SetOption; },
submenu => sub{ my $field= $_[0]{self}{type}[ $_[0]{depth} ];
my $gid= Songs::Get_gid($::SongID,$field); $gid=$gid->[0] if ref $gid;
return unless $gid; # option not shown if no current song, FIXME could try to find a song in the library
return [ map { AA::ReplaceFields( $gid,$_,$field,::TRUE ), ($_ eq "%a" ? 0 : $_) } @MenuMarkupOptions ];
}, submenu_ordered_hash => 1, submenu_use_markup => 1,
check => sub { $_[0]{self}{lmarkup}[$_[0]{depth}]}, istrue => 'aa', mode => 'LS', },
{ label => _"text mode", code => sub { $_[0]{self}->SetOption(mmarkup=>$_[1]); },
submenu => [ 0 => _"None", below => _"Below", right => _"Right side", ], submenu_ordered_hash => 1, submenu_reverse => 1,
check => 'self/mmarkup', mode => 'M', },
{ label => _"picture size", code => sub { $_[0]{self}->SetOption(mpicsize=>$_[1]); },
mode => 'M',
submenu => \@mpicsize_menu, submenu_ordered_hash => 1, check => 'self/mpicsize', istrue => 'aa' },
{ label => _"font size depends on", code => sub { $_[0]{self}->SetOption(cloud_stat=>$_[1]); },
mode => 'C',
submenu => \@cloudstats_menu, submenu_ordered_hash => 1, check => 'self/cloud_stat', },
{ label => _"minimum font size", code => sub { $_[0]{self}->SetOption(cloud_min=>$_[1]); },
mode => 'C',
submenu => sub { [2..::min(20,$_[0]{self}{cloud_max}-1)] }, check => 'self/cloud_min', },
{ label => _"maximum font size", code => sub { $_[0]{self}->SetOption(cloud_max=>$_[1]); },
mode => 'C',
submenu => sub { [::max(10,$_[0]{self}{cloud_min}+1)..40] }, check => 'self/cloud_max', },
{ label => _"sort by", code => sub { my $self=$_[0]{self}; $self->{'sort'}[$_[0]{depth}]=$_[1]; $self->SetOption; },
check => sub {$_[0]{self}{sort}[$_[0]{depth}]}, submenu => sub { $_[0]{field} eq 'album' ? \%sort_menu_album : \%sort_menu; },
submenu_reverse => 1, append => \@sort_menu_append,
},
{ label => _"group by",
code => sub { my $self=$_[0]{self}; my $d=$_[0]{depth}; $self->{type}[$d]=$self->{field}[$d].'.'.$_[1]; $self->Fill('rehash'); },
check => sub { my $n=$_[0]{self}{type}[$_[0]{depth}]; $n=~s#^[^.]+\.##; $n },
submenu=>sub { Songs::LookupCode( $_[0]{self}{field}[$_[0]{depth}], 'subtypes_menu' ); }, submenu_reverse => 1,
#test => sub { $FilterList::Field{ $_[0]{self}{field}[$_[0]{depth}] }{types}; },
},
{ repeat => sub { map [\@MenuSubGroup, depth=>$_, mode => 'S', subfield => $_[0]{self}{field}[$_], ], 1..$_[0]{self}{depth}+1; }, mode => 'L',
},
{ label => _"cloud mode", code => sub { my $self=$_[0]{self}; $self->set_mode(($self->{mode} eq 'cloud' ? 'list' : 'cloud'),1); },
check => sub {$_[0]{mode} eq 'C'}, notmode => 'S', },
{ label => _"mosaic mode", code => sub { my $self=$_[0]{self}; $self->set_mode(($self->{mode} eq 'mosaic' ? 'list' : 'mosaic'),1);},
check => sub {$_[0]{mode} eq 'M'}, notmode => 'S',
test => sub { Songs::FilterListProp($_[0]{field},'picture') },
},
{ label => _"show the 'All' row", code => sub { $_[0]{self}->SetOption; }, toggleoption => '!self/noall', mode => 'L',
},
{ label => _"show histogram background",code => sub { $_[0]{self}->SetOption; }, toggleoption => 'self/histogram', mode => 'L',
},
);
our @cMenu=
( { label=> _"Play", code => sub { ::Select(filter=>$_[0]{filter},song=>'first',play=>1); },
isdefined => 'filter', stockicon => 'gtk-media-play', id => 'play'
},
{ label=> _"Append to playlist", code => sub { ::DoActionForList('addplay',$_[0]{filter}->filter); },
isdefined => 'filter', stockicon => 'gtk-add', id => 'addplay',
},
{ label=> _"Enqueue", code => sub { ::EnqueueFilter($_[0]{filter}); },
isdefined => 'filter', stockicon => 'gmb-queue', id => 'enqueue',
},
{ label=> _"Set as primary filter",
code => sub {my $fp=$_[0]{filterpane}; ::SetFilter( $_[0]{self}, $_[0]{filter}, 1, $fp->{group} ); },
test => sub {my $fp=$_[0]{filterpane}; $fp->{nb}>1 && $_[0]{filter};}
},
#songs submenu :
{ label => sub { my $IDs=$_[0]{filter}->filter; ::__n("%d song","%d songs",scalar @$IDs); },
submenu => sub { ::BuildMenuOptional(\@::SongCMenu, { mode => 'F', IDs=>$_[0]{filter}->filter }); },
isdefined => 'filter',
},
{ label=> _"Rename folder", code => sub { ::AskRenameFolder($_[0]{rawpathlist}[0]); }, onlyone => 'rawpathlist', test => sub {!$::CmdLine{ro}}, },
{ label=> _"Open folder", code => sub { ::openfolder( $_[0]{rawpathlist}[0] ); }, onlyone => 'rawpathlist', },
#{ label=> _"move folder", code => sub { ::MoveFolder($_[0]{pathlist}[0]); }, onlyone => 'pathlist', test => sub {!$::CmdLine{ro}}, },
{ label=> _"Scan for new songs", code => sub { ::IdleScan( @{$_[0]{rawpathlist}} ); },
notempty => 'rawpathlist' },
{ label=> _"Check for updated/removed songs", code => sub { ::IdleCheck( @{ $_[0]{filter}->filter } ); },
isdefined => 'filter', stockicon => 'gtk-refresh', istrue => 'pathlist' }, #doesn't really need pathlist, but makes less sense for non-folder pages
{ label=> _"Set Picture", stockicon => 'gmb-picture',
code => sub { my $gid=$_[0]{gidlist}[0]; ::ChooseAAPicture(undef,$_[0]{field},$gid); },
onlyone=> 'gidlist', test => sub { Songs::FilterListProp($_[0]{field},'picture') && $_[0]{gidlist}[0]>0; },
},
{ label => _"Auto-select Pictures", code => sub { ::AutoSelPictures( $_[0]{field}, @{ $_[0]{gidlist} } ); },
onlymany=> 'gidlist', test => sub { $_[0]{field} eq 'album' }, #test => sub { Songs::FilterListProp($_[0]{field},'picture'); },
stockicon => 'gmb-picture',
},
{ label=> _"Set icon", stockicon => 'gmb-picture',
code => sub { my $gid=$_[0]{gidlist}[0]; Songs::ChooseIcon($_[0]{field},$gid); },
onlyone=> 'gidlist', test => sub { Songs::FilterListProp($_[0]{field},'icon') && $_[0]{gidlist}[0]>0; },
},
{ label=> _"Remove label", stockicon => 'gtk-remove',
code => sub { my $gid=$_[0]{gidlist}[0]; ::RemoveLabel($_[0]{field},$gid); },
onlyone=> 'gidlist', test => sub { $_[0]{field} eq 'label' && $_[0]{gidlist}[0] !=0 }, #FIXME make it generic rather than specific to field label ? #FIXME find a better way to check if gid is special than comparing it to 0
},
{ label=> _"Rename label",
code => sub { my $gid=$_[0]{gidlist}[0]; ::RenameLabel($_[0]{field},$gid); },
onlyone=> 'gidlist', test => sub { $_[0]{field} eq 'label' && $_[0]{gidlist}[0] !=0 }, #FIXME make it generic rather than specific to field label ? #FIXME find a better way to check if gid is special than comparing it to 0
},
# { separator=>1 },
# only 1 option for folderview so don't put it in option menu
{ label => _"Simplify tree", code => sub { $_[0]{self}->SetOption(simplify=>$_[1]); },
submenu => [ never=>_"Never", smart=>_"Only whole levels", always=>_"Always" ],
submenu_ordered_hash => 1, submenu_reverse => 1,
check => 'self/simplify', istrue=>'folderview',
},
{ label => _"Options", submenu => \@MenuPageOptions, stock => 'gtk-preferences', isdefined => 'field' },
{ label => _"Show buttons", toggleoption => '!filterpane/hidebb', code => sub { my $fp=$_[0]{filterpane}; $fp->{bottom_buttons}->set_visible(!$fp->{hidebb}); }, },
{ label => _"Show tabs", toggleoption => '!filterpane/hidetabs', code => sub { my $fp=$_[0]{filterpane}; $fp->{notebook}->set_show_tabs( !$fp->{hidetabs} ); }, },
);
our @DefaultOptions=
( pages => 'savedtree|artists|album|genre|date|label|folder|added|lastplay|rating',
nb => 1, # filter level
min => 1, # filter out entries with less than $min songs
hidebb => 0, # hide button box
tabmode => 'text', # text, icon or both
hscrollbar=>1,
);
sub new
{ my ($class,$opt)=@_;
my $self = bless Gtk2::VBox->new(FALSE, 6), $class;
$self->{SaveOptions}=\&SaveOptions;
%$opt=( @DefaultOptions, %$opt );
my @pids=split /\|/, $opt->{pages};
$self->{$_}=$opt->{$_} for qw/nb group min hidetabs tabmode/, grep(m/^activate\d?$/, keys %$opt);
$self->{main_opt}{$_}=$opt->{$_} for qw/group no_typeahead searchbox rules_hint hscrollbar/; #options passed to children
my $nb=$self->{nb};
my $group=$self->{group};
my $spin=Gtk2::SpinButton->new( Gtk2::Adjustment->new($self->{min}, 1, 9999, 1, 10, 0) ,10,0 );
$spin->signal_connect( value_changed => sub { $self->update_children($_[0]->get_value); } );
my $ResetB=::NewIconButton('gtk-clear',undef,sub { ::SetFilter($_[0],undef,$nb,$group); });
$ResetB->set_sensitive(0);
my $InterB=Gtk2::ToggleButton->new;
my $InterBL=Gtk2::Label->new;
$InterBL->set_markup('<b>&amp;</b>'); #bold '&'
$InterB->add($InterBL);
my $InvertB=Gtk2::ToggleButton->new;
my $optB=Gtk2::Button->new;
$InvertB->add(Gtk2::Image->new_from_stock('gmb-invert','menu'));
$optB->add(Gtk2::Image->new_from_stock('gtk-preferences','menu'));
$InvertB->signal_connect( toggled => sub {$self->{invert}=$_[0]->get_active;} );
$InterB->signal_connect( toggled => sub {$self->{inter} =$_[0]->get_active;} );
$optB->signal_connect( button_press_event => \&PopupOpt );
$optB->set_relief('none');
my $hbox = Gtk2::HBox->new (FALSE, 6);
$hbox->pack_start($_, FALSE, FALSE, 0) for $spin, $ResetB, $InvertB, $InterB, $optB;
$ResetB ->set_tooltip_text( ( $nb==1? _"reset primary filter" :
$nb==2? _"reset secondary filter":
::__x(_"reset filter {nb}",nb =>$nb)
) );
$InterB ->set_tooltip_text(_"toggle Intersection mode");
$InvertB->set_tooltip_text(_"toggle Invert mode");
$spin ->set_tooltip_text(_"only show entries with at least n songs"); #FIXME
$optB ->set_tooltip_text(_"options");
my $notebook = Gtk2::Notebook->new;
$notebook->set_scrollable(TRUE);
if (my $tabpos=$opt->{tabpos})
{ ($tabpos,$self->{angle})= $tabpos=~m/^(left|right|top|bottom)?(90|180|270)?/;
$notebook->set_tab_pos($tabpos) if $tabpos;
}
#$notebook->popup_enable;
$self->{hidetabs}= (@pids==1) unless defined $self->{hidetabs};
$notebook->set_show_tabs( !$self->{hidetabs} );
$self->{notebook}=$notebook;
my $setpage;
for my $pid (@pids)
{ my $n=$self->AppendPage($pid,$opt->{'page_'.$pid});
if ($opt->{page} && $opt->{page} eq $pid) { $setpage=$n }
}
$self->AppendPage('album') if $notebook->get_n_pages == 0; # fallback in case no pages has been added
$self->pack_end($hbox, FALSE, FALSE, 0);
$notebook->show_all; #needed to set page in this sub
$hbox->show_all;
$_->set_no_show_all(1) for $hbox,$spin,$InterB,$optB;
$self->{bottom_buttons}=$hbox;
$notebook->signal_connect( button_press_event => \&button_press_event_cb);
$notebook->signal_connect( switch_page => sub
{ my $p=$_[0]->get_nth_page($_[2]);
my $self=::find_ancestor($_[0],__PACKAGE__);
$self->{DefaultFocus}=$p;
my $pid= $self->{page}= $p->{pid};
my $mask= $Pages{$pid} ? $Pages{$pid}[2] :
Songs::FilterListProp($pid,'multi') ? 'oni' : 'on';
$optB->set_visible ( scalar $mask=~m/o/ );
$spin->set_visible ( scalar $mask=~m/n/ );
$InterB->set_visible( scalar $mask=~m/i/ );
});
$self->add($notebook);
$notebook->set_current_page( $setpage||0 );
$self->{hidebb}=$opt->{hidebb};
$hbox->hide if $self->{hidebb};
$self->{resetbutton}=$ResetB;
::Watch($self, Icons => \&icons_changed);
::Watch($self, SongsChanged=> \&SongsChanged_cb);
::Watch($self, SongsAdded => \&SongsAdded_cb);
::Watch($self, SongsRemoved=> \&SongsRemoved_cb);
::Watch($self, SongsHidden => \&SongsRemoved_cb);
$self->signal_connect(destroy => \&cleanup);
$self->{needupdate}=1;
::WatchFilter($self,$opt->{group},\&updatefilter);
::IdleDo('9_FPfull'.$self,100,\&updatefilter,$self);
return $self;
}
sub SaveOptions
{ my $self=shift;
my @opt=
( hidebb => $self->{hidebb},
min => $self->{min},
page => $self->{page},
hidetabs=> $self->{hidetabs},
pages => (join '|', map $_->{pid}, $self->{notebook}->get_children),
);
for my $page (grep $_->can('SaveOptions'), $self->{notebook}->get_children)
{ my %pageopt=$page->SaveOptions;
push @opt, 'page_'.$page->{pid}, { %pageopt } if keys %pageopt;
}
return \@opt;
}
sub AppendPage
{ my ($self,$pid,$opt)=@_;
my ($package,$col,$label);
if ($Pages{$pid})
{ ($package,$col,undef,$label)=@{ $Pages{$pid} };
}
elsif ( grep $_ eq $pid, Songs::FilterListFields() )
{ $package='FilterList';
$col=$pid;
$label=Songs::FieldName($col);
}
else {return}
$opt||={};
my %opt=( %{$self->{main_opt}}, %$opt);
my $page=$package->new($col,\%opt); #create new page
$page->{pid}=$pid;
$page->{page_name}=$label;
if ($package eq 'FilterList' || $package eq 'FolderList')
{ $page->{Depend_on_field}=$col;
}
my $notebook=$self->{notebook};
my $n=$notebook->append_page( $page, $self->create_tab($page) );
$n=$notebook->get_n_pages-1; # $notebook->append_page doesn't returns the page number before Gtk2-Perl 1.080
$notebook->set_tab_reorderable($page,TRUE);
$page->show_all;
return $n;
}
sub create_tab
{ my ($self,$page)=@_;
my $pid=$page->{pid};
my $img;
my $angle= $self->{angle} || 0;
my $label= Gtk2::Label->new( $page->{page_name} );
$label->set_angle($angle) if $angle;
# set base gravity to auto so that rotated tabs handle vertical scripts (asian languages) better
$label->get_pango_context->set_base_gravity('auto');
$label->signal_connect(hierarchy_changed=> sub { $_[0]->get_pango_context->set_base_gravity('auto'); }); # for some reason (gtk bug ?) the setting is reverted when the tab is dragged, so this re-set it
if ($self->{tabmode} ne 'text')
{ my $icon= "gmb-tab-$pid";
$img= Gtk2::Image->new_from_stock($icon,'menu') if Gtk2::IconFactory->lookup_default($icon);
$label=undef if $img && $self->{tabmode} eq 'icon';
}
my $tab;
if ($img && $label)
{ $tab= $angle%180 ? Gtk2::VBox->new(FALSE,0) : Gtk2::HBox->new(FALSE,0);
my @pack= $angle%180 ? ($label,TRUE,$img,FALSE) : ($img,FALSE,$label,TRUE);
$tab->pack_start( $pack[$_], $pack[$_+1],$pack[$_+1],0 ) for 0,2;
}
else { $tab= $img || $label; }
$tab->show_all;
return $tab;
}
sub icons_changed
{ my $self=shift;
if ($self->{tabmode} ne 'text')
{ my $notebook=$self->{notebook};
for my $page ($notebook->get_children)
{ $notebook->set_tab_label( $page, $self->create_tab($page) );
}
}
}
sub RemovePage_cb
{ my $self=$_[1];
my $nb=$self->{notebook};
my $n=$nb->get_current_page;
my $page=$nb->get_nth_page($n);
my $pid=$page->{pid};
my $col;
if ($Pages{$pid}) { $col=$Pages{$pid}[1] if $Pages{$pid}[0] eq 'FolderList'; }
else { $col=$pid; }
$nb->remove_page($n);
}
sub button_press_event_cb
{ my ($nb,$event)=@_;
return 0 if $event->button != 3;
return 0 unless ::IsEventInNotebookTabs($nb,$event); #to make right-click on tab arrows work
my $self=::find_ancestor($nb,__PACKAGE__);
my $menu=Gtk2::Menu->new;
my $cb=sub { $nb->set_current_page($_[1]); };
my %pages;
$pages{$_}= $Pages{$_}[3] for keys %Pages;
$pages{$_}= Songs::FieldName($_) for Songs::FilterListFields;
for my $page ($nb->get_children)
{ my $pid=$page->{pid};
my $name=delete $pages{$pid};
my $item=Gtk2::MenuItem->new_with_label($name);
$item->signal_connect(activate=>$cb,$nb->page_num($page));
$menu->append($item);
}
$menu->append(Gtk2::SeparatorMenuItem->new);
if (keys %pages)
{ my $new=Gtk2::ImageMenuItem->new(_"Add tab");
$new->set_image( Gtk2::Image->new_from_stock('gtk-add','menu') );
my $submenu=Gtk2::Menu->new;
for my $pid (sort {$pages{$a} cmp $pages{$b}} keys %pages)
{ my $item=Gtk2::ImageMenuItem->new_with_label($pages{$pid});
$item->set_image( Gtk2::Image->new_from_stock("gmb-tab-$pid",'menu') );
$item->signal_connect(activate=> sub { my $n=$self->AppendPage($pid); $self->{notebook}->set_current_page($n) });
$submenu->append($item);
}
$menu->append($new);
$new->set_submenu($submenu);
}
if ($nb->get_n_pages>1)
{ my $item=Gtk2::ImageMenuItem->new(_"Remove this tab");
$item->set_image( Gtk2::Image->new_from_stock('gtk-remove','menu') );
$item->signal_connect(activate=> \&RemovePage_cb,$self);
$menu->append($item);
}
#::PopupContextMenu(\@MenuTabbedL, { self=>$self, list=>$listname, pagenb=>$pagenb, page=>$page, pagetype=>$page->{tabbed_page_type} } );
::PopupMenu($menu,event=>$event,nomenupos=>1);
return 1;
}
sub SongsAdded_cb
{ my ($self,$IDs)=@_;
return if $self->{needupdate};
if ( $self->{filter}->added_are_in($IDs) )
{ $self->{needupdate}=1;
::IdleDo('9_FPfull'.$self,5000,\&updatefilter,$self);
}
}
sub SongsChanged_cb
{ my ($self,$IDs,$fields)=@_;
return if $self->{needupdate};
if ( $self->{filter}->changes_may_affect($IDs,$fields) )
{ $self->{needupdate}=1;
::IdleDo('9_FPfull'.$self,5000,\&updatefilter,$self);
}
else
{ for my $page ( $self->get_field_pages )
{ next unless $page->{valid} && $page->{hash};
my @depends= Songs::Depends( $page->{Depend_on_field} );
next unless ::OneInCommon(\@depends,$fields);
$page->{valid}=0;
$page->{hash}=undef;
::IdleDo('9_FP'.$self,1000,\&refresh_current_page,$self) if $page->mapped;
}
}
}
sub SongsRemoved_cb
{ my ($self,$IDs)=@_;
return if $self->{needupdate};
my $list=$self->{list};
my $changed=1;
if ($list!=$::Library) #CHECKME use $::Library or a copy ?
{ my $isin='';
vec($isin,$_,1)=1 for @$IDs;
my $before=@$list;
@$list=grep !vec($isin,$_,1), @$list;
$changed=0 if $before==@$list;
}
$self->invalidate_children if $changed;
}
sub updatefilter
{ my ($self,undef,$nb)=@_;
my $mynb=$self->{nb};
return if $nb && $nb> $mynb;
delete $::ToDo{'9_FPfull'.$self};
my $force=delete $self->{needupdate};
warn "Filtering list for FilterPane$mynb\n" if $::debug;
my $group=$self->{group};
my $currentf=$::Filters{$group}[$mynb+1];
$self->{resetbutton}->set_sensitive( !Filter::is_empty($currentf) );
my $filt=Filter->newadd(TRUE, map($::Filters{$group}[$_+1],0..($mynb-1)) );
return if !$force && $self->{list} && Filter::are_equal($filt,$self->{filter});
$self->{filter}=$filt;
my $lref=$filt->is_empty ? $::Library #CHECKME use $::Library or a copy ?
: $filt->filter;
$self->{list}=$lref;
#warn "filter :".$filt->{string}.($filt->{source}? " with source" : '')." songs=".scalar(@$lref)."\n";
$self->invalidate_children;
}
sub invalidate_children
{ my $self=shift;
for my $page ( $self->get_field_pages )
{ $page->{valid}=0;
$page->{hash}=undef;
}
::IdleDo('9_FP'.$self,1000,\&refresh_current_page,$self);
}
sub update_children
{ my ($self,$min)=@_;
$self->{min}=$min;
if (!$self->{list} || $self->{needupdate}) { $self->updatefilter; return; }
warn "Updating FilterPane".$self->{nb}."\n" if $::debug;
for my $page ( $self->get_field_pages )
{ $page->{valid}=0; # set dirty flag for this page
}
$self->refresh_current_page;
}
sub refresh_current_page
{ my $self=shift;
delete $::ToDo{'9_FP'.$self};
my ($current)=grep $_->mapped, $self->get_field_pages;
if ($current) { $current->Fill } # update now if page is displayed
}
sub get_field_pages
{ grep $_->{Depend_on_field}, $_[0]->{notebook}->get_children;
}
sub cleanup
{ my $self=shift;
delete $::ToDo{'9_FP'.$self};
delete $::ToDo{'9_FPfull'.$self};
}
sub Activate
{ my ($page,$button,$filter)=@_;
my $self=::find_ancestor($page,__PACKAGE__);
$button||=1;
my $action= $self->{"activate$button"} || $self->{activate} || ($button==2 ? 'queue' : 'play');
my $aftercmd;
$aftercmd=$1 if $action=~s/&(.*)$//;
::DoActionForFilter($action,$filter);
::run_command($self,$aftercmd) if $aftercmd;
}
sub PopupContextMenu
{ my ($page,$hash,$menu)=@_;
my $self=::find_ancestor($page,__PACKAGE__);
$hash->{filterpane}=$self;
$menu||=\@cMenu;
::PopupContextMenu($menu, $hash);
}
sub PopupOpt #Only for FilterList #FIXME should be moved in FilterList::, and/or use a common function with FilterList::PopupContextMenu
{ my $self=::find_ancestor($_[0],__PACKAGE__);
my $nb=$self->{notebook};
my $page=$nb->get_nth_page( $nb->get_current_page );
my $field=$page->{field}[0];
my $mainfield=Songs::MainField($field);
my $aa= ($mainfield eq 'artist' || $mainfield eq 'album') ? $mainfield : undef; #FIXME
my $mode= uc(substr $page->{mode},0,1); # C => cloud, M => mosaic, L => list
::PopupContextMenu(\@MenuPageOptions, { self=>$page, aa=>$aa, field => $field, mode => $mode, subfield => $field, depth =>0, usemenupos => 1,} );
return 1;
}
package FilterList;
use base 'Gtk2::Box';
use constant { GID_ALL => 2**31-1, GID_TYPE => 'Glib::Long' };
our %defaults=
( mode => 'list',
type => '',
lmarkup => 0,
lpicsize=> 0,
'sort' => 'default',
depth => 0,
noall => 0,
histogram=>0,
mmarkup => 0,
mpicsize=> 64,
cloud_min=> 5,
cloud_max=> 20,
cloud_stat=> 'count',
);
sub new
{ my ($class,$field,$opt)=@_;
my $self = bless Gtk2::VBox->new, $class;
$opt= { %defaults, %$opt };
$self->{$_} = $opt->{$_} for qw/mode noall histogram depth mmarkup mpicsize cloud_min cloud_max cloud_stat no_typeahead rules_hint hscrollbar/;
$self->{$_} = [ split /\|/, $opt->{$_} ] for qw/sort type lmarkup lpicsize/;
$self->{type}[0] ||= $field.'.'.(Songs::FilterListProp($field,'type')||''); $self->{type}[0]=~s/\.$//; #FIXME
::Watch($self, Picture_artist => \&AAPicture_Changed); #FIXME PHASE1
::Watch($self, Picture_album => \&AAPicture_Changed); #FIXME PHASE1
for my $d (0..$self->{depth})
{ my ($field)= $self->{type}[$d] =~ m#^([^.]+)#;
$self->{field}[$d]=$field;
$self->{icons}[$d]= Songs::FilterListProp($field,'icon') ? (Gtk2::IconSize->lookup('menu'))[0] : 0;
}
#search box
if ($opt->{searchbox} && Songs::FilterListProp($field,'search'))
{ $self->pack_start( make_searchbox() ,::FALSE,::FALSE,1);
}
::Watch($self,'SearchText_'.$opt->{group},\&set_text_search);
#interactive search box
$self->{isearchbox}=GMB::ISearchBox->new($opt,$self->{type}[0],'nolabel');
$self->pack_end( $self->{isearchbox} ,::FALSE,::FALSE,1);
$self->signal_connect(key_press_event => \&key_press_cb); #only used for isearchbox
$self->signal_connect(map => \&Fill);
$self->set_mode($self->{mode});
return $self;
}
sub SaveOptions
{ my $self=$_[0];
my %opt;
$opt{$_} = join '|', @{$self->{$_}} for qw/type lmarkup lpicsize sort/;
$opt{$_} = $self->{$_} for qw/mode noall histogram depth mmarkup mpicsize cloud_min cloud_max cloud_stat/;
for (keys %opt) { delete $opt{$_} if $opt{$_} eq $defaults{$_}; } #remove options equal to default value
delete $opt{type} if $opt{type} eq $self->{pid}; #remove unneeded type options
return %opt, $self->{isearchbox}->SaveOptions;
}
sub SetField
{ my ($self,$field,$depth)=@_;
$self->{field}[$depth]=$field;
my $type=Songs::FilterListProp($field,'type');
$self->{type}[$depth]= $type ? $field.'.'.$type : $field;
$self->{lpicsize}[$depth]||=0;
$self->{lmarkup}[$depth]||=0;
$self->{'sort'}[$depth]||='default';
$self->{icons}[$depth]||= Songs::FilterListProp($field,'icon') ? (Gtk2::IconSize->lookup('menu'))[0] : 0;
my $i=0;
$i++ while $self->{field}[$i];
$self->{depth}=$i-1;
$self->Fill('optchanged');
}
sub SetOption
{ my ($self,$key,$value)=@_;
$self->{$key}=$value if $key;
$self->Fill('optchanged');
}
sub set_mode
{ my ($self,$mode,$fillnow)=@_;
for my $child ($self->get_children)
{ $self->remove($child) if $child->{is_a_view};
}
my ($child,$view)= $mode eq 'cloud' ? $self->create_cloud :
$mode eq 'mosaic'? $self->create_mosaic :
$self->create_list;
$self->{view}=$view;
$self->{DefaultFocus}=$view;
$child->{is_a_view}=1;
$view->signal_connect(focus_in_event => sub { my $self=::find_ancestor($_[0],__PACKAGE__); $self->{isearchbox}->parent_has_focus; 0; }); #hide isearchbox when focus goes to the view
my $drag_type= Songs::FilterListProp( $self->{field}[0], 'drag') || ::DRAG_FILTER;
::set_drag( $view, source => [$drag_type,\&drag_cb]);
MultiTreeView::init($view,__PACKAGE__) if $mode eq 'list'; #should be in create_list but must be done after set_drag
$child->show_all;
$self->add($child);
$self->{valid}=0;
$self->Fill if $fillnow;
}
sub create_list
{ my $self=$_[0];
$self->{mode}='list';
my $field=$self->{field}[0];
my $sw=Gtk2::ScrolledWindow->new;
# $sw->set_shadow_type('etched-in');
$sw->set_policy('automatic','automatic');
::set_biscrolling($sw);
my $store=Gtk2::TreeStore->new(GID_TYPE);
my $treeview=Gtk2::TreeView->new($store);
$treeview->set_rules_hint(1) if $self->{rules_hint};
$sw->add($treeview);
$treeview->set_headers_visible(::FALSE);
$treeview->set_search_column(-1); #disable gtk interactive search, use my own instead
$treeview->set_enable_search(::FALSE);
#$treeview->set('fixed-height-mode' => ::TRUE); #only if fixed-size column
my $renderer= CellRendererGID->new;
my $column=Gtk2::TreeViewColumn->new_with_attributes('',$renderer);
$renderer->set(prop => [@$self{qw/type lmarkup lpicsize icons hscrollbar/}]); #=> $renderer->get('prop')->[0] contains $self->{type} (which is a array ref)
#$column->add_attribute($renderer, gid => 0);
$column->set_cell_data_func($renderer, sub
{ my (undef,$cell,$store,$iter)=@_;
my $gid=$store->get($iter,0);
my $depth=$store->iter_depth($iter);
$cell->set( gid=>$gid, depth=>$depth);# 'is-expander'=> $depth < $store->{depth});
});
$treeview->append_column($column);
$treeview->signal_connect(row_expanded => \&row_expanded_cb);
#$treeview->signal_connect(row_collapsed => sub { my $store=$_[0]->get_model;my $iter=$_[1]; while (my $iter=$store->iter_nth_child($iter,1)) { $store->remove($iter) } });
my $selection=$treeview->get_selection;
$selection->set_mode('multiple');
$selection->signal_connect(changed =>\&selection_changed_cb);
$treeview->signal_connect( row_activated => sub { Activate($_[0],1); });
return $sw,$treeview;
}
sub Activate
{ my ($view,$button)=@_;
my $self=::find_ancestor($view,__PACKAGE__);
my $filter= $self->get_selected_filters;
return unless $filter; #nothing selected
FilterPane::Activate($self,$button,$filter);
}
sub create_cloud
{ my $self=$_[0];
$self->{mode}='cloud';
my $sw=Gtk2::ScrolledWindow->new;
$sw->set_policy('never','automatic');
my $sub=Songs::DisplayFromGID_sub($self->{type}[0]);
my $cloud= GMB::Cloud->new(\&child_selection_changed_cb,\&get_fill_data, \&Activate,\&PopupContextMenu,$sub);
$sw->add_with_viewport($cloud);
return $sw,$cloud;
}
sub create_mosaic
{ my $self=$_[0];
$self->{mode}='mosaic';
$self->{mpicsize}||=64;
my $hbox=Gtk2::HBox->new(0,0);
my $vscroll=Gtk2::VScrollbar->new;
$hbox->pack_end($vscroll,0,0,0);
my $mosaic= GMB::Mosaic->new(\&child_selection_changed_cb,\&get_fill_data,\&Activate,\&PopupContextMenu,$self->{type}[0],$vscroll);
$hbox->add($mosaic);
return $hbox,$mosaic;
}
sub get_cursor_row
{ my $self=$_[0];
if ($self->{mode} eq 'list')
{ my ($path)=$self->{view}->get_cursor;
return $path ? $path->to_string : undef;
}
else { return $self->{view}->get_cursor_row; }
}
sub set_cursor_to_row
{ my ($self,$row)=@_;
if ($self->{mode} eq 'list')
{ $self->{view}->set_cursor(Gtk2::TreePath->new_from_indices($row));
}
else { $self->{view}->set_cursor_to_row($row); }
}
sub make_searchbox
{ my $entry=Gtk2::Entry->new; #FIXME tooltip
my $clear=::NewIconButton('gtk-clear',undef,sub { $_[0]->{entry}->set_text(''); },'none' ); #FIXME tooltip
$clear->{entry}=$entry;
my $hbox=Gtk2::HBox->new(0,0);
$hbox->pack_end($clear,0,0,0);
$hbox->pack_start($entry,1,1,0);
$entry->signal_connect(changed =>
sub { ::IdleDo('6_UpdateSearch'.$entry,300,sub
{ my $entry=$_[0];
my $self=::find_ancestor($entry,__PACKAGE__);
my $s=$entry->get_text;
$self->set_text_search( $entry->get_text, 0,0 )
},$_[0]);
});
$entry->signal_connect(activate =>
sub { ::DoTask('6_UpdateSearch'.$entry);
});
return $hbox;
}
sub set_text_search
{ my ($self,$search,$is_regexp,$is_casesens)=@_;
return if defined $self->{search} && $self->{search} eq $search
&& !($self->{search_is_regexp} xor $is_regexp)
&& !($self->{search_is_casesens} xor $is_casesens);
$self->{search}=$search;
$self->{search_is_regexp}= $is_regexp||0;
$self->{search_is_casesens}= $is_casesens||0;
$self->{valid}=0;
$self->Fill if $self->mapped;
}
sub AAPicture_Changed
{ my ($self,$key)=@_;
return if $self->{mode} eq 'cloud';
return unless $self->{valid} && $self->{hash} && $self->{hash}{$key} && $self->{hash}{$key} >= ::find_ancestor($self,'FilterPane')->{min};
$self->queue_draw;
}
sub selection_changed_cb
{ my $treesel=$_[0];
child_selection_changed_cb($treesel->get_tree_view);
}
sub child_selection_changed_cb
{ my $child=$_[0];
my $self=::find_ancestor($child,__PACKAGE__);
return if $self->{busy};
my $filter=$self->get_selected_filters;
return unless $filter;
my $filterpane=::find_ancestor($self,'FilterPane');
::SetFilter( $self, $filter, $filterpane->{nb}, $filterpane->{group} );
}
sub get_selected_filters
{ my $self=::find_ancestor($_[0],__PACKAGE__);
my @filters;
my $types=$self->{type};
if ($self->{mode} eq 'list')
{ my $store=$self->{view}->get_model;
my $sel=$self->{view}->get_selection;
my @rows=$sel->get_selected_rows;
for my $path (@rows)
{ my $iter=$store->get_iter($path);
if ($store->get_value($iter,0)==GID_ALL) { return Filter->new; }
my @parents= $iter;
unshift @parents,$iter while $iter=$store->iter_parent($iter);
next if grep $sel->iter_is_selected($parents[$_]), 0..$#parents-1;#skip if one parent is selected
my @f=map Songs::MakeFilterFromGID( $types->[$_], $store->get_value($parents[$_],0)), 0..$#parents;
push @filters,Filter->newadd(1, @f);
}
}
else
{ my $vals=$self->get_selected;
@filters=map Songs::MakeFilterFromGID($types->[0],$_), @$vals;
}
return undef unless @filters;
my $field=$self->{field}[0];
my $filterpane=::find_ancestor($self,'FilterPane');
my $i= $filterpane->{inter} && Songs::FilterListProp($field,'multi');
my $filter=Filter->newadd($i,@filters);
$filter->invert if $filterpane->{invert};
return $filter;
}
sub get_selected #not called for list => only called for cloud or mosaic
{ return [$_[0]->{view}->get_selected];
}
sub get_selected_list
{ my $self=$_[0];
my $field=$self->{field}[0];
my @vals;
if ($self->{mode} eq 'list') #only returns selected rows if they are all at the same depth
{{ my $store=$self->{view}->get_model;
my @iters=map $store->get_iter($_), $self->{view}->get_selection->get_selected_rows;
last unless @iters;
if ($store->get_value($iters[0],0)==GID_ALL) # assumes "All row" first iter
{ my $iter= $store->get_iter_first; # this iter is "All row" -> not added
# "all row" is selected, replace iters list by list of all iters of first depth
@iters=();
push @iters,$iter while $iter=$store->iter_next($iter);
last unless @iters;
}
my $depth=$store->iter_depth($iters[0]);
last if grep $depth != $store->iter_depth($_), @iters;
@vals=map $store->get_value($_,0) , @iters;
$field=$self->{field}[$depth];
}}
else { @vals=$self->{view}->get_selected }
return $field,\@vals;
}
sub drag_cb
{ my $self=::find_ancestor($_[0],__PACKAGE__);
my $field=$self->{field}[0];
if (my $drag=Songs::FilterListProp($field,'drag')) #return artist or album gids
{ if ($self->{mode} eq 'list')
{ my $store=$self->{view}->get_model;
my @rows=$self->{view}->get_selection->get_selected_rows;
unless (grep $_->get_depth>1, @rows)
{ my @gids=map $store->get_value($store->get_iter($_),0), @rows;
warn "dnd : gids=@gids\n";
if (grep $_==GID_ALL, @gids) {return ::DRAG_FILTER,'';} #there is an "all-row"
return $drag,@gids;
}
#else : rows of depth>0 selected => fallback to get_selected_filters
}
}
my $filter=$self->get_selected_filters;
return ($filter? (::DRAG_FILTER,$filter->{string}) : undef);
}
sub row_expanded_cb
{ my ($treeview,$piter,$path)=@_;
my $self=::find_ancestor($treeview,__PACKAGE__);
my $filterpane=::find_ancestor($self,'FilterPane');
my $store=$treeview->get_model;
my $depth=$store->iter_depth($piter);
my @filters;
for (my $iter=$piter; $iter; $iter=$store->iter_parent($iter) )
{ push @filters, Songs::MakeFilterFromGID($self->{type}[$store->iter_depth($iter)], $store->get($iter,0));
}
my $list=$filterpane->{list};
$list= Filter->newadd(1,@filters)->filter($list);
my $type=$self->{type}[$depth+1];
my $h=Songs::BuildHash($type,$list,'gid');
my $children=AA::SortKeys($type,[keys %$h],$self->{'sort'}[$depth+1]);
for my $i (0..$#$children)
{ my $iter= $store->iter_nth_child($piter,$i) || $store->append($piter);
$store->set($iter,0,$children->[$i]);
}
while (my $iter=$store->iter_nth_child($piter,$#$children+1)) { $store->remove($iter) }
if ($depth<$self->{depth}-1) #make sure every child has a child if $depth not the deepest
{ for (my $iter=$store->iter_children($piter); $iter; $iter=$store->iter_next($iter) )
{ $store->append($iter) unless $store->iter_children($iter);
}
}
}
sub get_fill_data
{ my ($child,$opt)=@_;
my $self=::find_ancestor($child,__PACKAGE__);
my $filterpane=::find_ancestor($self,'FilterPane');
my $type=$self->{type}[0];
$self->{hash}=undef if $opt && $opt eq 'rehash';
my $href= $self->{hash} ||= Songs::BuildHash($type,$filterpane->{list},'gid');
$self->{valid}=1;
$self->{all_count}= keys %$href; #used to display how many artists/album/... there is in this filter
my $min=$filterpane->{min};
my $search=$self->{search};
my @list;
if ($min>1)
{ @list=grep $$href{$_} >= $min, keys %$href;
}
else { @list=keys %$href; }
if (defined $search && $search ne '')
{ @list= @{ AA::GrepKeys($type,$search,$self->{search_is_regexp},$self->{search_is_casesens},\@list) };
}
AA::SortKeys($type,\@list,$self->{'sort'}[0]);
my $always_first= Songs::Field_property($type,'always_first_gid');
if (defined $always_first) #special gid that should always appear first
{ my $before=@list;
@list= grep $_!=$always_first, @list;
unshift @list,$always_first if $before!=@list;
}
$self->{array}=\@list; #used for interactive search
if ($self->{mode} eq 'cloud' && $self->{cloud_stat} ne 'count') #FIXME update cloud when used fields change
{ $href=Songs::BuildHash($type,$filterpane->{list},'gid',$self->{cloud_stat});
}
return \@list,$href;
}
sub Fill
{ warn "filling @_\n" if $::debug;
my ($self,$opt)=@_;
$opt=undef unless $opt && ($opt eq 'optchanged' || $opt eq 'rehash');
return if $self->{valid} && !$opt;
if ($self->{mode} eq 'list')
{ my $treeview=$self->{view};
$treeview->set('show-expanders', ($self->{depth}>0) ) if Gtk2->CHECK_VERSION(2,12,0);
my $store=$treeview->get_model;
my $col=$self->{col};
my ($renderer)=($treeview->get_columns)[0]->get_cell_renderers;
$renderer->reset;
$self->{busy}=1;
$store->clear; #FIXME keep selection ? FIXME at least when opt is true (ie lmarkup or lpicsize changed)
my ($list,$href)=$self->get_fill_data($opt);
$renderer->set('all_count', $self->{all_count});
my $max= $self->{histogram} ? ::max(values %$href) : 0;
$renderer->set( hash=>$href, max=> $max );
$self->{array_offset}= $self->{noall} ? 0 : 1; #row number difference between store and $list, needed by interactive search
$store->set($store->prepend(undef),0,$_) for reverse @$list; # prepend because filling is a bit faster in reverse
$store->set($store->prepend(undef),0,GID_ALL) unless $self->{noall};
if ($self->{field}[1]) # add a children to every row
{ my $first=$store->get_iter_first;
$first=$store->iter_next($first) if $first && $store->get($first,0)==GID_ALL; #skip "all" row
for (my $iter=$first; $iter; $iter=$store->iter_next($iter))
{ $store->append($iter);
}
}
$self->{busy}=undef;
}
else
{ $self->{view}->reset_selection unless $opt;
$self->{view}->Fill($opt);
}
}
sub PopupContextMenu
{ my $self=::find_ancestor($_[0],__PACKAGE__);
my ($field,$gidlist)=$self->get_selected_list;
my $mainfield=Songs::MainField($field);
my $aa= ($mainfield eq 'artist' || $mainfield eq 'album') ? $mainfield : undef; #FIXME
my $mode= uc(substr $self->{mode},0,1); # C => cloud, M => mosaic, L => list
FilterPane::PopupContextMenu($self,{ self=> $self, filter => $self->get_selected_filters, field => $field, aa => $aa, gidlist =>$gidlist, mode => $mode, subfield => $field, depth =>0 });
}
sub key_press_cb
{ my ($self,$event)=@_;
my $key=Gtk2::Gdk->keyval_name( $event->keyval );
my $unicode=Gtk2::Gdk->keyval_to_unicode($event->keyval); # 0 if not a character
my $state=$event->get_state;
my $ctrl= $state * ['control-mask'] && !($state * [qw/mod1-mask mod4-mask super-mask/]); #ctrl and not alt/super
my $mod= $state * [qw/control-mask mod1-mask mod4-mask super-mask/]; # no modifier ctrl/alt/super
my $shift=$state * ['shift-mask'];
if (lc$key eq 'f' && $ctrl) { $self->{isearchbox}->begin(); } #ctrl-f : search
elsif (lc$key eq 'g' && $ctrl) { $self->{isearchbox}->search($shift ? -1 : 1);} #ctrl-g : next/prev match
elsif ($key eq 'F3' && !$mod) { $self->{isearchbox}->search($shift ? -1 : 1);} #F3 : next/prev match
elsif (!$self->{no_typeahead} && $unicode && $unicode!=32 && !$mod)
{ $self->{isearchbox}->begin( chr $unicode ); #begin typeahead search
}
else {return 0}
return 1;
}
package FolderList;
use base 'Gtk2::ScrolledWindow';
use constant { IsExpanded=>1, HasSongs=>2 };
sub new
{ my ($class,$col,$opt)=@_;
my $self = bless Gtk2::ScrolledWindow->new, $class;
#$self->set_shadow_type ('etched-in');
$self->set_policy ('automatic', 'automatic');
::set_biscrolling($self);
my $store=Gtk2::TreeStore->new('Glib::String');
my $treeview=Gtk2::TreeView->new($store);
$treeview->set_headers_visible(::FALSE);
$treeview->set_search_equal_func(\&search_equal_func);
$treeview->set_enable_search(!$opt->{no_typeahead});
#$treeview->set('fixed-height-mode' => ::TRUE); #only if fixed-size column
$treeview->signal_connect(row_expanded => \&row_expanded_changed_cb);
$treeview->signal_connect(row_collapsed => \&row_expanded_changed_cb);
$treeview->{expanded}={};
my $renderer= Gtk2::CellRendererText->new;
$store->{displayfunc}= Songs::DisplayFromHash_sub('path');
my $column=Gtk2::TreeViewColumn->new_with_attributes(Songs::FieldName($col),$renderer);
$column->set_cell_data_func($renderer, sub
{ my (undef,$cell,$store,$iter)=@_;
my $folder=::decode_url($store->get($iter,0));
$cell->set( text=> $store->{displayfunc}->($folder));
});
$treeview->append_column($column);
$self->add($treeview);
$self->{treeview}=$treeview;
$self->{DefaultFocus}=$treeview;
$self->signal_connect(map => \&Fill);
my $selection=$treeview->get_selection;
$selection->set_mode('multiple');
$selection->signal_connect (changed =>\&selection_changed_cb);
::set_drag($treeview, source => [::DRAG_FILTER,sub
{ my @paths=_get_path_selection( $_[0] );
return undef unless @paths;
my $filter=_MakeFolderFilter(@paths);
return ::DRAG_FILTER,($filter? $filter->{string} : undef);
}]);
MultiTreeView::init($treeview,__PACKAGE__);
$self->{simplify}= $opt->{simplify} || 'smart';
return $self;
}
sub SaveOptions
{ return simplify => $_[0]{simplify};
}
sub search_equal_func
{ #my ($store,$col,$string,$iter)=@_;
my $store=$_[0];
my $folder= $store->{displayfunc}( ::decode_url($store->get($_[3],0)) );
#use ::superlc instead of uc ?
my $string=uc $_[2];
index uc($folder), $string;
}
sub SetOption
{ my ($self,$key,$value)=@_;
$self->{$key}=$value if $key;
$self->{valid}=0;
delete $self->{hash};
$self->Fill;
}
sub Fill
{ warn "filling @_\n" if $::debug;
my $self=$_[0];
return if $self->{valid};
my $treeview=$self->{treeview};
my $filterpane=::find_ancestor($self,'FilterPane');
my $href=$self->{hash}||= BuildTreeRef($filterpane->{list},$treeview->{expanded},$self->{simplify});
my $min=$filterpane->{min};
my $store=$treeview->get_model;
$self->{busy}=1;
$store->clear; #FIXME keep selection
#fill the store
my @toadd; my @toexpand;
push @toadd,$href->{$_},$_,undef for sort grep $href->{$_}[0]>=$min, keys %$href;
while (my ($ref,$name,$iter)=splice @toadd,0,3)
{ my $iter=$store->append($iter);
$store->set($iter,0, Songs::filename_escape($name));
push @toexpand,$store->get_path($iter) if ($ref->[2]||0) & IsExpanded;
if ($ref->[1]) #sub-folders
{ push @toadd, $ref->[1]{$_},$_,$iter for sort grep $ref->[1]{$_}[0]>=$min, keys %{$ref->[1]}; }
}
# expand tree to first fork
if (my $iter=$store->get_iter_first)
{ $iter=$store->iter_children($iter) while $store->iter_n_children($iter)==1;
$treeview->expand_to_path( $store->get_path($iter) );
}
#expand previously expanded rows
$treeview->expand_row($_,::FALSE) for @toexpand;
$self->{busy}=undef;
$self->{valid}=1;
}
sub BuildTreeRef
{ my ($IDs,$expanded,$simplify)=@_;
my $h= Songs::BuildHash('path',$IDs);
my @hier;
# build structure : each folder is [nb_of_songs,children_hash,flags]
# children_hash: {child_foldername}= arrayref_of_subfolder
# flags: IsExpanded HasSongs
while (my ($f,$n)=each %$h)
{ my $ref=\@hier;
$ref=$ref->[1]{$_}||=[] and $ref->[0]+=$n for split /$::QSLASH/o,$f;
$ref->[2]|= HasSongs;
}
# restore expanded state
for my $dir (keys %$expanded)
{ my $ref=\@hier; my $notfound;
$ref=$ref->[1]{$_} or $notfound=1, last for split /$::QSLASH/o,$dir;
if ($notfound) {delete $expanded->{$dir}}
else { $ref->[2]|= IsExpanded; }
}
# simplify tree by fusing folders with their sub-folder, if without songs and only one sub-folder
if ($simplify ne 'never')
{ my @tosimp= (\@hier);
while (@tosimp)
{ my $parent=shift @tosimp;
my (@tofuse,@nofuse);
while (my ($path,$ref)=each %{$parent->[1]})
{ my @child= keys %{$ref->[1]};
# if only one child and no songs of its own
if (@child==1 && !(($ref->[2]||0) & HasSongs)) { push @tofuse,$path; }
else { push @nofuse,$path }
}
# 'smart' mode: only simplify if all siblings can be simplified
if ($simplify eq 'smart' && @nofuse) { push @nofuse,@tofuse; @tofuse=(); }
push @tosimp, map $parent->[1]{$_}, @nofuse;
for my $path (@tofuse)
{ my $ref= $parent->[1]{$path};
my @child= keys %{$ref->[1]};
unless (@child==1 && !(($ref->[2]||0) & HasSongs)) { push @tosimp,$ref; next }
delete $parent->[1]{$path};
$path.= ::SLASH.$child[0];
$parent->[1]{$path}= delete $ref->[1]{$child[0]};
redo; #fuse until more than one child or songs of its own
}
}
}
$hier[1]{::SLASH}=delete $hier[1]{''} if exists $hier[1]{''};
return $hier[1];
}
sub row_expanded_changed_cb #keep track of which rows are expanded
{ my ($treeview,$iter,$path)=@_;
my $self=::find_ancestor($treeview,__PACKAGE__);
return if $self->{busy};
my $expanded=$treeview->row_expanded($path);
$path= ::decode_url(_treepath_to_foldername($treeview->get_model,$path));
my $ref=[undef,$self->{hash}];
$ref=$ref->[1]{($_ eq '' ? ::SLASH : $_)} for split /$::QSLASH/o,$path;
if ($expanded)
{ $ref->[2]|= IsExpanded; #for when reusing the hash
$treeview->{expanded}{$path}=undef; #for when reconstructing the hash
}
else
{ $ref->[2]&=~ IsExpanded; # remove IsExpanded flag
delete $treeview->{expanded}{$path};
}
}
sub selection_changed_cb
{ my $treesel=$_[0];
my $self=::find_ancestor($treesel->get_tree_view,__PACKAGE__);
return if $self->{busy};
my @paths=_get_path_selection( $self->{treeview} );
return unless @paths;
my $filter=_MakeFolderFilter(@paths);
my $filterpane=::find_ancestor($self,'FilterPane');
$filter->invert if $filterpane->{invert};
::SetFilter( $self, $filter, $filterpane->{nb}, $filterpane->{group} );
}
sub _MakeFolderFilter
{ return Filter->newadd(::FALSE,map( "path:i:$_", @_ ));
}
sub Activate
{ my ($self,$button)=@_;
my @paths=_get_path_selection( $self->{treeview} );
my $filter= _MakeFolderFilter(@paths);
FilterPane::Activate($self,$button,$filter);
}
sub PopupContextMenu
{ my $self=shift;
my $tv=$self->{treeview};
my @paths=_get_path_selection($tv);
my @raw= map ::decode_url($_), @paths;
FilterPane::PopupContextMenu($self,{self=>$self, rawpathlist=> \@raw, pathlist => \@paths, filter => _MakeFolderFilter(@paths), folderview=>1, });
}
sub _get_path_selection
{ my $treeview=$_[0];
my $store=$treeview->get_model;
my @paths=$treeview->get_selection->get_selected_rows;
return () if @paths==0; #if no selection
@paths=map _treepath_to_foldername($store,$_), @paths;
return @paths;
}
sub _treepath_to_foldername
{ my $store=$_[0]; my $tp=$_[1];
my @folders;
my $iter=$store->get_iter($tp);
while ($iter)
{ unshift @folders, $store->get_value($iter,0);
$iter=$store->iter_parent($iter);
}
$folders[0]='' if $folders[0] eq ::SLASH;
return join(::SLASH,@folders);
}
package Filesystem; #FIXME lots of common code with FolderList => merge it
use base 'Gtk2::ScrolledWindow';
sub new
{ my ($class,$col,$opt)=@_;
my $self = bless Gtk2::ScrolledWindow->new, $class;
#$self->set_shadow_type ('etched-in');
$self->set_policy ('automatic', 'automatic');
::set_biscrolling($self);
my $store=Gtk2::TreeStore->new('Glib::String','Glib::Uint');
my $treeview=Gtk2::TreeView->new($store);
$treeview->set_headers_visible(::FALSE);
$treeview->set_enable_search(!$opt->{no_typeahead});
#$treeview->set('fixed-height-mode' => ::TRUE); #only if fixed-size column
$treeview->signal_connect(test_expand_row => \&row_expand_cb);
my $renderer= Gtk2::CellRendererText->new;
my $column=Gtk2::TreeViewColumn->new_with_attributes('',$renderer);
$column->set_cell_data_func($renderer, \&cell_data_func_cb);
$treeview->append_column($column);
$self->add($treeview);
$self->{treeview}=$treeview;
$self->{DefaultFocus}=$treeview;
$self->signal_connect(map => \&Fill);
my $selection=$treeview->get_selection;
$selection->set_mode('multiple');
$selection->signal_connect (changed =>\&selection_changed_cb);
# drag and drop doesn't work with filter using a special source, which is the case here
# ::set_drag($treeview, source => [::DRAG_FILTER,sub
# { my @paths=_get_path_selection( $_[0] );
# return undef unless @paths;
# my $filter=_MakeFolderFilter(@paths);
# return ::DRAG_FILTER,($filter? $filter->{string} : undef);
# }]);
::set_drag($treeview, source => [::DRAG_ID,sub
{ my @paths=_get_path_selection( $_[0] );
return undef unless @paths;
my $filter=_MakeFolderFilter(@paths);
return undef unless $filter;
my @list= @{$filter->filter};
::SortList(\@list);
return ::DRAG_ID,@list;
}]);
MultiTreeView::init($treeview,__PACKAGE__);
return $self;
}
sub Fill
{ warn "filling @_\n" if $::debug;
my $self=$_[0];
return if $self->{valid};
my $treeview=$self->{treeview};
my $store=$treeview->get_model;
my $iter=$store->append(undef);
my $root= ::SLASH;
$root='C:' if $^O eq 'MSWin32'; #FIXME Win32 find a way to list the drives
$store->set($iter,0, ::url_escape($root));
my $treepath= $store->get_path($iter);
#expand to home dir
for my $folder (split /$::QSLASH/o, ::url_escape(Glib::get_home_dir))
{ next if $folder eq '';
$self->refresh_path($treepath,1);
$iter=$store->iter_children($iter);
while ($iter)
{ last if $folder eq $store->get($iter,0);
$iter=$store->iter_next($iter);
$treepath=$store->get_path($iter);
}
last unless $iter;
}
$self->refresh_path($treepath,1);
$treeview->expand_to_path($treepath);
$self->{valid}=1;
}
sub cell_data_func_cb
{ my ($tvcolumn,$cell,$store,$iter)=@_;
my $folder=::decode_url($store->get($iter,0));
$cell->set( text=> ::filename_to_utf8displayname($folder) );
my $treeview= $tvcolumn->get_tree_view;
Glib::Timeout->add(10,\&idle_load,$treeview) unless $treeview->{queued_load};
push @{$treeview->{queued_load}}, $store->get_path($iter);
}
sub idle_load
{ my $treeview=shift;
my $queue=$treeview->{queued_load};
return 0 unless $queue;
my ($first,$last)= $treeview->get_visible_range;
unless ($first && $last) { @$queue=(); return 0 }
while (my $path=shift @$queue)
{ next unless $path->compare($first)>=0 && $path->compare($last)<=0; # ignore if out of view
my $self=::find_ancestor($treeview,__PACKAGE__);
my $partial=$self->refresh_path($path);
if ($partial) { unshift @$queue,$path; return 1 }
last if Gtk2->events_pending;
}
return 1 if @$queue;
delete $treeview->{queued_load};
return 0;
}
sub row_expand_cb
{ my ($treeview,$iter,$path)=@_;
my $self=::find_ancestor($treeview,__PACKAGE__);
$self->refresh_path($path,1);
return !$treeview->get_model->iter_children($iter);
}
sub refresh_path
{ my ($self,$path,$force)=@_;
my $treeview=$self->{treeview};
my $store=$treeview->get_model;
my $parent=$store->get_iter($path);
my $folder=_treepath_to_foldername($store,$path);
return 0 unless $folder;
$folder= ::decode_url($folder);
my @subfolders;
my $full= $force || $treeview->row_expanded($path);
my $continue;
if ($self->{in_progress})
{ if ($full || $self->{in_progress}{folder} ne $folder) { delete $self->{in_progress}; }
else {$continue=1}
}
my $dh; my $lastmodif;
if (!$continue) # check folder is there and if treeview up-to-date
{ my $ok=opendir $dh,$folder;
unless ($ok) { $store->remove($parent) unless -d $folder; return 0; }
$lastmodif= (stat $dh)[9] || 1;# ||1 to ùake sure it isn't 0, as 0 means not read
my $lastrefresh=$store->get($parent,1);
return 0 if $lastmodif==$lastrefresh && !$force;
}
if ($full)
{ @subfolders= grep !m#^\.# && -d $folder.::SLASH.$_, readdir $dh;
close $dh;
}
else # the content of the folder will be search for subfolders in chunks (-d can sometimes be slow)
{ my $progress= $self->{in_progress} ||= { list=>[], found=>[], lastmodif=>$lastmodif, folder=>$folder };
my $list= $progress->{list};
my $found= $progress->{found};
if (!$continue)
{ @$list= grep !m#^\.#, readdir $dh;
close $dh;
}
while (@$list)
{ return 1 if Gtk2->events_pending; # continue later
my $dir=