gmusicbrowser-1.1.15~ds0.orig/0000775000175000017500000000000012565532270015461 5ustar unit193unit193gmusicbrowser-1.1.15~ds0.orig/gmusicbrowser_dbus.pm0000664000175000017500000001200412565212604021720 0ustar unit193unit193# Copyright (C) 2005-2009 Quentin Sculo # # 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 GMB::DBus::Object; use base 'Net::DBus::Object'; use Net::DBus::Exporter 'org.gmusicbrowser'; sub new { my ($class,$service) = @_; my $self = $class->SUPER::new($service, '/org/gmusicbrowser'); bless $self, $class; Glib::Idle->add( sub { ::Watch($self,CurSong => \&SongFieldsChanged); ::Watch($self,CurSongID =>\&SongChanged); ::Watch($self,PlayingSong =>\&PlayingSongChanged); #::Watch($self,Save => \&GMB::DBus::Quit); 0; }); return $self; } dbus_method('RunCommand', ['string'], [],{no_return=>1}); sub RunCommand { my ($self,$cmd) = @_; warn "Received DBus command : '$cmd'\n"; ::run_command(undef,$cmd); } dbus_method('CurrentSong', [], [['dict', 'string', 'string']]); sub CurrentSong { my $self=$_[0]; return {} unless defined $::SongID; my %h; $h{$_}=Songs::Get($::SongID,$_) for Songs::PropertyFields(), qw/uri album_picture/; #warn "$_:$h{$_}\n" for sort keys %h; return \%h; } dbus_method('CurrentSongFields', [['array', 'string']], [['array', 'string']]); sub CurrentSongFields { my ($self,$fields)=@_; return [] unless defined $::SongID; my @ret= Songs::Get($::SongID,@$fields); return \@ret; } dbus_method('GetPosition', [], ['double']); sub GetPosition { my $self=$_[0]; return $::PlayTime || 0; } dbus_method('Playing', [], ['bool']); sub Playing { return $::TogPlay ? 1 : 0; } dbus_method('Set', [['struct', 'string', 'string', 'string']], ['bool']); sub Set { my ($self,$array)=@_; Songs::SetTagValue(@$array); #return false on error, true if ok } dbus_method('Get', [['struct', 'string', 'string']], ['string']); sub Get { my ($self,$array)=@_; Songs::GetTagValue(@$array); } dbus_method('GetLibrary', [], [['array', 'uint32']]); sub GetLibrary { $::Library; } dbus_method('GetAlbumCover', ['uint32'], ['string']); sub GetAlbumCover { my ($self,$ID)=@_; my $file=Songs::Get($ID,'album_picture'); return $file; } #slow, not a good idea dbus_method('GetAlbumCoverData', ['uint32'], [['array', 'byte']]); sub GetAlbumCoverData { my ($self,$ID)=@_; my $file=GetAlbumCover($self,$ID); return undef unless $file && -r $file; my $data; if ($file=~m/\.(?:mp3|flac)$/i) { $data=ReadTag::PixFromMusicFile($file); } else { open my$fh,'<',$file; binmode $fh; read $fh,$data, (stat $file)[7]; close $fh; } return [map ord,split //, $data]; } dbus_method(CopyFields => [['array', 'string']], ['bool']); #copy fields from one song to another #1st arg : filename of ID of source, 2nd arg filename or ID of dest, both must be in the library, #following args are list of fields, example : added, lastplay, playcount, lastskip, skipcount, rating, label sub CopyFields { my ($self,$array)=@_; #my ($file1,$file2,@fields)=@$array; Songs::CopyFields(@$array); #returns true on error } dbus_signal(SongFieldsChanged => ['uint32']); sub SongFieldsChanged { $_[0]->emit_signal(SongFieldsChanged => $::SongID||0); } dbus_signal(SongChanged => ['uint32']); sub SongChanged { $_[0]->emit_signal(SongChanged => $::SongID||0); } dbus_signal(PlayingSongChanged => ['uint32']); sub PlayingSongChanged { $_[0]->emit_signal(PlayingSongChanged => $::SongID||0); } package GMB::DBus; use Net::DBus; use Net::DBus::Service; my $not_glib_dbus; our $bus; eval { require Net::DBus::GLib; $bus=Net::DBus::GLib->session; }; unless ($bus) { #warn "Net::DBus::GLib not found (not very important)\n"; $not_glib_dbus=1; $bus= Net::DBus->session; } Glib::Idle->add(\&init); #initialize once the main gmb init is finished sub init { #my $bus = Net::DBus->session; my $service= $bus->export_service($::DBus_id); # $::DBus_id is 'org.gmusicbrowser' by default my $object = GMB::DBus::Object->new($service); DBus_mainloop_hack() if $not_glib_dbus; 0; #called in an idle, return 0 to run only once } sub DBus_mainloop_hack { # use Net::DBus internals to connect it to the Glib mainloop, though unlikely, it may break with future version of Net::DBus use Net::DBus::Reactor; my $reactor=Net::DBus::Reactor->main; for my $ref (['in','read'],['out','write'], ['err','exception']) { my ($type1,$type2)=@$ref; for my $fd (keys %{$reactor->{fds}{$type2}}) { #warn "$fd $type2"; Glib::IO->add_watch($fd,$type1, sub{ my $cb=$reactor->{fds}{$type2}{$fd}{callback}; $cb->invoke if $cb; $_->invoke for $reactor->_dispatch_hook; 1; }) if $reactor->{fds}{$type2}{$fd}{enabled}; #Glib::IO->add_watch($fd,$type1,sub { Net::DBus::Reactor->main->step;Net::DBus::Reactor->main->step;1; }) if $reactor->{fds}{$type2}{$fd}{enabled}; } } # run the dbus mainloop once so that events already pending are processed # needed if events already waiting when gmb is starting my $timeout=$reactor->add_timeout(1, Net::DBus::Callback->new( method => sub {} )); Net::DBus::Reactor->main->step; $reactor->remove_timeout($timeout); } 1; gmusicbrowser-1.1.15~ds0.orig/layouts/0000775000175000017500000000000012565212604017155 5ustar unit193unit193gmusicbrowser-1.1.15~ds0.orig/layouts/makeitlooklike.layout0000664000175000017500000005404512565212604023430 0ustar unit193unit193# Make it look like: ##################################### [Quodlibet] Type=G+ Category = _"Make it look like" Default = Window(size=700x600,sticky=1) HPbrowser(size=160-600) FilterPane0(artistinfo=1,albumpsize=32,albuminfo=1) MBmenu = MainMenuItem LayoutItem PSortItem PFilterItem QueueItem HBButtons1 = Prev Play Next HBButtons2 = OpenBrowser Pref Quit VBButtons = HBButtons1 HBButtons2 HBTitle = Filler0 _Title -Stars HBArtist = LockArtist _Artist HBAlbum = LockAlbum _Album -Year HBTime = Time _TimeBar VBText = 2HBTitle 2HBArtist 2HBAlbum HBleft = VBButtons 5_VBText HBmenu = MBmenu 5Sort 5Filter 5Queue 5Pos VBleft2 = HBmenu HBleft 3HBTime VBVol = VolumeIcon _VolumeSlider HBupper = _VBleft2 -VBVol 5-Cover HPbrowser = FilterPane0(nb=1) _SongList VBmain = HBupper 5_HPbrowser HSize0 = Filler0 LockArtist LockAlbum VolumeScroll = HBupper [Exaile] Author = shimmerproject.org Type = G+ Category = _"Make it look like" Title = %t by %a Default = Window(size=80%x80%) KeyBindings = c-J GoToCurrentSong VolumeScroll = HBVolume # Window = hidden=FilterPane2|FilterPane3|FilterPane4|FilterPane5|FilterPane6|FilterPane7 VBMain = HBMenubar _HPSidebarCenter HBStatusbar HBStatusbar = 10-Total(mode=filter) HBMenubar = _MBEdit MBEdit = SMMusic0 SMEdit1 SMView2 SMControl3 SMHelp4 SMMusic0 = (label=_"File") \ MenuItem00(command="RunPerlCode(::ChooseAddPath(0,1))",label=_"Add folder ...",icon="gtk-add") \ SeparatorMenuItem01 \ MenuItem02(command=Quit,label=_"Quit",icon="gtk-quit") SMEdit1 = (label=_"Edit") \ MenuItem10(command=EnqueueSelected,label=_"Queue",icon="gmb-queue") \ # MenuItem11(command=,label=_"Add to playlist") \ \ SeparatorMenuItem10 \ MenuItem14(command=OpenPref,label=_"Preferences",icon="gtk-preferences") # SMView menu for common mode SMView2 = (label=_"View") \ MenuItem25(command=GoToCurrentSong,label=_"Go to Playing Track",icon="go-jump") \ SeparatorMenuItem21 \ LayoutItem \ MenuItem24(togglewidget=HBSearchAndToolbar,label=_"Playlist Utilities Bar") SMControl3 = (label=_"Tools") \ MenuItem32(command="RunPerlCode(::IdleScan)",label=_"Rescan Collection",icon="gtk-refresh") \ MenuItem33(command="OpenSongProp",label=_"Track Properties",icon="gtk-properties") \ MenuItem34(click1=OpenCustom(Equalizer),label=_"Equalizer") SMHelp4 = (label=_"Help") \ MenuItem41(command="RunPerlCode(::AboutDialog)",label=_"About",icon="gtk-about") HPSidebarCenter = VBSidebar VBCenter VBSidebar = _NBSidebar VProgress NBSidebar = (tabpos=left90,typesubmenu='PC') \ NBSidebar1 \ FilterPane10(tabtitle=_"Playlists",nb=0,hidebb=1,tabpos=left90,page=savedtree,pages=savedtree) \ FilterPane11(tabtitle=_"Files",nb=0,hidebb=1,tabpos=left90,page=folder,pages=folder) NBSidebar1 = (tabtitle=_"Collection",typesubmenu='') \ FilterPane1(tabtitle=_"Artist",\ group=99,\ nb=0,\ hidebb=1,\ pages=artist,\ page=artist,\ page_artist/depth=1,\ page_artist/lmarkup="0|%a%Y %s %l",\ page_artist/lpicsize='0|32',\ page_artist/noall=1,\ page_artist/sort=default|default,\ page_artist/type=artist|album\ ) \ FilterPane2(tabtitle=_"Album",\ group=99,\ nb=0,\ hidebb=1,\ page=album,\ pages=album,\ page_album/noall=1\ ) \ FilterPane3(tabtitle=_"Genre - Artist",\ group=99,\ nb=0,\ hidebb=1,\ page=genre,\ pages=genre,\ page_genre/depth=2,\ page_genre/lmarkup="0|0|%a%Y %s %l",\ page_genre/lpicsize='0|32|32',\ page_genre/noall=1,\ page_genre/sort=default|default|default,\ page_genre/type=genre|artist|album\ ) \ FilterPane4(tabtitle=_"Genre - Album",\ group=99,\ nb=0,\ hidebb=1,\ page=genre,\ pages=genre,\ page_genre/depth=2,\ page_genre/lmarkup="0|0|%a%Y %s %l",\ page_genre/lpicsize='0|32|32',\ page_genre/noall=1,\ page_genre/sort=default|default|default,\ page_genre/type=genre|album|artist\ ) \ FilterPane5(tabtitle=_"Year - Artist",\ group=99,\ nb=0,\ hidebb=1,\ page=year,\ pages=year,\ page_year/depth=2,\ page_year/lmarkup="0|0|%a%Y %s %l",\ page_year/lpicsize='0|32|32',\ page_year/noall=1,\ page_year/sort=default|default|default,\ page_year/type=year|artist|album\ ) \ FilterPane6(tabtitle=_"Year - Album",\ group=99,\ nb=0,\ hidebb=1,\ page=year,\ pages=year,\ page_year/depth=2,\ page_year/lmarkup="0|0|%a%Y %s %l",\ page_year/lpicsize='0|32|32',\ page_year/noall=1,\ page_year/sort=default|default|default,\ page_year/type=year|album|artist\ ) \ FilterPane7(tabtitle=_"Artist (Year - Album)",\ group=99,\ nb=0,\ hidebb=1,\ pages=artist,\ page=artist,\ page_artist/depth=1,\ page_artist/lmarkup="0|%y %a",\ page_artist/lpicsize='0|32',\ page_artist/noall=1,\ page_artist/sort=default|default,\ page_artist/type=artist|album\ ) VBCenter = 10HBPlayer _HBSongLists 5HBSearchAndToolbar HBControls HBSongLists = _NBSongLists \ 3Filler7 NBSongLists = (typesubmenu='L') SongList(tabtitle=_"Playlist 1",mode=playlist,tabicon="gmb-list",cols="playandqueue track title album artist length") HBPlayer = (maxheight=100,minheight=100) \ HBCover \ 5_VBSongInfo \ VBVolume \ 5Filler5 HBCover = (maxheight=100,minheight=100,minwidth=100,maxwidth=100) \ Cover(default=elementary/no-cover.svg,showcover=0,minsize=100,maxsize=100) VBVolume = 100Filler3 HBVolume HBVolume = \ -VolSlider(horizontal=1,minwidth=125,maxwidth=125) \ -VolumeIcon(click1=TogMute,size=button,button=1) VBSongInfo = \ _Title(yalign=0.5,ellipsize=end,markup="%t",click1="") \ _Artist(yalign=1,ellipsize=end,markup="by %a") \ _Album(yalign=1,ellipsize=end,markup="from %l")\ 100Filler2 HBSearchAndToolbar = _HBToolbar \ Text1(text=_"Search:") \ 10_SimpleSearch HBToolbar = Sort(button=1,relief=none,minwidth=28) \ ResetFilter(button=1,relief=none,minwidth=28) HBControls = Prev(size=button,relief=normal) \ Play(size=button,relief=normal) \ Stop(size=button,relief=normal) \ Next(size=button,relief=normal) \ _VBTimeBar \ 3Filler4 VBTimeBar = 2Filler0 TimeBar(text="$current / %m") 1Filler1 [Rhythmbox] Author = shimmerproject.org Type = G+ Category = _"Make it look like" Title = %a - %t Default = Window(size=80%x80%) HPSidebarAndCenter(size=200) DefaultFocus = Window = hidden=Context|HBSearch|FilterPane2|SimpleSearch1|SimpleSearch2|SimpleSearch3|QueueList KeyBindings = a-Return OpenSongProp c-space PlayPause a-Left PrevSong a-Right NextSong c-Down DecVolume c-Up IncVolume VolumeScroll = HBVolume VBMain = HBMenubar HBToolbar VBSliderbar _HPSidebarCenterContext HBStatusbar HBMenubar = _MBEdit MBEdit = SMMusic0 SMEdit1 SMView20 SMControl3 SMHelp4 SMMusic0 = (label=_"Music") \ MenuItem00(command="RunPerlCode(::ChooseAddPath(0,1))",label=_"Add folder ...") \ SeparatorMenuItem01 \ MenuItem01(command="OpenSongProp",label=_"Song Properties") \ SeparatorMenuItem02 \ MenuItem02(command=Quit,label=_"Quit") SMEdit1 = (label=_"Edit") \ MenuItem10(command=EnqueueSelected,label=_"Add to queue") \ # MenuItem11(command=,label=_"Add to playlist") \ \ SeparatorMenuItem10 \ # MenuItem10(command=OpenPref,label=_"Remove") \ MenuItem11(command=DeleteSelected,label=_"Delete") \ SeparatorMenuItem11 \ MenuItem14(command=OpenPref,label=_"Options ...") # SMView menu for common mode SMView20 = (label=_"View") \ LayoutItem \ SeparatorMenuItem20 \ SMBrowserViews20 \ SMSearchBox21 \ SeparatorMenuItem21 \ MenuItem20(togglewidget=VBSidebar,label=_"Sidebar") \ MenuItem21(togglewidget=QueueList,label=_"Queue in Sidebar") \ MenuItem22(togglewidget=HBFilters,label=_"Search") \ MenuItem23(togglewidget=HBStatusbar,label=_"Statusbar") \ MenuItem24(togglewidget=HBToolbar,label=_"Toolbar") SMBrowserViews20 = (label=_"Browser views") \ MenuItem201(togglewidget=FilterPane2,label=_"Show genre") \ MenuItem202(togglewidget=FilterPane4,label=_"Show album") SMSearchBox21 = (label=_"Search box type") \ MenuItem211(togglegroup=2,togglewidget=HBSearchGMB,label=_"gmusicbrowser") \ MenuItem212(togglegroup=2,togglewidget=HBSearch,label=_"Rhythmbox") SMControl3 = (label=_"Control") \ MenuItem30(command=PlayPause,label=_"Play") \ SeparatorMenuItem31 \ MenuItem31(command=PrevSong,label=_"Previous") \ MenuItem32(command=NextSong,label=_"Next") \ SeparatorMenuItem32 \ MenuItem33(command=IncVolume,label=_"Increase volume") \ MenuItem34(command=DecVolume,label=_"Decrease volume") SMHelp4 = (label=_"Help") \ MenuItem41(command="RunPerlCode(::AboutDialog)",label=_"About",icon="gtk-about") HBToolbar = Play Prev Next \ 3VSeparator1 \ Sort(button=1,relief=none,minwidth=35) \ ToggleButton4(relief=none,minwidth=35,size=button,icon=gtk-find,widget=HBFilters,tip=_"Show/Hide Browser") \ ToggleButton5(relief=none,minwidth=35,size=button,icon=gtk-info,widget=Context,tip=_"Change the context visibility") \ -HBVolume HBVolume = VolumeIcon(size=menu,button=1) VBSliderbar = 5HBText 1HBTimeSlider HBTimeSlider = 5_TimeSlider(direct_mode=0) HBText = 2Filler0 \ _Title(expand_max=300,ellipsize=end,markup="%t ",yalign=0.5) \ Artist(expand_max=200,yalign=1,ellipsize=end,markup=" by %a ") \ Album(expand_max=200,yalign=1,ellipsize=end,markup=" on %l ",showcover=0) \ -Time HPSidebarCenterContext = HPSidebarAndCenter Context HPSidebarAndCenter = VBSidebar VBCenter VBSidebar = _VPListAndQueue 3Cover(maxsize=400) VPListAndQueue = _FilterPane0(minheight=200,nb=0,hidebb=1,page=savedtree,pages=savedtree) _QueueList(songtree=1,cols="playandqueue RhythmboxQueue") VBCenter = 5HBSearch 5HBSearchGMB _VPFilterAndSongList1 _VPFilterAndSongList2 _VPFilterAndSongList3 HBSearchGMB = 5Text1(text=_"Search:") _SimpleSearch4(maxwidth=500,noselector=0) HBSearch = 5Text(text=_"Search:") _SimpleSearch0(maxwidth=500,noselector=1) \ _SimpleSearch1(maxwidth=500,fields=artist,noselector=1) \ _SimpleSearch2(maxwidth=500,fields=album,noselector=1) \ _SimpleSearch3(maxwidth=500,fields=title,noselector=1) \ 15HBSearchSelector HBSearchSelector = ToggleButton0(relief=none,minwidth=60,size=button,widget=SimpleSearch0,togglegroup=1,label=_"All", tip=_"Search all fields") \ ToggleButton1(relief=none,minwidth=60,size=button,widget=SimpleSearch1,togglegroup=1,label=_"Artist",tip=_"Search Artist") \ ToggleButton2(relief=none,minwidth=60,size=button,widget=SimpleSearch2,togglegroup=1,label=_"Album",tip=_"Search album") \ ToggleButton3(relief=none,minwidth=60,size=button,widget=SimpleSearch3,togglegroup=1,label="Title",tip=_"Search title") VPFilterAndSongList1 = _HBFilters _SongList(mode=playlist) HBFilters = _FilterPane2(nb=2, hidebb=1,pages=genre,page=genre) \ _FilterPane3(nb=3, hidebb=1,pages=artist,page=artist) \ _FilterPane4(nb=4,hidebb=1,pages=album,page=album) HBStatusbar = Total(mode=filter) -Progress(maxheight=1) [Rhythmbox Compact] based on Rhythmbox Default = Window(size=600x126) Category = _"Make it look like" VBMain = HBMenubar HBToolbar VBSliderbar HBMenubar = _MBEdit MBEdit = SMMusic0 SMEdit1 SMView20 SMControl3 SMHelp4 SMMusic0 = (label=_"Music") \ MenuItem00(command="RunPerlCode(::ChooseAddPath(0,1))",label=_"Add folder ...") \ # SeparatorMenuItem01 \ MenuItem01(command="OpenSongProp",label=_"Properties") \ SeparatorMenuItem02 \ MenuItem02(command=Quit,label=_"Quit") SMEdit1 = (label=_"Edit") \ MenuItem14(command=OpenPref,label=_"Options ...") # SMView menu for compact mode SMView20 = (label=_"View") \ LayoutItem \ SeparatorMenuItem20 \ MenuItem24(togglewidget=HBToolbar,label=_"Toolbar") SMControl3 = (label=_"Control") \ MenuItem30(command=PlayPause,label=_"Play") \ SeparatorMenuItem31 \ MenuItem31(command=PrevSong,label=_"Previous") \ MenuItem32(command=NextSong,label=_"Next") \ SeparatorMenuItem32 \ MenuItem33(command=IncVolume,label=_"Increase volume") \ MenuItem34(command=DecVolume,label=_"Decrease volume") HBToolbar = Play Prev Next \ 3VSeparator1 \ Sort(button=1,relief=none,minwidth=35) \ -HBVolume HBVolume = VolumeIcon(size=menu,button=1) {Column RhythmboxQueue} menutitle = _"Rhythmbox 2nd queue column" title = _"Queue" hreq=text:h width=200 sort= title:i text : text(markup=pesc($title).'\nby '.pesc($artist).'\non '.pesc($album).'',pad=2) [Audacious] Author = aboettger Type = G+ Category = _"Make it look like" Title = %n %t (%m) - gmusicbrowser Default = Window(size=480x400) #SkinPath = gmb-art_skins/audacious_skin #Skin = background.png::e KeyBindings = c-Q Quit \ P OpenCustom(Audacious_PLM) \ S ToggleRandom \ c-P OpenPref \ c-E OpenCustom(Equalizer) \ cs-M ShowHideWidget(HBMenubar) \ cs-P ShowHideWidget(HBPlaylist) \ cs-I ShowHideWidget(HBSonginfo) \ cs-S ShowHideWidget(HBStatusbar) VolumeScroll = HBVolume VBMain = HBMenubar HBToolbar _HBPlaylist HBSonginfo 3HBStatusbar HBSonginfo = 10HBCover \ _VBSongInfo \ 10Filler10 \ 6_Visuals(minwidth=200,maxwidth=200) VBSongInfo = 3Filler4 \ Title(minwidth=200,ellipsize=end,markup="%t") \ Artist(ellipsize=end,markup="%a",click1="") \ Album(ellipsize=end,markup="%l",click1="") HBCover = (maxheight=80,minheight=80,minwidth=80,maxwidth=80) \ Cover(default=elementary/no-cover.svg,showcover=0,minsize=100,maxsize=100) HBMenubar = _MBMenubar MBMenubar = SMMenu1 SMMenu2 SMMenu3 SMMenu5 SMMenu6 SMMenu7 SMMenu1 = (label=_"File") \ # MenuItem11(command=,label=_"Open files") \ MenuItem13(command="RunPerlCode(::ChooseAddPath(0,1))",label=_"Add Files ...") \ # SeparatorMenuItem11 \ MenuItem15(command=OpenPref,label=_"Preferences",icon="gtk-preferences") \ MenuItem16(command=Quit,label=_"Quit",icon="gtk-quit") SMMenu2 = (label=_"Playback") \ SeparatorMenuItem21 \ MenuItem26(command=PlayPause,label=_"Play",icon="gtk-media-play") \ MenuItem27(command=Stop,label=_"Stop",icon="gtk-media-stop") \ MenuItem28(command=PrevSong,label=_"Previous",icon="gtk-media-previous") \ MenuItem29(command=NextSong,label=_"Next",icon="gtk-media-next") \ SeparatorMenuItem215 \ MenuItem215(command="OpenSongProp",label=_"Show song details",icon="gtk-properties") SMMenu3 = (label=_"Playlist") \ LSortItem \ MenuItem32(command=ToggleRandom,label=_"Random Playback",icon="gmb-random") \ SeparatorMenuItem31 \ MenuItem33(command="RunPerlCode(::IdleCheck)",label=_"Refresh",icon="gtk-refresh") \ MenuItem34(command="RunPerlCode(::IdleScan)",label=_"Rescan",icon="gtk-refresh") \ SeparatorMenuItem32 \ MenuItem39(command="OpenCustom(Audacious_PLM)",label=_"Playlists",icon="gmb-playlist") SMMenu5 = (label=_"Output") \ MenuItem51(command=OpenCustom(Equalizer),label=_"Equalizer") SMMenu6 = (label=_"View") \ LayoutItem \ SeparatorMenuItem61 \ MenuItem61(togglewidget=HBMenubar,label=_"Show main window") \ MenuItem62(togglewidget=HBPlaylist,label=_"Show playlist",resize="v") \ MenuItem63(togglewidget=HBSonginfo,label=_"Show info panel",resize="") \ MenuItem64(togglewidget=HBStatusbar,label=_"Show status bar",resize="") SMMenu7 = (label=_"Help") \ MenuItem71(command="RunPerlCode(::AboutDialog)",label=_"About",icon="gtk-about") HBPlaylist = _SongList(mode=playlist,colwidth="track 20 title 380 playandqueue 20 length 50",cols="track title playandqueue length",sort="artist album disc track") HBStatusbar = _Progress(maxheight=10) -Total(mode=filter) HBPlaytime = \ PlayingTime(markup="%s",initsize="XX:XX",xalign=1) Text(markup="/") Length(markup="$length",initsize="XX:XX",xalign=0) # Text(markup=''.$current.'/%m') HBToolbar = HBControls 1HBIndicators _HBTimeBar 5Filler1 HBPlaytime -HBVolume HBIndicators = Sort(button=1) HBVolume = VolumeIcon(button=1) HBTimeBar = _TimeSlider(direct_mode=0) HBControls = Play Stop Prev Next [Audacious_PLM] Default = Window(size=400x250) VBmain = 5_HBMain HBMain = 5_FilterPane4(nb=0,hidebb=0,page=savedtree,pages=savedtree) Title = _"Playlists" [itunes-like] Type=G+ Name = "itunes" Category = _"Make it look like" Default = Window(size=1120x820,hidden=QueueList) VPfp_list(size=40) HPfp0(size=300) HPfp_list_src(size=175) HBIndic = MBmenu Sort 10Filter Queue 10Pos MBmenu = MainMenuItem LayoutItem PSortItem PFilterItem QueueItem SMView SMView = (label=_"View") MenuItem0(togglewidget=FilterPane0,label=_"Genres pane") MenuItem1(togglewidget=HPfp0,label=_"Categories pane") MenuItem2(togglewidget=FilterPane3,label=_"Left pane") MenuItem3(togglewidget=QueueList,label=_"Queue") VBleft = HBIndic _HBButtons HBButtons = Prev Play Next Time _ABtime ABtime = (xalign=0,yscale=0) TimeBar HBupper = VBleft 5_VBright -VBVol VBVol = VolumeIcon _VolumeSlider HBTitle = Filler0 _Title -Stars HBArtist = LockArtist _Artist HBAlbum = LockAlbum _Album -Year VBright = 2HBTitle 2HBArtist 2HBAlbum VBmain = HBupper 5_HPfp_list_src #VBmain = HBupper 5_HPfp_list_src HBStatus HPfp_list_src = VBfpane3 _VBbar_fp_list VBfpane3 = _FilterPane3(nb=1,hidebb=1,pages=savedtree) VProgress Cover(hover_layout=CoverPopup,hover_delay=100) VBbar_fp_list = HBbar _VPfp_list VPfp_list = HPfp0 VPlists VPlists = _SongList QueueList(group=2) HBbar = SimpleSearch MBlist Refresh ResetFilter PlayFilter -Total HPfp0 = FilterPane0(nb=2,hidebb=1,pages=genre) HPfp1 HPfp1 = FilterPane1(nb=3,hidebb=1,pages=artist) FilterPane2(nb=4,hidebb=1,pages=album) #HBStatus = Total _Progress MBlist = HistItem LSortItem PlayItem HSize0 = Filler0 LockArtist LockAlbum VSize1 = 50 VolumeSlider VolumeScroll = HBupper gmusicbrowser-1.1.15~ds0.orig/layouts/songtree.layout0000664000175000017500000000633712565212604022253 0ustar unit193unit193#SongTree Group & Columns definitions ##################################### {Group pic} title= _"with picture" head=title:h vcollapse=head vmin=pic:y+pic:h+2 left=pic:w+2 #title: text(markup=''.pesc($title).'',pad=2) title: text(pad=2, markup=''.pesc($title).\ if(showyear && $year,' ('.pesc($year).')').'',\ init_markup=' ',) pic: +aapic(y=title:h+title:y+4,picsize=picsize,ypad=2) picsize : OptionNumber(default=100,min=20,max=1000,step=10) showyear : OptionBoolean(default=1,name='show year') myfont : OptionFont(default='Arial Bold 18',name='font') {Group simple} title= _"Simple title" head=title:h left=4 vcollapse=head title: text(markup=''.pesc($title).'',pad=4) {Group artistalbumside} title= _"Album and artist on the left side" vmin= pic:y + pic:h +2 left=width title: text(markup=''.pesc($album).'\n'.pesc($artist).'',pad=2,w=left) pic: aapic(y=title:y + title:h +2, picsize=min(picsize,width), pad=2) width: OptionNumber(default=200,min=20,max=1000,step=10) picsize : OptionNumber(default=100,min=20,max=1000,step=10) {Column testtitleandprogress} title= _"Title & progress" sort=album:i title:i width=200 songbl=text hreq=text:h progress: pbar( fill=$progress, hide=!$playing,y=1,h=-2) text: text(markup=playmarkup(pesc($title))) {Column playandqueue} menutitle = _"Playing and queue icons" width = 20 ico: icon(pad=2,icon=$playicon) {Column icolabel} menutitle= _"Labels' icons" sort = label:i width = 50 ico : icon(pad=2,icon=$labelicons) {Column titleaa} menutitle = _"Title - Artist - Album" title = _"Song" hreq=text:h width=200 sort= title:i text : text(markup=''.pesc($title).''.pesc($version_or_empty).'\n'.pesc($artist).' - '.pesc($album).'',pad=2) {Column titleandicon} title = _"Title & icon" sort = title:i width = 200 songbl= text hreq = text:h text : text(pad=2,markup=playmarkup(pesc($title).''.pesc($version_or_empty).'')),w=-icolabel:w) icolabel : icon(pad=2,x=text:w,icon=$labelicons) {Column albumminipic} title = _"Small album picture" sort = album:i width = 20 hreq = 20 album : aapic(picsize=$_h,aa='album') {Column ratingpic} title = _"Rating" menutitle = _"Rating (picture)" sort = rating width=100 #hreq = pic:h #pic : picture(file=ratingpic($rating),init_file=ratingpic(0)) pic : picture(file=ratingpic($rating),h=$_h,w=$_w,resize='ratio',init_file=ratingpic(0)) {Column right_aligned_folder} menutitle = _"Folder (right-aligned)" title = _"Folder" hreq=text:h width=200 sort= path songbl= text text : text(markup= pesc($path), x=-text:w) {Group artistalbum} title= _"Album and artist" head=title:h vcollapse=head vmin=pic:y+pic:h+2 left=pic:w+2 title2: text(markup=''.pesc($artist).'',pad=2,x=-title2:w) #title2: text(markup='by '.pesc($artist).'',pad=2,x=title:w) #title: text(markup=''.pesc($album).' ',pad=2) title: text(markup=''.pesc($album).' ' . if($year,'('.pesc($year).')'),pad=2,w=min($_w-title2:wr-10,title:wr+2*title:xpad)) pic: +aapic(y=title:h+title:y+4,picsize=picsize,ypad=2,aa='album') picsize : OptionNumber(default=100,min=20,max=1000,step=10) gmusicbrowser-1.1.15~ds0.orig/layouts/fullscreen.layout0000664000175000017500000000301712565212604022557 0ustar unit193unit193 [default fullscreen] Type=F Name = _"Default fullscreen" Window = fullscreen=1,sticky=0 HBPics = _ArtistPic(maxsize=0) _Cover(maxsize=0) HBButtons1 = Prev(size=dialog) Next(size=dialog) HBButtons2 = Stop(size=dialog) Play(size=dialog) VBButtons = (border=20) _HBButtons1 _HBButtons2 VBVol = Vol(size=dialog) _VolumeSlider HBLower = VBButtons _VBText 20VBVol HBIndic = 10Sort(size='large_toolbar') 10Filter(size='large_toolbar') Queue(size='large_toolbar') 10Pos -Stars HBTitle = Filler0 _Title HBArtist = LockArtist(size='large_toolbar') _Artist HBAlbum = LockAlbum(size='large_toolbar') _Album -Year HBTime = Time _TimeBar VBmain = (border=20) _HBPics FBLower FBLower = .1,0,.8,0 HBLower VBText = HBIndic 2HBTitle 2HBArtist 2HBAlbum -HBTime VolumeScroll = VBmain HSize0 = Filler0 LockArtist LockAlbum DefaultFont = 20 KeyBindings = Escape CloseWindow [Fullscreen simple] Type=F Name = _"Fullscreen simple" Window = fullscreen=1,sticky=0 HBPics = _ArtistPic(maxsize=0) _Cover(maxsize=0) HBButtons1 = Prev(size=dialog) Next(size=dialog) HBButtons2 = Stop(size=dialog) Play(size=dialog) VBButtons = _HBButtons1 _HBButtons2 HBLower = VBButtons _VBText HBTitle = Filler0 _Title HBArtist = LockArtist(size='large_toolbar') _Artist HBAlbum = LockAlbum(size='large_toolbar') _Album -Year HBTime = Time _TimeBar VBmain = (border=20) _HBPics FBLower FBLower = .1,0,.8,0 HBLower VBText = 2HBTitle 2HBArtist 2HBAlbum -HBTime VolumeScroll = VBmain HSize0 = Filler0 LockArtist LockAlbum DefaultFont = 20 KeyBindings = Escape CloseWindow gmusicbrowser-1.1.15~ds0.orig/layouts/titlebar.layout0000664000175000017500000000176212565212604022230 0ustar unit193unit193 #### Overlay layouts for Titlebar plugin [O_play] Type= O Name= _"Play button" HBmain = Play(size=menu,button=0,tip="%t by %a",group=Play) [O_stop_play_next] Type= O Name= _"Stop, Play and Next buttons" HBmain = Stop(size=menu,button=0) Play(size=menu,button=0) Next(size=menu,button=0) [O_stop_play_next_timebar] Type= O Name= _"Stop, Play and Next buttons and Time" HBmain = Stop(size=menu,button=0) Play(size=menu,button=0) TimeBar(minwidth=100,text=%t) Next(size=menu,button=0) [O_stop_play_next_title_artist] Type= O Name= _"Stop, Play and Next buttons and Title/Artist" HBmain = Stop(size=menu,button=0) Play(size=menu,button=0) Next(size=menu,button=0) _Title_by(minwidth=200) [O_title_artist_insens_left] Type= O Name= _"Insensitive title-artist (left-aligned)" HBmain = _Title_by(ellipsize=end) Window = insensitive=1,size=250x1 [O_title_artist_insens_right] Type= O Name= _"Insensitive title-artist (right-aligned)" HBmain = _Title_by(ellipsize=end,xalign=1) Window = insensitive=1,size=250x1 gmusicbrowser-1.1.15~ds0.orig/layouts/contrib.layout0000664000175000017500000007763412565212604022075 0ustar unit193unit193[Conz Aardvark] Author = "Satoshi Hayazaki" Type=G+ Category = Conz Default = Window(size=80%x80%) VBall = MBmenu HBStatus _HPmain MBmenu = MainMenuItem LayoutItem PSortItem PFilterItem QueueItem HBStatus = Sort Prev(size=menu) Stop(size=menu) Play(size=menu) Next(size=menu) _TimeBar(minwidth=500,text="%t by %a") LabelTime Length _Album(markup=' %l (%y)') -TogButton(label="Filter",widget=HBSearch,size=menu) Pref(size=menu) -Quit(size=menu) HPmain = _HPpart HPpart = VBpart2 NBpart3 VBpart2 = HBSearch _VPcenter NBpart3 = PluginLyrics(shadow=none,HideToolbar=1) QueueList(hideif=empty,songtree=1,cols="albumminipic titleaa length") PluginArtistinfo(tabtitle="Artist Info") VPAlbum VPAlbum = (tabtitle="Album") AABox(aa=album) _SongTree1(group=Play:artist) VPcenter = _VBcenterdown VBcenterdown = _SongTree(grouping="artist|pic(myfont=Arial%20Italic%2018,picsize=20,showyear=)|album|pic(myfont=Arial%20Bold%2018,picsize=250,showyear=1)") HBSearch = SimpleSearch -Total [Conz Glimm (different controls)] Author = "Satoshi Hayazaki" Type=G+ Category = Conz Name = _"Conz Glimm (different controls)" Default = Window(size=80%x80%) VBall = MBmenu 3Filler1 HBplaybar _HPmain 2Filler6 MBmenu = MainMenuItem LayoutItem PSortItem PFilterItem QueueItem HBplaybar = Play(size=large-toolbar) 2Filler4 _VBplaycenter 4Filler8 -Quit VBplaycenter = HBtitles HBtimebar HBtitles = Title1(markup=" ◄◄ %t〈 %a 〉", group=Recent, click1=PrevSong, markup_empty="", minsize=50, tip='Click for previous song') \ _10Title_by(xalign=.5, tip='Click to play/pause song', click1=PlayPause) \ Title2(markup="〈 %a 〉%t ►► ",group=Next, click1=NextSong, xalign=1, markup_empty="Next", minsize=50, tip='Click for next song') HBtimebar = Time(markup=%s,initsize="-XX:XX") _Scale LabelTime(markup='%m ') HPmain = _HPpart HPpart = VBpart2 NBpart3 VBpart2 = HBSearch _VPcenter NBpart3 = PluginLyrics(shadow=none,HideToolbar=1) QueueList(hideif=empty,songtree=1,cols="albumminipic titleaa length") PluginArtistinfo(tabtitle="Artist Info") VPAlbum VPAlbum = (tabtitle='Album') AABox(aa=album) _SongTree1(group=Play:artist) VPcenter = _VBcenterdown VBcenterdown = _SongTree(grouping="artist|pic(myfont=Arial%20Italic%2018,picsize=20,showyear=)|album|pic(myfont=Arial%20Bold%2018,picsize=600,showyear=1)" cols="Spacer playandqueueandtrack titleartist year length" ) HBSearch = SimpleSearch Refresh PlayFilter FLock _Total(xalign=.5) -Sort -Pref(size=menu) -VolumeIcon [Conz Aishi (with Filter Panes)] Author = "Satoshi Hayazaki" Type=G Category = Conz Name = _"Conz Aishi (with Filter Panes)" Default = Window(size=80%x80%) VBfirst = 5Filler5 _HPall 2Filler6 HPall = VBall VPpanelyr VBall = VBControls _HPmain VBControls = HBStatus HBProgress HBControls HBStatus = 4Filler1 _HBTitle(xalign=.5, yalign=1) -Stars(yalign=0.5) HBTitle = _Title_by(expand_max=300,ellipsize=end,markup="%t by %a in %l (%y)",tip=_"Title: %t (Track No. %n)",yalign=0.5) HBControls = Prev Stop Play Next 10Filler7 VolumeIcon(click1="") _VolumeSlider(horizontal=1, maxwidth=120) -ABSearch ABSearch = (yscale=0) SimpleSearch HPmain = _HPpart HPpart = _VPcenter VPpanelyr = HPFilters NBpart3 #HPFilters = HPFilter1 FPane2(pages=album, hidebb=1, rules_hint=1) #HPFilter1 = FPane0(pages=genre, hidebb=1) FPane1(pages=artist, hidebb=1) HPFilters = FPane0(pages=genre, hidebb=1) HPFilter1 HPFilter1 = FPane2(pages=album, hidebb=1, rules_hint=1) FPane1(pages=artist, hidebb=1) NBpart3 = (tabpos="bottom") PluginLyrics(shadow=none,HideToolbar=1) QueueList(hideif=empty,songtree=1,cols="albumminipic titleaa length") PluginArtistinfo(tabtitle="Artist Info") VPcenter = _VBcenterdown VBcenterdown = _SongTree(grouping="artist|pic(myfont=Arial%20Italic%2018,picsize=20,showyear=)|album|pic(myfont=Arial%20Bold%2018,picsize=250,showyear=1) cols="Spacer playandqueuetrack titleartist year length") HBTotal HBTotal = Pref(size=menu) Sort #VolumeIcon #_VolumeSlider(horizontal=1) -Total -150Filler3 HBProgress = Time(markup=%s,initsize="-XX:XX") _Scale LabelTime(markup='%m ') [Garage5] Author= MajorGrubert (with aboettger's inspiration) Type=G+ Category = "Garage" # Major Grubert was a fictional character being used by Moebius in his comics from the 1970s. # Major Grubert appeared in "Le Garage Hermétique" Default = Window(size=90%x80%) HPMaincenterandright(size=600-650) HPNowplaying(size=210-450) VPtri3(size=340-580) VolumeScroll = VBVolume HPNowplaying VBMain = HBMenu _VBMainContainer HBMenu = _MBmenu MBmenu = MainMenuItem LayoutItem PSortItem PFilterItem QueueItem SMViews SMViews = (label=_"View") \ MenuItem01(togglewidget=HBtri1,label="Filter 1") \ MenuItem02(togglewidget=VPtri2,label="Filter 2") \ MenuItem03(togglewidget=VPtri3,label="Filter 3") \ MenuItem04(togglewidget=HBAlbums1,label="Albums 1") \ MenuItem05(togglewidget=HBAlbums2,label="Albums 2") VBMainContainer = 3Filler1 _HPMainPannel 3Filler2 HPMainPannel = _VBMainleft _HPMaincenterandright ############################ # Left ############################ VBMainleft = HBSearchBar _HPTri HBSearchBar = (minheight=34,maxheight=34) ResetFilter2 2_SimpleSearch(noselector=1,suggest=0) HPTri = _HBtri1 _VPtri2 HBtri1 = _FilterPane1(nb=1,\ hidetabs=0,\ hidebb=1,\ page=savedtree,\ pages=savedtree) VPtri2 = HBArtist HBAlbums1 HBArtist = _FilterPane2(nb=2,\ hidebb= 1,\ hidetabs= 0,\ min= 1,\ page= artists,\ page_artists/depth= 1,\ page_artists/lmarkup= 1,\ page_artists/lpicsize= '-1|16',\ pages= artists) HBAlbums1 = _FilterPane3(nb=3,\ hidebb= 1,\ hidetabs= 0,\ min= 1,\ page= album,\ page_album/lmarkup= 1,\ page_album/lpicsize= 64,\ page_album/mmarkup= below,\ page_album/mode=mosaic,\ page_album/sort= year,\ pages=album) ############################ # Center ############################ HPMaincenterandright = _VPtri3 _VPMainright VPtri3 = _HBAlbums2 VBSongtree HBAlbums2 = _FilterPane4(nb=3,\ hidebb= 1,\ hidetabs= 0,\ min= 1,\ page= album,\ page_album/lmarkup= 1,\ page_album/lpicsize= 64,\ page_album/mmarkup= below,\ page_album/mode=mosaic,\ page_album/sort= year,\ pages=album) VBSongtree = _SongTree HBTotal HBTotal = -Total(size=small) ############################ # Right ############################ VPMainright = HPNowplaying _HBListAndBouton ########Top HPNowplaying = VBCover _HBSongInfoEtControlsEtTimebarEtVolume VBCover = _Filler6 Cover(default=elementary/no-cover.svg,showcover=0,minsize=200,maxsize=400) Stars _Filler7 HBSongInfoEtControlsEtTimebarEtVolume = _VBSongInfoEtControlsEtTimebar VBVolume VBVolume = VolumeIcon(button=1) _VolumeSlider VBSongInfoEtControlsEtTimebar = -HBControls _VBSongInfoEtTimebar VBSongInfoEtTimebar = _Filler8 VBSongInfo _Filler9 -HBTimeBar VBSongInfo = \ _HBInfoTitre \ _HBInfoArtist \ _HBInfoalbum \ _HBInfoGenre HBInfoTitre = 2Filler10 _Title(ellipsize=end,markup="%t",click1="",yalign=0.5) HBInfoArtist = 2Filler11 _Artist(yalign=1,ellipsize=end,markup=_"by %a",click1="") HBInfoalbum = 2Filler12 _Album(ellipsize=end,yalign=1,markup=_"in %l",click1="") -2Filler13 -Date HBInfoGenre = -2Filler20 -_Text2(yalign=1,ellipsize=end,markup="%g ") group HBTimeBar = 2Filler14 _TimeBar 2Filler15 HBControls = \ _Filler16 \ Prev(size=large-toolbar,tip=_"Previous Song") \ Play(size=large-toolbar,tip=_"Play/Pause") \ Stop(size=large-toolbar,tip=_"Stop") \ Next(size=large-toolbar,tip=_"Next Song") \ _Filler17 ########Bottom HBListAndBouton = _NBSidebar1 VBBouton #TBRight = _"Library" HPAlbumAndSongs _"Queue" VBQueueList _"Context" Context NBSidebar1 = (tabpos=left90,typesubmenu='PC') \ VBQueueList \ VBPlayList \ Context VBQueueList = (tabtitle=_"Queue") \ EditListButtons(group=2,small=1,relief=none) \ _QueueList(cols="queuenumber titleaa",colwidth="queuenumber 20",sort="path album:i disc track file",songtree=0,group=2) \ HBQueueActions HBQueueActions = Total1(size="small",group=2) -QueueActions VBPlayList = (tabtitle=_"Playlist") \ _SongList(cols="playandqueue titleaa",colwidth="queuenumber 20",sort="path album:i disc track file",mode=playlist,group=1,follow=1) ############## Boutons ############################ VBBouton = \ Fullscreen(size=button,minwidth=34,maxwidth=34) \ Playlist(size=button,minwidth=34,maxwidth=34) \ ResetFilter \ HSeparator1 \ Sort(size=button,minwidth=34,maxwidth=34) \ 10Filter(size=button,minwidth=34,maxwidth=34) \ Queue(size=button,minwidth=34,maxwidth=34) \ -Quit(size=button,minwidth=34,maxwidth=34,tip=_"Quit") \ -Pref(stock=gtk-preferences,size=button,minwidth=34,maxwidth=34) \ -HSeparator2 \ #-MBLayouts \ -HSeparator3 \ -Button1(click1="RunPerlCode(::ChooseAddPath(0,1))",stock=gtk-add,size=button,minwidth=34,maxwidth=34,tip=_"Add folder ...") \ -Button2(click1="RunPerlCode(::IdleCheck)",stock=gtk-refresh,size=button,minwidth=34,maxwidth=34,tip=_"check now") \ -Button3(click1="RunPerlCode(::IdleScan)",stock=gtk-zoom-in,size=button,minwidth=34,maxwidth=34,tip=_"scan now") [Garage6] Type=G+ #Title = "gmusicbrowser joue %t dans l'album %l (%Y) de %a" Title = "gmusicbrowser is playing %t from %l (%Y) by %a" Category = "Garage" DefaultFocus = SimpleSearch1 VolumeScroll = HBVolume Default = Window(size=1000x750) Window = hidden=VBCover|Context|VBQueue|ABSearchBox2|HBSongPlaylist Author = Major Grubert (Shimmer adaptation and Shimmer is simon@shimmerproject.org) ### main window containers : top bar and main ### VBMain = VBTop HSeparator _HBMain ### top bar from left to right ### VBTop = HBTop HBTop = HBButtons VSeparator1 HBVolume VSeparator2 _15VBPlayer VSeparator3 -HBSettings HBButtons = Prev Play Stop Next(click2=NextAlbum) HBVolume = (yalign=0,yscale=0.0) VolumeIcon(button=1,click2=mute,tip=_"Left-clic or scrollwheel to change, right-click to mute") VBPlayer = 1Filler0 HBTitle HBTimeSlider HBTitle = \ Title(expand_max=300,ellipsize=end,markup="%t ",tip=_"Title: %t (Track No. %n)",yalign=0.5) \ LockAlbum(stock="on:gmb-lock gmb-lockopen off:gmb-breadcrumb gmb-locklight") \ Album(tip=_"Album: %l (%Y)",ellipsize=end,expand_max=200,yalign=0.5,markup=" %l ",showcover=0) \ LockArtist(stock="on:gmb-lock gmb-lockopen off:gmb-breadcrumb gmb-locklight") \ Artist(tip=_"Artist: %a",expand_max=200,yalign=0.5,ellipsize=end,markup=" %a") \ -Stars(yalign=0.5) HBTimeSlider = \ PlayingTime(markup="%s",initsize="XX:XX",xalign=0) \ _TimeSlider(direct_mode=1) \ -Length(markup="$length",initsize="XX:XX",xalign=1) HBSettings = \ ToggleButton11(relief=none,size=menu,icon=gmb-library,widget=VBLeft,tip=_"Library") \ ToggleButton12(relief=none,size=menu,icon=gmb-artist,widget=VBArtist,tip=_"Artists") \ ToggleButton13(icon=gmb-queue,relief=none,size=menu,widget=VBQueue,tip=_"Queue") \ ToggleButton14(icon=gtk-about,relief=none,size=menu,widget=Context,tip=_"Context") \ VSeparator4 \ ExtraButtons(size=large-toolbar) \ BMSettings \ Quit2(size=large-toolbar,minwidth=34,maxwidth=34,tip=_"Quit") BMSettings = (icon=gtk-preferences,size="large-toolbar") \ SMLibrary \ LayoutItem \ PlayItem \ SeparatorMenuItem01 \ MenuItem34(command=OpenCustom(Equalizer),label=_"Equalizer",icon=gmb-equalizer) \ SeparatorMenuItem20 \ MenuItem14(command=OpenPref,label=_"Settings",icon="gtk-preferences") \ MenuItem05(command=Quit,label=_"Quit",icon="gtk-quit") SMLibrary = (label=_"Library") \ MenuItem00(command="RunPerlCode(::ChooseAddPath(0,1))",label=_"Add Music",icon="gtk-add") \ MenuItem32(command="RunPerlCode(::IdleScan)",label=_"Scan Collection",icon="gtk-refresh") \ MenuItem33(command="RunPerlCode(::IdleCheck)",label=_"Check Collection",icon="gtk-zoom-in") ### main : Filter1, Artist, Songlist, Queue, Context ### HBMain = VBLeft VBArtist _VBSongStatus VBQueue Context ### Filter1 VBLeft = \ ABSearchBox \ _VPFilterCover \ HBStatus ABSearchBox = (yalign=0) SimpleSearch1(suggest=1) VPFilterCover = _NBFilter1 VBCover NBFilter1 = (tabpos="bottom") VBFilter10 VBFilter11 VBFilter12 VBFilter13 VBFilter14 VBFilter10 = (tabtitle=_"Filter") _FilterPane10(nb=2,hidebb=1,pages=filter,page_filter) VBFilter11 = (tabtitle=_"List") _FilterPane11(nb=2,hidebb=1,pages=list) VBFilter12 = (tabtitle=_"Folder") _FilterPane12(nb=2,hidebb=1,pages=folder,page_folder) VBFilter13 = (tabtitle=_"Genre") _FilterPane13(nb=2,hidebb=1,pages=genre,page_genre/mode=cloud,page_genre/cloud_max=30,page_genre/cloud_min=12) VBFilter14 = (tabtitle=_"Artists") _FilterPane14(nb=2,hidebb=1,pages=artists,page_artists/lmarkup=1,hidetabs=1,lmarkup="%a%Y\x0a%x / %s / %l") VBCover = _Filler1 HBCover _Filler2 HBCover = _Filler3 Cover(overlay=6x6:350x350:elementary/overlay.png,default=elementary/no-cover.svg,showcover=0) _Filler4 HBStatus = \ ToggleButton2(icon=gmb-picture,relief=none,size=menu,widget=VBCover,tip=_"Show/Hide Cover") \ -3Total(format=short,relief=none,button=1,mode=library,size="small") ### Artist VBArtist = _FilterPane1(nb=3,hidebb=1,pages=artist,page_artist/lmarkup=1,hidetabs=0,lmarkup="%a%Y\x0a%x / %s / %l",page=artists,page=genre,page=year,page=rating) ### Songlist VBSongStatus = (tabtitle=_"Playlist") ABToggle _VBMosaicSongList HBTotal ABToggle = (yalign=0,yscale=0.0) HBToggle HBToggle = \ ToggleButton31(relief=none,size=menu,icon=gmb-album,widget=VPMosaic,tip=_"Albums") \ VSeparator5 \ ToggleButton32(relief=none,size=menu,icon=gmb-view-list,widget=HBSongPlaylist,togglegroup=1,tip=_"Simple List View") \ ToggleButton33(relief=none,size=menu,icon=gmb-view-tree,widget=SongTree,togglegroup=1,tip=_"Songtree View") \ VSeparator6 \ ToggleButton34(relief=none,size=menu,icon=gtk-find,widget=ABSearchBox2,tip=_"Search") \ _ABSearchBox2 \ -Sort(button=1,size=menu,tip=_"Right-click to toggle shuffle/random") \ -2Filter35(button=1,size=menu,tip=_"Right-click to remove filters") \ -VSeparator7 ABSearchBox2 = (yalign=0) SimpleSearch2(suggest=1) VBMosaicSongList = VPMosaic _HBSongListtree VPMosaic = _FilterPane3(nb=4,hidebb=1,pages=album,page_album/mode=mosaic,page_album/mmarkup=1,page_album/mpicsize=96,hidetabs=1) HBSongListtree = \ _SongTree(cols="playandqueueandtrack title length ratingpic",colwidth="artist 124 lastplay 107 length 49 playandqueue 19 playandqueueandtrack 20 playcount 22 ratingpic 100 title 390 titleaa 397 track 21",grouping="album|artistalbum_breadcrumbs(picsize=100)|disc|discleft(width=15)",follow=1,sort="year album disc track") \ _HBSongPlaylist HBSongPlaylist = _SongList(cols="playandqueue track title artist album year length playcount",sort=artist,colwidth="album 200 artist 200 file 400 lastplay 100 length 41 path 24 playcount 96 rating 80 title 270 track 21 year 31",follow=1,sort="year album disc track") HBTotal = -Total2(size=small) ### Queue VBQueue = \ EditListButtons(group=3,small=1,relief=none) \ _QueueList(group=3,songtree=1,tabicon="",cols="queuenumber titleaa",colwidth="queuenumber 20 titleaa 248",showbb=1) \ HBQueueActions HBQueueActions = -Total3(size="small",group=3) ### positioning and sizing ### HSize0 = 300 VBLeft VBArtist VBQueue HSize1 = 400 Context VSize0 = 300 HBCover VSize1 = 144 VPMosaic ################################################################################# [EeePC] Author= MajorGrubert (with aboettger's help and inspiration) Type=G+ Category = "Garage" Default = Window(size=90%x80%) HPMainPannel(size=200-800) HPSubMainPannel(size=440-320) VPMainCenter(size=200-380) VPMainright(size=200) VolumeScroll = VBVolume HPNowplaying VBMain = HBMenu 1Filler2 _HBMainContainer 1Filler3 HBMenu = _MBmenu MBmenu = MainMenuItem LayoutItem HBMainContainer = _HPMainPannel VBBouton VBBouton = \ Prev(size=button,minwidth=34,maxwidth=34) \ Play(size=button,minwidth=34,maxwidth=34) \ Stop(size=button,minwidth=34,maxwidth=34) \ Next(size=button,minwidth=34,maxwidth=34) \ _VBVolume \ -Quit(size=button,minwidth=34,maxwidth=34,tip=_"Quit") \ -Pref(stock=gtk-preferences,size=button,minwidth=34,maxwidth=34) \ -Button1(click1="RunPerlCode(::ChooseAddPath(0,1))",stock=gtk-add,size=button,minwidth=34,maxwidth=34,tip=_"Add folder ...") \ -Button2(click1="RunPerlCode(::IdleCheck)",stock=gtk-refresh,size=button,minwidth=34,maxwidth=34,tip=_"check now") \ -Button3(click1="RunPerlCode(::IdleScan)",stock=gtk-zoom-in,size=button,minwidth=34,maxwidth=34,tip=_"scan now") \ -Queue(size=button,minwidth=34,maxwidth=34) \ -10Filter(size=button,minwidth=34,maxwidth=34) \ -Sort(size=button,minwidth=34,maxwidth=34) \ -ResetFilter \ -Playlist(size=button,minwidth=34,maxwidth=34) \ -Fullscreen(size=button,minwidth=34,maxwidth=34) VBVolume = VolumeIcon(button=1) _VolumeSlider HPMainPannel = _VBLeftPanel _VBCenterAndRight ############################ # Left ############################ VBLeftPanel = HBSearchBar \ _FilterPane1(nb=1,hidetabs=0,hidebb=1,page=savedtree,pages=savedtree) HBSearchBar = (minheight=34,maxheight=34) \ ResetFilter2 2_SimpleSearch(noselector=1,suggest=0) VBCenterAndRight = 2Filler60 HBNowplaying 2Filler61 _HPSubMainPannel HBNowplaying = 5_TimeBar(text="%t by %a in %l (%m)",minwidth=240) HPSubMainPannel = _VPMainCenter _VPMainright ############################ # Center ############################ VPMainCenter = _HBAlbums VBSongtree HBAlbums = _FilterPane2(nb=2,\ hidebb= 1,\ hidetabs= 0,\ min= 1,\ page= album,\ page_album/lmarkup= 1,\ page_album/lpicsize= 64,\ page_album/mmarkup= below,\ page_album/mode=mosaic,\ page_album/sort= year,\ pages=album) VBSongtree = _SongTree HBTotal HBTotal = -Total(size=small) ############################ # Right ############################ VPMainright = _NBSidebar1 NBSidebar1 = (tabpos=left90,typesubmenu='PC') \ VBQueueList \ VBPlayList \ Context VBQueueList = (tabtitle=_"Queue") \ EditListButtons(group=2,small=1,relief=none) \ _QueueList(cols="queuenumber titleaa",colwidth="queuenumber 20",sort="path album:i disc track file",songtree=0,group=2) \ HBQueueActions HBQueueActions = Total1(size="small",group=2) -QueueActions VBPlayList = (tabtitle=_"Playlist") \ _SongList(cols="playandqueue titleaa",colwidth="queuenumber 20",sort="path album:i disc track file",mode=playlist,group=1,follow=1) [EeePC2] Type=G+ #Title = "gmusicbrowser joue %t dans l'album %l (%Y) de %a" Title = _"gmusicbrowser is playing %t from %l (%Y) by %a" Category = "Garage" DefaultFocus = SimpleSearch1 VolumeScroll = HBVolume Default = Window(size=90%x80%) Window = hidden=VBCover|Context|VBQueue|ABSearchBox2|HBSongPlaylist Author = Major Grubert (Shimmer adaptation and Shimmer is simon@shimmerproject.org) ### main window containers : main and buttons ### HBMain = 1Filler1 _VBMain VBButtons VBMain = 1Filler2 VBTop 1Filler3 _HBMain2 ### top bar from left to right ### VBTop = HBTop HBTop = \ ABSearchBox \ _HBNowplaying ABSearchBox = (yalign=0) SimpleSearch1(suggest=1) HBNowplaying = 5_TimeBar(text="%t by %a in %l (%m)",minwidth=240) ### main : Filter Songlist, Queue, Context ### HBMain2 = HBLeft _VBSongStatus 1Filler4 VBQueue Context 1Filler5 ### Filter1 HBLeft = _VBLeft 2Filler6 VBLeft = \ _VPFilterCover \ HBStatus VPFilterCover = _NBFilter1 VBCover NBFilter1 = (tabpos="bottom") VBFilter10 VBFilter11 VBFilter12 VBFilter13 VBFilter14 VBFilter10 = (tabtitle=_"Filter") _FilterPane10(nb=2,hidebb=1,pages=filter,page_filter) VBFilter11 = (tabtitle=_"List") _FilterPane11(nb=2,hidebb=1,pages=list) VBFilter12 = (tabtitle=_"Genre") _FilterPane13(nb=2,hidebb=1,pages=genre,page_genre/mode=cloud,page_genre/cloud_max=30,page_genre/cloud_min=12) VBFilter13 = (tabtitle=_"Artists") _FilterPane14(nb=2,hidebb=1,pages=artists,page_artists/lmarkup=1,hidetabs=1,lmarkup="%a%Y\x0a%x / %s / %l") VBCover = _Filler7 HBCover _Filler8 HBCover = _Filler9 Cover(overlay=6x6:350x350:elementary/overlay.png,default=elementary/no-cover.svg,showcover=0) _Filler10 HBStatus = \ ToggleButton1(icon=gmb-picture,relief=none,size=button,widget=VBCover,tip=_"Show/Hide Cover") \ -3Total(format=short,relief=none,button=1,mode=library,size="small") ### Songlist VBSongStatus = (tabtitle=_"Playlist") _VBMosaicSongList HBTotal VBMosaicSongList = VPMosaic _HBSongListtree VPMosaic = _FilterPane1(nb=4,hidebb=1,pages=album,page_album/mode=mosaic,page_album/mmarkup=1,page_album/mpicsize=64,hidetabs=1) HBSongListtree = \ _SongTree(cols="playandqueueandtrack title length ratingpic",colwidth="artist 124 lastplay 107 length 49 playandqueue 19 playandqueueandtrack 20 playcount 22 ratingpic 100 title 390 titleaa 397 track 21",grouping="album|artistalbum_breadcrumbs(picsize=100)|disc|discleft(width=15)",follow=1,sort="year album disc track") \ _HBSongPlaylist HBSongPlaylist = _SongList(cols="playandqueueandtrack title artist album year length playcount",sort=artist,colwidth="album 200 artist 200 file 400 lastplay 100 length 41 path 413 playandqueueandtrack 24 playcount 96 rating 80 title 270 track 21 year 31",follow=1,sort="year album disc track") HBTotal = \ ToggleButton21(relief=none,size=button,icon=gmb-view-list,widget=HBSongPlaylist,togglegroup=1,tip=_"Simple List View") \ ToggleButton22(relief=none,size=button,icon=gmb-view-tree,widget=SongTree,togglegroup=1,tip=_"Songtree View") \ Sort(button=1,size=button,tip=_"Right-click to toggle shuffle/random") \ 2Filter35(button=1,size=button,tip=_"Right-click to remove filters") \ -Total2(size=small) ### Queue VBQueue = \ EditListButtons(group=3,small=1,relief=none) \ _QueueList(group=3,songtree=1,tabicon="",cols="queuenumber titleaa",colwidth="queuenumber 20 titleaa 248",showbb=1) \ HBQueueActions HBQueueActions = -Total3(size="small",group=3) ### Buttons VBButtons = VBPlayingControls _VBVolume VBPanelsControls VBPlayingControls = Prev(size=button) Play(size=button) Next(click2=NextAlbum,size=button) VBVolume = VolumeIcon(button=1,click2=mute,tip=_"Left-clic or scrollwheel to change, right-click to mute") _VolumeSlider VBPanelsControls = \ ToggleButton31(icon=gtk-about,relief=none,size=menu,widget=Context,tip=_"Context") \ -Quit2(size=button,minwidth=34,maxwidth=34,tip=_"Quit") \ -BMSettings \ -ExtraButtons(size=button) BMSettings = (icon=gtk-preferences,size="button") \ SMLibrary \ LayoutItem \ PlayItem \ SMViews \ SeparatorMenuItem01 \ MenuItem11(command=OpenCustom(Equalizer),label=_"Equalizer",icon=gmb-equalizer) \ SeparatorMenuItem20 \ MenuItem12(command=OpenPref,label=_"Settings",icon="gtk-preferences") \ MenuItem13(command=Quit,label=_"Quit",icon="gtk-quit") SMLibrary = (label=_"Library") \ MenuItem21(command="RunPerlCode(::ChooseAddPath(0,1))",label=_"Add Music",icon="gtk-add") \ MenuItem22(command="RunPerlCode(::IdleScan)",label=_"Scan Collection",icon="gtk-refresh") \ MenuItem23(command="RunPerlCode(::IdleCheck)",label=_"Check Collection",icon="gtk-zoom-in") SMViews = (label="Show/Hide") \ MenuItem31(togglewidget=HBLeft,label=_"Library",icon="gmb-library") \ MenuItem32(togglewidget=VBQueue,label=_"Queue",icon="gmb-queue") \ MenuItem34(togglewidget=VPMosaic,label=_"Albums",icon="gmb-album") \ MenuItem35(togglewidget=ABSearchBox,label=_"Search",icon="gtk-find") ### positioning and sizing ### HSize0 = 300 VBQueue Context HSize1 = 250 VBLeft ABSearchBox VSize0 = 250 HBCover VSize1 = 105 VPMosaic ################################################################################# ################################################################################# ################################################################################# [Garage Fullscreen] Author= MajorGrubert Type=F Name = _"Garage Fullscreen" Window = fullscreen=1 VBmain = _HPContentFull -HBReplayButtonsFull HBReplayButtonsFull = Prev(size=menu) Play(size=menu) Next(size=menu) 20VSeparator 5_TimeBar(text=_"%t by %a (%m)",minwidth=240) 10-Stars HPContentFull = HBCoverFull _NBSidebarFull HBCoverFull = _Cover(click1="",reflection=1,overlay=6x6:778x778:awoken/overlay.png,default=awoken/no-cover.png,showcover=0) NBSidebarFull = (tabpos=right90,typesubmenu='PC') \ VBQueueList \ VBPlayList VBQueueList = (tabtitle=_"Queue") \ EditListButtons(group=2,small=1,relief=none) \ _QueueList(cols="queuenumber titleaa",colwidth="queuenumber 20",sort="path album:i disc track file",songtree=0,group=2) \ HBQueueActions HBQueueActions = Total1(size="small",group=2) -QueueActions VBPlayList = (tabtitle=_"Playlist") \ _SongList(cols="playandqueue titleaa",colwidth="queuenumber 20",sort="path album:i disc track file",mode=playlist,group=1,follow=1) KeyBindings = Escape CloseWindow VolumeScroll = VBmain [Parking5] Type=G+ Title = "gmusicbrowser" Category = "Garage" DefaultFocus = SimpleSearch1 VolumeScroll = VBVolume HBVolume Default = Window(size=1370x750) Window = hidden=HBSongPlaylist|HBMosaic|VBButtonMore2|NBContextMain|HPLeft2|HPLeft3|VBAlbum|HBButtomLess2|HBButtomLess3|HBButtomLess4|HBButtomMore2|HBButtomMore3 Author = Major Grubert (inspired by Shimmer layouts) ## main : left / right HPMain = _VBLeft _VBRight ### Left VBLeft = _HPLeft1 HBStatus HPLeft1 = _VBLibrary _HPLeft2 HPLeft2 = _VBGenre _HPLeft3 HPLeft3 = _VBArtist _VBAlbum HBStatus = \ 3Total1(format=short,relief=none,button=1,mode=library,size="small") \ -HBButtomLess2 \ -HBButtomLess3 \ -HBButtomLess4 \ -HBButtomMore1 \ -HBButtomMore2 \ -HBButtomMore3 HBButtomLess2 = ToggleButton11(relief=none,togglegroup=40,size=menu,icon=gtk-clear,widget=HBButtomMore1,tip=_"Close a filter") HBButtomLess3 = ToggleButton12(relief=none,togglegroup=40,size=menu,icon=gtk-clear,widget=HPLeft2|HBButtomMore2|HBButtomLess2,tip=_"Close a filter") HBButtomLess4 = ToggleButton13(relief=none,togglegroup=40,size=menu,icon=gtk-clear,widget=HPLeft3|HPLeft2|HBButtomMore3|HBButtomLess3,tip=_"Close a filter") HBButtomMore1 = ToggleButton14(relief=none,togglegroup=40,size=menu,icon=gtk-add,widget=HPLeft2|HBButtomLess2|HBButtomMore2,tip=_"Add a filter") HBButtomMore2 = ToggleButton15(relief=none,togglegroup=40,size=menu,icon=gtk-add,widget=HPLeft3|HPLeft2|HBButtomLess3|HBButtomMore3,tip=_"Add a filter") HBButtomMore3 = ToggleButton16(relief=none,togglegroup=40,size=menu,icon=gtk-add,widget=HPLeft3|HPLeft2|VBAlbum|HBButtomLess4,tip=_"Add a filter") #### Library filter VBLibrary = ABSearchBox _FilterPane0(nb=2,hidebb=1,hidetabs=0,tabpos=left90,\ pages=savedtree|folder,\ page=savedtree) ABSearchBox = (yalign=0) SimpleSearch1(suggest=1) #### Genre Filter VBGenre = _FilterPane1(nb=3,hidebb=1,hidetabs=0,\ page=genre,\ pages=genre|year|added,\ page_genre/mode=cloud,\ page_genre/cloud_max=35,\ page_genre/cloud_min=10,\ page_year/cloud_max=40,\ page_year/cloud_min=12,\ page_year/mode=cloud,\ page_added/depth=1,\ page_added/type="added.year|added.month") #### Artist filter VBArtist = _FilterPane2(nb=4,hidebb=1,hidetabs=0,\ pages=artists,page=artists) #####Album filter VBAlbum = _FilterPane3(nb=5,hidebb=1,hidetabs=0,\ pages=album,page=album,\ page_album/mode=mosaic,page_album/mmarkup=0,page_album/mpicsize=56) ### Right : top / main right panel VBRight = HBTop _HPRight #### Top right HBTop = HBCover VBButtons _15VBPlayer(yalign=0.5) VBSettings(yalign=0.5) HBCover = _Filler10 VBCover _Filler11 VBCover = _Filler20 Cover(overlay=6x6:350x350:elementary/overlay.png,default=elementary/no-cover.svg,showcover=0) _Filler21 VBButtons = _Filler30 HBPlaypause HBPrevnext _Filler31 HBPrevnext = _Filler40 Prev(size=menu) Next(click2=NextAlbum,size=menu,tip=_"Middle-clic for next album") _Filler41 HBPlaypause = _Filler50 Play _Filler51 VBPlayer = _Filler60 HBTitle HBAlbumAndArtist HBTimeSlider _Filler61 HBTitle = _Title(expand_max=300,ellipsize=end,markup="%t ",click1="",tip=_"Title: %t (Track No. %n)",yalign=0.5) \ -Stars(yalign=0.5) HBAlbumAndArtist = \ _Album(expand_max=200,yalign=0.5,ellipsize=end,markup="%l (%Y)",showcover=0) \ LockAlbum \ -_Artist(expand_max=200,yalign=0.5,ellipsize=end,markup="%a") \ -LockArtist HBTimeSlider = \ PlayingTime(markup="%s",initsize="XX:XX",xalign=0) \ _TimeSlider(direct_mode=1) \ -Length(markup="$length",initsize="XX:XX",xalign=1) VBSettings = _Filler70 HBSettings1 HBSettings2 _Filler71 HBSettings1 = _Filler80 HBVolume BMSettings Quit1(icon=gmb-turnoff,size=large-toolbar,minwidth=34,maxwidth=34,tip=_"Quit") _Filler81 HBSettings2 = _Filler90 \ HBButtonContext \ ExtraButtons(size=menu) \ _Filler91 HBVolume = VolumeIcon(button=1,size=large-toolbar,click2=mute,tip=_"Scrollwheel to change, right-click to mute") HBButtonContext = ToggleButton24(relief=none,size=menu,icon=gtk-about,widget=NBContextMain,tip=_"Open context & queue panel") BMSettings = (icon=gtk-preferences,size=large-toolbar) \ SMLibrary \ LayoutItem \ PlayItem \ SeparatorMenuItem02 \ MenuItem03(click1=OpenCustom(Equalizer),label=_"Equalizer",icon=gmb-equalizer) \ SeparatorMenuItem03 \ MenuItem04(command=OpenPref,label=_"Settings",icon="gtk-preferences") SMLibrary = (label=_"Library") \ MenuItem10(command="RunPerlCode(::ChooseAddPath(0,1))",label=_"Add Music",icon="gtk-add") \ MenuItem11(command="RunPerlCode(::IdleScan)",label=_"Scan Collection",icon="gtk-refresh") \ MenuItem12(command="RunPerlCode(::IdleCheck)",label=_"Check Collection",icon="gtk-zoom-in") #### Right main panel HPRight = _VBSongStatus _NBContextMain ##### Library VBSongStatus = (tabtitle=_"Library",tabicon="gmb-library") _VBMosaicSongList HBTotal VBMosaicSongList = _HBSongListtree HBSongListtree = \ _SongTree(cols="playandqueueandtrack title length playcount genre",colwidth="artist 124 lastplay 107 length 49 playandqueue 19 playandqueueandtrack 20 playcount 22 title 344 titleaa 397 track 21",grouping="album|artistalbum_breadcrumbs(picsize=100)|disc|discleft(width=15)",follow=1,sort="year album disc track") \ _HBSongPlaylist \ _HBMosaic HBSongPlaylist = _SongList(cols="playandqueueandtrack title artist album year length playcount",sort=artist,colwidth="album 200 artist 200 file 400 lastplay 100 length 41 path 413 playandqueueandtrack 24 playcount 96 rating 80 title 270 track 21 year 31",follow=1,sort="year album disc track") HBMosaic = _FilterPane4(nb=4,hidebb=1,pages=album,page_album/mode=mosaic,page_album/mmarkup=1,page_album/mpicsize=72,hidetabs=1) HBTotal = _HBToggle HBStatutSongList HBToggle = \ ToggleButton30(relief=none,size=menu,icon=gmb-view-list,widget=HBSongPlaylist,togglegroup=30,tip=_"Simple List View") \ ToggleButton31(relief=none,size=menu,icon=gmb-view-tree,widget=SongTree,togglegroup=30,tip=_"Songtree View") \ ToggleButton32(relief=none,size=menu,icon=gmb-view-mosaic,widget=HBMosaic,togglegroup=30,tip=_"Mosaic View") \ VSeparator30 \ Sort(button=1,size=menu,tip=_"Right-click to toggle shuffle/random") \ 2Filter35(button=1,size=menu,tip=_"Right-click to remove filters") \ VSeparator31 \ Queue1 10Pos HBStatutSongList = -Total2(size=small) ##### Context NBContextMain = HBLyrics VBQueue NBContext ##### Lyrics tab HBLyrics = (tabtitle=_"Lyrics",tabicon="gtk-about") _PluginLyrics ##### Queue tab VBQueue = (tabtitle=_"Queue",tabicon="gmb-queue") \ HBQueueActions \ _QueueList(group=3,songtree=1,tabicon="",cols="queuenumber titleaa",colwidth="queuenumber 20 titleaa 248",showbb=1) \ HBQueueStatut HBQueueActions = VSeparator40 EditListButtons(group=3,small=1,relief=none) HBQueueStatut = QueueActions -Total3(size="small",group=3) ##### Other context NBContext = (tabtitle=_"More info",tabicon="gtk-about",tabpos=left90) @same_artist @song_info ### positioning and sizing ### HSize0 = 110 VBCover HSize1 = 400 VBLeft VBRight HSize2 = 200 VBLibrary VBGenre VBArtist VBAlbum HSize3 = 400 HBTitle VSize0 = 110 HBCover gmusicbrowser-1.1.15~ds0.orig/layouts/shimmer.layout0000664000175000017500000002661312565212604022070 0ustar unit193unit193[Shimmer Desktop] Type=G+ Title = "gmusicbrowser" DefaultFocus = SimpleSearch Default = Window(size=1000x750) Window = hidden=VPSongPlaylist|FilterPane2 Author = simon@shimmerproject.org ### main window containers: top bar, main and statusbar ### VBMain = HBTop _HPMain HPMain = VBLeft _VBRight HBTop = ABButtons _15VBPlayer 10ABToggle -5ABSettings ### top bar from left to right ### ABButtons = (yalign=0,yscale=0.0) HBButtons HBButtons = Prev Play Next(click2=NextAlbum) VBPlayer = 1Filler0 HBTitle HBTimeSlider HBTitle = _Title(expand_max=500,markup="%t ",tip=_"Title: %t (Track No. %n)",yalign=0.5,ellipsize=end) LockAlbum(stock="on:gmb-lock gmb-lockopen off:gmb-breadcrumb gmb-locklight") Album(tip=_"Album: %l (%Y)",expand_max=200,yalign=0.5,markup=" %l ",showcover=0,ellipsize=end) LockArtist(stock="on:gmb-lock gmb-lockopen off:gmb-breadcrumb gmb-locklight") Artist(tip=_"Artist: %a",expand_max=300,yalign=0.5,markup=" %a",ellipsize=end) -Stars(yalign=0.5) HBTimeSlider = PlayingTime(markup="%s",initsize="XX:XX",xalign=0) _TimeSlider(direct_mode=1) -Length(markup="$length",initsize="XX:XX",xalign=1) ABToggle = (yalign=0,yscale=0.0) HBToggle HBToggle = ToggleButton0(relief=none,size=large-toolbar,icon=gmb-view-list,widget=VPSongPlaylist,togglegroup=1,tip=_"Simple List View") ToggleButton1(relief=none,size=large-toolbar,icon=gmb-view-tree,widget=SongTree,togglegroup=1,tip=_"Songtree View") ToggleButton2(relief=none,size=large-toolbar,icon=gmb-view-mosaic,widget=FilterPane2,togglegroup=1,tip=_"Mosaic View") Fullscreen(stock=gmb-view-fullscreen,size=large-toolbar) 10Filler2 ABSearchBox ABSearchBox = (yalign=0) SimpleSearch(suggest=1) ABSettings = (yalign=0,yscale=0.0) HBSettings HBSettings = ExtraButtons(size=large-toolbar) BMSettings BMSettings = (icon=gtk-preferences,size="large-toolbar") SMLibrary LayoutItem PlayItem SeparatorMenuItem01 MenuItem34(command=OpenCustom(Equalizer),label=_"Equalizer",icon=gmb-equalizer) SeparatorMenuItem20 MenuItem14(command=OpenPref,label=_"Settings",icon="gtk-preferences") MenuItem05(command=Quit,label=_"Quit",icon="gtk-quit") SMLibrary = (label=_"Library") MenuItem00(command="RunPerlCode(::ChooseAddPath(1,1))",label=_"Add Music",icon="gtk-add") MenuItem32(command="RunPerlCode(::IdleScan)",label=_"Rescan Collection",icon="gtk-refresh") ### main left: artist pane and album-cover ### VBLeft = _VBListCover HBStatus #VBListCover = _NBList Cover(overlay=6x6:350x350:elementary/overlay.png,default=elementary/no-cover.svg,showcover=0) # uncomment this line to add overlay shadow VBListCover = _NBList 1Cover(default=elementary/no-cover.svg,showcover=0) NBList = (tabpos="bottom") QueueList(songtree=1,tabtitle=_"Queue (%n)",tabicon="",cols="queuenumber titleaa",colwidth="queuenumber 20 titleaa 248",showbb=1,hscrollbar=0) HBLyrics VBAlbuminfo VBArtistinfo HBLyrics = (tabtitle=_"Lyrics") _PluginLyrics VBArtistinfo = (tabtitle=_"Artist") _PluginArtistinfo VBAlbuminfo = (tabtitle=_"Album") _PluginAlbuminfo HBStatus = 3Total(noheader=1,format=short,relief=none,button=1,mode=library) -2Sort(button=1,tip=_"Right-click to toggle shuffle/random") -2Filter(button=1,tip=_"Right-click to remove filters") -2ToggleButton3(icon=gmb-picture,relief=none,size=menu,widget=Cover,tip=_"Show/Hide Cover") ### main right: list/tree/mosaic widgets ### VBRight = _HBSongListtree Progress HBSongListtree = _SongTree(cols="playandqueueandtrack title length ratingpic",colwidth="artist 124 lastplay 107 length 49 playandqueue 19 playandqueueandtrack 20 playcount 22 ratingpic 100 title 390 titleaa 397 track 21",grouping="album|artistalbum_breadcrumbs(picsize=100)|disc|discleft(width=15)",follow=1,sort="year album disc track") _VPSongPlaylist _FilterPane2(nb=3,hidebb=1,pages=album,page_album/mode=mosaic,page_album/mmarkup=1,page_album/mpicsize=96,hidetabs=1) VPSongPlaylist = HBFilters _HBSonglist HBFilters = _FilterPane3(nb=2,hidebb=1,page=genre,hidetabs=1) _FilterPane10(nb=3,hidebb=1,page=artists,page_artists/lmarkup="%a%Y\n%x « %s",hidetabs=1) _FilterPane5(nb=4,hidebb=1,page=album,page_album/lpicsize=32,page_album/lmarkup="%a%Y\n%s « %l",hidetabs=1) HBSonglist = _SongList(cols="playandqueue track title artist album year length playcount",sort=artist,colwidth="album 200 artist 200 file 400 lastplay 100 length 41 path 413 playandqueue 24 playcount 96 rating 80 title 270 track 21 year 31",follow=1,sort="year album disc track") #VBMosaic = FRToggleMosaic _VPMosaicAlbum _VPMosaicArtist #FRToggleMosaic = (shadow=in) HBToggleMosaicClose #HBToggleMosaicClose = ABToggleMosaic -ToggleButton6(widget=FRToggleMosaic,label="",icon="gtk-close",tip=_"Hide Artist/Album bar") #ABToggleMosaic = (xalign=0.5,xscale=0.0) HBToggleMosaic #HBToggleMosaic = 3ToggleButton4(widget=VPMosaicAlbum,togglegroup=2,label=" Album ",relief=none) 3ToggleButton5(widget=VPMosaicArtist,togglegroup=2,label=" Artist ",relief=none) #VPMosaicAlbum = _FilterPane2(nb=3,hidebb=1,pages=album,page_album/mode=mosaic,page_album/mmarkup=1,page_album/mpicsize=96,hidetabs=1) #VPMosaicArtist = _FilterPane3(nb=3,hidebb=1,pages=artist,page_artist/mode=mosaic,page_artist/mmarkup=1,page_artist/mpicsize=96,hidetabs=1) ### bottom: statusbar ### Pref(size=small-toolbar,button=0) ### positioning and sizing ### DefaultFocus = SimpleSearch KeyBindings = c-l SetFocusOn(SimpleSearch) [Shimmer Netbook] Type=G+ Title = "gmusicbrowser" DefaultFocus = SimpleSearch Default = Window(size=1000x750) Author = simon@shimmerproject.org Window = hidden=SimpleSearch ### main window containers: top bar, main and statusbar ### VBMain = VBTop _NBList VBTop = 3Filler8 HBTop HBTop = VBButtons _15VBPlayer -5VBSettings VBButtons = HBButtons 3Filler4 HBButtons = Prev Play Next(click2=NextAlbum) VBPlayer = HBTitle VBTime HBTitle = Title(expand_max=300,markup="%t ",tip=_"Title: %t (Track No. %n)".if($track,pesc($track)),yalign=0.5,ellipsize=end) LockAlbum(stock="on:gmb-lock gmb-lockopen off:gmb-breadcrumb gmb-locklight") Album(tip=_"Album: %l (%Y)",expand_max=200,yalign=0.5,markup=" %l ",showcover=0,ellipsize=end) LockArtist(stock="on:gmb-lock gmb-lockopen off:gmb-breadcrumb gmb-locklight") Artist(tip=_"Artist: %a",expand_max=200,yalign=0.5,markup=" %a",ellipsize=end) -Stars(yalign=0.5) VBTime = _HBTimeSlider _SimpleSearch(suggest=1) HBTimeSlider = PlayingTime(markup="%s",initsize="XX:XX",xalign=0) _TimeSlider(direct_mode=1) -Length(markup="$length",initsize="XX:XX",xalign=1) VBSettings = HBPrefSearch 5Filler5 HBPrefSearch = ToggleButton0(size=large-toolbar,relief=none,icon=gtk-find,widget=SimpleSearch) ExtraButtons BMSettings BMSettings = (icon=gtk-preferences,size="large-toolbar") SMLibrary LayoutItem PlayItem SeparatorMenuItem01 MenuItem34(command=OpenCustom(Equalizer),label=_"Equalizer") SeparatorMenuItem20 MenuItem14(command=OpenPref,label=_"Settings",icon="gtk-preferences") MenuItem05(command=Quit,label=_"Quit",icon="gtk-quit") SMLibrary = (label=_"Library") MenuItem00(command="RunPerlCode(::ChooseAddPath(1,1))",label=_"Add Music",icon="gtk-add") MenuItem32(command="RunPerlCode(::IdleScan)",label=_"Rescan Collection",icon="gtk-refresh") NBList = (tabpos="bottom") HBSongListtree QueueList(songtree=1,tabtitle=_"Queue (%n)",tabicon="",cols="queuenumber titleaa",colwidth="queuenumber 20 titleaa 248",showbb=1) HBLyrics VBAbout HBLyrics = (tabtitle=_"Lyrics") _PluginLyrics VBAbout = (tabtitle=_"Info") _PluginArtistinfo HBSongListtree = (tabtitle=_"Playlist") _SongTree(cols="playandqueueandtrack title length ratingpic",colwidth="artist 124 lastplay 107 length 49 playandqueue 19 playandqueueandtrack 20 playcount 22 ratingpic 100 title 390 titleaa 397 track 21",grouping="album|Compact(picsize=50)|disc|discleft(width=15)",follow=1,sort="year album disc track") DefaultFocus = Play KeyBindings = c-l SetFocusOn(SimpleSearch) # Trayicon-Layouts ##################################### [Shimmer Traytip] Type=T VBMain = HBTime Filler0 VBMain1 VBMain1 = HBLeft _HBRight HBLeft = Cover(forceratio=1,default=elementary/no-cover.svg,maxsize=80) _VBText VBText = _2HBArtist _HBAlbum _2HBTitle HBButtons = Prev(size=small-toolbar) Play(size=small-toolbar) Next(size=small-toolbar) HBTitle = LockSong _Title(font=12,tip=_"Title: %t",ellipsize=end,minwidth=200) HBArtist = LockArtist _Artist(font=9,tip=_"Artist: %a",ellipsize=end,minwidth=200) HBAlbum = LockAlbum _Album(font=9,tip=_"Album: %l",ellipsize=end,minwidth=200) Date(font=8,markup=" » %y") HBRating = Filler1 -Stars HBTime = _TimeBar(minheight=7) HBRight = HBButtons _2HBRating VSize0 = 3 Filler0 HSize0 = Filler1 LockArtist LockAlbum LockSong HSize1 = Cover HBButtons # Fullscreen Layouts ##################################### [Shimmer Party] Type=F Window = fullscreen=1,sticky=0,hidden=VBSidebar VBMain = _HPMain HBNowPlaying HPMain = _FilterPane2(nb=3,hidebb=1,pages=album,page_album/mode=mosaic,page_album/mmarkup=1,page_album/mpicsize=128,hidetabs=1) VBSidebar HBNowPlaying = HBButtons 15Filler0 _HBTitle HBButtons = Prev Play Next HBTitle = Title(expand_max=300,ellipsize=end,markup="%t ",tip=_"Title: %t (Track No. %n)".if($track,pesc($track)),yalign=0.5) \ LockAlbum(stock="on:gmb-lock gmb-lockopen off:gmb-breadcrumb gmb-locklight") \ Album(tip=_"Album: %l (%Y)",ellipsize=end,expand_max=200,yalign=0.5,markup=" %l ",showcover=0) \ LockArtist(stock="on:gmb-lock gmb-lockopen off:gmb-breadcrumb gmb-locklight") \ Artist(tip=_"Artist: %a",expand_max=200,yalign=0.5,ellipsize=end,markup=" %a") \ -ToggleButton1(size=button,relief=none,icon=gtk-find,widget=VBSidebar) \ -20Stars(yalign=0.5) VBSidebar = 4Filler1 7HBSearch _7HBQueue 4Filler2 HBQueue = 14_QueueList(group=1,songtree=1,cols="queuenumber titleaa",colwidth="queuenumber 20 titleaa 248",showbb=1) HBSearch = 4Filler3 6Label0(markup='«') _SimpleSearch(suggest=1) 7Filler4 KeyBindings = Escape CloseWindow # Groups and Columns for Songtree ##################################### {Group discleft} title=disc on the left side head=3 left=width vcollapse=head+title:h+line:h+2 title: text(markup=''.pesc($title).''.if(!$_expanded,'»'),pad=2,w=left)) width: OptionNumber(default=15,min=10,max=100,step=1) line: line(x1=1,y1=1,x2=$_w,y2=1,color='#ccc',width=1) {Group artistalbum_breadcrumbs} title=album and artist breadcrumbs head=title:h tail=25 vcollapse=head vmin=pic:y+pic:h+25 left=pic:w+2 title: text(markup=''.pesc($album).''. if($year,' « '.pesc($year)) . ' « '.pesc($artist),pad=2) pic: +aapic(y=title:h+title:y,picsize=picsize,ypad=2,xpad=1,aa='album') picsize : OptionNumber(default=100,min=20,max=1000,step=10) picstars : picture(file=ratingpic($rating_avrg),x=(picsize/2)-(picstars:w/2),y=pic:y+pic:h,hide=$rating_avrg==50 || picsize < 80) {Group Compact} title=Compact head=pic:h tail=25 vcollapse=head vmin=pic:y+pic:h+25 title: text(markup=''.pesc($album).''. if($year,'\n'.pesc($year)) . '\n'.pesc($artist),pad=2,x=pic:w) pic: aapic(y=title:y,picsize=picsize,ypad=2,xpad=1,aa='album') picsize : OptionNumber(default=50,min=20,max=1000,step=10) {Column queuenumber} menutitle = _"Row number" title = # width = 20 text: text(markup=$_row+1, x=-text:w) {Column playandqueueandtrack} menutitle = _"Playing/Queue Icon or Track" title = # width = 20 sort = track ico: icon(pad=2,icon=$playicon, hide= !$playing && !$queued) text: text(markup=pesc($track.' '.$queued), hide= $playing || $queued) gmusicbrowser-1.1.15~ds0.orig/layouts/popups.layout0000664000175000017500000000022012565212604021734 0ustar unit193unit193 [play_controls] Window = transparent=1 HBmain= Prev Stop Play Next [CoverPopup] VBmain= Cover(minsize=650,maxsize=650,click1=CloseWindow) gmusicbrowser-1.1.15~ds0.orig/layouts/tray.layout0000664000175000017500000000160312565212604021373 0ustar unit193unit193[full with buttons] Type=T Name = _"Normal with buttons" Window= borderwidth=5 HBButtons = VolumeIcon Prev Stop Play Next OpenBrowser ExtraButtons Pref Quit HBIndic = Sort 10Filter Queue 10Pos -Stars HBTitle = LockSong _Title LabelsIcons HBArtist = LockArtist _Artist HBAlbum = LockAlbum _Album -Year HBTime = Time _TimeBar VBmain = HBButtons HBIndic _VBText -HBTime VBText = 2HBTitle 2HBArtist 2HBAlbum HBmain = _VBmain 2Filler1 -Cover(forceratio=1) VolumeScroll = HBmain [full] based on full with buttons Type=T Name = _"Normal" HBButtons = VBmain = HBIndic _VBText -HBTime [info] Type=T Name = _"Small" HBAlbum = _Album -Year VBText = Title(30) Artist HBAlbum HBmain = _VBText 2Filler1 -Cover(forceratio=1) VolumeScroll = HBmain [minimaltip] Type=T Window= borderwidth=0 HBmain = Play(size=menu) Next(size=menu) 2_TimeBar(text="%t by %a",minwidth=240) Cover Name= _"Minimal tip" gmusicbrowser-1.1.15~ds0.orig/layouts/search.layout0000664000175000017500000000142512565212604021663 0ustar unit193unit193[Search] Type=S Default = Window(size=320x540) Name = _"Search" Title = _"Search" TBmain = _"Artist" ArtistSearch(buttons=1) _"Album" AlbumSearch(buttons=1) _"Song" SongSearch(activate=play,buttons=1) DefaultFocus = TBmain KeyBindings = Escape CloseWindow [Quick Search] Type=S Name = _"Quick Search" Title = _"Search" Default = Window(size=620x540) VBmain = SimpleSearch(activate=SetFocusOn(SongList)) _SongList(activate=play&CloseWindow) DefaultFocus = SimpleSearch KeyBindings = Escape CloseWindow [Quick Search with SongTree] Type=S Name = _"Quick Search with SongTree" Title = _"Search" Default = Window(size=620x540) VBmain = SimpleSearch(activate=SetFocusOn(SongTree)) _SongTree(activate=play&CloseWindow) DefaultFocus = SimpleSearch KeyBindings = Escape CloseWindow gmusicbrowser-1.1.15~ds0.orig/layouts/main.layout0000664000175000017500000001565112565212604021350 0ustar unit193unit193[Lists, Library & Context] Type=G+ Default = Window(size=1120x820) VPRight(size=200-550) HPmain(size=400) VolumeScroll = VBplayer DefaultFocus = Play Name = _"Lists, Library & Context" VBmain = HBmenu _HPmain Progress HBmenu = _MBmenu MBmenu = MainMenuItem LayoutItem PSortItem PFilterItem QueueItem HPmain = VBLeft _TBRight VBLeft = 5VBplayer _TabbedLists(group=1,pages="+PlayList +QueueList +@song_info +PictureBrowser") VBplayer = HBButtons3 HBText_Cover HBButtons3 = 5Sort 5Filter 5Queue 5Pos -Stars HBText_Cover= _VBText 5-Cover VBText = HBButtons1 2HBTitle 2HBArtist 2HBAlbum HBTime HBButtons1= Prev Stop Play Next ExtraButtons 5-VolumeIcon -Pref -OpenBrowser(toggle) HBTitle = LockSong _Title LabelsIcons HBArtist = LockArtist _Artist HBAlbum = LockAlbum _Album -Year HBTime = Time _TimeBar TBRight = _"Library" VPRight _"Context" Context VPRight = HPfp0 _VBSongList HPfp0 = FilterPane0(nb=1,hidebb=1,page=genre) HPfp1 HPfp1 = FilterPane1(nb=2,hidebb=1,page=artist,page_artist/lmarkup=1) \ FilterPane2(nb=3,hidebb=1,page=album,page_album/lpicsize=32,page_album/lmarkup=1) VBSongList = HBSongList _SongList HBSongList = SimpleSearch(maxwidth=250) -FilterLock -PlayFilter -Refresh -ResetFilter -MBlist MBlist = HistItem LSortItem PlayItem [Small_player] Type=G Category= _"Small" Name= _"Small player" Default = Window(sticky=1) VolumeScroll = HBmain DefaultFocus = Play HBmain = _VBmain 2Filler1 -Cover VBmain = HBButtons HBIndic _VBText -HBTime HBButtons = VolumeIcon Prev Stop Play Next OpenBrowser ExtraButtons Pref Quit HBIndic = Sort 10Filter Queue 10Pos -Stars VBText = 2HBTitle 2HBArtist 2HBAlbum HBTitle = LockSong _Title LabelsIcons HBArtist = LockArtist _Artist HBAlbum = LockAlbum _Album -Year HBTime = Time _TimeBar [with queue] based on Small_player Type=G Category = _"Small" Name = _"with queue" VBmain2 = HBmain 5_VBQueue VBQueue = _QueueList(group=1,hidewidget=VBQueue,hideif=empty,shrinkonhide=v) HBQueueButtons HBQueueButtons = EditListButtons(group=1) 4QueueActions HBButtons = VolumeIcon Prev Stop Play Next Choose OpenBrowser Pref Quit [with search] Type=G Category = _"Small" Name = _"with search" Default = Window(size=500x1) VBmain = @Small_player 2HBsearch _SongList(hideif=nofilter,shrinkonhide=v) HBsearch = _SimpleSearch PlayFilter [with playlist] Type=G Name = _"with playlist" Default = Window(size=820x500) VBmain = @Small_player 2HBsearch _PlayList Progress HBsearch = _SimpleSearch ResetFilter [with lists] Type=G Name = _"with lists" Default = Window(size=820x500) VBmain = @Small_player 4_TabbedLists(pages="+PlayList +QueueList",match="context page") Progress [with search and lists] Type=G Name = _"with search and lists" Default = Window(size=1000x500) VBmain = _HPmain Progress HPmain = _VBsearch VBright VBsearch = HBbar _SongList HBbar = _SimpleSearch VBright = @Small_player 4_TabbedLists(pages="+PlayList +QueueList",match="context page") [with search and lists 2] Type=G Name = _"with search and lists 2" Default = Window(size=1000x500) VBmain = _HPmain Progress HPmain = NBsearch VBright NBsearch = (match="context page") ArtistSearch(tabtitle=_"Artist",activate=addplay) AlbumSearch(tabtitle=_"Album",activate=addplay) SongSearch(tabtitle=_"Song",activate=play) VBright = @Small_player 4_TabbedLists(pages="+PlayList +QueueList") [minimal] Type=G Name = _"minimal" Category= _"Small" Default = Window(sticky=1) HBmain = OpenBrowser Pref Play Next 5_ABTitle ABTitle = (xalign=0,yscale=0) Title_by(minsize=20) VolumeScroll = HBmain [with browser] Type=G+ Name = _"with browser" DefaultFocus = Play Default = Window(size=1120x820) HPbig(size=400-700) HPfp(size=140-140) VPlistAA(size=575-150) HBIndic = MBmenu Sort 10Filter Queue 10Pos MBmenu = MainMenuItem LayoutItem PSortItem PFilterItem QueueItem VBleft = HBIndic HBButtons HBButtons = VolumeIcon Prev Stop Play Next Time 5_ABtimebar ABtimebar = (yscale=0) TimeBar HBupper = VBleft _VBright 5-Cover HBTitle = Filler0 _Title -Stars HBArtist = LockArtist _Artist HBAlbum = LockAlbum _Album -Year VBright = 2HBTitle 2HBArtist 2HBAlbum VBmain = HBupper 5HBstatus 5_HPbig Progress HBstatus = _SimpleSearch(maxwidth=250) 10MBlist ResetFilter Refresh PlayFilter FilterLock -Total MBlist = HistItem LSortItem PlayItem HPbig = HPfp _VPlistAA HPfp = FilterPane0(nb=1,hidebb=1,page=artists,page_artists/lmarkup=1) FilterPane1(nb=2,hidebb=1,page=album,page_album/lpicsize=32,page_album/lmarkup=1) VPlistAA = _SongList HBAA HBAA = _ArtistBox _AlbumBox HSize0 = Filler0 LockArtist LockAlbum VolumeScroll = HBupper [with browser (SongTree)] based on with browser Name = _"with browser (SongTree)" VPlistAA = _SongTree HBAA [with browser & queue] based on with browser Name = _"with browser & queue" Default = Window(size=1120x820) HPbig(size=400-700) HPfp(size=140-140) VPlistAA(size=575-150) VPBrowserQueue(size=490-210) HPbig = HPfp _VPBrowserQueue VPBrowserQueue = _VPlistAA VBQueue VBQueue = _QueueList(group=1,hidewidget=VBQueue,hideif=empty) HBQueueButtons HBQueueButtons = EditListButtons(group=1) 4QueueActions [Playlist] Type=G+ Name = _"Playlist" Default = Window(size=1120x820) HPfp0(size=140-140) HPfp_list(size=780-300) HBIndic = MBmenu Sort 10Filter Queue 10Pos MBmenu = MainMenuItem LayoutItem PSortItem PFilterItem QueueItem HistItem PlayItem VBleft = HBIndic _HBButtons HBButtons = Prev Play Next Time 5_ABtimebar ABtimebar = (yscale=0) TimeBar HBupper = VBleft 5Cover 5_VBright -VBVol VBVol = VolumeIcon _VolumeBar(vertical=1) HBTitle = Filler0 _Title -Stars HBArtist = LockArtist _Artist HBAlbum = LockAlbum _Album -Year VBright = 2_HBTitle 2_HBArtist 2_HBAlbum VBmain = HBupper 5_HPfp_list Progress HPfp_list = _PlayList HPfp0 HPfp0 = FilterPane0(nb=1,hidebb=1,page=artists,page_artists/lmarkup=1) FilterPane1(nb=2,hidebb=1,page=album,page_album/lpicsize=32,page_album/lmarkup=1) HSize0 = Filler0 LockArtist LockAlbum VSize1 = 50 VolumeSlider VolumeScroll = HBupper # layouts used internally [Volume] VSize= 100 VolumeSlider VBox = Volume(xalign=.5) _VolumeSlider(vertical=1) [Equalizer] Window = size=300x160,uniqueid=Equalizer,ifexist=replace VBmain = 2EqualizerPresets _Equalizer Title = _"Equalizer" [Progress] VBmain = _VProgress(lastclose=1) Title = _"Progress" [Karaoke] Type= K VBmain= PluginKaraoke Window= pos=50%x100%,size=80%x1,insensitive=1,nodecoration=1,ontop=1 [Context] Type=C Title= _"Context" Default = Window(size=500x300) VBmain = _Context [Queue] Type=Q Title=_"Queue Edit" Window = size=500x300,uniqueid=Equalizer,ifexist=replace VBmain = 3_QueueList(activate=play) HBButtons HBButtons = EditListButtons 4QueueActions [EditList] Title=List Edit Default = Window(size=500x300) VBmain = 3_SongList(mode=editlist,activate=playlist) EditListButtons #used by the "view in new window" of the PictureBrowser [PictureBrowser] Title=_"Picture Browser" Default = Window(size=600x400) VBmain = _PictureBrowser KeyBindings = q CloseWindow gmusicbrowser-1.1.15~ds0.orig/layouts/pages.layout0000664000175000017500000000132312565212604021512 0ustar unit193unit193 #type P layouts : pages that can be added to a TabbedLists [same_title] Type= P Name= _"Same title" VBmain = _SongTree(group=Play:title) Icon=gmb-song Title= _"Title" [same_album] Type= P Name= _"Same album" VBmain = _SongTree(group=Play:album) Icon=gmb-album Title= _"Album" [same_artist] Type= P Name= _"Same artist" VBmain = _SongTree(group=Play:first_artist) Icon=gmb-artist Title= _"Artist" [same_year] Type= P Name= _"Same year" VBmain = _SongTree(group=Play:year) Title= _"Year" [search_page] Type= P Name= _"Search" VBmain = HBSearch _SongList(group=search) HBSearch= _SimpleSearch(group=search) Icon=gtk-find [song_info] Type= P Name= _"Song informations" Title= _"Info" VBmain= _SongInfo Icon= gtk-info gmusicbrowser-1.1.15~ds0.orig/layouts/desktop.layout0000664000175000017500000000325112565212604022066 0ustar unit193unit193 # type D layouts : for desktop widgets [D_insens_song_cover] Type= D Name= _"Insensitive, Song & Cover" Window = insensitive=1,transparent=1 DefaultFontColor= white HBTitle = _Title LabelsIcons HBArtist = _Artist HBAlbum = _Album -Year VBText = 2HBTitle 2HBArtist 2HBAlbum VBmain = VBText _Cover(maxsize=0,xalign=0,yalign=0) [D_buttons_song_cover] Type= D Name= _"Buttons, Song & Cover" Window = transparent=1 DefaultFontColor= white HBTitle = _Title LabelsIcons HBArtist = _Artist HBAlbum = _Album -Year HBbuttons = Prev Stop Play Next VBmain = HBbuttons 2HBTitle 2HBArtist 2HBAlbum -Cover(forceratio=1,maxsize=0) [D_buttons] Type= D Name = _"Buttons" Window = transparent=1 HBbuttons = Prev Stop Play Next [D_hotspot] Type= D Name = _"Invisible hot spot" Window = transparent=1 HBmain = _EventBox(hover_layout="full with buttons",hover_delay=1) [D_lyrics] Type=D Name = _"Lyrics (requires lyrics plugin)" Window = transparent=1 VBmain = _PluginLyrics(shadow=none,HideToolbar=1) [D_clementine] Type= D Name = Clementine Window = insensitive=1,transparent=1 DefaultFontColor= white VBmain= Cover(minsize=100,xalign=0) HBartist HBalbum HBtrack HBtitle TimeBar HBartist=Text1(color=grey,text=_"Artist") Artist(markup=" : $artist") HBalbum= Text2(color=grey,text=_"Album") Album(markup=" : $album") HBtrack= Text3(color=grey,text=_"Track") Text5(markup=" : $track") HBtitle= Text4(color=grey,text=_"Title") Title(markup=" : $title_or_file") [D_screenlet] Type=D Name = screenlet DefaultFont=8 Window = size=240x240,transparent=1 VBmain = _Cover(forceratio=1,hover_delay=1,hover_layout_pos=.5w x w,hover_layout=play_controls) TimeBar Title(xalign=.5) Artist(xalign=.5) Album(xalign=.5) gmusicbrowser-1.1.15~ds0.orig/layouts/browser.layout0000664000175000017500000000352212565212604022101 0ustar unit193unit193[Browser] Type=B Name = _"Browser" Default = Window(size=1120x820) HPfp(size=150-150) HPbig(size=780-350) VPlistAA(size=645-140) FilterPane0(page=artist) FilterPane1(page=album) VBmain = HBstatus 5_HPbig HBstatus = SimpleSearch(maxwidth=250) 10MBlist ResetFilter Refresh PlayFilter FilterLock -Total MBlist = HistItem LSortItem PlayItem HPbig = _VPlistAA HPfp HPfp = FilterPane0(nb=1) FilterPane1(nb=2) VPlistAA = _SongList HBAA HBAA = _ArtistBox _AlbumBox [Smaller browser] Type=B Name = _"Smaller browser" Default = Window(size=1120x820) HPfp(size=150-150) HPbig(size=800-300) VPlistAA(size=600-125) FilterPane0(page=artist) FilterPane1(page=album) VBmain = HBstatus 5_HPbig HBstatus = SimpleSearch 10MBlist ResetFilter Refresh PlayFilter FilterLock MBlist = HistItem LSortItem PlayItem HPbig = _VPlistAA HPfp HPfp = FilterPane0(nb=1) FilterPane1(nb=2) VBleft = Total _SongList VPlistAA = _VBleft HBAA HBAA = _ArtistBox _AlbumBox [Browser with SongTree] based on Browser VPlistAA = _SongTree HBAA Name = _"Browser with SongTree" [3 Filter panes] based on Browser Type=B Name = _"3 Filter panes" Default = Window(size=1120x820) HPfp(size=125-250) HPbig(size=750-450) HPfp2(size=125-125) VPlistAA(size=600-150) FilterPane0(page=savedtree) FilterPane1(page=artists) FilterPane2(page=album) HPfp = _FilterPane0(nb=1) HPfp2 HPfp2 = _FilterPane1(nb=2) FilterPane2(nb=3) [left-side filter panes] Type=B Name = _"left-side filter panes" Default = Window(size=1120x820) HPfp(size=150-150) HPbig(size=350-780) VPlistAA(size=645-140) FilterPane0(page=artist) FilterPane1(page=album) VBmain = HBstatus 5_HPbig HBstatus = SimpleSearch 10MBlist ResetFilter Refresh PlayFilter FilterLock -Total MBlist = HistItem LSortItem PlayItem HPbig = HPfp _VPlistAA HPfp = FilterPane0(nb=1) FilterPane1(nb=2) VPlistAA = _SongList HBAA HBAA = _ArtistBox _AlbumBox gmusicbrowser-1.1.15~ds0.orig/gmusicbrowser.desktop0000664000175000017500000000227312565212604021747 0ustar unit193unit193[Desktop Entry] Name=gmusicbrowser Comment=Jukebox for large collections of mp3/ogg/flac/mpc Exec=gmusicbrowser %F Type=Application Icon=gmusicbrowser Categories=Audio;AudioVideo; StartupNotify=true Comment[fr]=Jukebox pour de grandes collections de mp3/ogg/flac/mpc #MimeType=audio/x-musepack;application/x-musepack;audio/musepack;application/musepack;audio/mpc;audio/x-mpc;audio/x-mp3;audio/mpeg;audio/x-mpeg;audio/x-mpeg-3;audio/mpeg3;application/ogg;application/x-ogg;audio/vorbis;audio/x-vorbis;audio/ogg;audio/x-ogg;audio/x-flac;application/x-flac;audio/flac; Actions=PlayPause;Next;Previous;LockArtist;LockAlbum [Desktop Action PlayPause] Name=Play-Pause Exec=gmusicbrowser -cmd PlayPause Icon=media-playback-start-symbolic OnlyShowIn=Unity; [Desktop Action Next] Name=Next Exec=gmusicbrowser -cmd NextSong Icon=media-skip-backward-symbolic OnlyShowIn=Unity; [Desktop Action Previous] Name=Previous Exec=gmusicbrowser -cmd PrevSong Icon=media-skip-forward-symbolic OnlyShowIn=Unity; [Desktop Action LockArtist] Name=Toggle Artist Lock Exec=gmusicbrowser -cmd TogArtistLock OnlyShowIn=Unity; [Desktop Action LockAlbum] Name=Toggle Album Lock Exec=gmusicbrowser -cmd TogAlbumLock OnlyShowIn=Unity; gmusicbrowser-1.1.15~ds0.orig/gmusicbrowser_layout.pm0000664000175000017500000057776112565212604022332 0ustar unit193unit193# Copyright (C) 2005-2015 Quentin Sculo # # This file is part of Gmusicbrowser. # Gmusicbrowser is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3, as # published by the Free Software Foundation. use strict; use warnings; package Layout; use constant { TRUE => 1, FALSE => 0, SIZE_BUTTONS => 'large-toolbar', SIZE_FLAGS => 'menu', }; our @MenuQueue= ( {label => _"Queue album", code => sub { ::EnqueueSame('album',$_[0]{ID}); }, istrue=>'ID', }, {label => _"Queue artist", code => sub { ::EnqueueSame('artist',$_[0]{ID});}, istrue=>'ID', }, # or use field 'artists' or 'first_artist' ? { include => sub { my $menu=$_[1]; my @modes= map { $_=>$::QActions{$_}{long} } ::List_QueueActions(0); ::BuildChoiceMenu( \@modes, menu=>$menu, ordered_hash=>1, 'reverse'=>1, check=> sub {$::QueueAction}, code=> sub { ::EnqueueAction($_[1]); }, ); }, }, {label => _"Clear queue", code => \&::ClearQueue, test => sub{@$::Queue}}, {label => _"Shuffle queue", code => sub {$::Queue->Shuffle}, test => sub{@$::Queue}}, {label => _"Auto fill up to", code => sub { $::Options{MaxAutoFill}=$_[1]; ::HasChanged('QueueAction','maxautofill'); }, submenu => sub { my $m= ::max(1,$::Options{MaxAutoFill}-5); return [$m..$m+10]; }, check => sub {$::Options{MaxAutoFill};}, }, {label => _"Edit...", code => \&::EditQueue, test => sub { !$_[0]{mode} || $_[0]{mode} ne 'Q' }, }, { separator=>1}, { include => sub { my $menu=$_[1]; my @modes= map { $_=>$::QActions{$_}{long_next} } grep $_ ne '', ::List_QueueActions(1); ::BuildChoiceMenu( \@modes, menu=>$menu, ordered_hash=>1, 'reverse'=>1, radio_as_checks=>1, check=> sub {$::NextAction}, code=> sub { my $m=$_[1]; $m='' if $m eq $::NextAction; ::SetNextAction($m); }, ); }, }, ); our @MainMenu= ( {label => _"Add files or folders",code => sub {::ChooseAddPath(0,1)}, stockicon => 'gtk-add' }, {label => _"Settings", code => 'OpenPref', stockicon => 'gtk-preferences' }, {label => _"Open Browser", code => \&::OpenBrowser,stockicon => 'gmb-playlist' }, {label => _"Open Context window",code => \&::ContextWindow, stockicon => 'gtk-info'}, {label => _"Switch to fullscreen mode",code => \&::ToggleFullscreenLayout, stockicon => 'gtk-fullscreen'}, {label => _"About", code => \&::AboutDialog,stockicon => 'gtk-about' }, {label => _"Quit", code => \&::Quit, stockicon => 'gtk-quit' }, ); our %Widgets= ( Prev => { class => 'Layout::Button', #size => SIZE_BUTTONS, stock => 'gtk-media-previous', tip => _"Recently played songs", text => _"Previous", group => 'Recent', activate=> \&::PrevSong, options => 'nbsongs', nbsongs => 10, click3 => sub { ::ChooseSongs([::GetPrevSongs($_[0]{nbsongs})]); }, }, Stop => { class => 'Layout::Button', stock => 'gtk-media-stop', tip => _"Stop", activate=> \&::Stop, click2 => 'EnqueueAction(stop)', click3 => 'SetNextAction(stop)', }, Play => { class => 'Layout::Button', state => sub {$::TogPlay? 'pause' : 'play'}, stock => {pause => 'gtk-media-pause', play => 'gtk-media-play' }, tip => sub {$::TogPlay? _"Pause" : _"Play"}, activate=> \&::PlayPause, click3 => 'Stop', event => 'Playing', }, Next => { class => 'Layout::Button', stock => 'gtk-media-next', tip => _"Next Song", text => _"Next", group => 'Next', activate=> \&::NextSong, options => 'nbsongs', nbsongs => 10, click3 => sub { ::ChooseSongs([::GetNextSongs($_[0]{nbsongs})]); }, }, OpenBrowser => { class => 'Layout::Button', oldopt1 => 'toggle', options => 'toggle', stock => 'gmb-playlist', tip => _"Open Browser window", activate=> sub { ::OpenSpecialWindow('Browser',$_[0]{toggle}); }, click3 => sub { ::OpenSpecialWindow('Browser'); }, }, OpenContext => { class => 'Layout::Button', oldopt1 => 'toggle', options => 'toggle', stock => 'gtk-info', tip => _"Open Context window", activate=> sub { ::OpenSpecialWindow('Context',$_[0]{toggle}); }, click3 => sub { ::OpenSpecialWindow('Context'); }, }, OpenQueue => { class => 'Layout::Button', stock => 'gmb-queue-window', tip => _"Open Queue window", options => 'toggle', activate=> sub { ::OpenSpecialWindow('Queue',$_[0]{toggle}); }, }, Pref => { class => 'Layout::Button', stock => 'gtk-preferences', tip => _"Edit Settings", text => _"Settings", activate=> 'OpenPref', click3 => sub {Layout::Window->new($::Options{Layout});}, #mostly for debugging purpose click2 => \&::AboutDialog, }, Quit => { class => 'Layout::Button', stock => 'gtk-quit', tip => _"Quit", activate=> \&::Quit, click2 => 'EnqueueAction(quit)', click3 => 'SetNextAction(quit)', }, Lock => { class => 'Layout::Button', button => 0, size => SIZE_FLAGS, options => 'field', field => 'fullfilename', #default field to make sure it's defined state => sub { ($::TogLock && $::TogLock eq $_[0]{field})? 'on' : 'off' }, stock => { on => 'gmb-lock', off => '. gmb-locklight' }, tip => sub { ::__x(_"Lock on {field}", field=> Songs::FieldName($_[0]{field})) }, click1 => sub {::ToggleLock($_[0]{field});}, event => 'Lock', }, LockSong => { parent => 'Lock', field => 'fullfilename', tip => _"Lock on song", }, LockArtist => { parent => 'Lock', field => 'first_artist', tip => _"Lock on Artist", click2 => 'EnqueueArtist', }, LockAlbum => { parent => 'Lock', field => 'album', tip => _"Lock on Album", click2 => 'EnqueueAlbum', }, Sort => { class => 'Layout::Button', button => 0, size => SIZE_FLAGS, state => sub { my $s=$::Options{'Sort'};($s=~m/^random:/)? 'random' : ($s eq 'shuffle')? 'shuffle' : 'sorted'; }, stock => { random => 'gmb-random', shuffle => 'gmb-shuffle', sorted => 'gtk-sort-ascending' }, tip => sub { _("Play order") ." :\n". ::ExplainSort($::Options{Sort}); }, text => sub { ::ExplainSort($::Options{Sort},1); }, click1 => 'MenuPlayOrder', click3 => 'ToggleRandom', event => 'Sort SavedWRandoms SavedSorts', }, Filter => { class => 'Layout::Button', button => 0, size => SIZE_FLAGS, state => sub { defined $::ListMode ? 'list' : $::SelectedFilter->is_empty ? 'library' : 'filter'; }, stock => { list => 'gmb-list', library => 'gmb-library', filter => 'gmb-filter' }, tip => sub { defined $::ListMode ? _"static list" : _("Playlist filter :\n").$::SelectedFilter->explain; }, text => sub { $::ListMode ? _"static list" : $::SelectedFilter->name; }, click1 => 'MenuPlayFilter', click3 => 'ClearPlayFilter', event => 'Filter SavedFilters', }, Queue => { class => 'Layout::Button', button => 0, size => SIZE_FLAGS, state => sub { $::NextAction? $::NextAction : @$::Queue? 'queue' : $::QueueAction? $::QueueAction : 'noqueue' }, stock => sub {$_[0] eq 'queue' ? 'gmb-queue' : $_[0] eq 'noqueue'? '. gmb-queue' : $::QActions{$_[0]}{icon} ; }, tip => sub { if ($::NextAction) { return $::QActions{$::NextAction}{long_next} } ::CalcListLength($::Queue,'queue') .($::QueueAction? "\n". ::__x( _"then {action}", action => $::QActions{$::QueueAction}{short} ) : ''); }, text => _"Queue", click1 => 'MenuQueue', click3 => sub { ::EnqueueAction(''); ::SetNextAction(''); ::ClearQueue(); }, #FIXME replace with 3 gmb commands once new command system is done event => 'Queue QueueAction', dragdest=> [::DRAG_ID,sub {shift;shift;::Enqueue(@_);}], }, VolumeIcon => { class => 'Layout::Button', button => 0, state => sub { ::GetMute() ? 'm' : ::GetVol() }, stock => sub { 'gmb-vol'.( $_[0] eq 'm' ? 'm' : int(($_[0]-1)/100*$::NBVolIcons) ); }, tip => sub { _("Volume : ").::GetVol().'%' }, click1 => sub { ::PopupLayout('Volume',$_[0]); }, click3 => sub { ::ChangeVol('mute') }, event => 'Vol', }, Button => { class => 'Layout::Button', }, EventBox => { class => 'Layout::Button', button => 0, }, Text => { class => 'Layout::Label', oldopt1 => sub { 'text',$_[0] }, group => 'Play', }, Pos => { class => 'Layout::Label', group => 'Play', initsize=> ::__n("%d song in queue","%d songs in queue",99999), #longest string that will be displayed click1 => sub { ::ChooseSongs([::GetNeighbourSongs(5)]) unless $::RandomMode || @$::Queue; }, update => sub { my $t=(@$::ListPlay==0) ? '': @$::Queue ? ::__n("%d song in queue","%d songs in queue", scalar @$::Queue): !defined $::Position ? ::__n("%d song","%d songs",scalar @$::ListPlay): ($::Position+1).'/'.@$::ListPlay; $_[0]->set_markup_with_format( '%s', $t ); }, event => 'Pos Queue Filter', }, Title => { class => 'Layout::Label', group => 'Play', minsize => 20, markup => '%S%V', markup_empty => '<'._("Playlist Empty").'>', click1 => \&PopupSongsFromAlbum, click3 => sub { my $ID=::GetSelID($_[0]); ::PopupContextMenu(\@::SongCMenu,{mode=> 'P', self=> $_[0], IDs => [$ID]}) if defined $ID;}, dragsrc => [::DRAG_ID,\&DragCurrentSong], dragdest=> [::DRAG_ID,sub {::Select(song => $_[2]);}], cursor => 'hand2', }, Title_by => { class => 'Layout::Label', parent => 'Title', markup => ::__x(_"{song} by {artist}",song => "%S%V", artist => "%a"), }, Artist => { class => 'Layout::Label', group => 'Play', minsize => 20, markup => '%a', click1 => sub { ::PopupAA('artists'); }, click3 => sub { my $ID=::GetSelID($_[0]); ::ArtistContextMenu( Songs::Get_gid($ID,'artists'),{self =>$_[0], ID=>$ID, mode => 'P'}) if defined $ID; }, dragsrc => [::DRAG_ARTIST,\&DragCurrentArtist], cursor => 'hand2', }, Album => { class => 'Layout::Label', group => 'Play', minsize => 20, markup => '%l', click1 => sub { my $ID=::GetSelID($_[0]); ::PopupAA( 'album', from=> Songs::Get_gid($ID,'artists')) if defined $ID; }, click3 => sub { my $ID=::GetSelID($_[0]); ::PopupAAContextMenu({self =>$_[0], field=>'album', ID=>$ID, gid=>Songs::Get_gid($ID,'album'), mode => 'P'}) if defined $ID; }, dragsrc => [::DRAG_ALBUM,\&DragCurrentAlbum], cursor => 'hand2', }, Year => { class => 'Layout::Label', group => 'Play', markup => ' %y', markup_empty=> '', }, Comment => { class => 'Layout::Label', group => 'Play', markup => '%C', }, Length => { class => 'Layout::Label', group => 'Play', initsize=> ::__x( _" of {length}", 'length' => "XX:XX"), markup => ::__x( _" of {length}", 'length' => "%m" ), markup_empty=> ::__x( _" of {length}", 'length' => "0:00" ), # font => 'Monospace', }, PlayingTime => { class => 'Layout::Label::Time', group => 'Play', markup => '%s', xalign => 1, options => 'remaining markup_stopped', saveoptions => 'remaining', markup_stopped=> '--:--', initsize=> '-XX:XX', # font => 'Monospace', event => 'Time', click1 => sub { $_[0]{remaining}=!$_[0]{remaining}; $_[0]->update_time; }, update => sub { $_[0]->update_time unless $_[0]{busy}; }, }, Time => { parent => 'PlayingTime', xalign => .5, markup => '%s' . ::__x( _" of {length}", 'length' => "%m" ), markup_empty=> '%s' . ::__x( _" of {length}", 'length' => "0:00" ), initsize=> '-XX:XX' . ::__x( _" of {length}", 'length' => "XX:XX"), }, TimeBar => { class => 'Layout::Bar', group => 'Play', event => 'Time', update => sub { $_[0]->set_val($::PlayTime); }, fields => 'length', schange => sub { $_[0]->set_max( defined $_[1] ? Songs::Get($_[1],'length') : 0); }, set => sub { ::SkipTo($_[1]) }, scroll => sub { $_[1] ? ::Forward(undef,10) : ::Rewind (undef,10) }, set_preview => \&Layout::Bar::update_preview_Time, cursor => 'hand2', text_empty=> '', }, TimeSlider => { class => 'Layout::Bar::Scale', parent => 'TimeBar', cursor => undef, }, VolumeBar => { class => 'Layout::Bar', orientation => 'left-to-right', event => 'Vol', update => sub { $_[0]->set_val( ::GetVol() ); }, set => sub { ::UpdateVol($_[1]) }, scroll => sub { ::ChangeVol($_[1] ? 'up' : 'down') }, max => 100, cursor => 'hand2', }, VolumeSlider => { class => 'Layout::Bar::Scale', orientation => 'bottom-to-top', parent => 'VolumeBar', cursor => undef, }, Volume => { class => 'Layout::Label', initsize=> '000', event => 'Vol', update => sub { $_[0]->set_label(sprintf("%d",::GetVol())); }, }, Stars => { New => \&Stars::new_layout_widget, group => 'Play', field => 'rating', event => 'Icons', schange => \&Stars::update_layout_widget, update => sub { $_[0]->update_layout_widget( ::GetSelID($_[0]) ); }, cursor => 'hand2', }, Cover => { class => 'Layout::AAPicture', group => 'Play', aa => 'album', oldopt1 => 'maxsize', schange => sub { my $key=(defined $_[1])? Songs::Get_gid($_[1],'album') : undef ; $_[0]->set($key); }, click1 => \&PopupSongsFromAlbum, click3 => sub { my $ID=::GetSelID($_[0]); ::PopupAAContextMenu({self =>$_[0], field=>'album', ID=>$ID, gid=>Songs::Get_gid($ID,'album'), mode => 'P'}) if defined $ID; }, event => 'Picture_album', update => \&Layout::AAPicture::Changed, noinit => 1, dragsrc => [::DRAG_ALBUM,\&DragCurrentAlbum], fields => 'album', }, ArtistPic => { class => 'Layout::AAPicture', group => 'Play', aa => 'artist', oldopt1 => 'maxsize', schange => sub { my $key=(defined $_[1])? Songs::Get_gid($_[1],'artists') : undef ;$_[0]->set($key); }, click1 => sub { ::PopupAA('artist'); }, event => 'Picture_artist', update => \&Layout::AAPicture::Changed, noinit => 1, dragsrc => [::DRAG_ARTIST,\&DragCurrentArtist], fields => 'artist', }, LabelsIcons => { New => sub { Gtk2::Table->new(1,1); }, group => 'Play', field => 'label', options => 'field', schange => \&UpdateLabelsIcon, update => \&UpdateLabelsIcon, event => 'Icons', tip => '%L', }, Filler => { New => sub { Gtk2::HBox->new; }, }, QueueList => { New => sub { $_[0]{type}='Q'; SongList::Common->new($_[0]); }, tabtitle=> _"Queue", tabicon => 'gmb-queue', issonglist=>1, }, PlayList => { New => sub { $_[0]{type}='A'; SongList::Common->new($_[0]); }, tabtitle=> _"Playlist", tabicon => 'gtk-media-play', issonglist=>1, }, SongList => { New => sub { SongList->new($_[0]); }, oldopt1 => 'mode', issonglist=>1, }, SongTree => { New => sub { SongTree->new($_[0]); }, issonglist=>1, }, EditList => { New => sub { $_[0]{type}='L'; SongList::Common->new($_[0]); }, tabtitle=> \&SongList::Common::MakeTitleLabel, tabrename=>\&SongList::Common::RenameTitleLabel, tabicon => 'gmb-list', issonglist=>1, }, TabbedLists => { class => 'Layout::NoteBook', EndInit => \&Layout::NoteBook::EndInit, default_child => 'PlayList', #these options will be passed to songlist/songtree children : options_for_list => 'songlist songtree sort songxpad songypad no_typeahead cols grouping', }, Context => { class => 'Layout::NoteBook', EndInit => \&Layout::NoteBook::EndInit, group => 'Play', typesubmenu=> 'C', match => 'context page', #these options will be passed to context children : options_for_context => 'group', }, SongInfo=> { class => 'Layout::SongInfo', group => 'Play', expander=> 1, hide_empty => 1, tabicon => 'gtk-info', tabtitle=> _"Song informations", }, PictureBrowser=> { class => 'Layout::PictureBrowser', group => 'Play', field => 'album', options => 'field', xalign => .5, yalign => .5, follow => 1, scroll_zoom => 1, show_list => 0, show_folders => 1, show_toolbar => 0, pdf_mode => 1, hpos => 140, vpos => 80, reset_zoom_on=>'folder', #can be group, folder, file or never nowrap => 0, schange => \&Layout::PictureBrowser::queue_song_changed, autoadd_type => 'context page pictures', tabicon => 'gmb-picture', tabtitle => _"Album pictures", }, AABox => { class => 'GMB::AABox', oldopt1 => sub { 'aa='.( $_[0] ? 'artist' : 'album' ) }, }, ArtistBox => { class => 'GMB::AABox', aa => 'artists', }, AlbumBox => { class => 'GMB::AABox', aa => 'album', }, FilterPane => { class => 'FilterPane', oldopt1 => sub { my ($nb,$hide,@pages)=split ',',$_[0]; return (nb => ++$nb,hide => $hide,pages=>join('|',@pages)); }, }, Total => { class => 'LabelTotal', oldopt1 => 'mode', saveoptions=> 'mode', }, FilterBox => { New => \&Browser::makeFilterBox, dragdest => [::DRAG_FILTER,sub { ::SetFilter($_[0],$_[2]);}], }, FilterLock=> { New => \&Browser::makeLockToggle, relief => 'none', }, HistItem => { New => \&Layout::MenuItem::new, text => _"Recent Filters", updatemenu => \&Browser::fill_history_menu, }, PlayItem => { New => \&Layout::MenuItem::new, text => _"Playing", updatemenu => sub { my $sl=::GetSonglist($_[0]); unless ($sl) {warn "Error : no associated songlist with $_[0]{name}\n"; return} ::BuildMenu(\@Browser::MenuPlaying, { self => $_[0], songlist => $sl }, $_[0]->get_submenu); }, }, LSortItem => { New => \&Layout::MenuItem::new, text => _"Sort", updatemenu => \&Browser::make_sort_menu, }, PSortItem => { New => \&Layout::MenuItem::new, text => _"Play order", updatemenu => sub { SortMenu($_[0]->get_submenu); }, }, PFilterItem => { New => \&Layout::MenuItem::new, text => _"Playlist filter", updatemenu => sub { FilterMenu($_[0]->get_submenu); }, }, QueueItem => { New => \&Layout::MenuItem::new, text => _"Queue", updatemenu => sub{ ::BuildMenu(\@MenuQueue,{ID=>$::SongID}, $_[0]->get_submenu); }, }, LayoutItem => { New => \&Layout::MenuItem::new, text => _"Layout", updatemenu => sub{ ::BuildChoiceMenu( Layout::get_layout_list(qr/G.*\+/), tree=>1, check=> sub {$::Options{Layout}}, code => sub { $::Options{Layout}=$_[1]; ::IdleDo('2_ChangeLayout',500, \&::CreateMainWindow ); }, menu => $_[0]->get_submenu, # re-use menu ); }, }, MainMenuItem => { New => \&Layout::MenuItem::new, text => _"Main", updatemenu => sub{ ::BuildMenu(\@MainMenu,undef, $_[0]->get_submenu); }, }, MenuItem => { New => \&Layout::MenuItem::new, }, SeparatorMenuItem=> { New => sub { Gtk2::SeparatorMenuItem->new }, }, Refresh => { class => 'Layout::Button', size => 'menu', stock => 'gtk-refresh', tip => _"Refresh list", activate=> sub { ::RefreshFilters($_[0]); }, }, PlayFilter => { class => 'Layout::Button', size => 'menu', stock => 'gtk-media-play', tip => _"Play filter", activate=> sub { ::Select( filter => ::GetFilter($_[0]), song=> 'trykeep', play =>1 ); }, click2 => sub { ::EnqueueFilter( ::GetFilter($_[0]) ); }, }, QueueFilter => { class => 'Layout::Button', size => 'menu', stock => 'gmb-queue', tip => _"Enqueue filter", activate=> sub { ::EnqueueFilter( ::GetFilter($_[0]) ); }, }, ResetFilter => { class => 'Layout::Button', size => 'menu', stock => 'gtk-clear', tip => _"Reset filter", activate=> sub { ::SetFilter($_[0],undef); }, }, ToggleButton => { class => 'Layout::TogButton', size => 'menu', }, HSeparator => { New => sub {Gtk2::HSeparator->new}, }, VSeparator => { New => sub {Gtk2::VSeparator->new}, }, Choose => { class => 'Layout::Button', stock => 'gtk-add', tip => _"Choose Artist/Album/Song", activate=> sub { Layout::Window->new('Search'); }, }, ChooseRandAlbum => { class => 'Layout::Button', stock => 'gmb-random-album', tip => _"Choose Random Album", options => 'action', activate=> sub { my $al=AA::GetAAList('album'); my $r=int rand(@$al); my $key=$al->[$r]; my $list=AA::GetIDs('album',$key); if (my $ac=$_[0]{action}) { ::DoActionForList($ac,$list); } else { my $ID=::FindFirstInListPlay($list); ::Select( song => $ID)}; }, click3 => sub { my @list; my $al=AA::GetAAList('album'); my $nb=5; while ($nb--) { my $r=int rand(@$al); push @list, splice(@$al,$r,1); last unless @$al; } ::PopupAA('album', list=>\@list, format=> ::__x( _"{album}\nby {artist}", album => "%a", artist => "%b")); }, }, AASearch => { class => 'AASearch', }, ArtistSearch => { class => 'AASearch', aa => 'artists', }, AlbumSearch => { class => 'AASearch', aa => 'album', }, SongSearch => { class => 'SongSearch', }, SimpleSearch => { class => 'SimpleSearch', dragdest=> [::DRAG_FILTER,sub { ::SetFilter($_[0],$_[2]);}], }, Visuals => { New => sub {my $darea=Gtk2::DrawingArea->new; return $darea unless $::Play_package->{visuals}; $::Play_package->add_visuals($darea); my $eb=Gtk2::EventBox->new; $eb->add($darea); return $eb}, click1 => sub {$::Play_package->set_visual('+') if $::Play_package->{visuals};}, #select next visual click2 => \&ToggleFullscreen, #FIXME use a fullscreen layout instead, click3 => \&VisualsMenu, minheight=>50, minwidth=>200, }, Connections => #FIXME could be better { class => 'Layout::Label', update => sub { unless ($::Play_package->can('get_connections')) { $_[0]->hide; $_[0]->set_no_show_all(1); return }; $_[0]->show; $_[0]->child->show_all; my @c= $::Play_package->get_connections; my $t= @c? _("Connections from :")."\n".join("\n",@c) : _("No connections"); $_[0]->child->set_text($t); }, event => 'connections', }, ShuffleList => { class => 'Layout::Button', stock => 'gmb-shuffle', size => SIZE_FLAGS, tip => _"Shuffle list", activate=> sub { my $songarray= ::GetSongArray($_[0]) || return; $songarray->Shuffle; }, event => 'SongArray', update => \&SensitiveIfMoreOneSong, PostInit=> \&SensitiveIfMoreOneSong, }, EmptyList => { class => 'Layout::Button', stock => 'gtk-clear', size => SIZE_FLAGS, tip => _"Empty list", activate=> sub { my $songarray= ::GetSongArray($_[0]) || return; $songarray->Replace(); }, event => 'SongArray', update => \&SensitiveIfMoreZeroSong, PostInit=> \&SensitiveIfMoreZeroSong, }, EditListButtons => { class => 'EditListButtons', }, QueueActions => { class => 'QueueActions', }, Fullscreen => { class => 'Layout::Button', stock => 'gtk-fullscreen', tip => _"Toggle fullscreen mode", text => _"Fullscreen", activate=> \&::ToggleFullscreenLayout, click3 => \&ToggleFullscreen, autoadd_type => 'button main', autoadd_option => 'AddFullscreenButton', }, Repeat => { New => sub { my $w=Gtk2::CheckButton->new(_"Repeat"); $w->signal_connect(clicked => sub { ::SetRepeat($_[0]->get_active); }); return $w; }, event => 'Repeat Sort', update => sub { if ($_[0]->get_active xor $::Options{Repeat}) { $_[0]->set_active($::Options{Repeat});} $_[0]->set_sensitive(!$::RandomMode); }, }, AddLabelEntry => { New => \&AddLabelEntry, group => 'Play', }, LabelToggleButtons => { class => 'Layout::LabelToggleButtons', group => 'Play', field => 'label', }, PlayOrderCombo => { New => \&PlayOrderComboNew, event => 'Sort SavedWRandoms SavedSorts', update => \&PlayOrderComboUpdate, minwidth=> 100, }, Progress => { class => 'Layout::Progress', compact=>1, }, VProgress => { class => 'Layout::Progress', vertical=>1, }, Equalizer => { New => \&Layout::Equalizer::new, event => 'Equalizer', update => \&Layout::Equalizer::update, preamp => 1, labels => 'x-small', }, EqualizerPresets => { class => 'Layout::EqualizerPresets', event => 'Equalizer', update => \&Layout::EqualizerPresets::update, onoff => 1, }, EqualizerPresetsSimple => { parent => 'EqualizerPresets', open =>1, notoggle=>1, } # RadioList => # { class => 'GMB::RadioList', # }, ); # aliases for previous widget names { my %aliases= ( Playlist => 'OpenBrowser', BContext => 'OpenContext', Date => 'Year', Label => 'Text', Vol => 'VolumeIcon', LabelVol => 'Volume', FLock => 'FilterLock', TogButton => 'ToggleButton', ProgressV => 'VProgress', FBox => 'FilterBox', Scale => 'TimeSlider', VolSlider => 'VolumeSlider', VolBar => 'VolumeBar', FPane => 'FilterPane', LabelTime => 'PlayingTime', #Pos => 'PlaylistPosition', 'Position', ? #SimpleSearch => 'Search', ? ); while ( my($alias,$real)= each %aliases ) { $Widgets{$alias}||=$Widgets{$real}; } } our %Layouts; sub get_layout_list { my $type=$_[0]; my @list=keys %Layouts; @list=grep defined $Layouts{$_}{Type} && $Layouts{$_}{Type}=~m/$type/, @list if $type; #return { map { $_ => _ ($Layouts{$_}{Name} || $_) } @list }; #use name instead of id if it exists, and translate my %cat; my @tree; for my $id (@list) { my $name2=$id; my $cat= $Layouts{$id}{Category}; my $name= $Layouts{$id}{Name} || _( $name2 ); my $array= $cat ? ($cat{$cat}||=[]) : \@tree; push @$array, $id, $name; } push @tree, $cat{$_},$_ for keys %cat; return \@tree; } sub get_layout_name { my $layout=shift; my $def= $Layouts{$layout}; return sprintf(_"Unknown layout '%s'",$layout) unless $def; my $name= $def->{Name} || _( $layout ); return $name; } sub InitLayouts { undef %Layouts; my @files= ::FileList( qr/\.layout$|(?:$::QSLASH)layouts$/o, $::DATADIR.::SLASH.'layouts', $::HomeDir.'layouts', $::CmdLine{searchpath} ); ReadLayoutFile($_) for @files; die "No layouts file found.\n" unless keys %Layouts; if ($::CmdLine{layoutlist}) { print "Available layouts : ((type) id\t: name)\n"; my ($max)= sort {$b<=>$a} map length, keys %Layouts; for my $id (sort keys %Layouts) { my $name= get_layout_name($id); my $type= $Layouts{$id}{Type} || ''; $type="($type)" if $type; printf "%-4s %-${max}s : %s\n",$type,$id,$name; } exit; } ::QHasChanged('Layouts'); } sub ReadLayoutFile { my $file=shift; return unless -f $file; warn "Reading layouts in $file\n" if $::debug; open my$fh,"<:utf8",$file or do { warn $!; return }; my $first; my $linecount=0; my ($linefirst,$linenext); while (1) { my ($next,$longline); my @lines=($first); while (local $_=<$fh>) { $linecount++; s#^\s+##; next if m/^#/; s#\s*[\n\r]+$##; if (s#\\$##) {$longline.=$_;next} next if $_ eq ''; if ($longline) {$_=$longline.$_;undef $longline;} if (m#^[{[]#) { $next=$_; $linenext=$linecount; last} push @lines,$_; } if ($first) { if ($first=~m#^\[#) {ParseLayout(\@lines,$file,$linefirst)} else {ParseSongTreeSkin(\@lines)} } $first=$next; $linefirst=$linenext; last unless $first; } close $fh; } sub ParseLayout { my ($lines,$file,$line)=@_; my $first=shift @$lines; my $name; if ($first=~m/^\[([^]=]+)\](?:\s*based on (.+))?$/) { if (defined $2 && !exists $Layouts{$2}) { warn "Ignoring layout '$1' because it is based on unknown layout '$2'\n"; return; } $name=$1; if (defined $2) { %{$Layouts{$name}}=%{$Layouts{$2}}; delete $Layouts{$name}{Name}; } else { delete $Layouts{$name}; } } else {return} my $currentkey; for (@$lines) { s#_\"([^"]+)"#my $tr=_( $1 ); $tr=~y/"/'/; qq/"$tr"/#ge; #translation, escaping the " so it is not picked up as a translatable string. Replace any " in translations because they would cause trouble unless (m/^(\w+)\s*=\s*(.*)$/) { $Layouts{$name}{$currentkey} .= ' '.$1 if m/\s*(.*)$/; next } #continuation of previous line if doesn't begin with "word=" $currentkey=$1; if ($2 eq '') {delete $Layouts{$name}{$currentkey};next} $Layouts{$name}{$currentkey}= $2; } for my $key (qw/Name Category Title/) { $Layouts{$name}{$key}=~s/^"(.*)"$/$1/ if $Layouts{$name}{$key}; #remove quotes from layout name and category } my $path=$file; $path=~s#([^/]+)$##; $file=$1; $Layouts{$name}{PATH}=$path; $Layouts{$name}{FILE}=$file; $Layouts{$name}{LINE}=$line; } sub ParseSongTreeSkin { my $lines=$_[0]; my $first=shift @$lines; my $ref; my $name; if ($first=~m#{(Column|Group) (.*)}#) { $ref= $1 eq 'Column' ? \%SongTree::STC : \%SongTree::GroupSkin; $name=$2; $ref=$ref->{$name}={}; } else {return} for (@$lines) { my ($key,$e,$string)= m#^(\w+)\s*([=:])\s*(.*)$#; next unless defined $key; if ($e eq '=') { if ($key eq 'elems' || $key eq 'options') { warn "Can't use reserved keyword $key in SongTreee column $name\n"; next } $string= _( $1 ) if $string=~m/_\"([^"]+)"/; #translation, escaping the " so it is not picked up as a translatable string $ref->{$key}=$string; } elsif ($string=~m#^Option(\w*)\((.+)\)$#) { my $type=$1; my $opt=::ParseOptions($2); $opt->{type}=$type; $ref->{options}{$key}=$opt; } else { push @{$ref->{elems}}, $key.'='.$string; } } } sub GetDefaultLayoutOptions { my $layout=$_[0]; my %default; my $options= $Layout::Layouts{$layout}{Default} || ''; if ($options=~m/^\w+\(/) #new format (v1.1.2) { for my $nameopt (::ExtractNameAndOptions($options)) { $default{$1}=$2 if $nameopt=~m/^(\w+)\((.+)\)$/; } } else # old format (version <1.1.2) { #warn "Old options format not supported for layout $layout => ignored\n"; #$opt2={}; my @optlist=split /\s+/,$options; unshift @optlist,'Window' if @optlist%2; #very old format (v<0.9573) %default= @optlist; } $_=::ParseOptions($_) for values %default; $default{DEFAULT_OPTIONS}=1; $default{Window}{DEFAULT_OPTIONS}=1; return \%default; } sub SaveWidgetOptions #Save options for this layout by collecting options of its widgets { my @widgets=@_; my %states; for my $widget (@widgets) { my $key=$widget->{name}; unless ($key) { warn "Error: no name for widget $widget\n"; next } my $opt; if (my $sub=$widget->{SaveOptions}) { my @opt=$sub->($widget); $opt= @opt>1 ? {@opt} : $opt[0]; } if (my $keys=$widget->{options_to_save}) { $opt->{$_}=$widget->{$_} for grep defined $widget->{$_}, split / /,$keys; } next unless $opt; if (!ref $opt) { warn "invalid options returned from $key\n";next } $opt=+{@$opt} if ref $opt eq 'ARRAY'; next unless keys %$opt; $states{$key}=$opt; } if ($::debug) { warn "Saving widget options :' :\n"; for my $key (sort keys %states) { warn " $key:\n"; warn " $_ = $states{$key}{$_}\n" for sort keys %{$states{$key}}; } } return \%states; } sub InitLayout { my ($self,$layout,$opt2)=@_; $self->{layout}=$layout; $self->set_name($layout); my $boxes=$Layouts{$layout}; $self->{KeyBindings}=::make_keybindingshash($boxes->{KeyBindings}) if $boxes->{KeyBindings}; $self->{widgets}={}; $self->{global_options}{default_group}=$self->{group}; for (qw/PATH SkinPath SkinFile DefaultFont DefaultFontColor/) { my $val= $self->{options}{$_} || $boxes->{$_}; $self->{global_options}{$_}=$val if defined $val; } my $mainwidget= $self->CreateWidgets($boxes,$opt2); $mainwidget ||= do { my $l=Gtk2::Label->new("Error : empty layout"); my $hbox=Gtk2::HBox->new; $hbox->add($l); $hbox; }; $self->add($mainwidget); if (my $name=$boxes->{DefaultFocus}) { $self->SetFocusOn($name); } } sub CreateWidgets { my ($self,$boxes,$opt2)=@_; if ($self->{layoutdepth} && $self->{layoutdepth}>10) { warn "Too many imbricated layouts\n"; return } $self->{layoutdepth}++; my $widgets=$self->{widgets}; # create boxes my @boxlist; my $defaultgroup= $self->{global_options}{default_group}; for my $key (keys %$boxes) { my $fullname=$key; my $type=substr $key,0,2; $type=$Layout::Boxes::Boxes{$type}; next unless $type; my $line=$boxes->{$key}; my $opt1={}; if ($line=~m#^\(#) { $opt1=::ExtractNameAndOptions($line); $line=~s#^\s+##; $opt1=~s#^\(##; $opt1=~s/\)$//; $opt1= ::ParseOptions($opt1); } my $opt2=$opt2->{$key} || {}; %$opt1= (group=>'',%$opt1,%$opt2); my $group=$opt1->{group}; $opt1->{group}= $defaultgroup.(length $group ? "-$group" : '') unless $group=~m/^[A-Z]/; my $box=$widgets->{$key}= $type->{New}( $opt1 ); $box->{$_}=$opt1->{$_} for grep exists $opt1->{$_}, qw/group tabicon tabtitle maxwidth maxheight expand_weight/; ApplyCommonOptions($box,$opt1); $box->{name}=$fullname; $box->set_border_width($opt1->{border}) if $opt1 && exists $opt1->{border} && $box->isa('Gtk2::Container'); $box->set_name($key); push @boxlist,$key,$line; } #pack boxes while (@boxlist) { my $key=shift @boxlist; my $line=shift @boxlist; my $type=substr $key,0,2; $type=$Layout::Boxes::Boxes{$type}; my $box=$widgets->{$key}; my @names= ::ExtractNameAndOptions($line,$type->{Prefix}); for my $name (@names) { my $packoptions; ($name,$packoptions)=@$name if ref $name; my $opt1; $opt1=$1 if $name=~s/\((.*)\)$//; #remove (...) and put it in $opt1 my $widget= $widgets->{$name}; my $placeholder; if (!$widget) #create widget if doesn't exist yet (only boxes have already been created) { $widget= NewWidget($name,$opt1,$opt2->{$name},$self->{global_options}); if ($widget) { $self->{widgets}{$name}=$widget; } else { $placeholder={name => $name, opt2=>$opt2->{$name}, }; } }; if ($widget) { if ($widget->parent) {warn "layout error: $name already has a parent -> can't put it in $key\n"; next;} $type->{Pack}( $box,$widget,$packoptions ); } elsif ($placeholder) { $placeholder->{opt1}=$opt1; $placeholder->{defaultgroup}=$defaultgroup; $placeholder=Layout::PlaceHolder->new( $type,$box,$placeholder,$packoptions); $self->{PlaceHolders}{$name}=$placeholder if $placeholder; } } $type->{EndInit}($box) if $type->{EndInit}; } for my $key (grep m/^[HV]Size/, keys %$boxes) { my $mode= ($key=~m/^V/)? 'vertical' : 'horizontal'; my @names=split /\s+/,$boxes->{$key}; if ( $names[0]=~m/^\d+$/ ) { my $s=shift @names; my @req=($mode eq 'vertical')? (-1,$s) : ($s,-1); $_->set_size_request(@req) for grep defined, map $widgets->{$_}, @names; next if @names==1; } my $sizegroup=Gtk2::SizeGroup->new($mode); for my $n (@names) { if (my $w=$widgets->{$n}) { $sizegroup->add_widget($w); } else { warn "Can't add unknown widget '$n' to sizegroup\n" } } } if (my $l=$boxes->{VolumeScroll}) { $widgets->{$_}->signal_connect(scroll_event => \&::ChangeVol) for grep $widgets->{$_}, split /\s+/,$l; } $self->signal_connect(key_press_event => \&KeyPressed,0); $self->signal_connect_after(key_press_event => \&KeyPressed,1); for my $widget (values %$widgets) { my $postinit= delete $widget->{PostInit}; $postinit->($widget) if $postinit; } $self->{layoutdepth}--; my @noparentboxes=grep m/^(?:[HV][BP]|[AMETNFSW]B|FR)/ && !$widgets->{$_}->parent, keys %$boxes; if (@noparentboxes==0) {warn "layout empty ('$self->{layout}')\n"; return;} elsif (@noparentboxes!=1) {warn "layout error: (@noparentboxes) have no parent -> can't find toplevel box\n"} return $widgets->{ $noparentboxes[0] }; } sub Parse_opt1 { my ($opt,$oldopt)=@_; my %opt; if (defined $opt) { if ($oldopt && $opt!~m/=/) { if (ref $oldopt) { %opt= $oldopt->($opt); } else { @opt{split / /,$oldopt}=split ',',$opt; } } else { #%opt= $opt=~m/(\w+)=([^,]*)(?:,|$)/g; return Hash_to_HoH( ::ParseOptions($opt) ); } } return \%opt; } sub Hash_to_HoH # turn { 'key1/key2' => value } into { key1 => { key2 => value } } { my $hash=shift; for my $key (grep m#/#, keys %$hash) { my $val=delete $hash->{$key}; my @keys=split '/',$key; $key= pop @keys; my $h=$hash; for (@keys) { $h= $h->{$_}||={}; last if !ref $h; } $h->{$key}=$val; } return $hash; # the hash ref hasn't changed, but can be handy to return it anyway } sub NewWidget { my ($name,$opt1,$opt2,$global_opt)=@_; my $namefull=$name; $name=~s/\d+$//; my $ref; $global_opt ||={}; if ($name=~m/^@(.+)$/) { $ref= { class => 'Layout::Embedded', }; $global_opt={ %$global_opt, layout=>$1 }; } else { $ref=$Widgets{$name} } unless ($ref) { return undef; } while (my $p=$ref->{parent}) #inherit from parent { my $pref=$Widgets{$p}; $ref= { %$pref, %$ref }; delete $ref->{parent} if $ref->{parent} eq $p; } $opt1=Parse_opt1($opt1,$ref->{oldopt1}) unless ref $opt1; $opt2||={}; my %options= (group=>'', %$ref, %$opt1, %$opt2, name=>$namefull, %$global_opt); $options{font} ||= $global_opt->{DefaultFont} if $global_opt->{DefaultFont}; my $group= $options{group}; #FIXME make undef group means parent's group ? my $defaultgroup= $options{default_group} || 'default_group'; $options{group}= $defaultgroup.($group=~m/^\w/ ? '-' : '').$group unless $group=~m/^[A-Z]/; #group local to window unless it begins with uppercase my $widget= $ref->{class} ? $ref->{class}->new(\%options,$ref) : $ref->{New}(\%options); return unless $widget; $widget->{$_}= $options{$_} for 'group',split / /, ($ref->{options} || ''); $widget->{$_}=$options{$_} for grep exists $options{$_}, qw/tabtitle tabicon tabrename maxwidth maxheight expand_weight/; $widget->{options_to_save}=$ref->{saveoptions} if $ref->{saveoptions}; $widget->{name}=$namefull; $widget->set_name($name); ApplyCommonOptions($widget,\%options); $widget->{actions}{$_}=$options{$_} for grep m/^click\d*/, keys %options; $widget->signal_connect(button_press_event => \&Button_press_cb) if $widget->{actions}; if (my $cursor=$options{cursor}) { $widget->signal_connect(realize => sub { my ($widget,$cursor)=@_; my $gdkwin= $widget->window; if ($widget->isa('Gtk2::EventBox') && !$widget->get_visible_window) { # for eventbox using an input-only gdkwindow, $widget->window is actually the parent's gdkwin, # the only way to get to the input-only gdkwin is looking at all the children of its parent :( for my $child ($gdkwin->get_children) { my $w= Glib::Object->new_from_pointer($child->get_user_data); if ($w && $w==$widget) { $gdkwin=$child; last } } } $gdkwin->set_cursor(Gtk2::Gdk::Cursor->new($cursor)); },$cursor); } my $tip= $options{tip}; if ( defined $tip) { if (!ref $tip) { my @fields=::UsedFields($tip); if (@fields) { $widget->{song_tip}=$tip; ::WatchSelID($widget,\&UpdateSongTip,\@fields); UpdateSongTip($widget,::GetSelID($widget)); } else { $tip=~s#\\n#\n#g; $widget->set_tooltip_text($tip); } } else { $widget->{state_tip}=$tip; } } if (my $schange=$ref->{schange}) { my $fields= $options{fields} || $options{field}; $fields= $fields ? [ split / /,$fields ] : undef; ::WatchSelID($widget,$schange, $fields); $schange->($widget,::GetSelID($widget)); } if ($ref->{event}) { my $sub=$ref->{update} || \&UpdateObject; ::Watch($widget,$_,$sub ) for split / /,$ref->{event}; $sub->($widget) unless $ref->{noinit}; } ::set_drag($widget,source => $ref->{dragsrc}, dest => $ref->{dragdest}); my $init= delete $widget->{EndInit} || $ref->{EndInit}; $init->($widget) if $init; $widget->{PostInit}||= $ref->{PostInit}; return $widget; } sub ApplyCommonOptions # apply some options common to both boxes and other widgets { my ($widget,$opt)=@_; if ($opt->{minwidth} or $opt->{minheight}) { my ($minwidth,$minheight)=$widget->get_size_request; $minwidth= $opt->{minwidth} || $minwidth; $minheight= $opt->{minheight} || $minheight; $widget->set_size_request($minwidth,$minheight); } if ($opt->{hover_layout}) # only works with widgets/boxes that have their own gdkwindow (put it into a WB box otherwise) { $widget->{$_}=$opt->{$_} for qw/hover_layout hover_delay hover_layout_pos/; Layout::Window::Popup::set_hover($widget); } } sub RegisterWidget { my ($name,$hash)=@_; my $action; if ($hash) { if ($Widgets{$name} && $Widgets{$name}!=$hash) { warn "Widget $name already registered\n"; return } $Widgets{$name}=$hash; ::HasChanged(Widgets=>'new',$name); } else { ::HasChanged(Widgets=>'remove',$name); delete $Widgets{$name}; } } sub WidgetChangedAutoAdd { my $name=shift; ::HasChanged(Widgets=>'option',$name) if $Widgets{$name}; } sub UpdateObject { my $widget=$_[0]; if ( my $tip=$widget->{state_tip} ) { $tip= $tip->($widget) if ref $tip; $widget->set_tooltip_text($tip); } if ($widget->{skin}) {$widget->queue_draw} elsif ($widget->{stock}) { $widget->UpdateStock } } sub Button_press_cb { my ($self,$event)=@_; my $actions=$self->{actions}; my $key='click'.$event->button; my $sub=$actions->{$key}; return 0 if !$sub && $self->{clicked_cmd}; $sub||= $actions->{click} || $actions->{click1}; return 0 unless $sub; if (ref $sub) {&$sub} else { ::run_command($self,$sub) } 1; } sub UpdateSongTip { my ($widget,$ID)=@_; if ($widget->{song_tip}) { my $tip= defined $ID ? ::ReplaceFields($ID,$widget->{song_tip}) : ''; $widget->set_tooltip_text($tip); } } #sub SetSort #{ my($self,$sort)=@_; # $self->{songlist}->Sort($sort); #} sub ShowHide { my ($self,$names,$resize,$show)=@_; $show= !grep $_ && $_->visible, map $self->{widgets}{$_}, split /\|/,$names unless defined $show; if ($show) { Show($self,$names,$resize); } else { Hide($self,$names,$resize); } } sub Hide { my ($self,$names,$resize)=@_; my @resize=split //,$resize||''; my $r; my ($ww,$wh)=$self->get_size; for my $name ( split /\|/,$names ) { my $widget=$self->{widgets}{$name}; $r=shift @resize if @resize; next unless $widget;# && $widget->visible; my $alloc=$widget->allocation; my $w=$alloc->width; my $h=$alloc->height; $self->{hidden}{$name}=$w.'x'.$h; if ($r) { if ($r eq 'v') {$wh-=$h} elsif ($r eq 'h') {$ww-=$w} } $widget->hide; } $self->resize($ww,$wh) if $resize && $resize ne '_'; ::HasChanged('HiddenWidgets'); } sub Show { my ($self,$names,$resize)=@_; my @resize=split //,$resize||''; my $r; my ($ww,$wh)=$self->get_size; for my $name ( split /\|/,$names ) { my $widget=$self->{widgets}{$name}; next unless $widget && !$widget->visible; $widget->show; my $oldsize=delete $self->{hidden}{$name}; next unless $oldsize && $oldsize=~m/x/; my ($w,$h)=split 'x',$oldsize; $r=shift @resize if @resize; if ($r) { if ($r eq 'v') {$wh+=$h} elsif ($r eq 'h') {$ww+=$w} } } $self->resize($ww,$wh) if $resize && $resize ne '_'; ::HasChanged('HiddenWidgets'); } sub GetShowHideState { my ($self,$names)=@_; my $hidden; for my $name ( split /\|/,$names ) { my $widget=$self->{widgets}{$name}; next unless $widget; $hidden++ unless $widget->visible; } return !$hidden; } sub ToggleFullscreen { return unless $_[0]; my $win= ::get_layout_widget($_[0])->get_toplevel; if ($win->{fullscreen}) { if ($::FullscreenWindow && $win==$::FullscreenWindow) { $win->close_window } else {$win->unfullscreen} } else {$win->fullscreen} } sub KeyPressed { my ($self,$event,$after)=@_; my $key=Gtk2::Gdk->keyval_name( $event->keyval ); my $focused=$self->get_toplevel->get_focus; return 0 if !$after && $focused && ($focused->isa('Gtk2::Entry') || $focused->isa('Gtk2::SpinButton')); my $mod; $mod.='c' if $event->state >= 'control-mask'; $mod.='a' if $event->state >= 'mod1-mask'; $mod.='w' if $event->state >= 'mod4-mask'; $mod.='s' if $event->state >= 'shift-mask'; $key= ($after? '':'+') . ($mod? "$mod-":'') . lc($key); my ($cmd,$arg); if ( exists $::CustomBoundKeys{$key} ) { $cmd= $::CustomBoundKeys{$key}; } elsif ($self->{KeyBindings} && exists $self->{KeyBindings}{$key} ) { $cmd= $self->{KeyBindings}{$key}; } elsif ( exists $::GlobalBoundKeys{$key} ) { $cmd= $::GlobalBoundKeys{$key}; } elsif ($after && $self->{fullscreen} && $key eq 'Escape') { $cmd='ToggleFullscreen' } return 0 unless $cmd; if ($self->isa('Gtk2::Window')) #try to find the focused widget (gmb widget, not gtk one), so that the cmd can act on it { my $widget=$self->get_focus; while ($widget) {last if exists $widget->{group}; $widget=$widget->parent} $self=$widget if $widget; } ::run_command($self,$cmd); return 1; } sub EnqueueSelected { my $self=shift; return unless $self; if (my $songlist=::GetSonglist($self)) { $songlist->EnqueueSelected; } } sub GoToCurrentSong { my $self=shift; return unless $self; if (my $songlist=::GetSonglist($self)) { $songlist->FollowSong; } } sub SetFocusOn { my ($self,$name)=@_; while ($name=~s#^([^/]+)/##) # if name contains slashes, divide it into parent and child, where parent can be an Embedded layout or a TabbedLists/Context/NB { $self=$self->{widgets}{$1}; return unless $self; } my $widget=$self->{widgets}{$name}; if ($widget) { $widget=$widget->{DefaultFocus} while $widget->{DefaultFocus}; TurnPagesToWidget($widget); $widget->grab_focus; } } sub TurnPagesToWidget #change the current page of all parent notebook so that widget is on it { my $parent=$_[0]; while (1) { my $child=$parent; $parent=$child->parent; last unless $parent; if ($parent->isa('Gtk2::Notebook')) { $parent->set_current_page($parent->page_num($child)); } } } sub SensitiveIfMoreOneSong { my $songarray= ::GetSongArray($_[0]); $_[0]->set_sensitive($songarray && @$songarray>1); } sub SensitiveIfMoreZeroSong { my $songarray= ::GetSongArray($_[0]); $_[0]->set_sensitive($songarray && @$songarray>0); } ################################################################################# sub PlayOrderComboNew { my $opt=$_[0]; my $store=Gtk2::ListStore->new(('Glib::String')x3); my $combo=Gtk2::ComboBox->new($store); my $cell=Gtk2::CellRendererPixbuf->new; $cell->set_fixed_size( Gtk2::IconSize->lookup('menu') ); $combo->pack_start($cell,0); $combo->add_attribute($cell,stock_id => 2); $cell=Gtk2::CellRendererText->new; $combo->pack_start($cell,1); $combo->add_attribute($cell, text => 0); $combo->signal_connect( changed => sub { my $combo=$_[0]; return if $combo->{busy}; my $store=$combo->get_model; my $sort=$store->get($combo->get_active_iter,1); if ($sort=~m/^EDIT (.)$/) { PlayOrderComboUpdate($combo); #so that the combo doesn't stay on Edit... if ($1 eq 'O') { ::EditSortOrder(undef,$::Options{Sort},undef, \&::Select_sort); } elsif ($1 eq 'R') { ::EditWeightedRandom(undef,$::Options{Sort},undef, \&::Select_sort); } } else { ::Select('sort' => $sort); } }); return $combo; } sub PlayOrderComboUpdate { my $combo=$_[0]; $combo->{busy}=1; my $store=$combo->get_model; $store->clear; my $check=$::Options{Sort}; my $found; my $iter; for my $name (sort keys %{$::Options{SavedWRandoms}}) { my $sort=$::Options{SavedWRandoms}{$name}; $store->set(($iter=$store->append), 0,$name, 1,$sort, 2,'gmb-random'); $found=$iter if $sort eq $check; } if (!$found && $check=~m/^random:/) { $store->set($iter=$store->append, 0, _"unnamed random mode", 1,$check,2,'gmb-random'); $found=$iter; } $store->set($store->append, 0, _"Edit random modes ...", 1,'EDIT R'); $store->set($iter=$store->append, 0, _"Shuffle", 1,'shuffle',2,'gmb-shuffle'); $found=$iter if 'shuffle' eq $check; if (defined $::ListMode) { $store->set($iter=$store->append, 0, _"List order", 1,'',2,'gmb-list'); $found=$iter if '' eq $check; } for my $name (sort keys %{$::Options{SavedSorts}}) { my $sort=$::Options{SavedSorts}{$name}; $store->set($iter=$store->append, 0, $name, 1,$sort,2,'gtk-sort-ascending'); $found=$iter if $sort eq $check; } if (!$found) { $store->set($iter=$store->append, 0, ::ExplainSort($check), 1,$check,2,'gtk-sort-ascending'); $found=$iter; } $store->set($store->append, 0, _"Edit ordered modes ...",1,'EDIT O'); $combo->set_active_iter($found); $combo->{busy}=undef; } sub SortMenu { my $nopopup= $_[0]; my $menu = $_[0] || Gtk2::Menu->new; my $return=0; $return=1 unless @_; my $check=$::Options{Sort}; my $found; my $callback=sub { ::Select('sort' => $_[1]); }; my $append=sub { my ($menu,$name,$sort,$true,$cb)=@_; $cb||=$callback; $true=($sort eq $check) unless defined $true; my $item = Gtk2::CheckMenuItem->new_with_label($name); $item->set_draw_as_radio(1); $item->set_active($found=1) if $true; $item->signal_connect (activate => $cb, $sort ); $menu->append($item); }; my $submenu= Gtk2::Menu->new; my $sitem = Gtk2::MenuItem->new(_"Weighted Random"); for my $name (sort keys %{$::Options{SavedWRandoms}}) { $append->($submenu,$name, $::Options{SavedWRandoms}{$name} ); } my $editcheck=(!$found && $check=~m/^random:/); $append->($submenu,_"Custom...", undef, $editcheck, sub { ::EditWeightedRandom(undef,$::Options{Sort},undef, \&::Select_sort); }); $sitem->set_submenu($submenu); $menu->prepend($sitem); $append->($menu,_"Shuffle",'shuffle') unless $check eq 'shuffle'; if ($check=~m/shuffle/) { my $item=Gtk2::MenuItem->new(_"Re-shuffle"); $item->signal_connect(activate => $callback, $check ); $menu->append($item); } { my $item=Gtk2::CheckMenuItem->new(_"Repeat"); $item->set_active($::Options{Repeat}); $item->set_sensitive(0) if $::RandomMode; $item->signal_connect(activate => sub { ::SetRepeat($_[0]->get_active); } ); $menu->append($item); } $menu->append(Gtk2::SeparatorMenuItem->new); #separator between random and non-random modes $append->($menu,_"List order", '' ) if defined $::ListMode; for my $name (sort keys %{$::Options{SavedSorts}}) { $append->($menu,$name, $::Options{SavedSorts}{$name} ); } $append->($menu,_"Custom...",undef,!$found,sub { ::EditSortOrder(undef,$::Options{Sort},undef, \&::Select_sort ); }); return $menu if $nopopup; ::PopupMenu($menu); } sub FilterMenu { my $nopopup= $_[0]; my $menu = $_[0] || Gtk2::Menu->new; my ($check,$found); $check=$::SelectedFilter->{string} if $::SelectedFilter; my $item_callback=sub { ::Select(filter => $_[1]); }; my $item0= Gtk2::CheckMenuItem->new(_"All songs"); $item0->set_active($found=1) if !$check && !defined $::ListMode; $item0->set_draw_as_radio(1); $item0->signal_connect ( activate => $item_callback ,'' ); $menu->append($item0); for my $list (sort keys %{$::Options{SavedFilters}}) { my $filt=$::Options{SavedFilters}{$list}->{string}; my $item = Gtk2::CheckMenuItem->new_with_label($list); $item->set_draw_as_radio(1); $item->set_active($found=1) if defined $check && $filt eq $check; $item->signal_connect ( activate => $item_callback ,$filt ); $menu->append($item); } my $item=Gtk2::CheckMenuItem->new(_"Custom..."); $item->set_active(1) if defined $check && !$found; $item->set_draw_as_radio(1); $item->signal_connect ( activate => sub { ::EditFilter(undef,$::SelectedFilter,undef, sub {::Select(filter => $_[0])}); }); $menu->append($item); if (my @SavedLists=::GetListOfSavedLists()) { my $submenu=Gtk2::Menu->new; my $list_cb=sub { ::Select( staticlist => $_[1] ) }; for my $list (@SavedLists) { my $item = Gtk2::CheckMenuItem->new_with_label($list); $item->set_draw_as_radio(1); $item->set_active(1) if defined $::ListMode && $list eq $::ListMode; $item->signal_connect( activate => $list_cb, $list ); $submenu->append($item); } my $sitem=Gtk2::MenuItem->new(_"Saved Lists"); #my $sitem=Gtk2::CheckMenuItem->new('Saved Lists'); #$item->set_draw_as_radio(1); $sitem->set_submenu($submenu); $menu->prepend($sitem); } return $menu if $nopopup; ::PopupMenu($menu); } sub VisualsMenu { my $menu=Gtk2::Menu->new; my $cb=sub { $::Play_package->set_visual($_[1]) if $::Play_package->{visuals}; }; return unless $::Play_package->{visuals}; my @l= $::Play_package->list_visuals; my $current= $::Options{gst_visual}||$l[0]; for my $v (@l) { my $item=Gtk2::CheckMenuItem->new_with_label($v); $item->set_draw_as_radio(1); $item->set_active(1) if $current eq $v; $item->signal_connect (activate => $cb,$v); $menu->append($item); } ::PopupMenu($menu); } sub UpdateLabelsIcon { my $table=$_[0]; $table->remove($_) for $table->get_children; return unless defined $::SongID; my $row=0; my $col=0; my $count=0; for my $stock ( Songs::Get_icon_list($table->{field},$::SongID) ) { my $img=Gtk2::Image->new_from_stock($stock,'menu'); $count++; $table->attach($img,$col,$col+1,$row,$row+1,'shrink','shrink',1,1); if (++$row>=1) {$row=0; $col++} } $table->show_all; } sub AddLabelEntry #create entry to add a label to the current song { my $entry=Gtk2::Entry->new; $entry->set_tooltip_text(_"Adds labels to the current song"); $entry->signal_connect(activate => sub { my $entry=shift; my $label= $entry->get_text; my $ID= ::GetSelID($entry); return unless defined $ID & defined $label; $entry->set_text(''); Songs::Set($ID,"+label",$label); }); GMB::ListStore::Field::setcompletion($entry,'label'); return $entry; } sub DragCurrentSong { ::DRAG_ID,$::SongID; } sub DragCurrentArtist { ::DRAG_ARTIST,@{Songs::Get_gid($::SongID,'artists')}; } sub DragCurrentAlbum { ::DRAG_ALBUM,Songs::Get_gid($::SongID,'album'); } sub PopupSongsFromAlbum { my $ID=::GetSelID($_[0]); return unless defined $ID; my $aid=Songs::Get_gid($ID,'album'); ::ChooseSongsFromA($aid,nocover=>0); } #################################### package Layout::Window; our @ISA; BEGIN {push @ISA,'Layout';} use base 'Gtk2::Window'; sub new { my ($class,$layout,%options)=@_; my @original_args=@_; my $fallback=delete $options{fallback} || 'Lists, Library & Context'; my $opt0={}; if (my $opt= $layout=~m/^[^(]+\(.*=/) { ($layout,$opt0)= $layout=~m/^([^(]+)\((.*)\)$/; #separate layout id and options $opt0= ::ParseOptions($opt0); } unless (exists $Layout::Layouts{$layout}) { if ($fallback eq 'NONE') { warn "Layout '$layout' not found\n"; return undef; } warn "Layout '$layout' not found, using '$fallback' instead\n"; $layout=$fallback; #FIXME if not a player window $Layout::Layouts{$layout} ||= { VBmain=>'Label(text="Error : fallback layout not found")' }; #create an error layout if fallback not found } my $opt2=$::Options{Layouts}{$layout}; $opt2||= Layout::GetDefaultLayoutOptions($layout); for my $child_key (grep m#./.#, keys %options) { my ($child,$key)=split "/",$child_key,2; $opt2->{$child}{$key}= delete $options{$child_key}; } my $opt1=::ParseOptions( $Layout::Layouts{$layout}{Window}||'' ); %options= ( borderwidth=>0, %$opt1, %{$opt2->{Window}||{}}, %options, %$opt0 ); #warn "window options (layout=$layout) :\n";warn " $_ => $options{$_}\n" for sort keys %options; my $uniqueid= $options{uniqueid} || 'layout='.$layout; # ifexist=toggle => if a window with same uniqueid exist it will be closed # ifexist=present => if a window with same uniqueid exist it presented if (my $mode=$options{ifexist}) { my ($window)=grep $_->isa('Layout::Window') && $_->{uniqueid} eq $uniqueid, Gtk2::Window->list_toplevels; if ($window) { if ($mode eq 'toggle' && !$window->{quitonclose}) { $window->close_window; return } elsif ($mode eq 'replace' && !$window->{quitonclose}) { $window->close_window; return Layout::Window::new(@original_args,ifexists=>0); } # destroying previous window make it save its settings, then restart new() from the start with new $opt2 but the same original arguments, add ifexists=>0 to make sure it doesn't loop elsif ($mode eq 'present') { $window->force_present; return } } } my $wintype= delete $options{wintype} || 'toplevel'; my $self=bless Gtk2::Window->new($wintype), $class; $self->{uniqueid}= $uniqueid; $self->set_role($layout); $self->set_type_hint(delete $options{typehint}) if $options{typehint}; $self->{options}=\%options; $self->{name}='Window'; $self->{SaveOptions}=\&SaveWindowOptions; $self->{group}= 'Global('.::refaddr($self).')'; ::Watch($self,Save=>\&SaveOptions); $self->set_title(::PROGRAM_NAME); if ($options{dragtomove}) { $self->add_events(['button-press-mask']); $self->signal_connect_after(button_press_event => sub { my $event=$_[1]; $_[0]->begin_move_drag($event->button, $event->x_root, $event->y_root, $event->time); 1; }); } #$self->signal_connect (show => \&show_cb); $self->signal_connect (window_state_event => sub { my $self=$_[0]; my $wstate=$_[1]->new_window_state(); warn "window $self is $wstate\n" if $::debug; $self->{sticky}=($wstate >= 'sticky'); #save sticky state $self->{fullscreen}=($wstate >= 'fullscreen'); $self->{ontop}=($wstate >= 'above'); $self->{below}=($wstate >= 'below'); $self->{withdrawn}=($wstate >= 'withdrawn'); $self->{iconified}=($wstate >= 'iconified'); 0; }); $self->signal_connect(focus_in_event=> sub { $_[0]{last_focused}=time;0; }); $self->signal_connect(delete_event => \&close_window); # ::set_drag($self, dest => [::DRAG_FILE,sub # { my ($self,$type,@values)=@_; # warn "@values"; # }], # motion => sub # { my ($self,$context,$x,$y,$time)=@_; # my $target=$self->drag_dest_find_target($context, $self->drag_dest_get_target_list); # $context->{get_data}=1; # $self->drag_get_data($context, $target, $time); # ::TRUE; # } # ); $self->InitLayout($layout,$opt2); $self->SetWindowOptions(\%options); if (my $skin=$Layout::Layouts{$layout}{Skin}) { $self->set_background_skin($skin) } $self->init; ::HasChanged('HiddenWidgets'); $self->set_opacity($self->{opacity}) if exists $self->{opacity} && $self->{opacity}!=1; ::QHasChanged('Windows'); return $self; } sub init { my $self=$_[0]; if ($self->{options}{transparent}) { if ($::CairoOK) { make_transparent($self); } else { warn "no Cairo perl module => can't make the window transparent\n" } } $self->child->show_all; #needed to get the true size of the window $self->realize; $self->Resize if $self->{size}; { my @hidden; # widgets that were saved as hidden @hidden=keys %{ $self->{hidden} } if $self->{hidden}; my $widgets=$self->{widgets}; # look for widgets asking for other widgets to be hidden at init for my $w (values %$widgets) { my $names= delete $w->{need_hide}; next unless $names; push @hidden, split /\|/, $names; } # hide them $_->hide for grep defined, map $widgets->{$_}, @hidden; } #$self->set_position();#doesn't work before show, at least with sawfish my ($x,$y)= $self->Position; $self->move($x,$y) if defined $x; $self->show; $self->move($x,$y) if defined $x; $self->parse_geometry( delete $::CmdLine{geometry} ) if $::CmdLine{geometry}; $self->set_workspace( delete $::CmdLine{workspace} ) if exists $::CmdLine{workspace}; if ($self->{options}{insensitive}) { my $mask=Gtk2::Gdk::Bitmap->create_from_data(undef,'',1,1); $self->input_shape_combine_mask($mask,0,0); } } sub layout_name { my $self=shift; my $id=$self->{layout}; return Layout::get_layout_name($id); } sub close_window { my $self=shift; $self->SaveOptions; unless ($self->{quitonclose}) { $_->destroy for values %{$self->{widgets}}; $self->destroy; return } if ($::Options{CloseToTray}) { ::ShowHide(0); return 1} else { &::Quit } } sub SaveOptions { my $self=shift; my $opt=Layout::SaveWidgetOptions($self,values %{ $self->{widgets} }, values %{ $self->{PlaceHolders} }); $::Options{Layouts}{$self->{layout}} = $opt; } sub SaveWindowOptions { my $self=$_[0]; my %wstate; $wstate{size}=join 'x',$self->get_size; #unless ($self->{options}{DoNotSaveState}) { $wstate{sticky}=1 if $self->{sticky}; $wstate{fullscreen}=1 if $self->{fullscreen}; $wstate{ontop}=1 if $self->{ontop}; $wstate{below}=1 if $self->{below}; $wstate{nodecoration}=1 unless $self->get_decorated; $wstate{skippager}=1 if $self->get_skip_pager_hint; if ($self->{saved_position}) { $wstate{pos}=$self->{saved_position}; $wstate{skiptaskbar}=1 if $self->{skip_taskbar_hint}; } else { $wstate{pos}=join 'x',$self->get_position; $wstate{skiptaskbar}=1 if $self->get_skip_taskbar_hint; } } my $hidden=$self->{hidden}; if ($hidden && keys %$hidden) { $wstate{hidden}= join '|', map { my $dim=$hidden->{$_}; $_.($dim ? ":$dim" : '') } sort keys %$hidden; } return \%wstate; } sub SetWindowOptions { my ($self,$opt)=@_; my $layouthash= $Layout::Layouts{ $self->{layout} }; if ($opt->{fullscreen}) { $self->fullscreen; } else { $self->{size}=$opt->{size}; #window position in format numberxnumber number can be a % of screen size $self->{pos}=$opt->{pos}; } $self->stick if $opt->{sticky}; $self->set_keep_above(1) if $opt->{ontop}; $self->set_keep_below(1) if $opt->{below}; $self->set_decorated(0) if $opt->{nodecoration}; $self->set_skip_pager_hint(1) if $opt->{skippager}; $self->set_skip_taskbar_hint(1) if $opt->{skiptaskbar}; $self->{opacity}=$opt->{opacity} if defined $opt->{opacity}; $self->{hidden}={ $opt->{hidden}=~m/(\w+)(?::?(\d+x\d+))?/g } if $opt->{hidden}; $self->{size}= $self->{fixedsize}= $opt->{fixedsize} if $opt->{fixedsize}; $self->set_border_width($self->{options}{borderwidth}); $self->set_gravity($opt->{gravity}) if $opt->{gravity}; my $title= $layouthash->{Title} || $opt->{title} || _"%S by %a"; $title=~s/^"(.*)"$/$1/; if (my @l=::UsedFields($title)) { $self->{TitleString}=$title; my %fields; $fields{$_}=undef for @l; ::Watch($self,'CurSong',\&UpdateWindowTitle,\%fields); $self->UpdateWindowTitle(); } else { $self->set_title($title) } } sub UpdateWindowTitle { my $self=shift; my $ID=$::SongID; if (my $title=$self->{TitleString}) { $title= defined $ID ? ::ReplaceFields($ID,$title) : '<'._("Playlist Empty").'>'; $self->set_title($title); } } sub Resize { my $self=shift; my ($w,$h)= split 'x',delete $self->{size}; return unless defined $h; my $screen=$self->get_screen; my $monitor=$screen->get_monitor_at_window($self->window); my (undef,undef,$monitorwidth,$monitorheight)=$screen->get_monitor_geometry($monitor)->values; $w= $1*$monitorwidth/100 if $w=~m/(\d+)%/; $h= $1*$monitorheight/100 if $h=~m/(\d+)%/; if ($self->{options}{DEFAULT_OPTIONS}) { $monitorwidth-=40; $monitorheight-=80; } # if using default layout size, reserve some space for potential panels and decorations #FIXME use gdk_screen_get_monitor_workarea once ported to gtk3 $w=$monitorwidth if $w>$monitorwidth; $h=$monitorheight if $h>$monitorheight; if ($self->{fixedsize}) { $w=-1 if $w<1; # -1 => do not override default minimum size $h=-1 if $h<1; $self->set_size_request($w,$h); $self->set_resizable(0); } else { $w=1 if $w<1; # 1 => resize to minimum size $h=1 if $h<1; $self->resize($w,$h); } } sub Position { my $self=shift; my $pos=delete $self->{pos}; return unless $pos; #format : 100x100 50%x100% -100x-100 500-100% x 500-50% 1@50%x100% my ($monitor,$x,$xalign,$y,$yalign)= $pos=~m/(?:(\d+)@)?\s*([+-]?\d+%?)(?:([+-]\d+)%)?\s*x\s*([+-]?\d+%?)(?:([+-]\d+)%)?/; my ($w,$h)=$self->get_size; # size of window to position my $screen=$self->get_screen; my $absolute_coords; if (defined $monitor) { $monitor=undef if $monitor>=$screen->get_n_monitors; } if (!defined($monitor) && $x!~m/[-%]/ && $y!~m/[-%]/) { $monitor=$screen->get_monitor_at_point($x,$y); $absolute_coords=1; } if (!defined $monitor) { $monitor=$screen->get_monitor_at_window($self->window); } my ($xmin,$ymin,$monitorwidth,$monitorheight)=$screen->get_monitor_geometry($monitor)->values; $xalign= $x=~m/%/ ? 50 : 0 unless defined $xalign; $yalign= $y=~m/%/ ? 50 : 0 unless defined $yalign; $x= $monitorwidth*$1/100 if $x=~m/(-?\d+)%/; $y= $monitorheight*$1/100 if $y=~m/(-?\d+)%/; $x= $monitorwidth-$x if $x<0; $y= $monitorheight-$y if $y<0; $x-= $xalign*$w/100; $y-= $yalign*$h/100; if ($absolute_coords) { $x-=$xmin if $x>$xmin; $y-=$ymin if $y>$ymin; } $x=0 if $x<0; $x=$monitorwidth -$w if $x+$w>$monitorwidth; $y=0 if $y<0; $y=$monitorheight-$h if $y+$h>$monitorheight; $x+=$xmin; $y+=$ymin; return $x,$y; } sub set_workspace #only works with Gnome2::Wnck { my ($self,$workspace)=@_; eval {require Gnome2::Wnck}; if ($@) { warn "Setting workspace : error loading Gnome2::Wnck : $@\n"; return } my $screen= Gnome2::Wnck::Screen->get_default; $screen->force_update; $workspace= $screen->get_workspace($workspace); return unless $workspace; my $xid= $self->window->get_xid; my $w=Gnome2::Wnck::Window->get($xid); return unless $w; $w->move_to_workspace($workspace); } sub make_transparent { my @children=($_[0]); my $colormap=$children[0]->get_screen->get_rgba_colormap; return unless $colormap; while (my $widget=shift @children) { push @children, $widget->get_children if $widget->isa('Gtk2::Container'); unless ($widget->no_window) { $widget->set_colormap($colormap); $widget->set_app_paintable(1); $widget->signal_connect(expose_event => \&transparent_expose_cb); } if ($widget->isa('Gtk2::container')) { $widget->signal_connect(add => sub { make_transparent($_[1]); } ); } } } sub transparent_expose_cb #use Cairo { my ($w,$event)=@_; my $cr=Gtk2::Gdk::Cairo::Context->create($event->window); $cr->set_operator('source'); $cr->set_source_rgba(0, 0, 0, 0); $cr->rectangle($event->area); $cr->fill; if (my $pixbuf=$w->{skinpb}) { $cr->set_source_pixbuf($pixbuf,0,0); $cr->paint; } return 0; #send expose to children } sub set_background_skin { my ($self,$skin)=@_; my ($file,$crop,$resize)=split /:/,$skin; $self->{pixbuf}=Skin::_load_skinfile($file,$crop,$self->{global_options}); return unless $self->{pixbuf}; $self->{resizeparam}=$resize; $self->{skinsize}='0x0'; $self->signal_connect(size_allocate => \&resize_skin_cb); return if $self->{options}{transparent}; # following not needed when using transparency $self->signal_connect(style_set => sub {warn "style set : @_" if $::debug;$_[0]->set_style($_[2]);} ,$self->get_style); #FIXME find the cause of these signals, seems related to stock icons my $rc_style= Gtk2::RcStyle->new; #$rc_style->bg_pixmap_name($_,'') for qw/normal selected prelight insensitive active/; $rc_style->bg_pixmap_name('normal',''); my @children=($self->child); while (my $widget=shift @children) { push @children, $widget->get_children if $widget->isa('Gtk2::Container'); $widget->modify_style($rc_style) unless $widget->no_window; } $self->set_app_paintable(1); } sub resize_skin_cb #FIXME needs to add a delay to better deal with a burst of resize events { my ($self,$alloc)=@_; my ($w,$h)=($alloc->width,$alloc->height); return unless $self->realized; return if $w.'x'.$h eq $self->{skinsize}; my $pb=Skin::_resize($self->{pixbuf},$self->{resizeparam},$w,$h); return unless $pb; if ($self->{options}{transparent}) { $self->{skinpb}=$pb; #will be used by transparent_expose_cb() if (my $shape= $self->{options}{shape}) { my $mask=Gtk2::Gdk::Pixmap->new(undef,$w,$h,1); $pb->render_threshold_alpha($mask,0,0,0,0,-1,-1, $shape); $self->input_shape_combine_mask($mask,0,0); } } else { #my ($pixmap,$mask)=$pb->render_pixmap_and_mask(1); #leaks X memory for Gtk2 <1.146 or <1.153 # create shape mask my $mask=Gtk2::Gdk::Pixmap->new(undef,$w,$h,1); $pb->render_threshold_alpha($mask,0,0,0,0,-1,-1,1); $self->shape_combine_mask($mask,0,0); # create pixmap background my $pixmap=Gtk2::Gdk::Pixmap->new($self->window,$w,$h,-1); $pb->render_to_drawable($pixmap, Gtk2::Gdk::GC->new($self->window), 0,0,0,0,-1,-1,'none',0,0); $self->window->set_back_pixmap($pixmap,0); } $self->{skinsize}=$w.'x'.$h; $self->queue_draw; } package Layout::Window::Popup; our @ISA; BEGIN {push @ISA,'Layout','Layout::Window';} sub new { my ($class,$layout,$widget)=@_; $layout||=$::Options{LayoutT}; my $self=Layout::Window::new($class,$layout, wintype=>'popup', 'pos'=>undef, size=>undef, fallback=>'full with buttons', popped_from=>$widget); if ($widget) #warning : widget can be a Gtk2::StatusIcon { ::weaken( $widget->{PoppedUpWindow}=$self ); $self->set_screen($widget->get_screen); #$self->set_transient_for($widget->get_toplevel); #$self->move( ::windowpos($self,$widget) ); $self->signal_connect(enter_notify_event => \&CancelDestroy); } else { $self->set_position('mouse'); } $self->show; return $self; } sub init { my $self=$_[0]; #add a frame my $child=$self->child; $self->remove($self->child); my $frame=Gtk2::Frame->new; $self->add($frame); $frame->add($child); my $shadow= $self->{options}{transparent} ? 'none' : 'out'; $frame->set_shadow_type($shadow); $child->set_border_width($self->get_border_width); $self->set_border_width(0); ##$self->set_type_hint('tooltip'); #TEST ##$self->set_type_hint('notification'); #TEST #$self->set_focus_on_map(0); #$self->set_accept_focus(0); #? $self->signal_connect(leave_notify_event => sub { $_[0]->CheckCursor if $_[1]->detail ne 'inferior'; 0; }); $self->SUPER::init; } sub CheckCursor # StartDestroy if popup is not ancestor of widget under cursor and cursor isn't grabbed (menu) { my $self=shift; $self->{check_timeout} ||= Glib::Timeout->add(800, \&CheckCursor, $self); return 1 if $self->get_display->pointer_is_grabbed; # to prevent destroying while a menu is open if (my $sicon=$self->{popped_from}) { return 1 if $sicon->isa('Gtk2::StatusIcon') && OnStatusIcon($sicon); #check if pointer above statusicon } my ($gdkwin)=Gtk2::Gdk::Window->at_pointer; my $widget= $gdkwin ? Glib::Object->new_from_pointer($gdkwin->get_user_data) : undef; while ($widget) { last if $widget->isa('Gtk2::StatusIcon'); $widget= ::find_ancestor($widget,'Layout::Window::Popup'); last unless $widget; return 1 if $widget==$self; # don't destroy if cursor is over child of self $widget= $widget->{popped_from};# parent popup } $self->StartDestroy; return 1 } sub OnStatusIcon #return true if pointer is above sicon { my $sicon=shift; my ($screen,$area)= $sicon->get_geometry; my ($x,$y,$w,$h)= $area->values; my ($pscreen,$px,$py)= $screen->get_display->get_pointer; return $pscreen==$screen && $px>=$x && $px<=$x+$w && $py>=$y && $py<=$y+$h; } sub Position { my $self=shift; if ( my $widget= delete $self->{options}{popped_from}) { ::weaken( $self->{popped_from}=$widget ); if (my $pos=$widget->{hover_layout_pos}) { my ($x0,$y0)= split /\s*x\s*/,$pos; my ($width,$height)=$self->get_size; my ($x,$y)= $widget->window->get_origin; my ($ww,$wh)=$widget->window->get_size; if ($widget->no_window) { (my$wx,my$wy,$ww,$wh)=$widget->allocation->values; $x+=$wx;$y+=$wy; } $x=$y=0 if $x0=~s/abs:\s*//; my $screen=$widget->get_screen; $x+=_compute_pos($x0,$width, $ww,$screen->get_width); $y+=_compute_pos($y0,$height,$wh,$screen->get_height); return $x,$y; } return ::windowpos($self,$widget); } $self->SUPER::Position; } sub _compute_pos { my ($def,$wp,$ww,$ws)=@_; my %h; $def="+$def" unless $def=~m/^[-+]/; ::setlocale(::LC_NUMERIC, 'C'); # so that decimal separator is the dot # can parse strings such as : +3s/2-w-p/2+20 for my $v ($def=~m/([-+][^-+]+)/g) { if ($v=~m#([-+]\d*\.?\d*)([pws])(?:/([0-9]+))?#) { $h{$2}= ($1 eq '+' ? 1 : $1 eq '-' ? -1 : $1) / ($3||1); } elsif ($v=~m/^[-+]\d+$/) { $h{n}=$v } } ::setlocale(::LC_NUMERIC, ''); # smart alignment if alignment not specified and only widget or screen relative if (!defined $h{p} && (defined $h{w} xor defined $h{s}) && !$h{n}) { my $ws= $h{w} || $h{s} || 0; $h{p}= $ws==0 ? 0 : $ws==1 ? -1 : -.5; } $h{$_}||=0 for qw/n p w s/; my $x= $h{n} + $h{p}*$wp + $h{w}*$ww + $h{s}*$ws; return $x; } sub HoverPopup { my $widget=shift; delete $widget->{hover_timeout}; return 0 if $widget->isa('Gtk2::StatusIcon') && !OnStatusIcon($widget); # for statusicon, don't popup if no longer above icon return 0 if $widget->{block_popup}; Popup($widget); 0; } sub Popup { my ($widget,$addtimeout)=@_; my $self= $widget->{PoppedUpWindow}; $addtimeout=0 if $self && !$self->{destroy_timeout}; #don't add timeout if there wasn't already one $self ||= Layout::Window::Popup->new($widget->{hover_layout},$widget); return 0 unless $self; $self->CancelDestroy; $self->{destroy_timeout}=Glib::Timeout->add( $addtimeout,\&DestroyNow,$self) if $addtimeout; $self->{check_timeout} ||= Glib::Timeout->add(400, \&CheckCursor, $self) if $widget->isa('Gtk2::StatusIcon') && !$addtimeout; 0; } sub set_hover { my $widget=$_[0]; if ($widget->isa('Gtk2::StatusIcon')) { $widget->set_has_tooltip(1); $widget->signal_connect(query_tooltip => sub { return if $_[0]{hover_timeout}; &PreparePopup }); } else { $widget->signal_connect(enter_notify_event => \&PreparePopup); $widget->signal_connect(leave_notify_event => \&CancelPopup ); } } sub PreparePopup { my $widget=shift; #widget can be a statusicon return 0 if $widget->{block_popup}; if (!$widget->{PoppedUpWindow}) { my $delay=$widget->{hover_delay}||1000; if (my $t=delete $widget->{hover_timeout}) { Glib::Source->remove($t); } $widget->{hover_timeout}= Glib::Timeout->add($delay,\&HoverPopup, $widget); } else {Popup($widget)} 0; } sub CancelPopup { my $widget=shift; if (my $t=delete $widget->{hover_timeout}) { Glib::Source->remove($t); } if (my $self=$widget->{PoppedUpWindow}) { $self->StartDestroy; $self->{check_timeout} ||= Glib::Timeout->add(1000, \&CheckCursor, $self); } } sub CancelDestroy { my $self=shift; if (my $t=delete $self->{destroy_timeout}) { Glib::Source->remove($t); } if (my $t=delete $self->{check_timeout}) { Glib::Source->remove($t); } } sub StartDestroy { my $self=shift; $self->{destroy_timeout} ||= Glib::Timeout->add(300,\&DestroyNow,$self); 0; } sub DestroyNow { my $self=shift; $self->CancelDestroy; $self->close_window; 0; } package Layout::Embedded; use base 'Gtk2::Container'; our @ISA; push @ISA,'Layout'; sub new { my ($class,$opt)=@_; my $layout=$opt->{layout}; my $def= $Layout::Layouts{$layout}; return undef unless $def; my $self=bless Gtk2::VBox->new(0,0), $class; $self->{SaveOptions}=\&SaveEmbeddedOptions; $self->{group}=$opt->{group}; my %children_opt; for my $child_key (grep m#./.#, keys %$opt) { my ($child,$key)=split "/",$child_key,2; $children_opt{$child}{$key}= $opt->{$child_key}; } %children_opt=( %children_opt, %{$opt->{children_opt}} ) if $opt->{children_opt}; $self->InitLayout($layout,\%children_opt); $self->{tabicon}= $self->{tabicon} || $def->{Icon}; $self->{tabtitle}= $self->{tabtitle} || $def->{Title} || $def->{Name} || $layout; $self->show_all; return $self; } sub SaveEmbeddedOptions { my $self=shift; my $opt=Layout::SaveWidgetOptions(values %{ $self->{widgets} }, values %{ $self->{PlaceHolders} }); return children_opt => $opt; } package Layout::Boxes; our %Boxes= ( HB => { New => sub { SHBox->new; }, #New => sub { Gtk2::HBox->new(::FALSE,0); }, Prefix => qr/([-_.0-9]*)/, Pack => \&SBoxPack, }, VB => { New => sub { SVBox->new; }, #New => sub { Gtk2::VBox->new(::FALSE,0); }, Prefix => qr/([-_.0-9]*)/, Pack => \&SBoxPack, }, HP => { New => sub { PanedNew('Gtk2::HPaned',$_[0]); }, Prefix => qr/([_+]*)/, Pack => \&PanedPack, }, VP => { New => sub { PanedNew('Gtk2::VPaned',$_[0]); }, Prefix => qr/([_+]*)/, Pack => \&PanedPack, }, TB => #tabbed #deprecated { New => \&NewTB, Prefix => qr/((?:"[^"]*[^\\]")|[^ ]*)\s+/, Pack => \&PackTB, }, NB => #tabbed 2 { New => sub { Layout::NoteBook->new(@_); }, Pack => \&Layout::NoteBook::Pack, EndInit => \&Layout::NoteBook::EndInit, }, MB => { New => sub { Gtk2::MenuBar->new }, Pack => sub { $_[0]->append($_[1]); }, }, SM => #submenu { New => sub { my $item=Gtk2::MenuItem->new($_[0]{label}); my $menu=Gtk2::Menu->new; $item->set_submenu($menu); return $item; }, Pack => sub { $_[0]->get_submenu->append($_[1]); }, }, BM => #button menu { New => sub { Layout::ButtonMenu->new(@_); }, Pack => sub { $_[0]->append($_[1]); }, }, EB => { New => sub { my $self=Gtk2::Expander->new($_[0]{label}); $self->set_expanded($_[0]{expand}); $self->{SaveOptions}=sub { expand=>$_[0]->get_expanded; }; return $self; }, Pack => \&SimpleAdd, }, FB => { New => sub { SFixed->new; }, Prefix => qr/^(-?\.?\d+,-?\.?\d+(?:,\.?\d+,\.?\d+)?),?\s+/, # "5,4 " or "-5,.4,5,.2 " Pack => \&Fixed_pack, }, FR => { New => sub { my $f=Gtk2::Frame->new($_[0]{label}); $f->set_shadow_type($_[0]{shadow}) if $_[0]{shadow};return $f; }, Pack => \&SimpleAdd, }, SB => { New => sub { my $sw=Gtk2::ScrolledWindow->new; }, Pack => sub { $_[0]->add_with_viewport($_[1]); }, }, AB => { New => sub { my %opt=(xalign=>.5, yalign=>.5, xscale=>1, yscale=>1, %{$_[0]}); Gtk2::Alignment->new(@opt{qw/xalign yalign xscale yscale/});}, Pack => \&SimpleAdd, }, WB => { New => sub { Gtk2::EventBox->new; }, Pack => \&SimpleAdd, }, ); sub SimpleAdd { $_[0]->add($_[1]); } sub NewTB { my ($opt)=@_; my $nb=Gtk2::Notebook->new; $nb->set_scrollable(::TRUE); $nb->popup_enable; #$nb->signal_connect( button_press_event => sub {return !::IsEventInNotebookTabs(@_);}); if (my $p=$opt->{tabpos}) { $nb->set_tab_pos($p); } if (my $p=$opt->{page}) { $nb->{SetPage}=$p; } $nb->{SaveOptions}=sub { page => $_[0]->get_current_page }; return $nb; } sub PackTB { my ($nb,$wg,$title)=@_; $title=~s/^"// && $title=~s/"$//; $nb->append_page($wg, Gtk2::Label->new($title) ); $nb->set_tab_reorderable($wg,::TRUE); my $n=$nb->{SetPage}||0; if ($n==($nb->get_n_pages-1)) {$wg->show; $nb->set_current_page($n); $nb->{DefaultFocus}=$wg; } } sub SBoxPack { my ($box,$wg,$opt)=@_; my $pad= $opt=~m/([0-9]+)/ ? $1 : 0; my $exp= $opt=~m/_/; my $end= $opt=~m/-/; my $fill=$opt!~m/\./; if ($end) { $box->pack_end( $wg,$exp,$fill,$pad ); } else { $box->pack_start( $wg,$exp,$fill,$pad ); } if ($Gtk2::VERSION<1.163 || $Gtk2::VERSION==1.170) { $wg->{SBOX_packoptions}=[$exp,$fill,$pad, ($end ? 'end' : 'start')]; } #to work around memory leak (gnome bug #498334) } sub PanedPack { my ($paned,$wg,$opt)=@_; my $expand= $opt=~m/_/; my $shrink= $opt!~m/\+/; if (!$paned->child1) {$paned->pack1($wg,$expand,$shrink);} elsif (!$paned->child2) {$paned->pack2($wg,$expand,$shrink);} else {warn "layout error : trying to pack more than 2 widgets in a paned container\n"} } sub PanedNew { my ($class,$opt)=@_; my $self=$class->new; ::setlocale(::LC_NUMERIC, 'C'); ($self->{size1},$self->{size2})= map $_+0, split /-|_/, $opt->{size} if defined $opt->{size}; # +0 to make the conversion to numeric while LC_NUMERIC is set to C ::setlocale(::LC_NUMERIC, ''); if (defined $self->{size1}) { $self->set_position($self->{size1}); $self->set('position-set',1); # in case $self->{size1}==0 'position-set' is not set to true if child1's size is 0 (which is the case here as child1 doesn't exist yet) } $self->{SaveOptions}=sub { ::setlocale(::LC_NUMERIC, 'C'); my $s=$_[0]{size1}; $s.='-'. $_[0]{size2} if $_[0]{size2}; ::setlocale(::LC_NUMERIC, ''); return size => $s }; $self->signal_connect(size_allocate => \&Paned_size_cb ); #needed to correctly save/restore the handle position return $self; } sub Paned_size_cb { my $self=shift; my $max=$self->get('max-position'); return unless $max; my $size1=$self->{size1}; my $size2=$self->{size2}; if (defined $size1 && defined $size2 && abs($max-$size1-$size2)>5 || $self->{need_resize}) { my $not_enough; if ($self->child1_resize && !$self->child2_resize) { $size1= ::max($max-$size2,0); $not_enough= $size2>$max; } elsif ($self->child2_resize && !$self->child1_resize) { $size1= $max if $not_enough= $size1>$max; } else { $size1= $max*$size1/($size1+$size2); } if ($not_enough) #don't change the saved value if couldn't restore the size properly { $self->{need_resize}=1; # => will retry in a later size_allocate event unless the position is set manually } else { $self->set_position( $size1 ); $self->{size1}= $size1; $self->{size2}= $max-$size1; delete $self->{need_resize}; } } else { my $size1=$self->get_position; $self->{size1}=$size1; $self->{size2}=$max-$size1; delete $self->{need_resize}; } } sub Fixed_pack { my ($self,$wg,$opt)=@_; if (my ($x,$y,$w,$h)= $opt=~m/^(-?\.?\d+),(-?\.?\d+)(?:,(\.?\d+),(\.?\d+))?/) { if ($1=~m/[-.]/ || $2=~m/[-.]/) { $wg->{SFixed_dynamic_pos}=[$x,$y]; $self->put($wg,0,0);} else {$self->put($wg,$x,$y); } if ($w||$h) { if ($w=~m/\./ || $h=~m/\./) { $wg->{SFixed_dynamic_size}=[$w,$h]; } else { my ($w2,$h2)=$wg->get_size_request; $wg->set_size_request($w||$w2,$h||$h2); } } } else { warn "Invalid position '$opt' for widget $wg\n" } } package SFixed; use Glib::Object::Subclass Gtk2::Fixed::, signals => { size_allocate => \&size_allocate, }; sub size_allocate { my ($self,$alloc)=@_; my ($ox,$oy,$w,$h)=$alloc->values; my $border=$self->get_border_width; $ox+=$border; $w-=$border*2; $oy+=$border; $h-=$border*2; for my $child ($self->get_children) { my ($x,$y)=$self->child_get_property($child,qw/x y/); if (my $ref=$child->{SFixed_dynamic_pos}) { my ($x2,$y2)=@$ref; $x=~m/\./ and $x*=$w; $x2=~m/\./ and $x2=int($x2*$w); $y2=~m/\./ and $y2=int($y2*$h); $x2=~m/^-/ and $x2+=$w; $y2=~m/^-/ and $y2+=$h; if ($x2!=$x || $y2!=$y) { $self->move($child,$x=$x2,$y=$y2); } } my ($ww,$wh); if (my $ref=$child->{SFixed_dynamic_size}) { ($ww,$wh)=@$ref; $ww=~m/\./ and $ww*=$w; $wh=~m/\./ and $wh*=$h; } $ww||=$child->size_request->width; $wh||=$child->size_request->height; $child->size_allocate(Gtk2::Gdk::Rectangle->new($ox+$x, $oy+$y, $ww,$wh)); } } package Layout::NoteBook; use base 'Gtk2::Notebook'; our @contextmenu= ( { label => _"New list", code => sub { $_[0]{self}->newtab('EditList',1,{songarray=>''}); }, type=> 'L', stockicon => 'gtk-add', }, { label => _"Open Queue", code => sub { $_[0]{self}->newtab('QueueList',1); }, type=> 'L', stockicon => 'gmb-queue', test => sub { !grep $_->{name} eq 'QueueList', $_[0]{self}->get_children } }, { label => _"Open Playlist", code => sub { $_[0]{self}->newtab('PlayList',1); }, type=> 'L', stockicon => 'gtk-media-play', test => sub { !grep $_->{name} eq 'PlayList', $_[0]{self}->get_children } }, { label => _"Open existing list", code => sub { $_[0]{self}->newtab('EditList',1, {songarray=>$_[1]}); }, type=> 'L', submenu => sub { my %h; $h{ $_->{array}->GetName }=1 for grep $_->{name}=~m/^EditList\d*$/, $_[0]{self}->get_children; return [grep !$h{$_}, ::GetListOfSavedLists()]; } }, { label => _"Open page layout", code => sub { $_[0]{self}->newtab('@'.$_[1],1); }, type=> 'P', submenu => sub { Layout::get_layout_list('P') }, submenu_tree=>1, }, { label => _"Open context page", type=> 'C', submenu => sub { $_[0]{self}->make_widget_list('context page'); }, submenu_reverse=>1, code => sub { $_[0]{self}->newtab($_[1],1); }, }, { label => _"Delete list", code => sub { $_[0]{page}->DeleteList; }, type=> 'L', istrue=>'page', test => sub { $_[0]{page}{name}=~m/^EditList\d*$/; } }, { label => _"Rename", code => \&pagerename_cb, istrue => 'rename',}, { label => _"Close", code => sub { $_[0]{self}->close_tab($_[0]{page},1); }, istrue => 'close', stockicon=> 'gtk-close',}, ); our @DefaultOptions= ( closebuttons => 1, tablist => 1, newbutton => 'end', ); sub new { my ($class,$opt)=@_; my $self= bless Gtk2::Notebook->new, $class; %$opt=( @DefaultOptions, %$opt ); $self->set_scrollable(1); $self->set_tab_hborder(0); $self->set_tab_vborder(0); if (my $tabpos=$opt->{tabpos}) { ($tabpos,$self->{angle})= $tabpos=~m/^(left|right|top|bottom)?(90|180|270)?/; $self->set_tab_pos($tabpos) if $tabpos; } $self->set_show_tabs(0) if $opt->{hidetabs}; $opt->{typesubmenu}='LPC' unless exists $opt->{typesubmenu}; $self->{$_}=$opt->{$_} for qw/group default_child match pages page typesubmenu closebuttons tablist/; for my $class (qw/list context layout/) # option begining with list_ / context_ / layout_ will be passed to children of this class { my @opt1; if (my $optkeys=$opt->{'options_for_'.$class}) #no need for a prefix for these options { push @opt1, $_=> $opt->{$_} for grep exists $opt->{$_}, split / /,$optkeys; } push @opt1, $_=> $opt->{$class.'_'.$_} for map m/^${class}_(.+)/, keys %$opt; $self->{children_opt1}{$class}={ @opt1 }; } $self->signal_connect(switch_page => \&SwitchedPage); $self->signal_connect(button_press_event => \&button_press_event_cb); ::Watch($self, SavedLists=> \&SavedLists_changed); ::Watch($self, Widgets => \&Widgets_changed_cb); $self->{groupcount}=0; $self->{SaveOptions}=\&SaveOptions; $self->{widgets}={}; $self->{widgets_opt}= $opt->{page_opt} ||={}; if (my $bl=$opt->{blacklist}) { $self->{blacklist}{$_}=undef for split / +/, $bl; } $opt->{newbutton}=0 unless *Gtk2::Notebook::set_action_widget{CODE}; # Gtk2::Notebook::set_action_widget requires gtk+ >= 2.20 and perl-Gtk2 >= 1.23 if ($opt->{typesubmenu} && $opt->{newbutton} && $opt->{newbutton} ne 'none') # add a button next to the tabs to show new-tab menu { my $button= ::NewIconButton('gtk-add'); $button->signal_connect(button_press_event => \&newbutton_cb); $button->signal_connect(clicked => \&newbutton_cb); $button->show_all; my $pos= $opt->{newbutton} eq 'start' ? 'start' : 'end'; $self->set_action_widget($button,$pos); } return $self; } sub SaveOptions { my $self=shift; my $i= $self->get_current_page; my @children= $self->get_children; my @dyn_widgets=values %{ $self->{widgets} }; my @pages; for my $child (@children) { my $name=$child->{name}; $name='+'.$name if grep $_==$child, @dyn_widgets; push @pages,$name; } my @opt= ( page => $pages[$i], pages => join(' ',@pages), page_opt=> Layout::SaveWidgetOptions( @dyn_widgets ), ); if (my $bl=$self->{blacklist}) { push @opt, blacklist=>join (' ',sort keys %$bl) if keys %$bl; } return @opt; } sub EndInit { my $self=shift; my %pagewidget; $pagewidget{ $_->{name} }=$_ for $self->get_children; if (my $pages=delete $self->{pages}) { my @pagelist=split / +/,$pages; $pagewidget{"+$_"}=$self->newtab($_) for map m/^\+(.+)$/, @pagelist; #recreate dynamic pages (page name begin with +) my $i=0; $self->reorder_child($_,$i++) for grep $_, map $pagewidget{$_}, @pagelist; #reorder pages to the saved order } if (my $name=delete $self->{page}) #restore selected tab { if (my $page= $pagewidget{$name}) { $page->show; #needed to set as current page $self->set_current_page( $self->page_num($page) ); } } $self->Widgets_changed_cb('init') if $self->{match}; $self->insert_default_page unless $self->get_children; } sub newtab { my ($self,$name,$setpage,$opt2)=@_; $self->SaveOptions if $setpage; #used to save default options of SongTree/SongList before creating a new one my $wtype= $name; $wtype=~s/\d+$//; $wtype= $Layout::Widgets{$wtype} || {}; my $wclass= $wtype->{issonglist} ? 'list' : $name=~m/^@/ ? 'layout' : 'context'; my $group=$self->{group}; $group= 'Global('.::refaddr($self).'-'.$self->{groupcount}++.')' if $wclass eq 'list'; # give songlist/songtree their own group if ($opt2) #new widget => use a new name not already used { my $n=0; $n++ while $self->{widgets}{$name.$n} || $self->{widgets_opt}{$name.$n}; $name.=$n; } else { $opt2= $self->{widgets_opt}{$name}; } return if $self->{widgets}{$name}; my $opt1= $self->{children_opt1}{$wclass} || {}; my $widget= Layout::NewWidget($name,$opt1,$opt2, {default_group=>$group}); return unless $widget; $self->{widgets}{$name}=$widget; $widget->{tabcanclose}=1; delete $self->{blacklist}{$name}; $self->Pack($widget); $widget->show_all; $self->set_current_page( $self->get_n_pages-1 ) if $setpage; #set current page to the new page return $widget; } sub Pack { my ($self,$wg)=@_; if (delete $self->{chooser_mode}) { $self->remove($_) for $self->get_children; } my $angle= $self->{angle} || 0; my $label= $wg->{tabtitle}; if (!defined $label) { $label= $wg->{name} } #FIXME ? what to do if no tabtitle given elsif (ref $label eq 'CODE') { $label= $label->($wg); } elsif ($wg->can('DynamicTitle')) { $label= $wg->DynamicTitle($label); } $label=Gtk2::Label->new($label) unless ref $label; $label->set_angle($angle) if $angle; ::weaken( $wg->{tab_page_label}=$label ) if $wg->{tabrename}; # set base gravity to auto so that rotated tabs handle vertical scripts (asian languages) better $label->get_pango_context->set_base_gravity('auto'); $label->signal_connect(hierarchy_changed=> sub { $_[0]->get_pango_context->set_base_gravity('auto'); }); # for some reason (gtk bug ?) the setting is reverted when the tab is dragged, so this re-set it my $icon= $wg->{tabicon}; $icon=Gtk2::Image->new_from_stock($icon,'menu') if defined $icon; my $close; if ($wg->{tabcanclose} && $self->{closebuttons}) { $close=Gtk2::Button->new; $close->set_relief('none'); $close->can_focus(0); ::weaken( $close->{page}=$wg ); $close->signal_connect(clicked => sub {my $page=$_[0]{page}; my $self=$page->parent; $self->close_tab($page,1);}); $close->add(Gtk2::Image->new_from_file(::PIXPATH.'smallclosetab.png')); $close->set_size_request(Gtk2::IconSize->lookup('menu')); $close->set_border_width(0); } my $tab= $angle%180 ? Gtk2::VBox->new(0,0) : Gtk2::HBox->new(0,0); my @icons= $angle%180 ? ($close,0,$icon,4) : ($icon,4,$close,0); my ($i,$pad)=splice @icons,0,2; $tab->pack_start($i,0,0,$pad) if $i; $tab->pack_start($label,1,1,2); ($i,$pad)=splice @icons,0,2; $tab->pack_start($i,0,0,$pad) if $i; $self->append_page($wg,$tab); $self->set_tab_reorderable($wg,1); $tab->show_all; } sub insert_default_page { my $self=shift; return if $self->get_children; $self->newtab( $self->{default_child} ) if $self->{default_child}; ::IdleDo('5_create_chooser_page',500, \&create_chooser_page, $self) if !$self->get_children && $self->{match}; } sub close_tab { my ($self,$page,$manual)=@_; my $name=$page->{name}; delete $self->{widgets}{$name}; if ($manual && $self->{match} && $Layout::Widgets{$name} && $Layout::Widgets{$name}{autoadd_type}) { $self->{blacklist}{$name}=undef } my $opt=$self->{widgets_opt}; my $pageopt= Layout::SaveWidgetOptions($page); %$opt= ( %$opt, %$pageopt ); $self->remove($page); delete $self->{DefaultFocus} if $self->{DefaultFocus} && $self->{DefaultFocus}==$page; $self->insert_default_page unless $self->get_children; } sub SavedLists_changed #remove EditList tab if corresponding list has been deleted { my ($self,$name,$action)=@_; return unless $action && $action eq 'remove'; my @remove=grep $_->{name}=~m/^EditList\d*$/ && !defined $_->{array}->GetName, $self->get_children; $self->close_tab($_) for @remove; } sub newbutton_cb { my $self= ::find_ancestor($_[0],__PACKAGE__); ::PopupContextMenu(\@contextmenu, { self=>$self, type=>$self->{typesubmenu}, usemenupos=>1 } ); 1; } sub button_press_event_cb { my ($self,$event)=@_; return 0 if $event->button != 3; return 0 unless ::IsEventInNotebookTabs($self,$event); #to make right-click on tab arrows work my $pagenb=$self->get_current_page; my $page=$self->get_nth_page($pagenb); #my $listname= $page? $page->{tabbed_listname} : undef; my @menu; my @opt= ( self=> $self, page=> $page, type => $self->{typesubmenu}, 'close'=> $page->{tabcanclose}, 'rename' => $page->{tabrename}, ); push @menu, @contextmenu; if ($self->{tablist} && !$self->{chooser_mode}) { push @menu, { separator=>1 }; for my $page ($self->get_children) #append page list to menu { my $label= $page->{tab_page_label} ? $page->{tab_page_label}->get_text : $page->{tabtitle}; my $icon= $page->{tabicon}; my $i= $self->page_num($page); my $cb= sub { $_[0]{self}->set_current_page($i); }; push @menu, {label=>$label, stockicon=>$icon, code=> $cb, }; } } ::PopupContextMenu(\@menu, { @opt } ); return 1; } sub pagerename_cb { my $page=$_[0]{page}; my $tab=$_[0]{self}->get_tab_label($page); my $renamesub=$_[0]{'rename'}; my $label=$page->{tab_page_label}; my $entry=Gtk2::Entry->new; $entry->set_has_frame(0); $entry->set_inner_border(undef) if *Gtk2::Entry::set_inner_border{CODE}; #Gtk2->CHECK_VERSION(2,10,0); $entry->set_text( $label->get_text ); $entry->set_size_request( 20+$label->allocation->width ,-1); $_->hide for grep !$_->isa('Gtk2::Image'), $tab->get_children; $tab->pack_start($entry,::FALSE,::FALSE,2); $entry->grab_focus; $entry->show_all; $entry->signal_connect(key_press_event => sub #abort if escape { my ($entry,$event)=@_; return 0 unless Gtk2::Gdk->keyval_name( $event->keyval ) eq 'Escape'; $entry->set_text(''); $entry->set_sensitive(0); #trigger the focus-out event 1; }); $entry->signal_connect(activate => sub {$_[0]->set_sensitive(0)}); #trigger the focus-out event $entry->signal_connect(populate_popup => sub { ::weaken($_[0]{popupmenu}=$_[1]); }); $entry->signal_connect(focus_out_event => sub { my $entry=$_[0]; my $popupmenu= delete $entry->{popupmenu}; return 0 if $entry->get_display->pointer_is_grabbed && $popupmenu && $popupmenu->mapped; # prevent error when context menu of the entry pops-up my $new=$entry->get_text; $tab->remove($entry); $_->show for $tab->get_children; if ($new ne '') #user has entered new name -> do the renaming { $renamesub->($label,$new); } 0; }); } sub create_chooser_page { my $self=shift; return if $self->get_children && !$self->{chooser_mode}; $self->remove($_) for $self->get_children; #remove a previous version of this page my $list= $self->make_widget_list; return unless keys %$list; $self->{chooser_mode}=1; my $cb=sub { my $self=::find_ancestor($_[0],__PACKAGE__); $self->newtab($_[1]); }; my $bbox=Gtk2::VButtonBox->new; $bbox->set_layout('start'); for my $name (sort { $list->{$a} cmp $list->{$b} } keys %$list) { my $button=Gtk2::Button->new($list->{$name}); $button->signal_connect(clicked=> $cb,$name); $bbox->add($button); } $bbox->show_all; $bbox->{name}=''; $self->append_page($bbox,_"Choose page to open"); } sub make_widget_list { my ($self,$match,@names)=@_; $match ||= $self->{match}; return unless $match; my @match= $match=~m/(?{$_}, @names : keys %$wdef; @names=grep $wdef->{$_}{autoadd_type}, @names; my %ok; for my $name (@names) { next if grep $name eq $_->{name}, $self->get_children; #or use $self->{widgets}{$name} ? my $autoadd=$wdef->{$name}{autoadd_type}; my %h; $h{$_}=1 for split / +/,$autoadd; next if grep !$h{$_}, @match; next if grep $h{$_}, @matchnot; $ok{$name}=$wdef->{$name}{tabtitle}; } return \%ok; } sub Widgets_changed_cb #new or removed widgets => check if a widget should be added or removed { my ($self,$changetype,@widgets)=@_; if ($changetype eq 'remove') { for my $name (@widgets) { $self->close_tab($_) for grep $name eq $_->{name}, $self->get_children; } return } my $match=$self->{match}; return unless $match; @widgets=keys %Layout::Widgets unless @widgets; @widgets=sort grep $Layout::Widgets{$_}{autoadd_type}, @widgets; for my $name (@widgets) { my $ref=$Layout::Widgets{$name}; my $add; if (my $autoadd= $ref->{autoadd_type}) { #every words in $match must be in $autoadd, except for words starting with - that must not my %h; $h{$_}=1 for split / +/,$autoadd; next if grep !$h{$_}, $match=~m/(?{autoadd_option}) { $add=$::Options{$opt} } my @already= grep $name eq $_->{name}, $self->get_children; if ($add) { next if exists $self->{blacklist}{$name}; $self->newtab($name,0,undef,1) unless @already; } else { $self->close_tab($_) for @already; } } ::IdleDo('5_create_chooser_page',500, \&create_chooser_page, $self) if !$self->get_children || $self->{chooser_mode}; } sub SwitchedPage { my ($self,undef,$pagenb)=@_; delete $self->{DefaultFocus}; if (defined(my $group=delete $self->{active_group})) { ::UnWatch($self,'SelectedID_'.$group); ::UnWatch($self,'Selection_'.$group); #::UnWatchFilter($self,$group); } my $page=$self->get_nth_page($pagenb); ::weaken( $self->{DefaultFocus}=$page ); my $metagroup= $self->{group}; return if !$page->{group} || $page->{group} eq $metagroup; my $group= $self->{active_group}= $page->{group}; my $ID= ::GetSelID($group); ::HasChangedSelID($metagroup,$ID) if defined $ID; if (my $songlist=$SongList::Common::Register{$group}) { $songlist->RegisterGroup($self->{group}); ::HasChanged('Selection_'.$self->{group}); ::Watch($self,'Selection_'.$group, sub { ::HasChanged('Selection_'.$_[0]->{group}); }); ::HasChanged('SongArray',$songlist->{array},'proxychange'); } # FIXME can't use WatchSelID, should special-case special groups : Play Recent\d *Next\d* ... ::Watch($self,'SelectedID_'.$group, sub { my ($self,$ID)=@_; ::HasChangedSelID($self->{group},$ID) if defined $ID; }); #::WatchFilter($self,$group, sub { }); } package Layout::PlaceHolder; our %PlaceHolders= ( #ContextPages => #{ event_Widgets => \&Widgets_changed_cb, # match => 'context page', # init => sub { $_[0]->Widgets_changed_cb('init'); }, #}, ExtraButtons => { event_Widgets => \&Widgets_changed_cb, match => 'button main', init => sub { $_[0]->Widgets_changed_cb('init'); }, }, ); sub new { my ($class,$boxfunc,$box,$ph,$packoptions)=@_; my $name= $ph->{name}; $name=~s/\d+$//; my $type= $PlaceHolders{$name}; unless ($type) { return Layout::PlaceHolder::Single->new($boxfunc,$box,$ph,$packoptions); } bless $ph,$class; ::weaken( $ph->{boxwidget}=$box ); $ph->{widgets}={}; $ph->{$_}||=$type->{$_} for qw/match/; $ph->{SaveOptions}=\&SaveOptions; $ph->{widgets_opt}=delete $ph->{opt2}{widgets_opt}; for my $event (grep m/^event_/, keys %$type) { my $cb= $type->{$event}; $event=~s/^event_//; ::Watch($ph, $event, $cb); } $ph->{packsub}= $boxfunc->{Pack}; $ph->{packoptions}= $packoptions; if (my $init=$type->{init}) { $init->($ph) } #used to create children at creation time for some placeholders return $ph; } sub DESTROY { ::UnWatch_all($_[0]); } sub SaveOptions { my $self=shift; my $opt= Layout::SaveWidgetOptions(values %{$self->{widgets}}); return unless keys %$opt; return widgets_opt => $opt; } sub AddWidget { my ($ph,$name)=@_; return if $ph->{widgets}{$name}; my $widget= Layout::NewWidget($name,$ph->{opt1},$ph->{widgets_opt}{$name}, { default_group => $ph->{group} }); return unless $widget; $ph->{widgets}{$name}= $widget; $ph->{packsub}->($ph->{boxwidget},$widget, $ph->{packoptions}); $widget->show_all; return $widget; } sub RemoveWidget { my ($ph,$name)=@_; $name=$name->{name} if ref $name; my $widget= delete $ph->{widgets}{$name}; return unless $widget; my $opt=$ph->{widgets_opt}||={}; %$opt= ( %$opt, %{ Layout::SaveWidgetOptions($widget) } ); $ph->{boxwidget}->remove($widget); } sub Widgets_changed_cb #new or removed widgets => check if a widget should be added or removed { my ($ph,$changetype,@widgets)=@_; @widgets=keys %Layout::Widgets unless @widgets; @widgets=sort grep $Layout::Widgets{$_}{autoadd_type}, @widgets; my $match=$ph->{match}; for my $name (@widgets) { my $ref=$Layout::Widgets{$name}; my $add= $changetype ne 'remove' ? 1 : 0; if (my $autoadd= $ref->{autoadd_type}) { #every words in $match must be in $autoadd, except for words starting with - that must not my %h; $h{$_}=1 for split / +/,$autoadd; next if grep !$h{$_}, $match=~m/(?{autoadd_option}) { $add=$::Options{$opt} } if ($add) { my $widget= $ph->AddWidget($name); } else { $ph->RemoveWidget($name); } } } sub Options_changed { my ($ph,$option)=@_; return unless exists $ph->{watchoptions}{$option}; $ph->Widgets_changed_cb('optchanged'); } package Layout::PlaceHolder::Single; sub new { my ($class,$boxfunc,$box,$ph,$packoptions)=@_; bless $ph,$class; ::weaken( $ph->{boxwidget}=$box ); ::Watch($ph, Widgets => \&Widgets_changed_cb); $ph->{packsub}= $boxfunc->{Pack}; $ph->{packoptions}= $packoptions; return $ph; } sub DESTROY { ::UnWatch_all(shift); } sub Widgets_changed_cb { my ($ph,$changetype,@widgets)=@_; my $name=$ph->{name}; $name=~s/\d+$//; return unless grep $name eq $_, @widgets; if ($changetype eq 'new' && !$ph->{widget}) { my $widget= Layout::NewWidget($ph->{name},$ph->{opt1},$ph->{opt2}, { default_group => $ph->{group} }); return unless $widget; $ph->{widget}= $widget; $ph->{SaveOptions}=\&SaveOptions; $ph->{packsub}->($ph->{boxwidget},$widget, $ph->{packoptions}); $widget->show_all; } elsif ($changetype eq 'remove' && $ph->{widget}) { my $widget= delete $ph->{widget}; $ph->{opt2}= Layout::SaveWidgetOptions($widget); $ph->{boxwidget}->remove($widget); delete $ph->{SaveOptions}; } } sub SaveOptions { my $ph=shift; Layout::SaveWidgetOptions($ph->{widget}); } package Layout::Button; use base 'Gtk2::Bin'; our @default_options= (button=>1, relief=>'none', size=> Layout::SIZE_BUTTONS, ellipsize=> 'none', ); sub new { my ($class,$opt,$ref)=@_; %$opt=( @default_options, %$opt ); my $isbutton= $opt->{button}; my $self; my $activate= $opt->{activate}; if ($isbutton) { $self=Gtk2::Button->new; $self->set_relief($opt->{relief}); $self->{clicked_cmd}= $activate; $self->signal_connect(clicked => \&clicked_cb); } else { $self=Gtk2::EventBox->new; $self->set_visible_window(0); $opt->{click} ||= $activate; } bless $self, $class; my $text= $opt->{text} || $opt->{label}; my $stock= $opt->{stock}; if (!ref $stock && $ref->{'state'}) { my $default= $ref->{stock}; my %hash; %hash = %$default if ref $default eq 'HASH'; #make a copy of the default setting if it is a hash # extract icon(s) for each state using format : "state1: icon1 facultative_icon2 state2: icon3" $hash{$1}=$2 while $stock=~s/(\w+) *: *([^:]+?) *$//; $stock=\%hash; #if default setting is a function, use a function that look in the hash, and fallback to the default function (this is the case for Queue and VolumeIcon widgets) $stock= sub { $hash{$_[0]} || &$default } if ref $default eq 'CODE'; } $self->{state}=$ref->{state} if $ref->{state}; if ($opt->{skin}) { my $skin=Skin->new($opt->{skin},$self,$opt); $self->signal_connect(expose_event => \&Skin::draw,$skin); $self->{skin}=1; # will force a repaint on stock state change $self->set_app_paintable(1); #needed ? if (0 && !$isbutton && $opt->{shape}) #mess up button-press cb TESTME { $self->{shape}=1; } } elsif ($stock) { $self->{stock}=$stock; $self->{size}= $opt->{size}; my $img= $self->{img}= Gtk2::Image->new; $img->set_size_request(Gtk2::IconSize->lookup($self->{size})); #so that it always request the same size, even when no icon if ($opt->{with_text}) { my $hbox=Gtk2::HBox->new(0,2); my $label= $self->{label}= Gtk2::Label->new; my $ellip= $opt->{ellipsize}; $ellip='end' if $ellip eq '1'; $label->set_ellipsize($ellip); $self->{string}= $text || $opt->{tip}; $self->{markup}= $opt->{markup} || ($opt->{size} eq 'menu' ? "%s" : "%s"); $hbox->pack_start($img,0,0,0); $hbox->pack_start($label,1,1,0); $self->add($hbox); } else { $self->add($img); } $self->{EndInit}=\&UpdateStock; } elsif (defined $text) { $self->add( Gtk2::Label->new($text) ); } return $self; } sub clicked_cb { my $self=$_[0]; my $sub=$self->{clicked_cmd}; return 0 unless $sub; if (ref $sub) {&$sub} else { ::run_command($self,$sub) } 1; } sub UpdateStock { my ($self,undef,$index)=@_; my $stock=$self->{stock}; if (my $state=$self->{state}) { $state=&$state; $stock = (ref $stock eq 'CODE')? $stock->($state) : $stock->{$state}; } if ($stock=~m/ /) { $stock= (split /\s+/,$stock)[ $index || 0 ]; $stock='' if $stock eq '.'; #needed ? the result is the same : no icon unless (exists $self->{hasenterleavecb}) { $self->{hasenterleavecb}=undef; $self->signal_connect(enter_notify_event => \&UpdateStock,1); $self->signal_connect(leave_notify_event => \&UpdateStock); } } $self->{img}->set_from_stock($stock,$self->{size}); if (my $l=$self->{label}) { my $string=$self->{string}; $string= $string->() if ref $string eq 'CODE'; $l->set_markup_with_format($self->{markup},$string); } 0; } package Layout::Label; use base 'Gtk2::EventBox'; use constant INCR => 1; #scroll increment in pixels our @default_options= ( xalign=>0, yalign=>.5, ); sub new { my ($class,$opt)=@_; %$opt=( @default_options, %$opt ); my $self = bless Gtk2::EventBox->new, $class; my $label=Gtk2::Label->new; $label->set_alignment($opt->{xalign},$opt->{yalign}); $self->set_visible_window(0); for (qw/markup markup_empty autoscroll interval/) { $self->{$_}=$opt->{$_} if exists $opt->{$_}; } my $font= $opt->{font} && Gtk2::Pango::FontDescription->from_string($opt->{font}); $label->modify_font($font) if $font; if (my $color= $opt->{color} || $opt->{DefaultFontColor}) { $label->modify_fg('normal', Gtk2::Gdk::Color->parse($color) ); } $self->add($label); #$self->signal_connect(enter_notify_event => sub {$_[0]->set_markup(''.$_[0]->child->get_label.'')}); #$self->signal_connect(leave_notify_event => sub {my $m=$_[0]->child->get_label; $m=~s#^##;$m=~s#$##; $_[0]->set_markup($m)}); $self->{expand_max}= $opt->{maxwidth} || -1 if $opt->{expand_max}; my $minsize= $opt->{minsize}; if (my $el=$opt->{ellipsize}) { $label->set_ellipsize($el); $minsize=undef; } if ($minsize && $minsize=~m/^\d+p?$/) { unless ($minsize=~s/p$//) { my $lay=$label->create_pango_layout( 'X' x $minsize ); $lay->set_font_description($font) if $font; ($minsize)=$lay->get_pixel_size; } $self->set_size_request($minsize,-1); $label->signal_connect(expose_event => \&expose_cb); if ($self->{autoscroll}) { $self->{interval} ||=50; # default to a scroll every 50ms $self->signal_connect(size_allocate => \&restart_scrollcheck); } else # scroll when mouse is over it { $self->{interval} ||=20; # default to a scroll every 20ms $self->signal_connect(enter_notify_event => \&enter_leave_cb, INCR()); $self->signal_connect(leave_notify_event => \&enter_leave_cb,-INCR()); } } elsif (defined $opt->{initsize}) { #$label->set_size_request($label->create_pango_layout( $opt->{initsize} )->get_pixel_size); my $lay=$label->create_pango_layout( $opt->{initsize} ); $lay->set_font_description($font) if $font; $label->set_size_request($lay->get_pixel_size); $self->{resize}=1; } if (exists $opt->{markup}) { my $m=$opt->{markup}; if (my @fields=::UsedFields($m)) { $self->{EndInit}=\&init; # needs $self->{group} set before this can be done } else { $self->set_markup($m) } } elsif (exists $opt->{text}) { $label->set_text($opt->{text}); } return $self; } sub init { my $self=shift; ::WatchSelID($self,\&update_text); update_text($self,::GetSelID($self)); } sub update_text { my ($self,$ID)=@_; if ($self->{markup}) { my $markup= defined $ID ? ::ReplaceFieldsAndEsc( $ID,$self->{markup} ) : defined $self->{markup_empty} ? $self->{markup_empty} : ''; $self->set_markup($markup); } } sub set_label { my $label=$_[0]->child; $label->set_label($_[1]); $label->{dx}=0; $_[0]->checksize; } sub set_markup { my $label=$_[0]->child; $label->set_markup($_[1]); $label->{dx}=0; $_[0]->checksize; } sub set_markup_with_format { my $self=shift; $self->set_markup(::MarkupFormat(@_)); } sub checksize #extend the requested size so that the string fit in initsize mode (in case the initsize string is not wide enough) { my $self=$_[0]; if ($self->{resize}) { my $label=$self->child; my ($w,$h)=$label->get_layout->get_pixel_size; my ($w0,$h0)=$label->get_size_request; $w=0 if $w0>$w; $h=0 if $h0>$h; $label->set_size_request($w||$w0,$h||$h0) if $w || $h; } elsif (my $emax=$self->{expand_max}) { # make it expand up to min(maxwidth,string_width) my $label=$self->child; $label->get_layout->set_width($emax * Gtk2::Pango->scale) if $label->get_ellipsize ne 'none'; my ($w)= $label->get_layout->get_pixel_size; $w=$emax if $emax>0 && $emax < $w; $self->{maxwidth}= $w ||1; } $self->restart_scrollcheck if $self->{autoscroll}; } sub restart_scrollcheck #only used for autoscroll { my $self=shift; $self->{scroll_inc}||=INCR(); $self->{scrolltimeout} ||= Glib::Timeout->add($self->{interval}, \&Scroll,$self); } sub enter_leave_cb { my ($self,$event,$inc)=@_; #$self->set_state($inc>0 ? 'selected' : 'normal'); $self->{scrolltimeout} ||= Glib::Timeout->add($self->{interval}, \&Scroll,$self); $self->{scroll_inc}=$inc; 0; } sub expose_cb #only for scrollable labels { my ($label,$event)=@_; my $layout=$label->get_layout; my ($lw,$lh)=$layout->get_pixel_size; return 1 unless $lw; #empty string -> nothing to draw my ($xoffset,$yoffset,$aw,$ah)=$label->allocation->values; my ($xalign,$yalign)=$label->get_alignment; my ($xpad,$ypad)=$label->get_padding; $xoffset+=$xpad; $aw-=2*$xpad; $aw=0 if $aw<0; $yoffset+=$ypad; $ah-=2*$ypad; $ah=0 if $ah<0; $xoffset+=($aw-$lw)*$xalign if $aw>$lw; $yoffset+=($ah-$lh)*$yalign if $ah>$lh; $label->get_style->paint_layout($label->window, $label->state, ::FALSE, $event->area, $label, 'label', $xoffset-$label->{dx}, $yoffset, $layout); 1; } sub Scroll { my $self=$_[0]; my $label=$self->child; return 0 unless $label; my $aw=$label->allocation->width; my $max= ($label->get_layout->get_pixel_size)[0] - $aw; my $dx=$label->{dx}; $dx+= $self->{scroll_inc}; $dx=$max if $max<$dx; $dx=0 if $dx<0 || $max<0; $label->{dx}=$dx; $label->parent->queue_draw; my $reached_max= ($max<0) || ($dx==0 && $self->{scroll_inc}<0) || ($dx==$max && $self->{scroll_inc}>0); if ($self->{autoscroll}) { $self->{scroll_inc}=-$self->{scroll_inc} if $reached_max; # reverse scrolling $self->{scrolltimeout}=$self->{scroll_inc}=0 if $max<0; # no need for scrolling => stop checks } else { $self->{scrolltimeout}=0 if $reached_max; } return $self->{scrolltimeout}; } package Layout::Label::Time; use base 'Layout::Label'; sub set_markup { my ($self,$markup)=@_; $self->{time_markup}=$markup; $self->update_time; } sub update_time { my ($self,$time)=@_; my $markup=$self->{time_markup}; $time= $::PlayTime if !defined $time; if (defined $time) { my $length= Songs::Get($::SongID,'length'); my $format= $length<600? '%01d:%02d' : '%02d:%02d'; if ($self->{remaining}) { $format= '-'.$format; $time= $length-$time; } $time= sprintf $format, $time/60, $time%60; } else { $time= $self->{markup_stopped}; return unless defined $time; # update_time() can be called before $self->{markup_stopped} is set, ignore } if ($markup) { $markup=~s/%s/$time/; } else { $markup=$time } $self->SUPER::set_markup($markup); } package Layout::Bar; use base 'Gtk2::ProgressBar'; sub new { my ($class,$opt,$ref)=@_; my $self=bless Gtk2::ProgressBar->new, $class; if ($opt->{text}) { $self->{text}=$opt->{text}; $self->{text_empty}=$opt->{text_empty}; $self->set_ellipsize( $opt->{ellipsize}||'end' ); my $font= $opt->{font}; $self->modify_font(Gtk2::Pango::FontDescription->from_string($font)) if $font; } my $orientation= $opt->{vertical} ? 'bottom-to-top' : $opt->{horizontal} ? 'left-to-right' : $opt->{orientation} || 'left-to-right'; $self->set_orientation($orientation); if ($opt->{skin} && $opt->{handle_skin}) { $self= Layout::Bar::skin->new($opt) || $self; # warning : replace $self } $self->add_events([qw/pointer-motion-mask button-press-mask button-release-mask scroll-mask/]); $self->signal_connect(button_press_event => \&button_press_cb); $self->signal_connect(button_release_event => \&button_release_cb); $self->signal_connect(scroll_event => \&scroll_cb); $self->{left} ||=0; $self->{right}||=0; $self->{max}= $ref->{max} || 1; $self->{scroll}=$ref->{scroll}; $self->{set}=$ref->{set}; $self->{set_preview}=$ref->{set_preview}; $self->{vertical}= $orientation eq 'bottom-to-top'; return $self; } sub set_val { $_[0]{now}=$_[1]; $_[0]->update; } sub set_max { $_[0]{max}=$_[1]; $_[0]->update; } sub update { my $self=$_[0]; return if $self->{pressed}; my $f= ($self->{now}||0) / ($self->{max}||1); $f=0 if $f<0; $f=1 if $f>1; $self->set_fraction($f); if (my $text=$self->{text}) { $text= $self->{text_empty} if !defined $::SongID && defined $self->{text_empty}; my $now=$self->{now}||0; my $max=$self->{max}||0; my $format= $max<600 ? '%01d:%02d' : '%02d:%02d'; my $left=$max-$now; $_=sprintf($format,int($_/60),$_%60) for $now,$max,$left; my %special= ( '$percent' => sprintf('%d',$f*100), '$current' => $now, '$left' => $left, '$total' => $max, ); $text=::ReplaceFields( $::SongID,$text,0,\%special ); $self->set_text($text); } } sub button_press_cb { my ($self,$event)=@_; $self->{pressed}||=$self->signal_connect(motion_notify_event => \&button_press_cb); my ($x,$w)= $self->{vertical} ? ($event->y, $self->allocation->height): ($event->x, $self->allocation->width) ; $w=1 if $w<1; $w-= $self->{left} +$self->{right}; $x-= $self->{left}; my $f=$x/$w; $f=0 if $f<0; $f=1 if $f>1; $f=1-$f if $self->{vertical}; $self->set_fraction($f); my $s= $f*$self->{max}; $self->{newpos}=$s; my $sub= $self->{set_preview} || $self->{set}; $sub->($self,$s); 1; } sub button_release_cb { my ($self,$event)=@_; return 0 unless $self->{pressed}; $self->signal_handler_disconnect(delete $self->{pressed}); if ($self->{set_preview}) { $self->{set_preview}->($self, undef); $self->{set}->( $self, $self->{newpos} ); } #$self->update; 1; } sub update_preview_Time { my ($self,$value)=@_; my $h=::get_layout_widget($self)->{widgets}; my @labels= grep $_->isa('Layout::Label::Time'), values %$h; #get list of Layout::Label::Time widgets in the layouts my $preview= defined $value ? 1 : 0; for my $label (@labels) { $label->{busy}=$preview; $label->update_time($value) if $preview; } } sub scroll_cb { my ($self,$event)=@_; my $d= $event->direction; if ($d eq 'down' || $d eq 'right') { $d=1 } elsif ($d eq 'up' || $d eq 'left' ) { $d=0 } else { return 0 } $d= !$d if $self->{vertical}; $self->{scroll}->($self,$d); return 1; } package Layout::Bar::skin; our @ISA=('Layout::Bar'); use base 'Gtk2::EventBox'; sub new { my ($class,$opt)=@_; my $self=bless Gtk2::EventBox->new,$class; my $hskin=$self->{handle_skin}=Skin->new($opt->{handle_skin},undef,$opt); my $bskin=$self->{back_skin}= Skin->new($opt->{skin},undef,$opt); unless ($hskin && $bskin) { warn "Error loading background skin='$opt->{skin}'\n" unless $bskin; warn "Error loading handle handle_skin='$opt->{skin}'\n" unless $hskin; return; } my $resize=$bskin->{resize}; my ($left)= $resize=~m/l[es](\d+)/; my ($right)=$resize=~m/r[es](\d+)/; my ($top)= $resize=~m/t[es](\d+)/; my ($bottom)=$resize=~m/b[es](\d+)/; $self->{left}= $left ||= 0; $self->{right}= $right||= 0; $self->{top}= $top ||= 0; $self->{bottom}=$bottom||=0; $self->set_size_request($left+$right+$hskin->{minwidth},$top+$bottom+$hskin->{minheight}); $self->signal_connect(expose_event=> \&expose_cb); return $self; } sub set_fraction { $_[0]{fraction}=$_[1]; $_[0]->queue_draw; } sub expose_cb { my ($self,$event)=@_; Skin::draw($self,$event,$self->{back_skin}); my ($w,$h)=($self->allocation->values)[2,3]; if ($self->{vertical}) { my $minh=$self->{handle_skin}{minheight}; $h-= $self->{top}+$self->{bottom}; my $y= $self->{top} + $h *(1-$self->{fraction}); $y-= $minh/2; Skin::draw($self,$event,$self->{handle_skin},$self->{left},int($y),$w-$self->{left}-$self->{right},$minh); } else { my $minw=$self->{handle_skin}{minwidth}; $w-= $self->{right}+$self->{left}; my $x= $self->{left} + $w *$self->{fraction}; $x-= $minw/2; Skin::draw($self,$event,$self->{handle_skin},int($x),$self->{top},$minw,$h-$self->{top}-$self->{bottom}); } 1; } package Layout::Bar::Scale; use base 'Gtk2::Scale'; sub new { my ($class,$opt,$ref)=@_; my $scale= $opt->{orientation} || 'left-to-right'; $scale= 'left-to-right' if $opt->{horizontal}; $scale= 'bottom-to-top' if $opt->{vertical}; $scale= $scale eq 'left-to-right' ? 'Gtk2::HScale' : 'Gtk2::VScale'; my $max= $ref->{max} || 1; my $self = bless $scale->new_with_range(0,$max,$max/10), $class; $self->set_inverted(1) if $scale eq 'Gtk2::VScale'; $self->{vertical}= $scale eq 'Gtk2::VScale'; $self->{max}= $max; $self->{step_mode}=$opt->{step_mode}; $self->set_draw_value(0); $self->signal_connect(button_press_event => \&button_press_cb); $self->signal_connect(button_release_event => \&button_release_cb); $self->signal_connect(scroll_event => \&Layout::Bar::scroll_cb); $self->{$_}=$ref->{$_} for qw/scroll set set_preview/; return $self; } sub set_val { $_[0]->set_value($_[1] || 0) unless $_[0]{pressed}; } sub set_max { $_[0]->{max}=$_[1]; $_[0]->get_adjustment->upper($_[1]); } sub button_press_cb { my ($self,$event)=@_; if (!$self->{step_mode}) # short-circuit normal Gtk2::Scale click behaviour { $self->{pressed}= $self->signal_connect(motion_notify_event => \&update_value_direct_mode); $self->update_value_direct_mode($event); return 1; # return 1 so that Gtk2::Scale won't get the mouse click } $self->{pressed}= $self->signal_connect(value_changed => \&value_changed_cb); return 0; } sub button_release_cb { my $self=$_[0]; return 0 unless $self->{pressed}; $self->signal_handler_disconnect( delete $self->{pressed} ); if ($self->{set_preview}) { $self->{set_preview}->($self, undef); $self->{set}->( $self, $self->{newpos} ); } 0; } sub value_changed_cb { my $self=$_[0]; my $s=$self->get_value; $self->{newpos}=$s; my $sub= $self->{set_preview} || $self->{set}; $sub->($self,$s); 1; } sub update_value_direct_mode { my ($self,$event)=@_; my ($x,$w)= $self->{vertical} ? ($event->y, $self->allocation->height): ($event->x, $self->allocation->width) ; $w=1 if $w<1; my $f=$x/$w; $f=0 if $f<0; $f=1 if $f>1; $f=1-$f if $self->{vertical}; $self->set_value( $f * $self->{max}); $self->value_changed_cb; 1; } package Layout::AAPicture; use base 'Gtk2::EventBox'; our @default_options= (maxsize=>500, xalign=>.5, yalign=>.5, r_height=>25, r_alpha1=>80, r_alpha2=>0, r_scale=>90); sub new { my ($class,$opt)=@_; %$opt=( @default_options, %$opt ); my $self = bless Gtk2::EventBox->new, $class; $self->set_visible_window(0); $self->{aa}=$opt->{aa}; my $minsize=$opt->{minsize}; $self->{$_}=$opt->{$_} for qw/maxsize xalign yalign multiple/; $self->{usable_w}=$self->{usable_h}=1; my $ratio=1; if ( (my $refl=$opt->{reflection}) && $::CairoOK) { $self->{$_}= $opt->{$_}/100 for qw/r_alpha1 r_alpha2 r_scale/; $self->{reflection}= $refl==1 ? $opt->{r_height}/100 : $refl/100; my $height= $self->{reflection} +1; $ratio/= $height; $self->{usable_h}/=$height; } if (my $o=$opt->{overlay}) {{ my ($x,$y,$xy_or_wh,$w,$h,$file)= $o=~m/^(\d+)x(\d+)([-:])(\d+)x(\d+):(.+)/; unless (defined $file) { warn "Invalid picture-overlay string : '$o' (format: XxY:WIDTHxHEIGHT:FILE)\n"; last } $file= ::SearchPicture( $file, $opt->{PATH} ); last unless $file; my $pb= GMB::Picture::pixbuf($file); last unless $pb; if ($xy_or_wh eq '-') { $w-=$x; $h-=$y; } my $w0=$pb->get_width; my $h0=$pb->get_height; warn "Bad picture-overlay values : rectangle bigger than the overlay picture\n" if $w0<$w+$x || $h0<$h+$y; my $ws= $w0/$w; my $hs= $h0/$h; $ratio*= $ws/$hs; $self->{usable_w}/= $ws; $self->{usable_h}/= $hs; $self->{overlay}=[$pb, $x/$w, $y/$h, $ws,$hs]; }} if ($opt->{forceratio}) { $self->{forceratio}=$ratio; } #not sure it's still needed with the natural_size mode else { $self->{expand_to_ratio}=$ratio; $self->{expand_weight}=10; } if (my $file=$opt->{'default'}) { $self->{'default'}= ::SearchPicture( $file, $opt->{PATH} ); } $self->signal_connect(size_allocate => \&size_allocate_cb); $self->signal_connect(expose_event => \&expose_cb); $self->signal_connect(destroy => sub {delete $::ToDo{'8_LoadImg'.$_[0]}}); $self->set_size_request($minsize,$minsize) if $minsize; $self->{key}=[]; $self->{natural_size}=1 unless $minsize; $self->{ratio}=$ratio; return $self; } sub Changed { my ($self,$key)=@_; return unless grep $_ eq $key, @{$self->{key}}; $self->set(delete $self->{key}); } sub set { my ($self,$key)=@_; $key=[] unless defined $key; $key=[$key] unless ref $key; return if $self->{key} && join("\x1D", @$key) eq join("\x1D", @{$self->{key}}); $self->{key}=$key; my $col=$self->{aa}; my @files; for my $k (@$key) { my $f=AAPicture::GetPicture($col,$k); push @files, $f if $f; } $self->{pixbuf}=undef; if ( !@files && (my $file=$self->{'default'}) ) { @files=($file); } #default picture if (@files) { if (@files>1 && !$self->{multiple}) {$#files=0} # use only the first file if not in multiple mode $self->show; $self->queue_draw; ::IdleDo('8_LoadImg'.$self,500,\&LoadImg,$self,@files); } else { $self->hide unless $self->{natural_size}; } $self->signal_connect('map'=>sub #undo the temporary settings set in size_allocate_cb for the natural_size mode #FIXME should be simpler { my $self=$_[0]; delete $self->{size} unless $self->{pixbuf} || $::ToDo{'8_LoadImg'.$self}; $self->set_size_request(-1,-1) unless $self->{forceratio}; $self->queue_resize; }) if $self->{natural_size}; } sub LoadImg { my ($self,@files)=@_; my ($w,$h)=split /x/, $self->{size}||'0x0'; $w*= $self->{usable_w}; $h*= $self->{usable_h}; my $size= ::min($w,$h); return if $size<8; # no need to draw such a small picture $size=int $size/@files; my @pix= grep $_, map GMB::Picture::pixbuf($_,$size), @files; my $pix=shift @pix; if (@pix) { $pix=collage($self->{multiple},$pix,@pix); } $pix= $self->add_overlay($pix) if $pix && $self->{overlay}; $self->{pixbuf}= $pix; $self->queue_draw; $self->hide unless $pix; } sub size_allocate_cb { my ($self,$alloc)=@_; my $ratio=$self->{ratio}; my $w=$alloc->width; my $h=$alloc->height; if (my $max=$self->{maxsize}) { $w=$max if $w>$max; $h=$max if $h>$max; } my $func= $self->{forceratio} ? \&::max : \&::min; $w= $func->($w, int $h*$ratio); $h= $func->($h, int $w/$ratio); my $size=$w.'x'.$h; if (delete $self->{natural_size})#set temporary settings for natural_size mode #FIXME should be simpler { $self->set_size_request($w,$h) if !defined $self->{size} || $size ne $self->{size}; $self->{size}=$size; return; } if (!defined $self->{size}) { unless ($self->{pixbuf} || $::ToDo{'8_LoadImg'.$self}) {$self->hide;return}; } elsif ($self->{size} eq $size) {return} $self->set_size_request($w,$h) if $self->{forceratio}; $self->{size}=$size; $self->set( delete $self->{key} ); #force reloading } sub expose_cb { my ($self,$event)=@_; my ($x,$y,$ww,$wh)=$self->allocation->values; my $pixbuf= $self->{pixbuf}; return 1 unless $pixbuf; my $w=$pixbuf->get_width; my $h=$pixbuf->get_height; $x+= int ($ww-$w)*$self->{xalign}; $y+= int ($wh-$h)*$self->{yalign}; if (!$self->{reflection}) { my $gc=Gtk2::Gdk::GC->new($self->window); $gc->set_clip_rectangle($event->area); $self->window->draw_pixbuf($gc,$pixbuf,0,0,$x,$y,-1,-1,'none',0,0); } else { my $cr= Gtk2::Gdk::Cairo::Context->create($self->window); $cr->rectangle($event->area); $cr->clip; $cr->translate($x,$y); $self->draw_with_reflection($cr,$pixbuf); } 1; } sub draw_with_reflection { my ($self,$cr,$pixbuf)=@_; my $w=$pixbuf->get_width; my $h=$pixbuf->get_height; my $scale= $self->{r_scale}; my $rh=$h * $self->{reflection}; #draw picture $cr->set_source_pixbuf($pixbuf,0,0); $cr->paint; #clip for reflection $cr->rectangle(0,$h,$w,$h+$rh); $cr->clip; #create alpha gradient my $pattern= Cairo::LinearGradient->create(0,$h, 0,$h-$rh*(1/$scale)); $pattern->add_color_stop_rgba(0, 0,0,0, $self->{r_alpha1} ); $pattern->add_color_stop_rgba(1, 0,0,0, $self->{r_alpha2} ); #draw reflection my $angle=::PI; $cr->translate(0,$h); $cr->rotate($angle); $cr->scale(1,-$scale); $cr->rotate(-$angle); $cr->translate(0,-$h); $cr->set_source_pixbuf($pixbuf,0,0); $cr->mask($pattern); } sub collage { my ($mode,@pixbufs)=@_; $mode= $mode eq 'h' ? 1 : 0; my ($x,$y,$w,$h)=(0,0,0,0); #find resulting width and height for my $pb (@pixbufs) { my $pw=$pb->get_width; my $ph=$pb->get_height; if ($mode) { $w+=$pw; $h=$ph if $ph>$h; } else { $h+=$ph; $w=$pw if $pw>$w; } } my $pixbuf= Gtk2::Gdk::Pixbuf->new( $pixbufs[0]->get_colorspace, 1,8, $w,$h); $pixbuf->fill(0); #fill with transparent black for my $pb (@pixbufs) { my $pw=$pb->get_width; my $ph=$pb->get_height; # center pixbuf if ($mode) { $y=int( ($h-$ph)/2 ); } else { $x=int( ($w-$pw)/2 ); } $pb->copy_area(0,0, $pw,$ph, $pixbuf, $x,$y); if ($mode) { $x+=$pw } else { $y+=$ph } } return $pixbuf; } sub add_overlay { my ($self,$pixbuf)=@_; my $w=$pixbuf->get_width; my $h=$pixbuf->get_height; my ($overlay,$xs,$ys,$ws,$hs)= @{$self->{overlay}}; my $wo= $w*$ws; my $ho= $h*$hs; my $x= $w*$xs; my $y= $h*$ys; my $result= Gtk2::Gdk::Pixbuf->new( $pixbuf->get_colorspace, 1,8, $wo,$ho); $result->fill(0); #fill with transparent black $pixbuf->copy_area(0,0, $w,$h, $result, $x,$y); $overlay->composite($result, 0,0, $wo,$ho, 0,0, $wo/$overlay->get_width,$ho/$overlay->get_height, 'bilinear',255); return $result; } package Layout::TogButton; use base 'Gtk2::ToggleButton'; sub new { my ($class,$opt)=@_; my $self = bless Gtk2::ToggleButton->new, $class; my ($icon,$label); my $text= $opt->{label} || $opt->{text}; $self->set_relief($opt->{relief}) if $opt->{relief}; $label=Gtk2::Label->new($text) if defined $text; $icon=Gtk2::Image->new_from_stock($opt->{icon},$opt->{size}) if $opt->{icon}; my $child= ($label && $icon) ? ::Hpack($icon,$label) : $icon || $label; $self->add($child) if $child; #$self->{gravity}=$opt->{gravity}; $self->{$_}=$opt->{$_} for qw/widget resize togglegroup/; $self->signal_connect( toggled => \&toggled_cb ); ::Watch($self,'HiddenWidgets',\&UpdateToggleState); if ($opt->{skin}) { my $skin=Skin->new($opt->{skin},$self,$opt); $self->signal_connect(expose_event => \&Skin::draw,$skin); $self->set_app_paintable(1); #needed ? if (0 && $opt->{shape}) #mess up button-press cb TESTME { $self->{shape}=1; } } return $self; } sub UpdateToggleState #also used by Layout::MenuItem { my $self=$_[0]; return unless $self->{widget}; my $layw=::get_layout_widget($self); return unless $layw; my $state=$layw->GetShowHideState($self->{widget}); $self->{busy}=1; $self->set_active($state); delete $self->{busy}; } sub toggled_cb #also used by Layout::MenuItem { my $self=$_[0]; return if $self->{busy} || !$self->{widget}; my $layw=::get_layout_widget($self); return unless $layw; my $show= $self->get_active; if (my $tg=$self->{togglegroup}) { unless ($show) { $show=1; UpdateToggleState($self); } # togglegroup mode, click on a pressed button just press it again, doesn't un-pressed it my @togbuttons= grep $_->{togglegroup} && $_!=$self && $_->{togglegroup} eq $tg, #get list of widgets of the same togglegroup values %{$layw->{widgets}}; my $hidewidgets=join '|',grep $_, map $_->{widget}, @togbuttons; $layw->Hide($hidewidgets,$self->{resize}) if $hidewidgets; } if (my $w=$self->{widget}) { $layw->ShowHide($w,$self->{resize},$show) } } package Layout::MenuItem; sub new { my $opt=shift; if ($opt->{button} && $opt->{updatemenu}) { return Layout::ButtonMenu->new($opt); } my $self; my $label= $opt->{label} || $opt->{text}; if ($opt->{togglewidget}) { $self=Gtk2::CheckMenuItem->new($label); } elsif ($opt->{icon}) { $self=Gtk2::ImageMenuItem->new($label); $self->set_image( Gtk2::Image->new_from_stock($opt->{icon}, 'menu')); } else { $self=Gtk2::MenuItem->new($label); } if ($opt->{updatemenu}) { $self->{updatemenu}=$opt->{updatemenu}; my $submenu=Gtk2::Menu->new; $self->set_submenu($submenu); $self->signal_connect( activate=>\&UpdateSubMenu ); ::IdleDo( '9_UpdateSubMenu_'.$self, undef, \&UpdateSubMenu,$self); # (delayed) initial filling of the menu, not needed but makes the menu work better with gnome2-globalmenu } if ($opt->{togglewidget}) { $self->{widget}=$opt->{togglewidget}; $self->{resize}=$opt->{resize}; if (my $tg=$opt->{togglegroup}) { $self->{togglegroup}=$tg; $self->set_draw_as_radio(1); } $self->signal_connect( toggled => \&Layout::TogButton::toggled_cb ); ::Watch($self,'HiddenWidgets',\&Layout::TogButton::UpdateToggleState); } if ($opt->{command}) { $self->signal_connect(activate => \&::run_command,$opt->{command}); } return $self; } sub get_player_window { my $menu=$_[0]->parent; while (ref $menu eq 'Gtk2::Menu') { $menu=$menu->get_attach_widget->parent; } return ::get_layout_widget($menu); } sub UpdateSubMenu { my $self=shift; my $menu=$self->get_submenu; return unless $menu; $menu->remove($_) for $menu->get_children; $self->{updatemenu}($self); $menu->show_all; } package Layout::ButtonMenu; use base 'Gtk2::ToggleButton'; sub new { my ($class,$opt0)=@_; my %opt= ( relief=>'none', size=> 'menu', text=>'', %$opt0 ); my $self= bless Gtk2::ToggleButton->new, $class; my $child; my $label= $opt{label} || $opt{text}; $child=Gtk2::Label->new($label) if length $label; if ($opt{icon}) { my $img= Gtk2::Image->new_from_stock($opt{icon},$opt{size}); if ($child) { my $hbox= Gtk2::HBox->new(0,4); $hbox->pack_start($img,0,0,2); $hbox->pack_start($child,0,0,2); $child=$hbox; } $child||=$img; } $self->add($child) if $child; $self->set_relief($opt{relief}); $self->{menu}=Gtk2::Menu->new; $self->{menu}->attach_to_widget($self,undef); $self->{updatemenu}=$opt{updatemenu}; $self->signal_connect(button_press_event => sub { my ($self,$event) = @_; my $menu= $self->{menu}; if ($self->{updatemenu}) { $menu->remove($_) for $menu->get_children; $self->{updatemenu}($self); } $self->set_active(1); ::PopupMenu($menu,event=>$event); }); $self->{menu}->signal_connect(deactivate => sub { my $self = shift; $self->get_attach_widget->set_active(0); } ); return $self; } sub append { $_[0]{menu}->append($_[1]) } sub get_submenu { $_[0]{menu} } package Layout::LabelToggleButtons; use base 'Gtk2::ScrolledWindow'; sub new { my ($class,$opt)=@_; my $self= bless Gtk2::ScrolledWindow->new, $class; $self->set_shadow_type('etched-in'); $self->set_policy('automatic','automatic'); $self->{table}=Gtk2::Table->new(1,1,::TRUE); $self->add_with_viewport($self->{table}); my $field= $opt->{field}; if (Songs::FieldType($field) ne 'flags') { warn "LabelToggleButtons : invalid field $field\n"; $field= 'label'; } $self->{field}= $field; $self->{$_}= $opt->{$_} for qw/hide_unset group/; my $songchange= $self->{hide_unset} ? sub { my $self=shift; $self->{width}=0; $self->update_columns; $self->update_song } : \&update_song; ::WatchSelID($self, $songchange, [$field]); ::Watch($self,"newgids_$field",\&update_labels); $self->signal_connect( size_allocate => sub { ::IdleDo( "resize_$self",1000, \&update_columns,$_[0] ); }); return $self; } sub update_labels { my $self=shift; my %checks; $self->{checks}=\%checks; for my $label ( @{Songs::ListAll($self->{field})} ) { my $check= $checks{$label}= Gtk2::CheckButton->new_with_label($label); $check->signal_connect(toggled => \&toggled_cb,$label); } $self->{width}=0; $self->update_columns; $self->update_song; } sub update_columns { my $self=shift; goto &update_labels unless $self->{checks}; #initialization my $width=$self->child->allocation->width; return unless $width; return if $self->{width} && $width == $self->{width}; $self->{width}=$width; my $table=$self->{table}; $table->remove($_) for $table->get_children; $table->resize(1,1); my @list; if ($self->{hide_unset}) { my $ID= ::GetSelID($self); @list= Songs::Get_list($ID,$self->{field}) if defined $ID; } else { @list= @{Songs::ListAll($self->{field})} } my @shown= grep defined, map $self->{checks}{$_}, @list; my $maxwidth=::max( 10,map 4+$_->size_request->width, @shown ); my $maxcol= int( $width / $maxwidth)||1; my $col=my $row=0; for my $widget (@shown) { $table->attach($widget,$col,$col+1,$row,$row+1,['fill','expand'],'shrink',1,1); if (++$col==$maxcol) {$col=0; $row++;} } $table->show_all; } sub update_song { my $self=shift; $self->{busy}=1; my $ID= ::GetSelID($self); $self->{table}->set_sensitive(defined $ID); my $checks=$self->{checks}; for my $label (keys %$checks) { my $check=$checks->{$label}; my $on= defined $ID ? Songs::IsSet($ID,$self->{field}, $label) : 0; $check->set_active($on); } $self->{busy}=0; } sub toggled_cb { my ($check,$label)=@_; return unless $check->parent; my $self=::find_ancestor($check,__PACKAGE__); return if $self->{busy}; my $field= ($check->get_active ? '+' : '-').$self->{field}; my $ID= ::GetSelID($self); Songs::Set($ID,$field,[$label]); } package Layout::SongInfo; use base 'Gtk2::ScrolledWindow'; our @default_options= ( markup_cat=>"%s", markup_field=>"%s :", markup_value=>"%s" ); sub new { my ($class,$opt)=@_; %$opt=( @default_options, %$opt ); my $self= bless Gtk2::ScrolledWindow->new, $class; $self->set_policy('automatic','automatic'); $self->{table}=Gtk2::Table->new(1,1,::FALSE); $self->add_with_viewport($self->{table}); $self->{$_}=$opt->{$_} for qw/group ID markup_cat markup_field markup_value font expander collapsed hide_empty/; if ($opt->{ID}) # for use in SongProperties window { ::Watch($self, SongsChanged=> \&update); #could check which ID changed } else #use group option to find ID { ::WatchSelID($self, \&update); } $self->{SaveOptions}= \&SaveOptions; ::Watch($self,fields_reset=>\&init); $self->init; return $self; } sub init { my $self=shift; my %collapsed; if ($self->{expander}) { $self->SaveOptions if $self->{cats}; # updates $self->{collapsed} $collapsed{$_}=1 for split / +/, $self->{collapsed}||''; } my $table=$self->{table}; $table->remove($_) for $table->get_children; my $labels1=$self->{labels1}={}; my $labels2=$self->{labels2}={}; my $cats=$self->{cats}={}; my @labels; $table->{row}=0; my $treelist=Songs::InfoFields; while (@$treelist) { my ($cat,$catname,$fields)= splice @$treelist,0,3; #category my $catlabel=Gtk2::Label->new_with_format($self->{markup_cat}, $catname); push @labels, $catlabel; my $table2=$table; if ($self->{expander}) { $table2=Gtk2::Table->new(1,1,::FALSE); $table2->{row}=0; my $expander= Gtk2::Expander->new; $expander->set_label_widget($catlabel); $expander->add($table2); $expander->set_expanded( !$collapsed{$cat} ); $catlabel=$expander; } $cats->{$cat}=$catlabel; my $row=$table->{row}++; $table->attach($catlabel,0,1,$row,$row+1,'fill','shrink',1,1); #fields for my $field (@$fields) { my $lab1=$labels1->{$field}=Gtk2::Label->new_with_format($self->{markup_field}, Songs::FieldName($field)); my $lab2=$labels2->{$field}=Gtk2::Label->new; push @labels, $labels1, $labels2; $lab1->set_padding(5,0); $lab1->set_alignment(1,0); $lab2->set_alignment(0,0); $lab2->set_line_wrap(1); $lab2->set_selectable(1); my $row=$table2->{row}++; $table2->attach($lab1,0,1,$row,$row+1,'fill','fill',1,1); $table2->attach($lab2,1,2,$row,$row+1,'fill','fill',1,1); } $row=$table->{row}++; $table->attach(Gtk2::HBox->new,0,3,$row,$row+1,[],[],0,5) if @$treelist; #space between categories } if (my $font=$self->{font}) { $font= Gtk2::Pango::FontDescription->from_string($font); $_->modify_font($font) for @labels; } if ($self->{expander}) { my $sg= Gtk2::SizeGroup->new('horizontal'); $sg->add_widget($_) for values %$labels1; } $table->set_no_show_all(0); $table->show_all; $table->set_no_show_all(1); $self->update; } sub update { my $self=shift; my $ID= $self->{ID} || ::GetSelID($self); my $labels1= $self->{labels1}; my $labels2= $self->{labels2}; my $func= defined $ID ? \&Songs::Display : sub {''}; my $treelist=Songs::InfoFields; while (@$treelist) { my ($cat,$catname,$fields)= splice @$treelist,0,3; my $found; for my $field (@$fields) { my $lab2= $labels2->{$field}; next unless $lab2; my $val= $func->($ID,$field); $lab2->set_markup_with_format($self->{markup_value}, $val); if ($self->{hide_empty}) { $_->set_visible($val ne '') for $labels1->{$field},$lab2; $found ||= $val ne ''; } } $self->{cats}{$cat}->set_visible($found) if $self->{hide_empty}; } $self->queue_resize; # for unclear reasons (bug in gtk2 ?), labels that were hidden when the widget is was last shown, and then are shown while the widget is hidden, have a row height of 0 (and thus hidden) when the widget is shown, forcing a queue resize fixes it } sub SaveOptions { my $self=shift; my %opt; if (my $cats=$self->{cats}) { $opt{collapsed}= $self->{collapsed}= join ' ', sort grep !$cats->{$_}->get_expanded, keys %$cats; } return %opt; } package Layout::PictureBrowser; use base 'Gtk2::Box'; our @toolbar= ( { stockicon=> 'gmb-view-list', label=>_"Show file list", toggleoption=>'self/show_list', cb=> sub { $_[0]{self}->update_showhide; }, }, { stockicon=> 'gtk-zoom-in', label=>_"Zoom in", cb=> sub { $_[0]{view}->change_zoom('+'); }, }, { stockicon=> 'gtk-zoom-out', label=>_"Zoom out", cb=> sub { $_[0]{view}->change_zoom('-'); }, }, { stockicon=> 'gtk-zoom-100', label=>_"Zoom 1:1", cb=> sub { $_[0]{view}->change_zoom(1); }, }, { stockicon=> 'gtk-zoom-fit', label=>_"Zoom to fit", cb=> sub { $_[0]{view}->set_zoom_fit; }, }, { stockicon=> 'gtk-fullscreen', label=>_"Fullscreen", cb=> sub { $_[0]{view}->set_fullscreen(1); }, }, # { stockicon=> 'gtk-go-back', label=>_"Previous picture", cb=> sub { $_[0]{self}->change_file(-1); }, }, # { stockicon=> 'gtk-go-forward', label=>_"Next picture", cb=> sub { $_[0]{self}->change_file(1); }, }, # { stockicon=> 'gtk-', label=>_"Rotate clockwise", cb=> sub { $_[0]{view}->rotate(1); }, }, # { stockicon=> 'gtk-', label=>_"Rotate counterclockwise", cb=> sub { $_[0]{view}->rotate(-1); }, }, { stockicon=> 'gtk-jump-to', label=> sub { $_[0]{self}{group} eq 'Play' ? _"Follow playing song" : _"Follow selected song"; }, toggleoption=>'self/follow', cb=> sub { $_[0]{self}->queue_song_changed; }, }, ); our @optionsubmenu= ( { label=> sub { $_[0]{self}{group} eq 'Play' ? _"Follow playing song" : _"Follow selected song"; }, code => sub { $_[0]{self}->queue_song_changed; ::UpdateToolbar($_[0]{toolbar}); }, toggleoption=>'self/follow', mode=>'V', }, { label=>_"Scroll to zoom", mode=>'VP', toggleoption=> 'view/scroll_zoom', }, { label=>_"Reset zoom", mode=>'V', submenu_ordered_hash => 1, check => sub {$_[0]{self}{reset_zoom_on}}, submenu=> [ _"when file changes"=>'file', _"when folder changes"=>'folder', _"never"=>'never'], code => sub { $_[0]{self}{reset_zoom_on}=$_[1] }, }, { separator=>1, mode=>'V', }, { label=>_"Show folder list", mode=>'VL', toggleoption=>'self/show_folders', test=> sub { $_[0]{self}{show_list}, }, code => sub { $_[0]{self}->update_showhide; }, }, { label=>_"Show file list", mode=>'VL', toggleoption=>'self/show_list', code=> sub { $_[0]{self}->update_showhide; }, }, { label=>_"Show toolbar", mode=>'VL', toggleoption=>'self/show_toolbar', code=> sub { $_[0]{self}->update_showhide; }, }, { label=>_"Show pdf pages", mode=>'VL', toggleoption=>'self/pdf_mode', code=> sub { $_[0]{self}->update; }, }, ); # mode L is for List, V for View, P for Pixbuf (Layout::PictureBrowser::View without a Layout::PictureBrowser, not used yet) our @ContextMenu= ( { label => _"Zoom in", code => sub { $_[0]{view}->change_zoom('+'); }, defined=>'pixbuf', stockicon=> 'gtk-zoom-in', mode=>'PV', }, { label => _"Zoom out", code => sub { $_[0]{view}->change_zoom('-'); }, defined=>'pixbuf', stockicon=> 'gtk-zoom-out', mode=>'PV', }, { label => _"Zoom 1:1", code => sub { $_[0]{view}->change_zoom(1); }, defined=>'pixbuf', stockicon=> 'gtk-zoom-100', mode=>'PV', }, { label => _"Zoom to fit", code => sub { $_[0]{view}->set_zoom_fit; }, defined=>'pixbuf', stockicon=> 'gtk-zoom-fit', mode=>'PV', }, { separator=>1, mode=>'V', }, { label=> _"View in new window", istrue=> 'file', code => sub { $_[0]{self}->view_in_new_window($_[0]{file}); }, mode=>'VL', }, { label=> _"Rename file", code => sub { my $tv=$_[0]{self}{filetv}; $tv->set_cursor($_[0]{treepaths}[0],$tv->get_column(0),::TRUE); }, onlyone => 'treepaths', isfalse=>'ispage', istrue=>'writeable', mode=>'L', }, { label=> _"Delete file", code => sub { $_[0]{self}->delete_selected }, istrue=>'writeable', isfalse=>'ispage', mode=>'VL', stockicon=>'gtk-delete', }, { label => _"Options", submenu=> \@optionsubmenu, mode=>'VL', }, { label=> sub { my $name=Songs::Gid_to_Display($_[0]{field},$_[0]{gid}); ::__x(_"Set as picture for '{name}'", name=>::Ellipsize($name,30)) }, code => sub { Songs::Picture($_[0]{gid},$_[0]{field},'set',$_[0]{file}); }, test=> sub { $_[0]{file} ne (Songs::Picture($_[0]{gid},$_[0]{field},'get')||''); }, istrue=>'file gid', mode=>'V', # test => sub { Songs::FilterListProp($_[0]{field},'picture') }, }, { label => _"Paste link", test => sub { return unless $_[0]{self}->can('drop_uris'); $_[0]{clip}= $_[0]{self}->get_clipboard(Gtk2::Gdk::Atom->new('PRIMARY',1))->wait_for_text; $_[0]{clip} && $_[0]{clip}=~m#^\s*\w+://#; }, code=> sub { $_[0]{self}->drop_uris(uris=>[grep m#^\s*\w+://#, split /[\n\r]+/, $_[0]{clip}]); }, }, { label=> sub { $_[0]{view}{fullwin} ? _"Exit full screen" : _"Full screen" }, code=>sub { $_[0]{view}->set_fullscreen }, mode=>'VP', stockicon=> sub { $_[0]{view}{fullwin} ? 'gtk-leave-fullscreen' : 'gtk-fullscreen' }, }, ); sub new { my ($class,$opt)=@_; my $self= bless Gtk2::VBox->new, $class; $self->{$_}= $opt->{$_} for qw/group follow show_list show_folders show_toolbar reset_zoom_on nowrap pdf_mode/; my $hbox= Gtk2::HBox->new; my $hpaned= Layout::Boxes::PanedNew('Gtk2::HPaned',{size=>$opt->{hpos}}); my $vpaned= $self->{vpaned}= Layout::Boxes::PanedNew('Gtk2::VPaned',{size=>$opt->{vpos}}); my $view= $self->{view}= Layout::PictureBrowser::View->new(%$opt,mode=>'V'); my $toolbar= $self->{toolbar}= ::BuildToolbar(\@toolbar, getcontext=>\&toolbarcontext, self=>$self); $self->{dirstore} = Gtk2::ListStore->new(qw/Glib::String Glib::String Glib::String/); $self->{filestore}= Gtk2::TreeStore->new(qw/Glib::String Glib::String Glib::Boolean Glib::Uint Glib::Uint/); my $treeview1= $self->{foldertv}= Gtk2::TreeView->new($self->{dirstore}); my $treeview2= $self->{filetv}= Gtk2::TreeView->new($self->{filestore}); my $renderer=Gtk2::CellRendererText->new; $renderer->signal_connect(edited => \&filename_edited_cb,$treeview2); $treeview1->insert_column_with_attributes(-1, 'Dir icon', Gtk2::CellRendererPixbuf->new, stock_id => 2) unless $opt->{no_folder_icons}; $treeview1->insert_column_with_attributes(-1, 'Dir name', Gtk2::CellRendererText->new, text => 0); $treeview2->insert_column_with_attributes(-1, 'File name',$renderer, text => 0, editable=> 2); #size and date column my $renderer_s= Gtk2::CellRendererText->new; my $renderer_d= Gtk2::CellRendererText->new; my $column_s=Gtk2::TreeViewColumn->new_with_attributes('size',$renderer_s); my $column_d=Gtk2::TreeViewColumn->new_with_attributes('date',$renderer_d); $renderer_s->set(xalign=>1); $column_s->set_cell_data_func($renderer_s, sub { my (undef,$cell,$store,$iter)=@_; my $depth=$store->iter_depth($iter); my $size= $depth ? '' : ::format_number($store->get($iter,3)); $cell->set(text=>$size); }); $treeview2->append_column($column_s); $renderer_d->set(xalign=>1); $column_d->set_cell_data_func($renderer_d, sub { my (undef,$cell,$store,$iter)=@_; my $depth=$store->iter_depth($iter); my $date= $depth ? '' : Songs::DateString($store->get($iter,4)); $cell->set(text=>$date); }); $treeview2->append_column($column_d); $_->set_sizing('autosize') for $treeview2->get_columns; $treeview1->set_headers_visible(0); $treeview2->set_headers_visible(0); $treeview2->get_selection->signal_connect(changed => \&treeview_selection_changed_cb); $vpaned->pack1(::new_scrolledwindow($treeview1), ::FALSE, ::FALSE); $vpaned->pack2(::new_scrolledwindow($treeview2), ::TRUE, ::TRUE); $hpaned->pack1($vpaned, ::FALSE, ::FALSE); $hpaned->add2($view); $hbox->add($hpaned); $self->pack_start($toolbar,0,0,2); $self->add($hbox); $self->{SaveOptions}= \&SaveOptions; $_->set_enable_search(0) for $treeview1,$treeview2; $self->signal_connect(key_press_event=> \&key_press_cb); $treeview1->signal_connect(row_activated=> \&folder_activated_cb); $treeview1->signal_connect(button_press_event=> \&folder_button_press_cb); $treeview2->signal_connect(button_press_event=> \&file_button_press_cb); $self->signal_connect(map => sub {$_[0]->queue_song_changed}); ::set_drag($view, dest => [::DRAG_ID,::DRAG_FILE,sub { my ($view,$type,@values)=@_; my $self= ::find_ancestor($view,__PACKAGE__); if ($type==::DRAG_FILE) { $self->Drop_Uris(uris=>\@values, is_move=>$view->{dragdest_suggested_action}); } elsif ($type==::DRAG_ID) { $self->queue_song_changed($values[0],'force'); } }], # motion cb needed to show the help message over the picture when dragging something over it motion=> sub { my ($view,$context,$x,$y,$time)=@_; my $datatype; for my $atom ($context->targets) { my $type= $::DRAGTYPES{$atom->name}||-1; if ($type==::DRAG_ID) { $datatype='songid'; last } elsif ($type==::DRAG_FILE){ $datatype= 'uri'; last } } $view->{dnd_message}= $datatype eq 'uri' ? _"Drop files in this folder" : _"View pictures from this album's folder"; #FIXME make second message depend $self->{field}; 1; } ); $view->signal_connect(drag_leave => sub { delete $_[0]{dnd_message}; }); ::set_drag($treeview2, source=> [::DRAG_FILE,sub { my $self= ::find_ancestor($_[0],__PACKAGE__); my $file=$self->{current_file}; $file=~s/:\w+$//; return $file ? (::DRAG_FILE,'file://'.::url_escape($file)) : () }]); $self->signal_connect(destroy => sub {$_[0]->destroy_cb}); $_->show_all, $_->set_no_show_all(1) for $vpaned,$toolbar; $self->update_showhide; $vpaned->signal_connect(show=> sub { my $self=::find_ancestor($_[0],__PACKAGE__); $self->refresh_treeviews; $self->update_selection; }); #updating of the file/folder list is disabled whe hidden, so needs to update it when shown if (my $file=$opt->{set_file}) { $self->set_file($file); } elsif (my $pb=$opt->{set_pixbuf}) { $self->{view}->set_pixbuf($pb); $self->{ignore_song}=1; } return $self; } sub destroy_cb { my $self=shift; $self->{drop_job}->Abort if $self->{drop_job}; } sub SaveOptions { my $self=shift; my %opt; my $vpaned= $self->{vpaned}; $opt{hpos}= ($vpaned->parent->{SaveOptions}($vpaned->parent))[1]; # The SaveOptions function of Layout::Boxes::PanedNew returns (size=>$value), $opt{vpos}= ($vpaned->{SaveOptions}($vpaned))[1]; # we only want the value $opt{$_}=$self->{view}{$_} for qw/scroll_zoom/; $opt{$_}=$self->{$_} for qw/follow show_list show_folders show_toolbar reset_zoom_on pdf_mode/; return %opt; } sub toolbarcontext { my $self= ::find_ancestor($_[0],__PACKAGE__); return self=>$self, view=>$self->{view}; } sub update_showhide { my $self=shift; my $vpaned= $self->{vpaned}; $vpaned->set_visible( $self->{show_list} ); $vpaned->child1->set_visible( $self->{show_folders} ); my $toolbar= $self->{toolbar}; $toolbar->set_visible( $self->{show_toolbar} ); ::UpdateToolbar($toolbar); } sub view_in_new_window { my ($self,$file)=@_; $file||= $self->{current_file}; return unless $file; Layout::Window->new('PictureBrowser', pos=>undef, 'PictureBrowser/follow'=>0,'PictureBrowser/set_file'=>$file); } sub delete_selected { my $self=shift; return if $::CmdLine{ro}; my @files= ($self->{current_file}); s/:\w+$// for @files; @files= ::uniq(@files); my $text= @files==1 ? ::filename_to_utf8displayname(::basename($files[0])) : __n("%d file","%d files",scalar @files); my $dialog = Gtk2::MessageDialog->new ( $self->get_toplevel, 'modal', 'warning','cancel','%s', ::__x(_("About to delete {files}\nAre you sure ?"), files => $text) ); $dialog->add_button("gtk-delete", 2); $dialog->show_all; if ('2' eq $dialog->run) { my $skip_all; my $done=0; for my $file (@files) { unless (unlink $file) { my $res= $skip_all; my $errormsg= _"Deletion failed"; $errormsg.= ' ('.($done+1).'/'.@files.')' if @files>1; $res ||= ::Retry_Dialog($!,$errormsg, details=>::__x(_("Failed to delete '{file}'"), file => ::filename_to_utf8displayname($file)), window=>$dialog, many=>(@files-$done)>1); $skip_all=$res if $res eq 'skip_all'; redo if $res eq 'retry'; last if $res eq 'abort'; } GMB::Cache::drop_file($file); #drop file from picture cache $done++; } # update selection my $file= $self->{current_file} || ''; my $list= $self->{filelist}; my $i= ::first { $list->[$_] eq $file } 0..$#$list; if (defined $i) { for my $j (reverse(0..$i), $i+1 .. $#$list) { if (-e $list->[$j]) { $self->{current_file}=$list->[$j]; last } } } $self->update; } $dialog->destroy; } sub filename_edited_cb { my ($cell,$path_string,$newutf8,$tv)= @_; return if $::CmdLine{ro}; my $store=$tv->get_model; my $iter=$store->get_iter_from_string($path_string); my $file= ::decode_url($store->get($iter,1)); my $suffix= $file=~s/(:\w+)$// ? $1 : ''; (my $path,$file)= ::splitpath($file); my $new= GMB::Picture::RenameFile($path, $file, $newutf8, $tv->get_toplevel) if $newutf8=~m/\S/ && $file ne $newutf8; return unless $new; my $self= ::find_ancestor($tv,__PACKAGE__); $self->{current_file}= ::catfile($path,$new.$suffix); $self->update; } sub key_press_cb { my ($self,$event)=@_; my $key=Gtk2::Gdk->keyval_name( $event->keyval ); if (::WordIn($key,'Delete KP_Delete')) { $self->delete_selected } elsif (lc$key eq 'l') { $self->{show_list}^=1; $self->update_showhide; $self->{view}->grab_focus unless $self->{show_list}; } elsif (lc$key eq 'n') { $self->view_in_new_window } elsif ($key eq 'F5') { $self->refresh_treeviews } else { return $self->{view}->key_press_cb($event); } #propagate event to the view widget #else {return 0} return 1; } sub folder_activated_cb { my ($tv,$treepath,$tvcol)=@_; my $self= ::find_ancestor($tv,__PACKAGE__); my $store= $tv->get_model; my $iter= $store->get_iter($treepath); return unless $iter; my $folder= ::decode_url($store->get($iter,1)); $self->set_path($folder); } sub folder_button_press_cb { my ($tv,$event)=@_; return 1 if $event->type ne 'button-press'; # ignore double or triple clicks my ($path,$column)=$tv->get_path_at_pos($event->get_coords); return 0 unless $path; $tv->row_activated($path,$column); return 1; } sub file_button_press_cb { my ($tv,$event)=@_; my $self= ::find_ancestor($tv,__PACKAGE__); #$self->grab_focus; my $button=$event->button; if ($button == 3) { my @rows=$tv->get_selection->get_selected_rows; ::PopupContextMenu( \@ContextMenu, {mode=>'L', treepaths=>\@rows, $self->context_menu_args} ); } else {return 0} 1; } sub treeview_selection_changed_cb { my ($selection)=@_; my ($store,$iter) = $selection->get_selected; return unless $iter; my $file= ::decode_url($store->get($iter,1)); my $self= ::find_ancestor($selection->get_tree_view,__PACKAGE__); return if $self->{busy}; $self->set_file($file); } sub drop_uris { my ($self,%args)=@_; return unless $self->{current_path}; $self->{drop_job} ||= GMB::DropURI->new(toplevel=>$self->get_toplevel, cb=>sub{$self->file_dropped($_[0])}, cb_end=>sub{$self->drop_end}); $self->{drop_job}->Add_URI(uris=>$args{uris}, is_move=>$args{is_move}, destpath=>$self->{current_path}); $self->{select_drop}=1; } sub file_dropped { my ($self,$file)=@_; return if ::dirname($file) ne ($self->{current_path}||''); #update list and picture if ($self->{select_drop}) #if selection hasn't changed since drop, select the new file { if ($file!~m/$::Image_ext_re/ && $file!~m/\.pdf$/i) {$file=undef} #do not select the file if not a picture/pdf $self->{current_file}= $file if $file; } $self->update; } sub drop_end { my $self=shift; delete $self->{drop_job}; delete $self->{select_drop}; } sub queue_song_changed { my ($self,$ID,$force)=@_; return if $self->{current_path} && !$self->{follow} && !$force; return if $self->{ignore_song}; return unless $self->mapped; ::IdleDo('8_ChangePicture'.$self,500,\&SongChanged,$self,$ID); } sub queue_change_picture { my ($self,$direction)=@_; ::IdleDo('8_ChangePicture'.$self,500,\&change_file,$self,$direction); } sub SongChanged { my ($self,$ID)=@_; $ID= ::GetSelID($self) unless defined $ID; $self->{ID}=$ID; unless (defined $ID) { $self->{gid}=undef; $self->set_list([]); return; } $self->{mode}= 'song'; my $oldgid= $self->{gid} ||0; $self->{gid}= Songs::Get_gid($ID,$self->{field}); $self->{view}->reset_zoom if $self->{reset_zoom_on} eq 'group' && $oldgid==$self->{gid}; $self->update; } sub set_list { my ($self,$list)=@_; $self->{mode}='list'; $self->{filelist}=$list; $self->update; } sub set_path { my ($self,$path)=@_; $self->{mode}='path'; $path= ::cleanpath($path) if $path; $self->{current_path}= $path; $self->update; } sub update { my $self=shift; my (@files,@paths,$default_path); if ($self->{mode} eq 'song') { my $field= $self->{field}; my $gid= $self->{gid}; my $path= AA::GuessBestCommonFolder($field,$gid); @paths=($default_path=$path) if $path; } elsif ($self->{mode} eq 'path') { my $path= $self->{current_path}; @paths=($default_path=$path) } elsif ($self->{mode} eq 'list') { @files= grep {my $f=$_; $f=~s/:\w+$//; -f $f} @{$self->{filelist}}; } my $pdfok= $GMB::Picture::pdf_ok && $self->{pdf_mode}; for my $path (@paths) { opendir my($dh),$path or do { warn $!; last; }; for my $file (map $path.::SLASH.$_, ::sort_number_aware(grep !m#^\.#, readdir $dh)) { next if -d $file; if ($file=~m/$::Image_ext_re/) { push @files, $file } elsif ($file=~m/\.pdf$/i && $pdfok) { push @files, $file, map "$file:$_", 1..GMB::Picture::pdf_pages($file)-1; } } closedir $dh; } $self->{filelist}=\@files; my $file=$self->{current_file}; unless ($file && (grep $file eq $_, @files)) { $file= $self->{current_file}= $files[0]; } my $oldpath= $self->{current_path}||''; $self->{current_path}= $file ? ::dirname($file) : ($default_path||''); $self->{view}->reset_zoom if $self->{reset_zoom_on} eq 'folder' && $oldpath ne $self->{current_path}; $self->refresh_treeviews; $self->update_file; } sub refresh_treeviews { my $self=shift; return unless $self->{show_list}; my $oldpath= $self->{loaded_path}; my $path= $self->{current_path}; # || $oldpath; my $dirstore= $self->{dirstore}; my $filestore=$self->{filestore}; my $filetv= $self->{filetv}; my $foldertv= $self->{foldertv}; $dirstore->clear; $filestore->clear; unless ($path && $oldpath && $oldpath eq $path) #folder has changed, reset scrollbars { $foldertv->get_vadjustment->set_value(0); $filetv->get_vadjustment->set_value(0); $self->{loaded_path}= $path; } return unless $path; my $parent= ::parentdir($path); $path= ::pathslash($path); # add a slash at the end $dirstore->set( $dirstore->append, 0,'..', 1,Songs::filename_escape($parent), 2,'gtk-go-up') if $parent; my $pdfok= $GMB::Picture::pdf_ok && $self->{pdf_mode}; my $show_expanders; my $folder_center_treepath; opendir my($dh),$path or do { warn $!; return; }; for my $file (::sort_number_aware( grep !m#^\.#, readdir $dh)) { if (-d $path.$file) { my $iter= $dirstore->append; $dirstore->set( $iter, 0,::filename_to_utf8displayname($file), 1,Songs::filename_escape($path.$file), 2,'gtk-directory'); if ($oldpath && $oldpath eq $path.$file) #select and center on previous folder if there { $folder_center_treepath= $dirstore->get_path($iter); $foldertv->set_cursor($folder_center_treepath); } next; } my @pages; my $suffix=''; if ($file=~m/\.pdf$/i && $pdfok) { @pages= (1..GMB::Picture::pdf_pages($path.$file)-1); $show_expanders=1; } elsif ($file!~m/$::Image_ext_re/) { next } my $efile= Songs::filename_escape($path.$file); my $iter= $filestore->append(undef); my ($size,$time)=(stat $path.$file)[7,9]; $filestore->set($iter, 0,::filename_to_utf8displayname($file), 1,$efile, 2,!$::CmdLine{ro}, 3,$size, 4,$time); for my $n (@pages) { $filestore->set($filestore->append($iter), 0, ::__x(_"page {number}",number=>$n+1), 1,"$efile:$n"); } } closedir $dh; $filetv->set_show_expanders($show_expanders); $filetv->expand_all; $foldertv->scroll_to_cell($folder_center_treepath,undef,::TRUE,.5,0) if $folder_center_treepath; #needs to be done once the list is filled } sub update_file { my $self=shift; my $old= $self->{loaded_file}||''; my $file= $self->{current_file}; delete $::ToDo{'8_ChangePicture'.$self}; if (!$file || $file ne $old) #file changed { $self->{view}->reset_zoom if $self->{reset_zoom_on} eq 'file'; delete $self->{select_drop}; } my $pixbuf= $file && GMB::Picture::pixbuf($file,undef,undef,'anim_ok'); #disable cache ? my %info; if ($file) { my $realfile=$file; $info{page}=$1 if $realfile=~s/:(\w+)$//; $info{filename}= ::filename_to_utf8displayname($realfile); $info{size}= (stat $realfile)[7]; } $self->{view}->set_pixbuf($pixbuf,%info); $self->{loaded_file}= $file; if ($file && ::dirname($file) ne ($self->{loaded_path}||'')) { $self->refresh_treeviews;} $self->update_selection; } sub update_selection { my $self=shift; return unless $self->{show_list}; my $treeview= $self->{filetv}; my $treesel= $treeview->get_selection; $treesel->unselect_all; my $file=$self->{current_file}; return unless $file; my $page= $file=~s/:(\d+)$// ? $1 : undef; $file= Songs::filename_escape($file); $self->{busy}=1; my $store= $self->{filestore}; my $iter= $store->get_iter_first; while ($iter) { if ($store->get($iter,1) eq $file) { if ($page) { $iter=$store->iter_nth_child($iter,$page-1); } $treesel->select_iter($iter); $treeview->scroll_to_cell($store->get_path($iter),undef,::FALSE,0,0); #scroll to row if needed last; } $iter= $store->iter_next($iter); } delete $self->{busy}; } sub change_file { my ($self,$direction)=@_; my $file= $self->{current_file}; my $list= $self->{filelist}; my $i; if ($file) { $i= ::first { $list->[$_] eq $file } 0..$#$list; } if (defined $i) { if ($self->{nowrap}) { $i= ::Clamp($i+$direction,0,$#$list); } else { $i= ($i+$direction) % @$list; } # wrap-around } else {$i=0} $self->set_file($list->[$i]); } sub set_file { my ($self,$file)=@_; $self->{current_file}=$file; return unless $file; if ($file && !(grep $file eq $_, @{$self->{filelist}})) { $self->set_path(::dirname($file)); } else { $self->update_file; } } sub context_menu_args { my $self=shift; my $file= $self->{current_file}; my $ispage= $file && $file=~m/:\w+$/; return self=>$self, field=>$self->{field}, ID=>$self->{ID}, gid=>$self->{gid}, file=>$file, ispage=>$ispage,writeable=> ($file && !$::CmdLine{ro}), toolbar=>$self->{toolbar}, view=>$self->{view}; } package Layout::PictureBrowser::View; use base 'Gtk2::Widget'; sub new { my ($class,%opt)=@_; #my $self= bless Gtk2::EventBox->new, $class; my $self= bless Gtk2::DrawingArea->new, $class; $self->add_events([qw/pointer-motion-mask scroll-mask key-press-mask button-press-mask button-release-mask/]); $self->can_focus(::TRUE); $self->signal_connect(expose_event => \&expose_cb); $self->signal_connect(size_allocate=> \&resize); $self->signal_connect(scroll_event => \&scroll_cb); $self->signal_connect(key_press_event=> \&key_press_cb); $self->signal_connect(button_press_event => \&button_press_cb); $self->signal_connect(button_release_event=> \&button_release_cb); $self->signal_connect(motion_notify_event => \&motion_notify_cb); $self->signal_connect(destroy => sub {delete $_[0]{pbanim}}); $self->{$_}=$opt{$_} for qw/xalign yalign scroll_zoom show_info oneshot/; $self->{mode}= $opt{mode}||'P'; $self->{offsetx}= $self->{offsety} =0; $self->{fit}=1; #default to zoom-to-fit if (my $c=$opt{bgcolor}) { $self->modify_bg('normal',Gtk2::Gdk::Color->parse($c)); } return $self; } sub set_pixbuf { my ($self,$pixbuf,%info)=@_; $self->{rotate}=0; $self->{pixbuf}=$pixbuf; delete $self->{pbanim}; Glib::Source->remove(delete $self->{anim_timeout}) if $self->{anim_timeout}; if ($pixbuf && $pixbuf->isa('Gtk2::Gdk::PixbufAnimation')) { $self->{pbanim}=$pixbuf; $self->animate; } if ($pixbuf) { my $dim= sprintf "%d x %d",$pixbuf->get_width,$pixbuf->get_height; my $file= ::basename($info{filename}); $file.= " (".sprintf(_"page %d",$info{page}).")" if $info{page}; my $size= ::format_number($info{size}).' '._"bytes"; $self->{info}= ::PangoEsc(sprintf "%s %s\n%s", $dim, $size, $file); } $self->resize; } sub animate { my $self=shift; my $anim= $self->{pbanim}; return 0 unless $anim; $self->invalidate_gdkwin; my $iter= $anim->{iter} ||= $anim->get_iter; $iter->advance; $self->{pixbuf}=$iter->get_pixbuf; my $ms= $iter->get_delay_time; $self->{anim_timeout}= Glib::Timeout->add($ms,\&animate,$self) if $ms>0; 0; } sub reset_zoom { my $self=shift; $self->{pixbuf}=undef; #force refresh $self->{fit}=1; } sub expose_cb { my ($self,$event)=@_; my $pixbuf= $self->{pixbuf}; return 1 unless $pixbuf; my $scale= $self->{scale}; unless ($scale) {$self->resize; return 1} my $pw= $pixbuf->get_width; my $ph= $pixbuf->get_height; my $x= $self->{x1} - $self->{offsetx}; my $y= $self->{y1} - $self->{offsety}; my $cr= Gtk2::Gdk::Cairo::Context->create($self->gdkwindow); $cr->rectangle($event->area); $cr->clip; $cr->save; $cr->translate($x,$y); $cr->scale($scale,$scale); if (my $angle=$self->{rotate}) { if ($angle %180){ $cr->translate(.5*$ph,.5*$pw); } else { $cr->translate(.5*$pw,.5*$ph); } $cr->rotate(::PI*$angle/180); $cr->translate(-.5*$pw,-.5*$ph); } if ($pixbuf->get_has_alpha) # if drawing a transparent image, fill with white first { $cr->set_source_rgb(1,1,1); $cr->rectangle(0,0,$pw,$ph); $cr->fill; } $cr->set_source_pixbuf($pixbuf,0,0); $cr->paint; if (my $msg=$self->{dnd_message}) # display a message when dragging a file/link/song above the picture { $cr->restore; $self->draw_overlay_text($cr,::PangoEsc($msg),.5,.5); } elsif ($self->{show_info} && defined $self->{info}) { $cr->restore; $self->draw_overlay_text($cr,$self->{info},.5,1); } 1; } sub draw_overlay_text { my ($self,$cr,$text,$x,$y)=@_; my $layout=Gtk2::Pango::Layout->new( $self->create_pango_context ); $layout->set_markup($text); my ($tw,$th)= map $_/Gtk2::Pango->scale, $layout->get_size; my ($w,$h)=$self->gdkwindow->get_size; my $pad=8; $x*= $w-$tw-$pad; $y*= $h-$th-$pad; $cr->set_source_rgba(0,0,0,.5); $cr->rectangle($x-$pad, $y-$pad, $tw+2*$pad, $th+2*$pad); $cr->fill; $cr->set_source_rgb(1,1,1); $cr->move_to($x,$y); Pango::Cairo::show_layout($cr,$layout); } sub resize { my $self=shift; my $gdkwin= $self->gdkwindow; return unless $gdkwin; my ($w,$h)=$gdkwin->get_size; my ($x,$y)= $self->no_window ? $gdkwin->get_position : (0,0); $self->invalidate_gdkwin; my $pixbuf= $self->{pixbuf}; return unless $pixbuf; my $pw= $pixbuf->get_width; my $ph= $pixbuf->get_height; if ($self->{rotate}%180) { ($pw,$ph)=($ph,$pw) } my $fit_scale= ::min($w/$pw, $h/$ph); my $scale= $self->{scale}||=1; if ($self->{fit}) { $scale=$fit_scale } elsif ($self->{fit_scale}) # multiply by new image ratio and divide by old image ratio { $scale*= $fit_scale/$self->{fit_scale}; # to keep the final size constant when changing image } $self->{fit_scale}= $fit_scale; $self->{scale}= $scale; $pw*=$scale; $ph*=$scale; my ($max_x,$max_y); if ($w>$pw) { $max_x= 0; $self->{x1}= $x+($w-$pw)*$self->{xalign}; $self->{x2}= $pw; } else { $max_x= $pw-$w; $self->{x1}= $x; $self->{x2}= $w; } if ($h>$ph) { $max_y= 0; $self->{y1}= $y+($h-$ph)*$self->{yalign}; $self->{y2}= $ph; } else { $max_y= $ph-$h; $self->{y1}= $y; $self->{y2}= $h; } $self->{max_x}=$max_x; $self->{max_y}=$max_y; $self->{offsetx}=$max_x if $self->{offsetx}>$max_x; $self->{offsety}=$max_y if $self->{offsety}>$max_y; } sub key_press_cb { my ($self,$event)=@_; my $key=Gtk2::Gdk->keyval_name( $event->keyval ); #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 (::WordIn($key,'plus KP_Add Home')) { $self->change_zoom('+'); } elsif (::WordIn($key,'minus KP_Subtract End')) { $self->change_zoom('-'); } elsif (::WordIn($key,'equal 1 KP_1')) { $self->change_zoom(1); } elsif (::WordIn($key,'Return KP_Enter')) { $self->set_zoom_fit; } elsif ($key eq 'Left') { $self->rotate(-1); } elsif ($key eq 'Right') { $self->rotate(1); } elsif (lc$key eq 'i') { $self->toggle_info; } elsif ($self->{oneshot}) { $self->get_toplevel->close_window; } # shortcuts before this point must only change zoom/orientation elsif (lc$key eq 'f') { $self->set_fullscreen } elsif ($key eq 'Escape' && $self->{fullwin}) { $self->set_fullscreen(0) } elsif (::WordIn($key,'Down space Page_Up')) { $self->change_picture(1); } elsif (::WordIn($key,'Up BackSpace Page_Down')) { $self->change_picture(-1); } else {return 0} return 1; } sub button_press_cb { my ($self,$event)=@_; $self->grab_focus; my $button=$event->button; if ($button == 3) { if ($self->{oneshot}) { $self->get_toplevel->close_window; return 1 } my @args= ( view=>$self, mode=> $self->{mode}, ); my $parent=$self; while ($parent=$parent->get_parent) { last if $parent->{view} && $parent->{view}==$self; } push @args,$parent->context_menu_args if $parent && $parent->can('context_menu_args'); ::PopupContextMenu( \@Layout::PictureBrowser::ContextMenu, {@args} ); } elsif ($button==9) { $self->change_picture(1); } elsif ($button==8) { $self->change_picture(-1);} elsif ($button!=1 && $event->type eq '2button-press') { $self->set_fullscreen } elsif (!$self->{pressed}) { ($self->{last_x},$self->{last_y})=$event->coords; $self->{pressed}=$button; } 1; } sub button_release_cb { my ($self,$event)=@_; my $button=$event->button; if (($self->{pressed}||0)==$button) { if ($button==1) { if ($self->{dragged}) { $event->window->set_cursor(undef) } elsif ($self->{oneshot}) { $self->get_toplevel->close_window; return } elsif (!$self->{scrolled}) { $self->change_picture(1); } } else { $self->set_zoom_fit unless $self->{zoomed} || $self->{prevnext}; if ($self->{zoomed}) { $event->window->set_cursor(undef) } } $self->{$_}= undef for qw/last_x last_y dragged pressed zoomed prevnext scrolled/; } else {return 0} 1; } sub motion_notify_cb { my ($self,$event)=@_; my $button= $self->{pressed}; return unless $button; my ($ex,$ey)=$event->coords; if ($button==1 && ($self->{max_x} || $self->{max_y})) { $event->window->set_cursor(Gtk2::Gdk::Cursor->new('fleur')) unless $self->{dragged}; $self->{dragged}=1; # move picture my $x= $self->{offsetx} + $self->{last_x} - $ex; my $y= $self->{offsety} + $self->{last_y} - $ey; $self->{offsetx}= ::Clamp($x,0,$self->{max_x}); $self->{offsety}= ::Clamp($y,0,$self->{max_y}); ($self->{last_x},$self->{last_y})= ($ex,$ey); $self->invalidate_gdkwin; # could be optimized, copy parts, not sure it's worth it } elsif ($button!=1 && !$self->{prevnext}) # zoom or prev/next with other button # only prev/next once by button press { my $zoom= int(($self->{last_y}-$ey)/20); # vertical movement >=20 my $next= int(($self->{last_x}-$ex)/30); # horizontal movement >=30 if ($zoom) { $zoom="+$zoom" if $zoom>0; $self->change_zoom($zoom,$ex,$ey); $self->{last_y}=$ey; $event->window->set_cursor(Gtk2::Gdk::Cursor->new('double_arrow')) unless $self->{zoomed}; $self->{zoomed}=1; } elsif ($next && !$self->{zoomed} && !$self->{oneshot}) { $self->change_picture($next>0 ? -1 : 1); $self->{last_x}=$ex; $self->{prevnext}=1; } } else {return 0} 1; } sub scroll_cb { my ($self,$event)=@_; my $d= $event->direction; 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']; my $updown; if ($d eq 'down') { $d=-1;$updown=1 } elsif ($d eq 'up') { $d=1; $updown=1 } elsif ($d eq 'right') { $d=1 } elsif ($d eq 'left') { $d=-1} else {return 0} my $button1= ($self->{pressed}||0)==1; $self->{scrolled}=1 if $button1; if ($updown && !$ctrl && ($shift || (!$self->{scroll_zoom} xor $button1))) #ctrl:zoom, shift:prev/next, else depend on scroll_zoom option (button1 inverts it) { $updown=0; $d*=-1; } if ($updown) { $self->change_zoom( ($d>0? '+':'-'), $event->x,$event->y); } else { $self->change_picture($d); } } sub change_picture { my ($self,$direction)=@_; if ($self->{oneshot}) { $self->get_toplevel->close_window; return } my $browser= ::find_ancestor($self,'Layout::PictureBrowser'); $browser->queue_change_picture($direction) if $browser; } sub change_zoom { my ($self,$zoom,$x,$y)=@_; return unless $self->{pixbuf}; if (defined $x) # translate event coordinates to image coordinates { $x= $x - $self->{x1}; $x= $self->{x2} if $x>$self->{x2}; $y= $y - $self->{y1}; $y= $self->{y2} if $y>$self->{y2}; } else # no zoom coordinates => zoom on center { $x= $self->{x2}/2; $y= $self->{y2}/2; } my $nx= $x+$self->{offsetx}; my $ny= $y+$self->{offsety}; my $s=$self->{scale}; $_/=$s for $nx,$ny; if ($zoom=~m/^\d*?\.?\d+$/) {$s=$zoom} else { my $change= $zoom=~m/^-(\d*)/ ? -.5 : .5; $change*=$1 if $1; if ($s==1) { if ($s+$change<1) {$s=1/($s-$change)} else {$s+=$change} } elsif ($s<1) { $s=1/$s; $s-=$change; $s=1/$s; $s=1 if $s>1; } else { $s+=$change; $s=1 if $s<1; } } $self->{scale}=$s; $self->{fit}=0; $self->resize; $self->{offsetx}= ::Clamp($s*$nx-$x,0,$self->{max_x}); $self->{offsety}= ::Clamp($s*$ny-$y,0,$self->{max_y}); } sub toggle_info { my $self=shift; $self->{show_info}^=1; $self->invalidate_gdkwin; } sub set_zoom_fit { my $self=shift; $self->{fit}=1; $self->resize; } sub rotate { my ($self,$rotate)=@_; my $r=$self->{rotate}||0; $r+= 90*$rotate; $self->{rotate}= $r % 360; $self->resize; } sub gdkwindow { $_[0]{fullwin} || $_[0]->window; } sub invalidate_gdkwin { my $gdkwin= $_[0]->gdkwindow; $gdkwin->invalidate_rect(Gtk2::Gdk::Rectangle->new(0,0,$gdkwin->get_size),0) if $gdkwin; } sub set_fullscreen { my ($self,$fullscreen)=@_; $fullscreen= !$self->{fullwin} if !defined $fullscreen; return unless $self->{fullwin} xor $fullscreen; if ($fullscreen) { my $screen=$self->get_screen; my $monitor=$screen->get_monitor_at_window($self->window); my (undef,undef,$monitorwidth,$monitorheight)= $screen->get_monitor_geometry($monitor)->values; my %attr= ( window_type => 'toplevel', x => 0, y => 0, width => $monitorwidth, height => $monitorheight, event_mask => [qw/exposure-mask pointer-motion-mask button-press-mask button-release-mask key-press-mask/], ); my $gdkwin= $self->{fullwin}= Gtk2::Gdk::Window->new(undef,\%attr); $gdkwin->set_user_data($self->window->get_user_data); $gdkwin->fullscreen; $gdkwin->show; $gdkwin->set_transient_for($self->window); $self->grab_focus; #make sure we have the focus } else { my $gdkwin= delete $self->{fullwin}; $gdkwin->set_user_data(0); #needed ? $gdkwin->destroy; } $self->{pressed}=undef; #reset mouse state, in particular when using double middle button to fullscreen, the mouse could stay in middle-button-pressed mode $self->resize; } package GMB::Context; sub new_follow_toolitem { my $self=shift; my $follow=Gtk2::ToggleToolButton->new_from_stock('gtk-jump-to'); $follow->set_active($self->{follow}); my $follow_text= $self->{group} eq 'Play' ? _"Follow playing song" : _"Follow selected song"; $follow->set_label($follow_text); $follow->set_tooltip_text($follow_text); $follow->signal_connect(clicked => \&ToggleFollow); ::set_drag($follow, dest => [::DRAG_ID,sub { my ($follow,$type,@IDs)=@_; my $self=::find_ancestor($_[0],'GMB::Context'); $self->SongChanged($IDs[0],1); }]); return $follow; } sub ToggleFollow { my $self=::find_ancestor($_[0],'GMB::Context'); $self->{follow}^=1; $self->SongChanged( ::GetSelID($self) ) if $self->{follow}; } package Stars; use base 'Gtk2::EventBox'; sub new_layout_widget { my $opt=shift; my $field= $opt->{field}; if (Songs::FieldType($field) ne 'rating') { warn "Stars: invalid field '$field'\n"; $field='rating'; } return Stars->new($field,0, \&set_rating_now_cb, %$opt); } sub update_layout_widget { my ($self,$ID)=@_; my $r= defined $ID ? Songs::Get($ID,$self->{field}) : 0; $self->set($r); } sub set_rating_now_cb { my $ID=::GetSelID($_[0]); return unless defined $ID; Songs::Set($ID, $_[0]{field} => $_[1]) } sub new { my ($class,$field,$nb,$sub, %opt) = @_; if (Songs::FieldType($field) ne 'rating') { warn "Stars: invalid field '$field'\n"; $field='rating'; } my $self = bless Gtk2::EventBox->new, $class; $self->set_visible_window(0); $self->{field}=$field; $self->{callback}=$sub; %opt=(xalign=>.5, yalign=>.5,%opt); my $image=$self->{image}=Gtk2::Image->new; $image->set_alignment($opt{xalign},$opt{yalign}); $self->add($image); $self->set($nb); $self->signal_connect(button_press_event => \&click); return $self; } sub callback { my ($self,$value)=@_; if (my $sub=$self->{callback}) {$sub->($self,$value);} else {$self->set($value)} } sub set { my ($self,$nb)=@_; $self->{nb}=$nb; $nb=$::Options{DefaultRating} if !defined $nb || $nb eq '' || $nb==255; $self->set_tooltip_text(_("Song rating")." : $nb %"); my $pixbuf= Songs::Stars($nb,$self->{field}); $self->{width}= $pixbuf->get_width; $self->{image}->set_from_pixbuf($pixbuf); } sub get { shift->{nb}; } sub click { my ($self,$event)=@_; if ($event->button == 3) { $self->popup($event); return 1 } my ($xalign)=$self->child->get_alignment; my $walloc= $self->allocation->width; my $width= $self->{width}; my ($x)=$event->coords; $x-= $xalign*($walloc-$width); $x/=$width; $x=0 if $x<0; $x=1 if $x>1; my $pb= $Songs::Def{$self->{field}}{pixbuf} || $Songs::Def{'rating'}{pixbuf}; my $nbstars=$#$pb; my $nb=1+int($x*$nbstars); $nb*=100/$nbstars; $self->callback($nb); return 1; } sub popup { my ($self,$event)=@_; my $menu=Gtk2::Menu->new; my $set=$self->{nb}; $set='' unless defined $set; my $sub=sub { $self->callback($_[1]); }; for my $nb (0,10,20,30,40,50,60,70,80,90,100,'') { my $item=Gtk2::CheckMenuItem->new( ($nb eq '' ? _"default" : $nb) ); $item->set_draw_as_radio(1); $item->set_active(1) if $set eq $nb; $item->signal_connect(activate => $sub, $nb); $menu->append($item); } ::PopupMenu($menu,event=>$event); } # not really part of Stars:: sub createmenu { my ($field,$IDs)=@_; if (Songs::FieldType($field) ne 'rating') { warn "Stars::createmenu : invalid field '$field'\n"; $field='rating'; } my $pixbufs= $Songs::Def{$field}{pixbuf} || $Songs::Def{rating}{pixbuf}; my $nbstars= $#$pixbufs; my %set; $set{$_}++ for Songs::Map($field,$IDs); my $set= (keys %set ==1) ? each %set : 'undef'; my $cb=sub { Songs::Set($IDs,$field => $_[1]); }; my $menu=Gtk2::Menu->new; for my $nb ('',0..$nbstars) { my $item=Gtk2::CheckMenuItem->new; my ($child,$rating)= $nb eq '' ? (Gtk2::Label->new(_"default"),'') : (Gtk2::Image->new_from_pixbuf($pixbufs->[$nb]),$nb*100/$nbstars); $item->add($child); $item->set_draw_as_radio(1); $item->set_active(1) if $set eq $rating; $item->signal_connect(activate => $cb, $rating); $menu->append($item); } return $menu; } package Layout::Progress; sub new { my ($class,$opt,$ref)=@_; my $self= $opt->{vertical} ? Gtk2::VBox->new : Gtk2::HBox->new; ::Watch($self,Progress=>\&update); update($self,$_,$::Progress{$_}) for keys %::Progress; $self->{lastclose}=$opt->{lastclose}; $self->{compact}= $opt->{compact}; return $self; } sub new_pid { my ($self,$prop)=@_; my $hbox=Gtk2::HBox->new(0,2); my $vbox=Gtk2::VBox->new; my $label; my $bar=Gtk2::ProgressBar->new; $bar->set(ellipsize=>'end'); unless ($self->{compact}) { $label=Gtk2::Label->new; $label->set_alignment(0,.5); $vbox->pack_start($label,0,0,2); } $hbox->pack_start($bar,1,1,2); $vbox->pack_start($hbox,0,0,2); $self->pack_start($vbox,1,1,2); if (my $sub=$prop->{abortcb}) { my $hint= $prop->{aborthint} || _"Abort"; my $abort= ::NewIconButton('gtk-stop',undef,$sub,'none',$hint); $hbox->pack_end($abort,0,0,0); } $vbox->show_all; return [$vbox,$bar,$label]; } sub update { my ($self,$pid,$prop)=@_; unless ($prop) #task finished => remove widgets { my $widgets= delete $self->{pids}{$pid}; $self->remove( $widgets->[0] ) if $widgets; if ($self->{lastclose} && !($self->get_children)) { $self->get_toplevel->close_window; } return; } return if $prop->{widget}; my $widgets= $self->{pids}{$pid} ||= new_pid($self,$prop); my (undef,$bar,$label)=@$widgets; my $title=$prop->{title}; my $details=$prop->{details}; $details='' unless defined $details; my $bartext=$prop->{bartext}; if ($bartext) { my $c= $prop->{current}+1; $bartext=~s/\$current\b/$c/g; $bartext=~s/\$end\b/$prop->{end}/g; } $bartext .= ' '.$prop->{bartext_append} if $prop->{bartext_append}; if ($self->{compact}) { $bartext=$title.' ... '.(defined $bartext ? $bartext : ''); $bar->set_tooltip_text($details) if $details; } else { my $format= '%s'; $format.= "\n%s" if $details; $label->set_markup_with_format( $format, $title, $details ); } $bar->set_fraction( $prop->{fraction} ); $bar->set_text( $bartext ) if defined $bartext; } package Layout::EqualizerPresets; use base 'Gtk2::Box'; use constant SEPARATOR => ' '; # must not be a possible name of a preset, use " " because EqualizerPresets won't let you create names that contain only spaces sub new { my ($class,$opt)=@_; my $self= bless Gtk2::HBox->new, $class; my $editmode= $self->{editmode}= $opt->{editmode} ? 1 : 0; $self->{open}= $opt->{open} ? 1 : -1; $self->{onoff}= $opt->{onoff}||0; $self->{turnoff}= $self->{onoff}>1 ? 1 : -1; my $mainbox= $self->{mainbox}= Gtk2::HBox->new; my $combo= $self->{combo}= Gtk2::ComboBox->new_text; $combo->signal_connect(changed=> \&combo_changed_cb); $combo->set_row_separator_func(sub { my $text=$_[0]->get_value($_[1]); defined $text && $text eq SEPARATOR}); my $turnon= $self->{turnon}= Gtk2::Button->new(_"Turn equalizer on"); $turnon->signal_connect(clicked=> \&button_cb, 'turn_on'); unless ($opt->{notoggle}) { my $toggle= $self->{toggle}= Gtk2::ToggleButton->new; $toggle->add(Gtk2::Image->new_from_stock('gtk-edit','menu')); $toggle->set_tooltip_text(_"Toggle edit mode"); $toggle->set_active(1) if $editmode; $toggle->signal_connect(toggled=>\&button_cb,'toggle_mode'); $mainbox->pack_start($toggle,0,0,0); } $mainbox->pack_start($combo,0,0,0); if (!$opt->{notoggle} || $editmode) { my $editbox= $self->{editbox}= Gtk2::HBox->new; my $entry = $self->{entry}= Gtk2::Entry->new; my $sbutton= $self->{sbutton}= ::NewIconButton('gtk-save', _"Save"); my $rbutton= $self->{rbutton}= ::NewIconButton('gtk-delete'); my $completion= Gtk2::EntryCompletion->new; $completion->set_model($combo->get_model); $completion->set_text_column(0); $entry->set_completion($completion); $entry->signal_connect(changed=> \&button_cb, 'entry'); $sbutton->signal_connect(clicked=> \&button_cb, 'save'); $rbutton->signal_connect(clicked=> \&button_cb, 'delete'); $sbutton->set_tooltip_text(_"Save preset"); $rbutton->set_tooltip_text(_"Delete preset"); $entry->set_tooltip_text(_"Save as..."); $editbox->pack_start($_,0,0,0) for $entry,$sbutton,$rbutton; $mainbox->pack_start($editbox,0,0,0); $editbox->show_all; $editbox->set_no_show_all(1); $editbox->set_visible($editmode); } for my $w ($turnon,$mainbox) { $self->pack_start($w,0,0,0); $w->show_all; $w->set_no_show_all(1); } return $self; } sub combo_changed_cb { my $self= ::find_ancestor($_[0],__PACKAGE__); return if $self->{busy}; my $current= $self->{combo}->get_active_text; my $index= $self->{combo}->get_active; my $event=Gtk2->get_current_event; if ($index>$self->{lastpreset}) # if an action is selected { my $action= $event->isa('Gtk2::Gdk::Event::Button'); if ($event->isa('Gtk2::Gdk::Event::Key')) { my $key= Gtk2::Gdk->keyval_name( $event->keyval ); $action=1 if grep $key eq $_, qw/space Return KP_Enter/; } #only execute actions if clicked with the mouse, or choose in the popup with the keyboard, not by scrolling if (!$action) { $self->update('preset'); #reset the combobox to the current preset } elsif ($self->{open} == $index) { ::OpenSpecialWindow('Equalizer'); $self->update; } elsif ($self->{turnoff}== $index) { ::SetEqualizer(active=>0); } return } ::SetEqualizer(preset=>$current) if $current ne ''; } sub button_cb { my $self= ::find_ancestor($_[0],__PACKAGE__); my $action= $_[1]; if ($action eq 'save') { ::SetEqualizer(preset_save=> $self->{entry}->get_text ); } elsif ($action eq 'delete') { ::SetEqualizer(preset_delete=> $self->{combo}->get_active_text ); } elsif ($action eq 'toggle_mode') { $self->{editmode}= $self->{toggle}->get_active ? 1 : 0; $self->{editbox}->set_visible($self->{editmode}); } elsif ($action eq 'entry') { $self->update_buttons; } elsif ($action eq 'turn_on') { ::SetEqualizer(active=>1); } } sub update { my ($self,$event)=@_; my $ok= $::Play_package->{EQ} && $::Options{use_equalizer}; if (!$::Play_package->{EQ}) { $self->{mainbox}->hide; $self->{turnon}->hide; } elsif ($self->{onoff}>0) { $self->{mainbox}->set_visible($ok); $self->{turnon}->set_visible(!$ok); } else { $self->{mainbox}->set_sensitive($ok); $self->{mainbox}->show; $self->{turnon}->hide; } my $full= !$event || $event eq 'package' || $event eq 'presetlist'; #first time or package changed or preset_list changed return unless $full || $event eq 'preset'; my $set= $::Options{equalizer_preset}; $set='' unless defined $set; my $changed= $set eq ''; # not a saved preset $full=1 if $changed xor $self->{changed}; $full=1 if !$changed && !exists $self->{presets}{$set}; $self->{changed}= $changed; my $combo= $self->{combo}; $self->{busy}=1; if ($full) # (re)fill the list { $combo->get_model->clear; delete $self->{presets}; my $i=0; if ($set eq '') { $combo->append_text(''); $combo->set_active($i); $i++; } for my $name (::GetPresets()) { $combo->append_text($name); $self->{presets}{$name}=$i; $combo->set_active($i) if $name eq $set; $i++; } # add actions $self->{lastpreset}= $i-1; if ($self->{open}>0 || $self->{turnoff}>0) { $combo->append_text(SEPARATOR); $i++; if ($self->{open}>0) { $combo->append_text(_"Open equalizer..."); $self->{open}=$i++; } if ($self->{turnoff}>0) { $combo->append_text(_"Turn equalizer off"); $self->{turnoff}=$i++; } } } elsif ($set ne '') { $combo->set_active( $self->{presets}{$set} ); } $self->{entry}->set_text($set) if $set ne '' && $self->{entry}; $self->{busy}=0; $self->update_buttons; } sub update_buttons { my $self=shift; if ($self->{entry}) { my $new= $self->{entry}->get_text; my $ok= $new=~m/\S/ && ($self->{changed} || !exists $self->{presets}{$new}); $self->{sbutton}->set_sensitive($ok); my $current= $self->{combo}->get_active_text; $self->{rbutton}->set_sensitive(defined $current && exists $self->{presets}{$current}); } } package Layout::Equalizer; sub new { my $opt=$_[0]; my $self=Gtk2::HBox->new(1,0); #homogenous $self->{labels}= $opt->{labels}; $self->{labels}=undef if $self->{labels} eq 'none'; if ($opt->{preamp}) { my $adj=Gtk2::Adjustment->new(1, 0, 2, .05, .1,0); my $scale=Gtk2::VScale->new($adj); $scale->set_draw_value(0); $scale->set_inverted(1); $scale->add_mark(1,'left',undef); $self->{adj_preamp}=$adj; $adj->signal_connect(value_changed => sub { ::SetEqualizer(preamp=>$_[0]->get_value) unless $_[0]{busy}; }); if ($self->{labels}) { my $vbox=Gtk2::VBox->new; my $label0=Gtk2::Label->new; $vbox->pack_start($label0,0,0,0); $self->{Valuelabel_preamp}=$label0; $vbox->add($scale); my $label1=Gtk2::Label->new; $label1->set_markup_with_format(qq(%s), $self->{labels},_"pre-amp"); $vbox->pack_start($label1,0,0,0); $scale=$vbox; } $self->{preamp_widget}=$scale; $self->pack_start($scale,1,1,2); $self->pack_start(Gtk2::HBox->new(0,0),1,1,2); #empty space } for my $i (0..9) { my $adj=Gtk2::Adjustment->new(0, -1, 1, .05, .1,0); my $scale=Gtk2::VScale->new($adj); $scale->set_draw_value(0); $scale->set_inverted(1); $scale->add_mark(0,'left',undef); $self->{'adj'.$i}=$adj; $adj->signal_connect(value_changed => sub { ::SetEqualizer($_[1],$_[0]->get_value) unless $_[0]{busy}; },$i); if ($self->{labels}) { my $vbox=Gtk2::VBox->new; my $label0=Gtk2::Label->new; $vbox->pack_start($label0,0,0,0); $self->{'Valuelabel'.$i}=$label0; $vbox->add($scale); my $label1=Gtk2::Label->new; $vbox->pack_start($label1,0,0,0); $self->{'Hzlabel'.$i}=$label1; $scale=$vbox; } $self->pack_start($scale,1,1,2); } return $self; } sub update { my ($self,$event)=@_; my $doall= !$event || $event eq 'package'; if ($doall || $event eq 'active') { my $ok= $::Play_package->{EQ} && $::Options{use_equalizer}; $self->set_sensitive($ok); $self->{preamp_widget}->set_sensitive( $ok && $::Play_package->{EQpre} ) if $self->{preamp_widget}; } if ($doall && $self->{labels}) { my ($min,$max,$unit)= $::Play_package->{EQ} ? $::Play_package->EQ_Get_Range : (-12,12,''); my $inc=abs($max-$min)/10; $unit=' '.$unit if $unit; $self->{unit}= $unit; for my $i (0..9) { if ($self->{labels}) { my $val='-'; $val= $::Play_package->EQ_Get_Hz($i)||'?' if $::Play_package->{EQ}; $self->{'Hzlabel'.$i}->set_markup_with_format(qq(%s), $self->{labels},$val); } my $adj=$self->{'adj'.$i}; $adj->{busy}=1; $adj->lower($min); $adj->upper($max); $adj->step_increment($inc/10); $adj->page_increment($inc); delete $adj->{busy}; } $self->queue_draw; } if ($doall || $event eq 'values') { my @val= split /:/, $::Options{equalizer}; for my $i (0..9) { my $val=$val[$i]; my $adj=$self->{'adj'.$i}; $adj->{busy}=1; $adj->set_value($val); delete $adj->{busy}; next unless $self->{labels}; $self->{'Valuelabel'.$i}->set_markup_with_format(qq(%.1f%s), $self->{labels},$val,$self->{unit}); } } if (($doall || $event eq 'preamp') && (my $adj= $self->{adj_preamp})) { my $val= $::Options{equalizer_preamp}; $adj->{busy}=1; $adj->set_value($val); delete $adj->{busy}; $self->{Valuelabel_preamp}->set_markup_with_format(qq(%d%%), $self->{labels},($val**3)*100) if $self->{Valuelabel_preamp}; } } # SVBox and SHBox are Gtk2::VBox and Gtk2::HBox with a smarter size_allocate function : the normal boxes divide the extra space equally among children with the expand mode. With these boxes the extra space can be allocated to children up to a ratio of their other dimension (expand_to_ratio), or according to a weight (expand_weight) package SVBox; use Glib::Object::Subclass Gtk2::VBox::, signals => { size_allocate => \&SHBox::size_allocate, }; package SHBox; use Glib::Object::Subclass Gtk2::HBox::, signals => { size_allocate => \&size_allocate, }; sub size_allocate { my ($self,$alloc)=@_; my $vertical= $self->isa('Gtk2::VBox'); my $max_key= $vertical ? 'maxheight' : 'maxwidth'; my ($x,$y,$bwidth,$bheight)=$alloc->values; my $olda=$self->allocation; $olda->x($x); $olda->y($y); $olda->width($bwidth); $olda->height($bheight); ($y,$x,$bheight,$bwidth)=($x,$y,$bwidth,$bheight) if $vertical; my $border=$self->get_border_width; $x+=$border; $bwidth-=$border*2; $y+=$border; $bheight-=$border*2; my $total_xreq=0; my $weightsum=my $ecount=0; my $spacing=$self->get_spacing; my @children; for my $child ($self->get_children) { next unless $child->visible; my $xreq=$vertical ? $child->size_request->height : $child->size_request->width; my ($expand,$fill,$pad,$type)= ($Gtk2::VERSION<1.163 || $Gtk2::VERSION==1.170) ? #work around memory leak in query_child_packing (gnome bug #498334) @{$child->{SBOX_packoptions}} : $self->query_child_packing($child); $total_xreq+=$pad*2+$xreq; my $eweight= $child->{expand_weight} || 1; my $max; my @attrib; if (my $r=$child->{expand_to_ratio}) {$max=$r*$bheight} else {$max=$child->{$max_key}} if ($max) { $max-=$xreq; if ($max>0) { $expand=$eweight; } else { $expand=$max=0; } } if ($expand) { $ecount++; $weightsum+=$eweight; $expand=$eweight; } my $end= $type eq 'end'; @attrib= ($child,$expand,$fill,$pad,$end,$xreq,$max); if ($end) {unshift @children,\@attrib} #to keep the same order as a HBox else {push @children,\@attrib} } my $total_spacing=0; if (@children>1) { $total_spacing= $#children*$spacing; if ($bwidth<$total_spacing) { $total_spacing=$bwidth; $spacing=$total_spacing/$#children } $total_xreq+= $total_spacing; } my $xend=$x+$bwidth; my $homogeneous; if ($self->get_homogeneous && @children) { $homogeneous=($bwidth-$total_spacing)/@children; } elsif ($total_xreq<$bwidth && $ecount) # if enough room for all, and some have expand attribute { my $w=$bwidth-$total_xreq; my $i=0; while ($ecount && $w>$ecount) { my $part= $w/$weightsum; # [1] is expand, [6] is max, [7] is extra space given for my $child (grep $_->[1], @children) #children that want to expand { my $max= $child->[6]; my $expand= $child->[1]; my $wpart= int($part*$expand); if ($max && $wpart>=$max) # enough to fill its max { $child->[7]+= $max; # give it its max $w-= $max; $child->[1]=0; # no longer want to expand $weightsum-=$expand; $ecount--; } elsif ($wpart>0) # give it its part { $child->[7]+= $wpart; $child->[6]-= $wpart if $max; $w-= $wpart; } } } if ($w>0 && $ecount) # less than one pixel by widget left { for my $child (sort {$b->[1] <=> $a->[1] } grep $_->[1], @children) # start with highest weight { $child->[7]++; # give 1 pixel to each widget last unless --$w; # until no more pixels } # note that weightsum, ecount, max and expand are not updated here as they are no longer used } } elsif ($total_xreq>$bwidth && @children) #not enough room for requested width { my $w=$bwidth-$total_spacing; # [5] is request [3] is padding my @tofit= sort { $a->[5]+$a->[3]*2 <=> $b->[5]+$b->[3]*2 } @children; # sort from smallest to largest while (my $child= shift @tofit) { my $give= int($w/(1+@tofit)); # space available for each child left $give=0 if $give<0; my $padding=$child->[3]*2; my $needed= $child->[5]+$padding; if ($give >= $needed) # need less than available { $give= $needed; # give it its request } elsif ($give >= $padding) # space available more than padding { $child->[5]= $give - $padding; # reduce request to fit } else # not even enough space for padding { $child->[5]=0; # set request to 0 $child->[3]= $give/2; # give as much padding as available } $w-= $give; # remove given space } } for my $ref (@children) { my ($child,undef,$fill,$pad,$end,$ww,undef,$extra)=@$ref; my $wwf= $ww; $wwf+=$extra if $extra; # space given by expand if (defined $homogeneous) { $wwf=$homogeneous-$pad*2; $wwf=0 if $wwf<0; $ww=$wwf if $ww>$wwf; } $ww=$wwf if $fill; my $wx; my $totalw=$pad*2+$wwf+$spacing; $pad+=($wwf-$ww)/2; if ($end) { $wx=$xend-$pad-$ww; $xend-=$totalw; } else { $wx=$x+$pad; $x+=$totalw; } my $wa= $vertical ? Gtk2::Gdk::Rectangle->new($y, $wx, $bheight, $ww): Gtk2::Gdk::Rectangle->new($wx, $y, $ww, $bheight); $child->size_allocate($wa); } } package Skin; sub new { my ($class,$string,$widget,$options)=@_; my $self=bless {},$class; ($self->{file},$self->{crop},$self->{resize},my$states1,my$states2)=split /:/,$string; my %states; my @states2= $states2 ? (map $_.'_', split '_',$states2) : (''); $states1||='normal'; my $n=0; for my $s (@states2) { $states{$s.$_}=$n++ for split '_',$states1; } $self->{states}=\%states; $self->{skin_options}{$_}=$options->{$_} for qw/PATH SkinPath SkinFile/; my $pb=$self->makepixbuf($states2[0].'normal'); return undef unless $pb; $self->{minwidth}=my $w=$pb->get_width; $self->{minheight}=my $h=$pb->get_height; $widget->set_size_request($w,$h) if $widget; return $self; } sub draw { my ($widget,$event,$self,$x,$y,$w,$h)=@_; return 0 unless $self; unless ($h) { ($x,$y,$w,$h)=$widget->allocation->values; $x=$y=0 unless $widget->no_window; #x and y are relative to the parent window, so are only useful if the widget use the parent window } my $state1=$widget->state; my $state2=$widget->{state}; my $state='normal'; if (my $states=$self->{states}) { my @l= ($state1,'normal'); if ($state2) { $state2=&$state2; unshift @l, $state2.'_'.$state1, $state2.'_normal'; } for (@l) { if (exists $states->{$_}) { $state=$_; last } } } my $pb=$self->{pb}{$state}; if ($pb && $self->{resize}) { $pb=undef if $pb->get_width != $w || $pb->get_height != $h; } $pb ||= $self->makepixbuf($state,$w,$h); return 0 unless $pb; my $pbw=$pb->get_width; my $pbh=$pb->get_height; $x+=int ($w-$pbw)/2; $y+=int ($h-$pbh)/2; my $style=$widget->get_style; my $gc=Gtk2::Gdk::GC->new($widget->window); $gc->set_clip_rectangle($event->area); $widget->window->draw_pixbuf($gc,$pb,0,0,$x,$y,$pbw,$pbh,'none',0,0); $style->paint_focus($widget->window, $state1, $event->area, $widget, undef, $x,$y,$pbw,$pbh) if $widget->has_focus; if ($widget->{shape}) #not sure it's a good idea { #my (undef,$mask)=$pb->render_pixmap_and_mask(1); #leaks X memory for Gtk2 <1.146 or <1.153 my $mask=Gtk2::Gdk::Pixmap->new(undef,$pbw,$pbh,1); $pb->render_threshold_alpha($mask,0,0,0,0,-1,-1,1); $widget->parent->shape_combine_mask($mask,$x,$y); } 1; } sub _load_skinfile { my ($file,$crop,$options)=@_; my $pb; if (ref $file) { $pb=$file if ref $file eq 'Gtk2::Gdk::Pixbuf'; } else { $options||={}; $file ||= $options->{SkinFile}; if ($file) { my $path= $options->{SkinPath}; $file= $path.::SLASH.$file if defined $path; $file= ::SearchPicture($file, $options->{PATH}); $pb= GMB::Picture::pixbuf($file) if $file; } } return unless $pb; if ($crop) { my @dim=split '_',$crop; if (@dim==4) { $pb=$pb->new_subpixbuf(@dim) } } return $pb; } sub makepixbuf { my ($self,$state,$w,$h)=@_; my $pb=_load_skinfile($self->{file},$self->{crop},$self->{skin_options}); return undef unless $pb; if (my $states=$self->{states}) { my $w= $pb->get_width / keys %$states; my $x= $states->{$state}*$w; $pb=$pb->new_subpixbuf($x,0,$w,$pb->get_height); } my $resize=$self->{resize}; if ($resize && $h) { $pb=_resize($pb,$resize,$w,$h); } return $self->{pb}{$state}=$pb; } sub _resize { my ($src,$opt,$width,$height)=@_; my $w= $src->get_width; my $h= $src->get_height; if ($opt eq 'ratio') { my $r=$w/$h; my $w2=int(($height||0)*$r); my $h2=int(($width ||0)/$r); if ($height && $h2>$height) {$width =$w2} else {$height=$h2} return undef unless $height && $width; return $src->scale_simple($width,$height,'hyper'); #or bilinear ? } my ($s_c)= $opt=~m/^([es])/; my ($s_t,$top)= $opt=~m/t([es])(\d+)/; my ($s_b,$bottom)= $opt=~m/b([es])(\d+)/; my ($s_l,$left)= $opt=~m/l([es])(\d+)/; my ($s_r,$right)= $opt=~m/r([es])(\d+)/; if ($opt=~m/v([es])/) { $s_c=$1; $width =$w;} #$s_t=$1; $top =$h; ? elsif ($opt=~m/h([es])/) { $s_c=$1; $height=$h;} #$s_l=$1; $left=$w; ? my $wi=$w -($left||=0) -($right||=0); my $dwi=$width -$left -$right; my $hi=$h -($top||=0) -($bottom||=0); my $dhi=$height -$top -$bottom; my $dest=Gtk2::Gdk::Pixbuf->new($src->get_colorspace, $src->get_has_alpha, $src->get_bits_per_sample, $width, $height); #4 corners $src->copy_area(0,0, $left,$top, $dest, 0,0) if $left && $top; $src->copy_area($w-$right,0, $right,$top, $dest, $width-$right,0) if $right && $top; $src->copy_area(0,$h-$bottom, $left,$bottom, $dest, 0,$height-$bottom) if $left && $bottom; $src->copy_area($w-$right,$h-$bottom, $right,$bottom, $dest, $width-$right,$height-$bottom) if $right && $bottom; my @parts; # borders : top, bottom, left, right push @parts, [$left,0, $wi,$top, $left,0, $dwi,$top, $s_t] if $top; push @parts, [$left,$h-$bottom, $wi,$bottom, $left,$height-$bottom, $dwi,$bottom, $s_b] if $bottom; push @parts, [0,$top, $left,$hi, 0,$top, $left,$dhi, $s_l] if $left; push @parts, [$w-$right,$top, $right,$hi, $width-$right,$top, $right,$dhi, $s_r] if $right; #center push @parts, [$left,$top, $wi,$hi, $left,$top, $dwi,$dhi, $s_c] if $hi && $wi; for my $ref (@parts) { my ($x,$y,$w,$h,$x2,$y2,$w2,$h2,$s)=@$ref; my $subp=$src->new_subpixbuf($x,$y,$w,$h); if ($s eq 's') #stretch { $subp=$subp->scale_simple($w2, $h2, 'hyper'); $subp->copy_area(0,0, $w2,$h2, $dest, $x2,$y2 ); } else #repeat to cover the area { my $w1=$w; for my $x3 (map $w*$_, 0..int($w2/$w)+1) { $w1=$w2-$x3 if $x3+$w1>$w2; next unless $w1; my $h1=$h; for my $y3 (map $h*$_, 0..int($h2/$h)+1) { $h1=$h2-$y3 if $y3+$h1>$h2; next unless $h1; $subp->copy_area(0,0, $w1,$h1, $dest, $x2+$x3,$y2+$y3 ); } } } } return $dest; } 1; gmusicbrowser-1.1.15~ds0.orig/gmusicbrowser_tags.pm0000664000175000017500000023476612565212604021746 0ustar unit193unit193# Copyright (C) 2005-2015 Quentin Sculo # # 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 BEGIN { require 'oggheader.pm'; require 'mp3header.pm'; require 'flacheader.pm'; require 'mpcheader.pm'; require 'apeheader.pm'; require 'wvheader.pm'; require 'm4aheader.pm'; } use strict; use warnings; use utf8; package FileTag; our %FORMATS; INIT { %FORMATS= # module format string tags to look for (order is important) ( mp3 => ['Tag::MP3', 'mp3 l{layer}v{versionid}', 'ID3v2 APE lyrics3v2 ID3v1',], oga => ['Tag::OGG', 'vorbis v{version}', 'vorbis',], flac => ['Tag::Flac', 'flac', 'vorbis',], mpc => ['Tag::MPC', 'mpc v{version}', 'APE ID3v2 lyrics3v2 ID3v1',], ape => ['Tag::APEfile', 'ape v{version}', 'APE ID3v2 lyrics3v2 ID3v1',], wv => ['Tag::WVfile', 'wv v{version}', 'APE ID3v1',], m4a => ['Tag::M4A', 'mp4 {traktype}', 'ilst',], ); $FORMATS{$_}=$FORMATS{ $::Alias_ext{$_} } for keys %::Alias_ext; } sub Read { my ($file,$findlength,$fieldlist)=@_; return unless $file=~m/\.([^.]+)$/; warn "Reading tags for $file".($findlength ? " findlength=$findlength" :'').($fieldlist ? " fieldlist=$fieldlist" :'')."\n" if $::debug; my $format=$FORMATS{lc $1}; return unless $format; my ($package,$formatstring,$plist)=@$format; my $filetag= eval { $package->new($file,$findlength); }; #filelength==1 -> may return estimated length (mp3 only) unless ($filetag) { warn $@ if $@; warn "Can't read tags for $file\n"; return } ::setlocale(::LC_NUMERIC, 'C'); my @taglist; my %values; #results will be put in %values if (my $info=$filetag->{info}) #audio properties { if ($findlength!=1 && $info->{estimated}) { delete $info->{$_} for qw/seconds bitrate estimated/; } $formatstring=~s/{(\w+)}/$info->{$1}/g; $values{filetype}=$formatstring; for my $f (grep $Songs::Def{$_}{audioinfo}, @Songs::Fields) { for my $key (split /\|/,$Songs::Def{$f}{audioinfo}) { my $v=$info->{$key}; if (defined $v) {$values{$f}=$v; last} } } } for my $tag (split / /,$plist) { if ($tag eq 'vorbis' || $tag eq 'ilst') { push @taglist, $tag => $filetag; } elsif ($filetag->{$tag}) { push @taglist, lc($tag) => $filetag->{$tag}; if ($tag eq 'ID3v2' && $filetag->{ID3v2s}) { push @taglist, id3v2 => $_ for @{ $filetag->{ID3v2s} }; } } } my @fields= $fieldlist ? split /\s+/, $fieldlist : grep $Songs::Def{$_}{flags}=~m/r/, @Songs::Fields; for my $field (@fields) { for (my $i=0; $i<$#taglist; $i+=2) { my $id=$taglist[$i]; #$id is type of tag : id3v1 id3v2 ape vorbis lyrics3v2 ilst my $tag=$taglist[$i+1]; my $value; my $def=$Songs::Def{$field}; if (defined(my $keys=$def->{$id})) #generic cases { my $joinwith= $def->{join_with}; my $split=$def->{read_split}; my $join= $def->{flags}=~m/l/ || defined $joinwith; for my $key (split /\s*[|&]\s*/,$keys) { if ($key=~m#%i#) { my $userid= $def->{userid}; next unless defined $userid && length $userid; $key=~s#%i#$userid#; } my $func='postread'; $func.=":$1" if $key=~s/^(\w+)\(\s*([^)]+?)\s*\)$/$2/; #for tag-specific postread function my $fpms_id; $fpms_id=$1 if $key=~m/FMPS_/ && $key=~s/::(.+)$//; my @v= $tag->get_values($key); next unless @v; if (defined $fpms_id) { @v= (FMPS_hash_read($v[0],$fpms_id)); next unless @v; } if (my $sub= $def->{$func}||$def->{postread}) { @v= map $sub->($_,$id,$key,$field), @v; next unless @v; } if ($join) { push @$value, grep defined, @v; } else { $value= $v[0]; last; } } next unless defined $value; if (defined $joinwith) { $value= join $joinwith,@$value; } elsif (defined $split) { $value= [map split($split,$_), @$value]; } } elsif (my $sub=$def->{"$id:read"}) #special cases with custom function { $values{$field}= $sub->($tag); last; } if (defined $value) { $values{$field}=$value; last } } } ::setlocale(::LC_NUMERIC, ''); return \%values; } sub Write { my ($file,$modif,$errorsub)=@_; warn "FileTag::Write($file,[@$modif],$errorsub)\n" if $::debug; my ($format)= $file=~m/\.([^.]*)$/; return unless $format and $format=$FileTag::FORMATS{lc$format}; ::setlocale(::LC_NUMERIC, 'C'); my $tag= $format->[0]->new($file); unless ($tag) {warn "can't read tags for $file\n";return } my ($maintag)=split / /,$format->[2],2; if (($maintag eq 'ID3v2' && !$::Options{TAG_id3v1_noautocreate}) || $tag->{ID3v1}) { my $id3v1 = $tag->{ID3v1} ||= $tag->new_ID3v1; my $i=0; while ($i<$#$modif) { my $field=$modif->[$i++]; my $val= $modif->[$i++]; my $n=$Songs::Def{$field}{id3v1}; next unless defined $n; $id3v1->[$n]= $val; # for genres $val is a arrayref } } my @taglist; if ($maintag eq 'ID3v2' || $tag->{ID3v2}) { my @id3tags= ($tag->{ID3v2} || $tag->new_ID3v2); push @id3tags, @{$tag->{ID3v2s}} if $tag->{ID3v2s}; for my $id3tag (@id3tags) { my ($ver)= $id3tag->{version}=~m/^(\d+)/; push @taglist, ["id3v2.$ver",'id3v2'], $id3tag; } } if ($maintag eq 'vorbis' || $maintag eq 'ilst') { push @taglist, $maintag,$tag; } if ($maintag eq 'APE' || $tag->{APE}) { my $ape = $tag->{APE} || $tag->new_APE; push @taglist, 'ape', $ape; } while (@taglist) { my ($id,$tag)=splice @taglist,0,2; my @ids= (ref $id ? @$id : ($id)); unshift @ids, map "$_:write", @ids; my $i=0; while ($i<$#$modif) { my $field=$modif->[$i++]; my $vals= $modif->[$i++]; $vals=[$vals] unless ref $vals; my $def=$Songs::Def{$field}; my ($keys)= grep defined, map $def->{$_}, @ids; next unless defined $keys; if (ref $keys) # custom ":write" functions { my @todo=$keys->($vals); while (@todo) { my ($key,$val)=splice @todo,0,2; if (defined $val) { $tag->insert($key,$val) } else { $tag->remove_all($key) } } next; } my $userid= $def->{userid}; my ($wkey,@keys)= split /\s*\|\s*/,$keys; my $toremove= @keys; #these keys will be removed push @keys, split /\s*&\s*/, $wkey; #these keys will be updated (first one and ones separated by &) for my $key (@keys) { if ($key=~m/%i/) { next unless defined $userid && length $userid; $key=~s#%i#$userid#g } my $func='prewrite'; $func.=":$1" if $key=~s/^(\w+)\(\s*([^)]+?)\s*\)$/$2/; #for tag-specific prewrite function "function( TAG )" my $sub= $def->{$func} || $def->{'prewrite'}; my @v= @$vals; if ($toremove-- >0) { @v=(); } #remove "deprecated" keys elsif ($sub) { @v= map $sub->($_,$ids[-1],$key,$field), @v; } if ($key=~m/FMPS_/ && $key=~s/::(.+)$//) # FMPS list field such as FMPS_Rating_User { my $v= FMPS_hash_write( $tag, $key, $1, $v[0] ); @v= $v eq '' ? () : ($v); } $tag->remove_all($key); $tag->insert($key,$_) for reverse grep defined, @v; } } } $tag->{errorsub}=$errorsub; $tag->write_file unless $::CmdLine{ro} || $::CmdLine{rotags}; ::setlocale(::LC_NUMERIC, ''); return 1; } sub FMPS_string_to_hash { my $vlist=shift; my %h; for my $pair (split /;;/, $vlist) { my ($key,$value)= split /::/,$pair,2; s#\\([;:\\])#$1#g for $key,$value; $h{$key}=$value; } return \%h; } sub FMPS_hash_to_string { my $h=shift; my @list; for my $key (sort keys %$h) { my $v=$h->{$key}; s#([;:\\])#\\$1#g for $key,$v; push @list, $key.'::'.$v; } return join ';;',@list; } sub FMPS_hash_read { my ($vlist,$id)=@_; return unless $vlist; my $h= FMPS_string_to_hash($vlist); my $v=$h->{$id}; return defined $v ? ($v) : (); } sub FMPS_hash_write { my ($tag,$key,$id,$value)=@_; my ($vlist)= $tag->get_values($key); my $h= FMPS_string_to_hash( $vlist||'' ); if (defined $value) { $h->{$id}=$value; } else { delete $h->{$id}; } return FMPS_hash_to_string($h); } sub PixFromMusicFile { my ($file,$nb,$quiet,$return_number)=@_; if ($file=~s/:(\w+)$//) {$nb=$1} # index can be specified as argument or in the filename my ($h)=Read($file,0,'embedded_pictures'); return unless $h; my $pix= $h->{embedded_pictures}; unless ($pix && @$pix) {warn "no picture found in $file\n" unless $quiet;return;} #FIXME filter out mimetype of "-->" (link) ? return ref $pix->[0] ? (map $pix->[$_][3],0..$#$pix) : @$pix if wantarray; if (!defined $nb) { $nb=0 } elsif ($nb=~m/\D/) { if (ref $pix->[0]) #for APIC structures { my $apic_id= $Songs::Def{$nb} && $Songs::Def{$nb}{apic_id}; if ($apic_id) { ($nb)= grep $pix->[$_][1]==$apic_id ,0..$#$pix; return unless defined $nb; } return unless defined $nb; } elsif ($nb eq 'album') { $nb=0 } else { return } } elsif ($nb>$#$pix) { $nb=0 } return $nb if $return_number; return ref $pix->[0] ? $pix->[$nb][3] : $pix->[$nb]; } sub GetLyrics { my $ID=shift; my $file= Songs::GetFullFilename($ID); my ($h)=Read($file,0,'embedded_lyrics'); return unless $h; my $lyrics= $h->{embedded_lyrics}; warn "no lyrics found in $file\n" unless $lyrics; return $lyrics; } sub WriteLyrics { my ($ID,$lyrics)=@_; Write($ID, [embedded_lyrics=>$lyrics], sub { my ($syserr,$details)= Error_Message(@_); return ::Retry_Dialog($syserr, _"Error writing lyrics", details=>$details, ID=>$ID); }); } #convert error details from tag writing to translated string with utf8 filenames sub Error_Message { my ($syserr,$type,$file)=@_; my $details= $type eq 'openwrite' ? ::__x(_"Error opening '{file}' for writing.",file=>::filename_to_utf8displayname($file)) : 'Unknown error'; #currently $type is always "openwrite" return $syserr,$details; } package MassTag; use constant { TRUE => 1, FALSE => 0, }; our @FORMATS; our @FORMATS_user; our @Tools; INIT { @Tools= ( { label=> _"Capitalize", for_all => sub { ucfirst lc $_[0]; }, }, { label=>_"Capitalize each word", for_all => sub { join '',map ucfirst lc, split /(\W+)/,$_[0]; }, }, ); @FORMATS= ( ['%a - %l - %n - %t', qr/(.+) - (.+) - (\d+) - (.+)$/], ['%a_-_%l_-_%n_-_%t', qr/(.+)_-_(.+)_-_(\d+)_-_(.+)$/], ['%n - %a - %l - %t', qr/(\d+) - (.+) - (.+) - (.+)$/], ['(%a) - %l - %n - %t', qr/\((.+)\) - (.+) - (\d+) - (.+)$/], ['%a - %l - %n-%t', qr/(.+) - (.+) - (\d+)-(.+)$/], ['%a-%l-%n-%t', qr/(.+)-(.+)-(\d+)-(.+)$/], ['%a - %l-%n. %t', qr/(.+) - (.+)-(\d+). (.+)$/], ['%l - %n - %t', qr/([^-]+) - (\d+) - (.+)$/], ['%a - %n - %t', qr/([^-]+) - (\d+) - (.+)$/], ['%n - %l - %t', qr/(\d+) - (.+) - (.+)$/], ['%n - %a - %t', qr/(\d+) - (.+) - (.+)$/], ['(%n) %a - %t', qr/\((\d+)\) (.+) - (.+)$/], ['%n-%a-%t', qr/(\d+)-(.+)-(.+)$/], ['%n %a %t', qr/(\d+) (.+) (.+)$/], ['%a - %n %t', qr/(.+) - (\d+) ([^-].+)$/], ['%l - %n %t', qr/(.+) - (\d+) ([^-].+)$/], ['%n - %t', qr/(\d+) - (.+)$/], ['%d%n - %t', qr/(\d)(\d\d) - (.+)$/], ['%n_-_%t', qr/(\d+)_-_(.+)$/], ['(%n) %t', qr/\((\d+)\) (.+)$/], ['%n_%t', qr/(\d+)_(.+)$/], ['%n-%t', qr/(\d+)-(.+)$/], ['%d%n-%t', qr/(\d)(\d\d)-(.+)$/], ['%d-%n-%t', qr/(\d)-(\d+)-(.+)$/], ['cd%d-%n-%t', qr/cd(\d+)-(\d+)-(.+)$/i], ['Disc %d - %n - %t', qr/Disc (\d+) - (\d+) - (.+)$/i], ['%n %t - %a - %l', qr/(\d+) (.+) - (.+) - (.+)$/], ['%n %t - %l - %a', qr/(\d+) (.+) - (.+) - (.+)$/], ['%n. %a - %t', qr/(\d+)\. (.+) - (.+)$/], ['%n. %t', qr/(\d+)\. (.+)$/], ['%n %t', qr/(\d+) ([^-].+)$/], ['Track%n', qr/[Tt]rack ?-? ?(\d+)/], ['%n', qr/^(\d+)$/], ['%a - %t', qr/(\D.+) - (.+)$/], ['%n - %a,%t', qr/(\d+) - (.+?),(.+)$/], #['TEST : %a %n %t',qr/(.+)(?: *|_)\W(?: *|_)(\d+)(?: *|_)\W(?: *|_)(.+)/], #['TEST : %n %t',qr/(\d+)(?: *|_)\W(?: *|_)(.+)/], ); # my %swap=(a => 'l', l => 'a',); # my @tmp; # for my $ref (@FORMATS) # { my ($f,$re)=@$ref; # push @tmp,$ref; # if ($f=~s/%([al])/%$swap{$1}/g) { push @tmp,[$f,$re] } # } # @FORMATS=@tmp; } use base 'Gtk2::Box'; sub new { my ($class,@IDs) = @_; @IDs= ::uniq(@IDs); my $self = bless Gtk2::VBox->new, $class; my $table=Gtk2::Table->new (6, 2, FALSE); my $row1=my $row2=0; my %widgets; $self->{widgets}=\%widgets; $self->{pf_widgets}={}; $self->{IDs}=\@IDs; # folder name at the top { my $folders= Songs::UniqList('path',\@IDs); my $folder=$folders->[0]; my $displaysub= Songs::DisplayFromHash_sub('path'); if (@$folders>1) { my $common= ::find_common_parent_folder(@$folders); $folder=_"different folders"; $folder.= "\n". ::__x(_"(common parent folder : {common})",common=> $displaysub->($common) ) if length($common)>5; } my $text= ::__n("%d file in {folder}","%d files in {folder}",scalar@IDs); $text= ::__x($text, folder => ::MarkupFormat('%s', $displaysub->($folder) ) ); my $labelfile = Gtk2::Label->new; $labelfile->set_markup($text); $labelfile->set_selectable(TRUE); $labelfile->set_line_wrap(TRUE); $self->pack_start($labelfile, FALSE, TRUE, 2); } for my $field ( Songs::EditFields('many') ) { my $check=Gtk2::CheckButton->new(Songs::FieldName($field)); my $widget=Songs::EditWidget($field,'many',\@IDs); next unless $widget; $widgets{$field}=$widget; $check->{widget}=$widget; $widget->set_sensitive(FALSE); $check->signal_connect( toggled => sub { my $check=shift; $check->{widget}->set_sensitive( $check->get_active ); }); my ($row,$col)= $widget->{noexpand} ? ($row2++,2) : ($row1++,0); $table->attach($check,$col++,$col,$row,$row+1,'fill','shrink',3,1); $table->attach($widget,$col++,$col,$row,$row+1,['fill','expand'],'shrink',3,1); } my $vpaned= $self->{vpaned}=Gtk2::VPaned->new; $self->add($vpaned); my $sw=Gtk2::ScrolledWindow->new; $sw->set_shadow_type('none'); $sw->set_policy('never', 'automatic'); $sw->add_with_viewport($table); $sw->show_all; $sw->set_size_request(-1,$table->size_request->height); $vpaned->pack1($sw,FALSE,TRUE); # do not add per-file part if LOTS of songs, building the GUI would be too long anyway $self->add_per_file_part unless @IDs>1000; $self->set_size_request(-1,400); #to allow resizing the window to a small height in spite of the height request of $sw return $self; } # for edition of file-specific tags (track title ...) sub add_per_file_part { my $self=shift; my $IDs=$self->{IDs}; Songs::SortList($IDs,'path album:i disc track file'); my $perfile_table=Gtk2::Table->new( scalar(@$IDs), 10, FALSE); $self->{perfile_table}=$perfile_table; my $row=0; $self->add_column('track'); $self->add_column('title'); my $lastcol=1; #for the filename column my $BSelFields=Gtk2::Button->new(_"Select fields"); { my $menu=Gtk2::Menu->new; my $menu_cb=sub {$self->add_column($_[1])}; for my $f ( Songs::EditFields('per_id') ) { my $item=Gtk2::CheckMenuItem->new_with_label( Songs::FieldName($f) ); $item->set_active(1) if $self->{'pfcheck_'.$f}; $item->signal_connect(activate => $menu_cb,$f); $menu->append($item); $lastcol++; } #$menu->append(Gtk2::SeparatorMenuItem->new); #my $item=Gtk2::CheckMenuItem->new(_"Select files"); #$item->signal_connect(activate => sub { $self->add_selectfile_column }); #$menu->append($item); $BSelFields->signal_connect( button_press_event => sub { ::PopupMenu($menu,event=>$_[1]); }); #$self->pack_start($menubar, FALSE, FALSE, 2); #$perfile_table->attach($menubar,7,8,0,1,'fill','shrink',1,1); } #add filename column $perfile_table->attach( Gtk2::Label->new(Songs::FieldName('file')) ,$lastcol,$lastcol+1,$row,$row+1,'fill','shrink',1,1); for my $ID (@$IDs) { $row++; my $label=Gtk2::Label->new( Songs::Display($ID,'file') ); $label->set_selectable(TRUE); $label->set_alignment(0,0.5); #left-aligned $perfile_table->attach($label,$lastcol,$lastcol+1,$row,$row+1,'fill','shrink',1,1); #filename } my $Btools=Gtk2::Button->new(_"tools"); { my $menu=Gtk2::Menu->new; my $menu_cb=sub {$self->tool($_[1])}; for my $ref (@Tools) #currently only able to transform all entrys with the for_all function { my $item=Gtk2::MenuItem->new($ref->{label}); $item->signal_connect(activate => $menu_cb,$ref->{for_all}); $menu->append($item) if $ref->{for_all}; } $Btools->signal_connect( button_press_event => sub { ::PopupMenu($menu,event=>$_[1]); }); } my $BClear=::NewIconButton('gtk-clear',undef, sub { my $self=::find_ancestor($_[0],__PACKAGE__); $self->tool(sub {''}) }, undef,_"Clear selected fields"); my $sw = Gtk2::ScrolledWindow->new; $sw->set_shadow_type('none'); $sw->set_policy('automatic', 'automatic'); $sw->add_with_viewport($perfile_table); # expander to hide/show the per-file part my $exp_label=Gtk2::Label->new_with_format("%s",_"Per-song values"); my $expander=Gtk2::Expander->new; $expander->set_expanded(TRUE); $expander->set_label_widget($exp_label); $expander->signal_connect(activate=>sub { my $on= !$_[0]->get_expanded; $_->set_visible($on) for $sw,$BSelFields; }); $self->{vpaned}->pack2( ::Vpack('compact',[$expander,$BSelFields],'_',$sw), TRUE,FALSE); my $vsizegroup=Gtk2::SizeGroup->new('vertical'); $vsizegroup->add_widget($_) for $exp_label,$BSelFields; # so that they are aligned $sw->set_size_request(-1,$exp_label->size_request->height); # so that the expander is always visible my $store= Gtk2::ListStore->new('Glib::String','Glib::Scalar'); $self->{autofill_combo}= my $Bautofill=Gtk2::ComboBox->new($store); my $renderer=Gtk2::CellRendererText->new; $Bautofill->pack_start($renderer,::TRUE); $Bautofill->add_attribute($renderer, markup => 0); $self->autofill_check; $Bautofill->signal_connect(changed => \&autofill_cb); ::Watch( $self, AutofillFormats => \&autofill_check); my $checkOBlank=Gtk2::CheckButton->new(_"Auto fill only blank fields"); $self->{AFOBlank}=$checkOBlank; my $hbox=Gtk2::HBox->new; $hbox->pack_start($_, FALSE, FALSE, 0) for Gtk2::VSeparator->new,$Bautofill,$BClear,$checkOBlank,$Btools, $self->pack_start($hbox, FALSE, FALSE, 4); } sub add_column { my ($self,$field)=@_; if ($self->{'pfcheck_'.$field}) #if already created -> toggle show/hide { my @w=( $self->{'pfcheck_'.$field}, @{ $self->{pf_widgets}{$field} } ); my $show= !$w[0]->visible; $_->set_visible($show) for @w; return; } my $table=$self->{perfile_table}; my $col=++$table->{col}; my $row=0; my $check=Gtk2::CheckButton->new( Songs::FieldName($field) ); my @entries; $self->{'pfcheck_'.$field}=$check; $self->{pf_widgets}{$field}=\@entries; for my $ID ( @{$self->{IDs}} ) { $row++; my $widget=Songs::EditWidget($field,'per_id',$ID); next unless $widget; $widget->set_sensitive(FALSE); $widget->signal_connect(focus_in_event=> \&scroll_to_entry); my $p= $widget->{noexpand} ? 'fill' : ['fill','expand']; $table->attach($widget,$col,$col+1,$row,$row+1,$p,'shrink',1,1); $widget->show_all; push @entries,$widget; } $check->signal_connect( toggled => sub { my $active=$_[0]->get_active; $_->set_sensitive($active) for @entries; }); # add auto-increment/auto-complete button to track/disc/year columns if ($field eq 'track' || $field eq 'disc' || $field eq 'year') { #$_->set_alignment(1) for @entries; my ($increment,$tip)= $field eq 'track' ? (1,_"Auto-increment track numbers") : (0,_"Copy missing values from previous line"); my $autosub=sub { my $i= $field ne 'year' ? 1 : 0; for my $e (@entries) { my $here=$e->get_text; if ($here && $here=~m/^\d+$/) { $i=$here; } elsif ($i>0) { $e->set_text($i) } $i++ if $increment; } }; my $button=::NewIconButton('gtk-go-down',undef,$autosub,'none',$tip); $button->set_border_width(0); $button->set_size_request(); $check->signal_connect( toggled => sub { $button->set_sensitive($_[0]->get_active) }); $button->set_sensitive(FALSE); my $hbox=Gtk2::HBox->new(0,0); $hbox->pack_start($_,0,0,0) for $check,$button; $check=$hbox; #$check= ::Hpack($check,$button); } $check->show_all; $table->attach($check,$col,$col+1,0,1,'fill','shrink',1,1); } sub add_selectfile_column { my $self=$_[0]; if (my $l=$self->{'filetoggles'}) #if already created -> toggle show/hide { my $show= !$l->[0]->visible; $_->set_visible($show) for @$l; return; } my @toggles; $self->{'filetoggles'}=\@toggles; my $table=$self->{perfile_table}; my $row=0; my $col=0; my $i=0; for my $ID ( @{$self->{IDs}} ) { $row++; my $check=Gtk2::CheckButton->new; $check->set_active(1); $check->signal_connect( toggled => sub { my ($check,$i)=@_; my $self=::find_ancestor($check,__PACKAGE__); my $active=$check->get_active; $self->{pf_widgets}{$_}[$i]->set_sensitive($active) for keys %{ $self->{pf_widgets} } },$i); #$widget->signal_connect(focus_in_event=> \&scroll_to_entry); $table->attach($check,$col,$col+1,$row,$row+1,'fill','shrink',1,1); $check->show_all; push @toggles,$check; $i++; } } sub scroll_to_entry { my $ent=$_[0]; if (my $sw=::find_ancestor($ent,'Gtk2::Viewport')) { my ($x,$y,$w,$h)= $ent->allocation->values; $sw->get_hadjustment->clamp_page($x,$x+$w); $sw->get_vadjustment->clamp_page($y,$y+$h); }; 0; } sub autofill_check { my $self=shift; my $combo=$self->{autofill_combo}; my $store=$combo->get_model; $store->clear; $store->set( $store->append, 0, ::PangoEsc(_"Auto fill based on filenames ...")); my @files= map ::filename_to_utf8displayname($_), Songs::Map('barefilename',$self->{IDs}); autofill_user_formats(); for my $ref (@FORMATS_user,@FORMATS) { my ($format,$re)=@$ref; next if @files/2 > (grep m/$re/, @files); # ignore patterns that match less than half of the filenames my $formatname= ''.::PangoEsc($format).''; $formatname= GMB::Edit::Autofill_formats::make_format_name($formatname,"%s"); $store->set($store->append, 0,$formatname, 1, $ref); } $store->set( $store->append, 0, ::PangoEsc(_"Edit auto-fill formats ..."), 1, \&GMB::Edit::Autofill_formats::new); $combo->set_active(0); } sub autofill_user_formats { my $h= $::Options{filename2tags_formats}; return if !$h || @FORMATS_user; for my $format (sort keys %$h) { my $re= $h->{$format}; if (!defined $re) { $re= GMB::Edit::Autofill_formats::make_default_re($format); } my $qr=eval { qr/$re/i; }; if ($@) { warn "Error compiling regular expression for '$format' : $re\n$@"; next} push @FORMATS_user, [$format,$qr]; } } sub autofill_cb { my $combo=shift; my $self=::find_ancestor($combo,__PACKAGE__); my $iter=$combo->get_active_iter; return unless $iter; my $ref=$combo->get_model->get($iter,1); return unless $ref; if (ref $ref eq 'CODE') { $ref->($self); return; } # for edition of filename formats my ($format,$pattern)=@$ref; my @fields= GMB::Edit::Autofill_formats::find_fields($format); $_ eq 'album_artist' and $_='album_artist_raw' for @fields; #FIXME find a more generic way to do that my $OBlank=$self->{AFOBlank}->get_active; my @vals; for my $ID (@{$self->{IDs}}) { my $file= Songs::Display($ID,'barefilename'); my @v=($file=~m/$pattern/); s/_/ /g, s/^\s+//, s/\s+$// for @v; @v=('')x scalar(@fields) unless @v; my $n=0; push @{$vals[$n++]},$_ for @v; } for my $f (@fields) { my $varray=shift @vals; my %h; $h{$_}=undef for @$varray; delete $h{''}; if ( (keys %h)==1 ) { my $entry=$self->{widgets}{$f}; if ($entry && $entry->is_sensitive) { next if $OBlank && !($entry->can('is_blank') ? $entry->is_blank : $entry->get_text eq ''); $entry->set_text(keys %h); next } } my $entries= $self->{pf_widgets}{$f}; next unless $entries; for my $e (@$entries) { my $v=shift @$varray; next if $OBlank && !($e->can('is_blank') ? $e->is_blank : $e->get_text eq ''); $e->set_text($v) if $e->is_sensitive && $v ne ''; } } } sub tool { my ($self,$sub)=@_; #my $OBlank=$self->{AFOBlank}->get_active; #$OBlank=0 if $ignoreOB; my $IDs=$self->{IDs}; for my $wdgt ( values %{$self->{widgets}}, map @$_, values %{$self->{pf_widgets}} ) { next unless $wdgt->is_sensitive && $wdgt->can('tool'); $wdgt->tool($sub); } #for my $entries (values %{$self->{pf_widgets}}) #{ next unless $entries->[0]->is_sensitive && $entries->[0]->can('tool'); # for my $e (@$entries) # { $wdgt->tool($sub); # } #} } sub save { my ($self,$finishsub)=@_; my $IDs=$self->{IDs}; my (%default,@modif); while ( my ($f,$wdgt)=each %{$self->{widgets}} ) { next unless $wdgt->is_sensitive; if ($wdgt->can('return_setunset')) { my ($set,$unset)=$wdgt->return_setunset; push @modif,"+$f",$set if @$set; push @modif,"-$f",$unset if @$unset; } else { my $v=$wdgt->get_text; $default{$f}=$v; $f='@'.$f if ref $v; push @modif, $f,$v; } } while ( my ($f,$wdgt)=each %{$self->{pf_widgets}} ) { next unless $wdgt->[0]->is_sensitive; my @vals; for my $ID (@$IDs) { my $v=(shift @$wdgt)->get_text; $v=$default{$f} if $v eq '' && exists $default{$f}; push @vals,$v; } push @modif, '@'.$f,\@vals; } unless (@modif) { $finishsub->(); return} $self->set_sensitive(FALSE); my $progressbar = Gtk2::ProgressBar->new; $self->pack_start($progressbar, FALSE, TRUE, 0); $progressbar->show_all; Songs::Set($IDs,\@modif, progress=>$progressbar, callback_finish=>$finishsub, window=> $self->get_toplevel); } package GMB::Edit::Autofill_formats; use base 'Gtk2::Dialog'; our $Instance; our %Override; INIT { %Override= ('%A'=> '$album_artist_raw'); } sub new { my $ID= $_[0]{IDs}[0]; if ($Instance) { $Instance->force_present; $Instance->{ID}=$ID; $Instance->preview_update; return }; my $self = Gtk2::Dialog->new ("Custom auto-fill filename formats", undef, [], 'gtk-close' => 'none'); $Instance=bless $self,__PACKAGE__; ::SetWSize($self,'AutofillFormats'); $self->set_border_width(4); $self->{ID}=$ID; $self->{store}=my $store= Gtk2::ListStore->new('Glib::String','Glib::String'); $self->{treeview}=my $treeview=Gtk2::TreeView->new($store); $treeview->append_column( Gtk2::TreeViewColumn->new_with_attributes(_"Custom formats", Gtk2::CellRendererText->new, text => 0 )); #$treeview->set_headers_visible(::FALSE); $treeview->signal_connect(cursor_changed=> \&cursor_changed_cb); my $label_format=Gtk2::Label->new(_"Filename format :"); my $label_re= Gtk2::Label->new(_"Regular expression :"); $self->{entry_format}= my $entry_format=Gtk2::Entry->new; $self->{entry_re}= my $entry_re= Gtk2::Entry->new; $self->{check_re}= my $check_re= Gtk2::CheckButton->new(_"Use default regular expression"); $self->{error}= my $error= Gtk2::Label->new; $self->{preview}= my $preview= Gtk2::Label->new; $self->{remove_button}= my $button_del= ::NewIconButton('gtk-remove',_"Remove"); $self->{add_button}= my $button_add= ::NewIconButton('gtk-save',_"Save"); my $button_new= ::NewIconButton('gtk-new', _"New"); $button_del->signal_connect(clicked=>\&button_cb,'remove'); $button_add->signal_connect(clicked=>\&button_cb,'save'); $button_new->signal_connect(clicked=>\&button_cb,'new'); $preview->set_alignment(0,.5); my $sg=Gtk2::SizeGroup->new('horizontal'); $sg->add_widget($_) for $label_format,$label_re; my $bbox= Gtk2::HButtonBox->new; $bbox->add($_) for $button_del, $button_add, $button_new; my $sw= ::new_scrolledwindow($treeview,'etched-in'); $sw->set_size_request(150,-1); #give the list a minimum width my $table= ::MakeReplaceTable('taAlCyndgL', A=>Songs::FieldName('album_artist_raw')); #AutoFillFields my $hbox= ::Vpack([$label_format,'_',$entry_format],$table,$check_re,[$label_re,'_',$entry_re],$error,$preview,'-',$bbox); my $hpaned= Gtk2::HPaned->new; $hpaned->pack1($sw,1,1); $hpaned->pack2($hbox,1,0); $self->vbox->add($hpaned); ::set_drag($preview, dest => [::DRAG_ID,\&song_dropped]); $entry_format->signal_connect(changed=> \&entry_changed); $entry_re->signal_connect(changed=> \&preview_update); $check_re->signal_connect(toggled=> sub { $entry_re->set_sensitive(!$_[0]->get_active); entry_changed($_[0]); }); $check_re->set_active(1); $entry_re->set_sensitive(0); $self->entry_changed; $self->fill_store; $self->show_all; $self->signal_connect( response => sub { $_[0]->destroy; $Instance=undef; }); } sub song_dropped { my ($preview,$type,$ID)=@_; my $self= ::find_ancestor($preview,__PACKAGE__); $self->{ID}=$ID; $self->preview_update; } sub entry_changed { my $self= ::find_ancestor($_[0],__PACKAGE__); my $text= $self->{entry_format}->get_text; my $match= exists $::Options{filename2tags_formats}{$text}; $self->{remove_button}->set_sensitive($match); $self->{busy}=1; my $selection= $self->{treeview}->get_selection; $selection->unselect_all; if ($match) { my $store=$self->{store}; my $iter=$store->get_iter_first; while ($iter) { if ($store->get($iter,1) eq $text) { $selection->select_iter($iter); last; } $iter=$store->iter_next($iter); } } $self->{add_button}->set_sensitive( length $text ); if ($self->{check_re}->get_active) { $self->{entry_re}->set_text( make_default_re($text) ); } $self->{busy}=0; $self->preview_update; } sub preview_update { my $self= ::find_ancestor($_[0],__PACKAGE__); return if $self->{busy}; my $re=$self->{entry_re}->get_text; my $qr=eval { qr/$re/i; }; if ($@) { $self->{error}->show; $self->{error}->set_markup_with_format("%s",_"Invalid regular expression"); $self->{preview}->set_text(''); return; } my $format=$self->{entry_format}->get_text; my @fields= map Songs::FieldName($_), find_fields($format); my $ID=$self->{ID}; my $file= Songs::Display($ID,'barefilename'); my @text=(_"Example :", Songs::FieldName('file'), $file); my $preview= "%s\n%s : %s\n\n"; my @v; @v= ($file=~m/$qr/) if $re; if (@v || !$re) { $self->{error}->hide; $self->{error}->set_text(''); } else { $self->{error}->show; $self->{error}->set_markup_with_format("%s",_"Regular expression didn't match"); } s/_/ /g, s/^\s+//, s/\s+$// for @v; for my $i (sort { $fields[$a] cmp $fields[$b] } 0..$#fields) { my $v= $v[$i]; $v='' unless defined $v; push @text, $fields[$i],$v; $preview.= "%s : %s\n"; } $self->{preview}->set_markup_with_format($preview,@text); } sub button_cb { my ($button,$action)=@_; my $self= ::find_ancestor($button,__PACKAGE__); my $formats= $::Options{filename2tags_formats}; my $format= $self->{entry_format}->get_text; if ($action eq 'remove') { delete $formats->{$format}; } if ($action eq 'new' || $action eq 'remove') { $self->{check_re}->set_active(1); $self->{entry_format}->set_text(''); } else { $formats->{$format}= $self->{check_re}->get_active ? undef : $self->{entry_re}->get_text; } return if $action eq 'new'; $self->fill_store; @FORMATS_user=(); ::HasChanged('AutofillFormats'); } sub fill_store { my $self=shift; my $store=$self->{store}; $store->clear; my $formats= $::Options{filename2tags_formats} ||= {}; for my $format (sort keys %$formats) { my $formatname= make_format_name($format); $store->set($store->append, 0,$formatname, 1,$format); } $self->entry_changed; } sub make_format_name { my ($format,$markup)=@_; $format=~s#(\$\w+|%[a-zA-Z]|\$\{\w+\})|([%\$])\2# $2 || do { my $f= $::ReplaceFields{ $Override{$1}||$1 }; $f=undef if $f && $Songs::Def{$f}{flags}!~m/e/; $f&&= Songs::FieldName($f); $f&&= ::MarkupFormat($markup,$f) if $markup; $f || $1 }#ge; return $format; } sub find_fields { my $format=shift; my @fields= map $::ReplaceFields{$Override{$_}||$_}, grep defined, $format=~m/ %% | \$\$ | ( \$\w+ | %[a-zA-Z] | \$\{\w+\} ) /gx; @fields= grep defined && $Songs::Def{$_}{flags}=~m/e/, @fields; return @fields; } sub make_default_re { my $re=shift; $re=~s#(\$\w+|%[a-zA-Z]|\$\{\w+\})|%(%)|\$(\$)|(%?[-,;\w ]+)|(.)# $1 ? Songs::ReplaceFields_to_re( $Override{$1}||$1 ) : $2 ? $2 : $3 ? '\\'.$3 : defined $4 ? $4 : '\\'.$5 #ge; return $re; } sub cursor_changed_cb { my $treeview=shift; my $self=::find_ancestor($treeview,__PACKAGE__); return if $self->{busy}; my $path=($treeview->get_cursor)[0]; return unless $path; my $store=$treeview->get_model; my $format= $store->get( $store->get_iter($path), 1); my $re= $::Options{filename2tags_formats}{$format}; $self->{entry_format}->set_text($format); $self->{check_re}->set_active( !defined $re ); $self->{entry_re}->set_text($re) if defined $re; } package GMB::TagEdit::EntryString; use base 'Gtk2::Entry'; sub new { my ($class,$field,$ID,$width,$completion) = @_; my $self = bless Gtk2::Entry->new, $class; #$self->{field}=$field; my $val=Songs::Get($ID,$field); $self->set_text($val); GMB::ListStore::Field::setcompletion($self,$field) if $completion; if ($width) { $self->set_width_chars($width); $self->{noexpand}=1; } return $self; } sub tool { my ($self,$sub)=@_; my $val= $sub->($self->get_text); $self->set_text($val) if defined $val; } package GMB::TagEdit::EntryText; use base 'Gtk2::Box'; sub new { my ($class,$field,$IDs) = @_; my $self = bless Gtk2::VBox->new, $class; my $textview= $self->{textview}= Gtk2::TextView->new; $textview->set_size_request(100,($textview->create_pango_layout("X")->get_pixel_size)[1]*4); #request 4 lines of height my $sw= ::new_scrolledwindow($textview,'etched-in'); $self->add($sw); my $val; if (ref $IDs) { my $values= Songs::BuildHash($field,$IDs); my @l=sort { $values->{$b} <=> $values->{$a} } keys %$values; #sort values by their frequency $val=$l[0]; $self->{IDs}=$IDs; $self->{field}=$field; $self->{append}=my $append=Gtk2::CheckButton->new(_"Append (only if not already present)"); $self->pack_end($append,0,0,0); } else { $val=Songs::Get($IDs,$field); } $self->set_text($val); return $self; } sub set_text { my $self=shift; $self->{textview}->get_buffer->set_text(shift); } sub get_text { my $self=shift; my $buffer=$self->{textview}->get_buffer; my $text=$buffer->get_text( $buffer->get_bounds, 1); if ($self->{append} && $self->{append}->get_active) #append { my @orig= Songs::Map($self->{field},$self->{IDs}); for my $orig (@orig) { next if $text eq ''; if ($orig eq '') { $orig=$text; } else { next if index("$orig\n","$text\n")!=-1; #don't append if the line(s) already exists $orig.="\n".$text; } } return \@orig; } return $text; } sub tool { &GMB::TagEdit::EntryString::tool; } package GMB::TagEdit::EntryNumber; use base 'Gtk2::SpinButton'; sub new { my ($class,$field,$IDs,%opt) = @_; #possible options in %opt : signed digits min max mode my $mode=$opt{mode}||''; my $max= $opt{max} || 10000000; my $min= $opt{min} || ($opt{signed} ? -$max : 0); my $digits= $opt{digits} || 0; my $adj=Gtk2::Adjustment->new(0,$min,$max,1,10,0); my $self = bless Gtk2::SpinButton->new($adj,10,$digits), $class; $self->{noexpand}=1; #$self->{field}=$field; my $val; if (ref $IDs) { my $values= Songs::BuildHash($field,$IDs); my @l=sort { $values->{$b} <=> $values->{$a} } keys %$values; #sort values by their frequency $val=$l[0]; #take the most common value } else { $val=Songs::Get($IDs,$field); } if ($mode) { if ($mode eq 'nozero') # 0 is displayed as "" { $self->signal_connect(output=> \&output_nozero); } elsif ($mode eq 'allow_empty') # non-numeric values are replaced with "" which is treated as different than 0 { $self->signal_connect(input => sub { my $v=Gtk2::Entry::get_text($_[0]); $_[0]{null}= $v!~/\d/; return 0}); $self->signal_connect(output=> sub { my $v=$_[0]->get_value; $_[0]{null}=0 if $v; return 0 if !$_[0]{null}; Gtk2::Entry::set_text($_[0],''); return 1; }); } elsif ($mode eq 'year') { $self->set_wrap(1); # set to current year when increasing or decreasing value from 0 $self->signal_connect(value_changed=>sub { my $v=$_[0]->get_value; $_[0]->set_value( (localtime)[5]+1900 ) if $v==1 || $v>=$max; }); $self->signal_connect(output=> \&output_nozero); } } if ($mode eq 'allow_empty' && !length $val) { $self->{null}=1; $self->set_text(''); } else { $self->set_value($val); } return $self; } sub get_text { $_[0]{null} ? '' : $_[0]->get_value; } sub set_text { my $v=$_[1]; $v=0 unless $v=~m/^\d+$/; $_[0]->set_value($v); } sub is_blank { my $self=shift; return ! $self->get_value; } sub tool { &GMB::TagEdit::EntryString::tool; } sub output_nozero { my $v=$_[0]->get_value; return 0 if $v; Gtk2::Entry::set_text($_[0],''); return 1; } package GMB::TagEdit::EntryBoolean; use base 'Gtk2::CheckButton'; sub new { my ($class,$field,$IDs) = @_; my $self = bless Gtk2::CheckButton->new, $class; $self->{noexpand}=1; #$self->{field}=$field; my $val; if (ref $IDs) { my $values= Songs::BuildHash($field,$IDs); my @l=sort { $values->{$b} <=> $values->{$a} } keys %$values; #sort values by their frequency $val=$l[0]; #take the most common value } else { $val=Songs::Get($IDs,$field); } $self->set_active($val); return $self; } sub get_text { $_[0]->get_active; } package GMB::TagEdit::Combo; use base 'Gtk2::Box'; sub new { my ($class,$field,$IDs,$listall) = @_; my $self= bless Gtk2::HBox->new, $class; my $combo= Gtk2::ComboBoxEntry->new_text; $self->add($combo); $self->{combo}=$combo; my $entry=$self->{entry}=$combo->child; #$self->{field}=$field; my $values= Songs::BuildHash($field,$IDs); my @l=sort { $values->{$b} <=> $values->{$a} } keys %$values; #sort values by their frequency my $first=$l[0]; @l= @{ Songs::Gid_to_Get($field,\@l) } if Songs::Field_property($field,'gid_to_get'); if ($listall) { my $cb=sub { ::PopupAA(Songs::MainField($field),noalt=>1, cb=> sub { $entry->set_text( Songs::Gid_to_Get($field,$_[0]{key}) ); }); }; my $pick= ::NewIconButton('gtk-index',undef,$cb,'none',_"Pick an existing one"); $self->pack_end($pick,0,0,0); } $combo->append_text($_) for @l; $entry->set_text($l[0]) if $values->{$first} > @$IDs/3; GMB::ListStore::Field::setcompletion($entry,$field) if $listall; return $self; } sub set_text { $_[0]{entry}->set_text($_[1]); } sub get_text { $_[0]{entry}->get_text; } sub tool { &GMB::TagEdit::EntryString::tool; } package GMB::TagEdit::EntryRating; use base 'Gtk2::Box'; sub new { my ($class,$field,$IDs) = @_; my $self = bless Gtk2::HBox->new, $class; #$self->{field}=$field; my $init; if (ref $IDs) { my $h= Songs::BuildHash($field,$IDs); $init=(sort { $h->{$b} <=> $h->{$a} } keys %$h)[0]; } else { $init=Songs::Get($IDs,$field); } my $adj=Gtk2::Adjustment->new(0,0,100,10,20,0); my $spin=Gtk2::SpinButton->new($adj,10,0); my $check=Gtk2::CheckButton->new(_"use default"); my $stars=Stars->new($field,$init,\&update_cb); $self->pack_start($_,0,0,0) for $stars,$spin,$check; $self->{stars}=$stars; $self->{check}=$check; $self->{adj}=$adj; $self->update_cb($init); #$self->{modif}=0; $adj->signal_connect(value_changed => sub{ $self->update_cb($_[0]->get_value) }); $check->signal_connect(toggled => sub{ update_cb($_[0], ($_[0]->get_active ? '' : $::Options{DefaultRating}) ) }); return $self; } sub update_cb { my ($widget,$v)=@_; my $self=::find_ancestor($widget,__PACKAGE__); return if $self->{busy}; $self->{busy}=1; $v='' unless defined $v && $v ne '' && $v!=255; #$self->{modif}=1; $self->{value}=$v; $self->{check}->set_active($v eq ''); $self->{stars}->set($v); $v=$::Options{DefaultRating} if $v eq ''; $self->{adj}->set_value($v); $self->{busy}=0; } sub get_text { $_[0]->{value}; } sub is_blank { my $v=$_[0]->{value}; $v eq '' || $v==255; } package GMB::TagEdit::FlagList; use base 'Gtk2::Box'; sub new { my ($class,$field,$ID) = @_; my $self = bless Gtk2::HBox->new(0,0), $class; $self->{field}=$field; $self->{ID}=$ID; my $button= Gtk2::Button->new; my $add= ::NewIconButton('gtk-add'); my $entry= $self->{entry}= Gtk2::Entry->new; my $label=$self->{label}=Gtk2::Label->new; $label->set_ellipsize('end'); $entry->set_width_chars(12); $button->add($label); $self->pack_start($button,1,1,0); $self->pack_start($entry,0,0,0); $self->pack_start($add,0,0,0); $add->signal_connect( button_press_event => sub { add_entry_text_cb($_[0]); $_[0]->grab_focus;1; } ); $add->signal_connect( clicked => \&add_entry_text_cb ); $button->signal_connect( clicked => \&popup_menu_cb); $button->signal_connect( button_press_event => sub { popup_menu_cb($_[0]); $_[0]->grab_focus;1; } ); $entry->signal_connect( activate => \&add_entry_text_cb ); GMB::ListStore::Field::setcompletion($entry,$field); $self->{selected}{$_}=1 for Songs::Get_list($ID,$field); delete $self->{selected}{''}; $self->update; return $self; } sub add_entry_text_cb { my $widget=shift; my $self=::find_ancestor($widget,__PACKAGE__); my $entry=$self->{entry}; my $text=$entry->get_text; if ($text eq '') { $self->popup_add_menu($widget); return } # split $text ? $self->{selected}{$text}=1; $entry->set_text(''); $self->update; } sub popup_add_menu { my ($self,$widget)=@_; my $cb= sub { $self->{selected}{ $_[1] }= 1; $self->update; }; my $menu=::MakeFlagMenu($self->{field},$cb); ::PopupMenu($menu, posfunction=>sub {::windowpos($_[0],$widget)} ); } sub popup_menu_cb { my $widget=shift; my $self=::find_ancestor($widget,__PACKAGE__); my $menu=Gtk2::Menu->new; my $cb= sub { $self->{selected}{ $_[1] }^=1; $self->update; }; my @keys= ::superlc_sort(keys %{$self->{selected}}); return unless @keys; for my $key (@keys) { my $item=Gtk2::CheckMenuItem->new_with_label($key); $item->set_active(1) if $self->{selected}{$key}; $item->signal_connect(toggled => $cb,$key); $menu->append($item); } ::PopupMenu($menu); } sub update { my $self=$_[0]; my $h=$self->{selected}; my $text=join ', ', map ::PangoEsc($_), ::superlc_sort(grep $h->{$_}, keys %$h); #$text= ::MarkupFormat("- %s -",_"None") if $text eq ''; $self->{label}->set_markup($text); $self->{label}->parent->set_tooltip_markup($text); } sub get_text { my $self=shift; my $h=$self->{selected}; return [grep $h->{$_}, keys %$h]; } sub is_blank { my $self=shift; my $list= $self->get_text; return !(@$list); } sub set_text # for setting from autofill-from-filename { my ($self,$val)=@_; my @vals= grep $_ ne '', split /\s*[;,]\s*/, $val; # currently split on ; or , my $selected= $self->{selected}; $selected->{$_}=0 for keys %$selected; #remove all $selected->{$_}=1 for @vals; $self->update; } package GMB::TagEdit::EntryMassList; #for mass-editing fields with multiple values use base 'Gtk2::Box'; sub new { my ($class,$field,$IDs) = @_; my $self = bless Gtk2::VBox->new(1,1), $class; $self->{field}=$field; my $sg= Gtk2::SizeGroup->new('horizontal'); my $entry= $self->{entry}= Gtk2::Entry->new; my $add= ::NewIconButton('gtk-add'); my $removeall= ::NewIconButton('gtk-clear', _"Remove all", \&clear); $add->signal_connect( button_press_event => sub { add_entry_text_cb($_[0]); $_[0]->grab_focus;1; } ); $add->signal_connect( clicked => \&add_entry_text_cb ); for my $ref (['toadd',1,_"Add"],['toremove',-1,_"Remove"]) { my ($key,$mode,$text)=@$ref; my $label=$self->{$key}=Gtk2::Label->new; $label->set_ellipsize('end'); $label->{mode}=$mode; my $button= Gtk2::Button->new; $button->add($label); $button->{mode}=$mode; $button->signal_connect( clicked => \&popup_menu_cb ); $button->signal_connect( button_press_event => sub { popup_menu_cb($_[0]); $_[0]->grab_focus;1; } ); my $sidelabel= Gtk2::Label->new($text); my $hbox= Gtk2::HBox->new(0,1); $hbox->pack_start($sidelabel,0,0,2); $hbox->pack_start($button,1,1,2); $hbox->pack_start($entry,0,0,0) if $mode>0; $hbox->pack_start($add,0,0,0) if $mode>0; $hbox->pack_start($removeall,0,0,0) if $mode<0; $self->pack_start($hbox,0,0,2); $sidelabel->set_alignment(0,.5); $sg->add_widget($sidelabel); } GMB::ListStore::Field::setcompletion($entry,$field); $entry->signal_connect(activate => \&add_entry_text_cb); my $valueshash= Songs::BuildHash($field,$IDs); my %selected; $selected{ Songs::Gid_to_Get($field,$_) }= $valueshash->{$_}==@$IDs ? 1 : 0 for keys %$valueshash; delete $selected{''}; $self->{selected}=\%selected; $self->{all}= [keys %selected]; #all values that are set for at least one song $self->update; return $self; } sub update #update the text and tooltips of buttons { my $self=shift; for my $key (qw/toadd toremove/) { my $label= $self->{$key}; my $mode= $label->{mode}; # -1 or 1 my $h= $self->{selected}; my $text=join ', ', map ::PangoEsc($_), ::superlc_sort(grep $h->{$_}==$mode, keys %$h); #$text= ::MarkupFormat("- %s -",_"None") if $text eq ''; $label->set_markup($text); $label->parent->set_tooltip_markup($text); # set tooltip on button } } sub add_entry_text_cb { my $widget=shift; my $self=::find_ancestor($widget,__PACKAGE__); my $entry=$self->{entry}; my $text=$entry->get_text; if ($text eq '') { $self->popup_add_menu($widget); return } # split $text ? $self->{selected}{$text}=1; $entry->set_text(''); $self->update; } sub clear # set to -1 all values present in at least one song, set to 0 values not present { my $self=::find_ancestor($_[0],__PACKAGE__); my $h= $self->{selected}; $_=0 for values %$h; $h->{$_}=-1 for @{$self->{all}}; $self->update; } sub popup_add_menu { my ($self,$widget)=@_; my $cb= sub { $self->{selected}{ $_[1] }= 1; $self->update; }; my $menu=::MakeFlagMenu($self->{field},$cb); ::PopupMenu($menu, posfunction=>sub {::windowpos($_[0],$widget)} ); } sub popup_menu_cb { my $child=shift; my $mode=$child->{mode}; my $self=::find_ancestor($child,__PACKAGE__); my $h= $self->{selected}; my $menu=Gtk2::Menu->new; my $cb= sub { $self->{selected}{ $_[1] }= $_[0]->get_active ? $mode : 0; $self->update; }; my @keys= ::superlc_sort(keys %$h); return unless @keys; for my $key (@keys) { my $item=Gtk2::CheckMenuItem->new_with_label($key); $item->set_active(1) if $h->{$key}==$mode; $item->signal_connect(toggled => $cb,$key); $menu->append($item); } ::PopupMenu($menu); 1; } sub return_setunset { my $self=$_[0]; my (@set,@unset); my $h=$self->{selected}; for my $value (keys %$h) { my $mode=$h->{$value}; if ($mode>0) { push @set,$value } elsif ($mode<0) { push @unset,$value } } return \@set,\@unset; } sub is_blank {1} sub set_text # for setting from autofill-from-filename { my ($self,$val)=@_; my @vals= grep $_ ne '', split /\s*[;,]\s*/, $val; # currently split on ; or , my $selected= $self->{selected}; #$selected->{$_}=0 for keys %$selected; #remove all $selected->{$_}=1 for @vals; $self->update; } package EditTagSimple; use base 'Gtk2::Box'; use constant { TRUE => 1, FALSE => 0, }; sub new { my ($class,$ID) = @_; my $self = bless Gtk2::VBox->new, $class; $self->{ID}=$ID; my $labelfile = Gtk2::Label->new; $labelfile->set_markup( ::ReplaceFieldsAndEsc($ID,'%u') ); $labelfile->set_selectable(TRUE); $labelfile->set_line_wrap(TRUE); my $sw=Gtk2::ScrolledWindow->new; $sw->set_shadow_type('none'); $sw->set_policy('never', 'automatic'); my $table=Gtk2::Table->new (6, 2, FALSE); $sw->add_with_viewport($table); $self->{table}=$table; $self->fill; $self->pack_start($labelfile,FALSE,FALSE,1); $self->pack_start($sw, TRUE, TRUE, 2); return $self; } sub fill { my $self=$_[0]; my $table=$self->{table}; my $ID=$self->{ID}; my $row1=my $row2=0; for my $field ( Songs::EditFields('single') ) { my $widget=Songs::EditWidget($field,'single',$ID); next unless $widget; my ($row,$col)= $widget->{noexpand} ? ($row2++,2) : ($row1++,0); if (my $w=$self->{fields}{$field}) #refresh the fields { $table->remove($w); } else #first time { my $label=Gtk2::Label->new( Songs::FieldName($field) ); $table->attach($label,$col,$col+1,$row,$row+1,'fill','shrink',2,2); } $table->attach($widget,$col+1,$col+2,$row,$row+1,['fill','expand'],'shrink',2,2); $self->{fields}{$field}=$widget; } $table->show_all; } sub get_changes { my $self=shift; my @modif; while (my ($field,$entry)=each %{$self->{fields}}) { push @modif,$field,$entry->get_text; } return @modif; } package Edit_Embedded_Picture; use base 'Gtk2::Box'; sub new { my ($class,$ID) = @_; my $self = bless Gtk2::VBox->new, $class; $self->{ID}=$ID; $self->{store}= Gtk2::ListStore->new(qw/Glib::Uint Glib::String/); my $treeview= $self->{treeview}= Gtk2::TreeView->new($self->{store}); $treeview->insert_column_with_attributes(-1, "type",Gtk2::CellRendererText->new, text => 1); $treeview->set_headers_visible(0); $treeview->get_selection->signal_connect(changed => \&selection_changed_cb,$self); my $view= $self->{view}= Layout::PictureBrowser::View->new(context_menu_sub=>\&context_menu, xalign=> .5, yalign=>.5, scroll_zoom=>1,); ::set_drag($view, dest => [::DRAG_FILE, sub { my ($view,$type,$uri,@ignored_uris)=@_; my $self= ::find_ancestor($view,__PACKAGE__); if ($uri=~s#^file://##) { my $file= ::decode_url($uri); my $data= GMB::Picture::load_data($file); $self->drop_data(\$data) if $data; } else { $self->drop_uris(uris=>[$uri]); } }], motion=> sub { my ($view,$context,$x,$y,$time)=@_; $view->{dnd_message}= _"Set picture using this file"; 1; } ); $view->signal_connect(drag_leave => sub { delete $_[0]{dnd_message}; }); $self->signal_connect(destroy=> sub { my $self=shift; $self->{drop_job}->Abort if $self->{drop_job}; }); my $button_del= ::NewIconButton('gtk-remove', _"Remove picture"); my $button_set= ::NewIconButton('gtk-open',_"Set picture"); my $button_new= $self->{button_new}= ::NewIconButton('gtk-add', _"Add picture"); my $combo_type= $self->{combo_type}= Gtk2::ComboBox->new_text; my $entry_desc= $self->{entry_desc}= Gtk2::Entry->new; my $info_label= $self->{info_label}= Gtk2::Label->new; $entry_desc->set_tooltip_text(_"Description"); $combo_type->set_tooltip_text(_"Picture type"); $combo_type->append_text($_) for @$EntryMulti::PICTYPE; $button_new->signal_connect(clicked=>\&new_picture_cb); $button_del->signal_connect(clicked=>\&remove_selected_cb); $button_set->signal_connect(clicked=>\&set_picture_cb); $combo_type->signal_connect(changed=>\&type_change_cb); $entry_desc->signal_connect(changed=>\&desc_changed_cb); $self->signal_connect(key_press_event=> \&key_press_cb); my $hbox= ::Hpack( '_',['_',::new_scrolledwindow($treeview),$button_new], [$combo_type,$entry_desc,$button_set,$button_del] ); $self->{editbox}= $combo_type->parent; $self->pack_start($hbox, 0,0,2); $self->pack_start($view, 1,1,2); $self->pack_start($info_label, 0,0,2); $self->signal_connect(map=>sub {$_[0]->load unless $_[0]{loaded}}); return $self; } sub update { $_[0]->load if $_[0]{loaded}; } sub load { my $self=shift; $self->{changed}=0; $self->{loaded}=1; my $ID=$self->{ID}; my $file= Songs::GetFullFilename($ID); if ($file!~m/$::EmbImage_ext_re$/) { $self->set_sensitive(0); $self->{view}->drag_dest_unset; return } my ($h)= FileTag::Read($file,0,'embedded_pictures',0); $self->{pix}= $h && $h->{embedded_pictures}; if ($file=~m/\.(?:m4a|m4b)$/i) { $self->{m4a_mode}=1; #only 1 picture, type "front cover", no description $self->{$_}->set_sensitive(0) for qw/combo_type entry_desc/; $self->{pix}= [[undef,3,'',$self->{pix}[0]]] if $self->{pix}; } $self->fill; } sub fill { my ($self,$select)=@_; my $store= $self->{store}; $store->clear; my $pix= $self->{pix}; return unless $pix && @$pix; my $select_path; for my $nb (0..$#$pix) { next unless $pix->[$nb]; #skip deleted my $iter= $store->append; $store->set($iter, 0,$nb, 1,$self->make_row_text($nb)); $select=$nb unless defined $select; #select first by default if (defined $select && $select==$nb) { $select_path=$store->get_path($iter); } } if ($self->{m4a_mode}) { my $count= grep defined,@$pix; $self->{button_new}->set_sensitive($count==0); } if ($select_path) { $self->{treeview}->scroll_to_cell($select_path); $self->{treeview}->get_selection->select_path($select_path); } } sub make_row_text { my ($self,$nb)=@_; my ($mime,$typeid,$desc,$data)= @{$self->{pix}[$nb]}; my $text= $EntryMulti::PICTYPE->[$typeid] || _"Unknown"; if (defined $desc && length $desc) { $text.=": $desc" } return $text; } sub selection_changed_cb { my ($selection,$self)=@_; my ($store,$iter) = $selection->get_selected; unless ($iter) { $self->{entry_desc}->set_text(''); $self->{combo_type}->set_active(0); } $self->{editbox}->set_sensitive(!!$iter); $self->{info_label}->set_text(""); my ($pixbuf,%info); { last unless $iter; (my $nb,$info{filename})= $store->get($iter,0,1); my $apic= $self->{pix}[$nb]; my ($mime,$typeid,$desc,$data)= @$apic; $self->{entry_desc}->set_text($desc); $self->{combo_type}->set_active($typeid); last unless $data; $info{size}=length $data; my $loader= GMB::Picture::LoadPixData($data); last unless $loader; if ($Gtk2::VERSION >= 1.092) { my $h=$loader->get_format; $self->{pix}[$nb][0]= $h->{mime_types}[0]; } $pixbuf= $loader->get_pixbuf; my $size= ::format_number($info{size}/::KB(),"%.1f").' '._"KB"; my $dim= sprintf "%d x %d",$pixbuf->get_width,$pixbuf->get_height; $self->{info_label}->set_text("($dim) $size"); } $self->{view}->reset_zoom; $self->{view}->set_pixbuf($pixbuf,%info); } sub new_picture_cb { my $self= ::find_ancestor($_[0],__PACKAGE__); my $type=0; $type=3 unless grep $_ && $_->[1]==3, @{$self->{pix}}; # default to 3 (front cover) if no other picture of that type my $new= push @{$self->{pix}}, [undef,$type,'',undef]; $self->fill($new-1); } sub remove_selected_cb { my $self= ::find_ancestor($_[0],__PACKAGE__); my $nb= $self->get_selected; return unless defined $nb; $self->{changed}=1; $self->{pix}[$nb]=undef; ($nb)= grep $self->{pix}[$_],reverse 0..$nb-1; #select previous entry if any $self->fill($nb); } sub set_picture_cb { my $self= ::find_ancestor($_[0],__PACKAGE__); my $nb= $self->get_selected; return unless defined $nb; my $file=::ChoosePix(); return unless defined $file; $self->{changed}=1; my $data= GMB::Picture::load_data($file); $self->{pix}[$nb][3]=$data if $data; $self->fill($nb); } sub type_change_cb { my $combo=shift; my $self= ::find_ancestor($combo,__PACKAGE__); my $nb= $self->get_selected; return unless defined $nb; $self->{changed}=1; my $type= $combo->get_active; $self->{pix}[$nb][1]= $type; $self->refresh_selected; } sub desc_changed_cb { my $entry=shift; my $self= ::find_ancestor($entry,__PACKAGE__); my $nb= $self->get_selected; return unless defined $nb; $self->{changed}=1; $self->{pix}[$nb][2]= $entry->get_text; $self->refresh_selected; } sub get_selected { my $self=shift; my ($store,$iter) = $self->{treeview}->get_selection->get_selected; return unless $iter; return $store->get($iter,0); } sub refresh_selected { my $self=shift; my ($store,$iter) = $self->{treeview}->get_selection->get_selected; return unless $iter; my $nb=$store->get($iter,0); $store->set($iter, 1,$self->make_row_text($nb)); } sub drop_uris { my ($self,%args)=@_; $self->{drop_job}->Abort if $self->{drop_job}; $self->{drop_job}= GMB::DropURI->new(toplevel=>$self->get_toplevel, cb=>sub{$self->drop_data($_[0]); delete $self->{drop_job}; }); my $uri= $args{uris}[0]; #only take first one my $data; $self->{drop_job}->Add_URI(uris=>[$uri], destpath=>\$data); } sub drop_data { my ($self,$dataref)=@_; my $nb= $self->get_selected; unless (defined $nb) { $self->new_picture_cb; $nb= $self->get_selected; return unless defined $nb; } $self->{changed}=1; $self->{pix}[$nb][3]=$$dataref if $$dataref; $self->fill($nb); } sub context_menu_args { my $self=shift; return self=>$self, mode=>'P'; } sub key_press_cb { my ($self,$event)=@_; my $key=Gtk2::Gdk->keyval_name( $event->keyval ); if (::WordIn($key,'Insert KP_Insert')) { $self->new_picture_cb; } elsif (::WordIn($key,'Delete KP_Delete')) { $self->remove_selected_cb; } else {return 0} return 1; } sub get_changes { my $self=shift; return () unless $self->{changed}; my @apics= grep $_->[3], @{$self->{pix}}; #only keep those that have a picture if ($self->{m4a_mode} && @apics) { @apics=($apics[0][3]); } return embedded_pictures=>\@apics; } ############################## Advanced tag editing ############################## package EditTag; use base 'Gtk2::Box'; sub new { my ($class,$window,$ID) = @_; my $file= Songs::GetFullFilename($ID); return undef unless $file; my $self = bless Gtk2::VBox->new, $class; $self->{window}=$window; my $labelfile=Gtk2::Label->new; $labelfile->set_markup( ::ReplaceFieldsAndEsc($ID,'%u') ); $labelfile->set_selectable(::TRUE); $labelfile->set_line_wrap(::TRUE); $self->pack_start($labelfile,::FALSE,::FALSE,1); $self->{filename}=$file; my ($format)= $file=~m/\.([^.]*)$/; return undef unless $format and $format=$FileTag::FORMATS{lc$format}; $self->{filetag}=my $filetag= $format->[0]->new($file); unless ($filetag) {warn "can't read tags for $file\n";return undef;} my @boxes; $self->{boxes}=\@boxes; my @tags; for my $t (split / /,$format->[2]) { if ($t eq 'vorbis' || $t eq 'ilst') {push @tags,$filetag;} elsif ($t eq 'APE') { if ($filetag->{APE}) { push @tags,$filetag->{APE}; } elsif (!@tags) { push @tags,$filetag->new_APE; } } elsif ($t eq 'ID3v2') { if ($filetag->{ID3v2}) { push @tags,$filetag->{ID3v2};push @tags, @{ $filetag->{ID3v2s} } if $filetag->{ID3v2s}; } elsif (!@tags) { push @tags,$filetag->new_ID3v2; } } } push @tags,$filetag->{lyrics3v2} if $filetag->{lyrics3v2}; $self->{filetag}=$filetag; push @boxes,TagBox->new(shift @tags); push @boxes,TagBox->new($_,1) for grep defined,@tags; push @boxes,TagBox_id3v1->new($filetag,1) if $filetag->{ID3v1}; my $notebook=Gtk2::Notebook->new; for my $box (grep defined, @boxes) { $notebook->append_page($box,$box->{title}); } $self->add($notebook); return $self; } sub save { my $self=shift; my $modified; for my $box (@{ $self->{boxes} }) { $modified=1 if $box->save; } $self->{filetag}{errorsub}= sub { my ($syserr,$details)= FileTag::Error_Message(@_); return ::Retry_Dialog($syserr,_"Error writing tag", details=>$details, window=>$self->{window}); }; $self->{filetag}->write_file if $modified && !$::CmdLine{ro} && !$::CmdLine{rotags}; } package TagBox; use base 'Gtk2::Box'; use constant { TRUE => 1, FALSE => 0, #contents of types hashes : TAGNAME => 0, TAGORDER => 1, TAGTYPE => 2, }; my %DataType; my %tagprop; INIT { my $id3v2_types= { #id3v2.3/4 TIT2 => [_"Title",1], TIT3 => [_"Version",2], TPE1 => [_"Artist",3], TPE2 => [_"Album artist",4.5], TALB => [_"Album",4], TPOS => [_"Disc #",5], TRCK => [_"Track",6], TYER => [_"Date",7], COMM => [_"Comments",9], TCON => [_"Genre",8], TLAN => [_"Languages",20], USLT => [_"Lyrics",14], APIC => [_"Picture",15], TOPE => [_"Original Artist",40], TXXX => [_"Custom Text",50], WOAR => [_"Artist URL",50], WXXX => [_"Custom URL",50], PCNT => [_"Play counter",44], POPM => [_"Popularimeter",45], GEOB => [_"Encapsulated object",60], PRIV => [_"Private Data",98], UFID => [_"Unique file identifier",99], TCOP => [_("Copyright")." ©",80], TPRO => [_"Produced (P)",81], #FIXME find (P) symbol TCOM => [_"Composer",12], TIT1 => [_"Grouping",13], TENC => [_"Encoded by",51], TSSE => [_"Encoded with",52], TMED => [_"Media type"], TFLT => [_"File type"], TOAL => [_"Originaly from"], TOFN => [_"Original Filename"], TORY => [_"Original release year"], TPUB => [_"Label/Publisher"], TRDA => [_"Recording Dates"], TSRC => ["ISRC"], TCMP => [_"Compilation",60,'f'], }; my $vorbis_types= { title => [_"Title",1], version => [_"Version",2], artist => [_"Artist",3], album => [_"Album",4], discnumber => [_"Disc #",5], tracknumber => [_"Track",6], date => [_"Date",7], comments => [_"Comments",9,'M'], description => [_"Description",9,'M'], genre => [_"Genre",8], lyrics => [_"Lyrics",14,'L'], fmps_lyrics => [_"Lyrics",14,'L'], author => [_"Original Artist",40], metadata_block_picture=> [_"Picture",15,'tCTb'], }; my $ape_types= { title => [_"Title",1], artist => [_"Artist",3], album => [_"Album",4], subtitle => [_"Subtitle",5], publisher => [_"Publisher",14], conductor => [_"Conductor",13], track => [_"Track",6], genre => [_"Genre",8], composer => [_"Composer",12], comment => [_"Comment",9], copyright => [_"Copyright",80], publicationright=> [_"Publication right",81], year => [_"Year",7], 'debut album' => [_"Debut Album",8], fmps_lyrics => [_"Lyrics",14,'L'], }; my $lyrics3v2_types= { LYR => [_"Lyrics",7,'M'], INF => [_"Info",6,'M'], AUT => [_"Author",5], EAL => [_"Album",4], EAR => [_"Artist",3], ETT => [_"Title",1], }; my $ilst_types= { "\xA9nam" => [_"Title",1], "\xA9ART" => [_"Artist",3], "\xA9alb" => [_"Album",4], "\xA9day" => [_"Year",8], "\xA9cmt" => [_"Comment",12,'M'], "\xA9gen" => [_"Genre",10], "\xA9wrt" => [_"Author",14], "\xA9lyr" => [_"Lyrics",50], "\xA9too" => [_"Encoder",51], '----' => [_"Custom",52,'ttt'], trkn => [_"Track",6], disk => [_"Disc #",7], aART => [_"Album artist",9], covr => [_"Picture",20,'p'], cpil => [_"Compilation",19,'f'], # pgap => gapless album # pcst => podcast }; %tagprop= ( ID3v2 =>{ addlist => [qw/COMM TPOS TIT3 TCON TXXX TOPE WOAR WXXX USLT APIC POPM PCNT GEOB/], default => [qw/COMM TIT2 TPE1 TALB TYER TRCK TCON/], infosub => sub { Tag::ID3v2::get_fieldtypes($_[1]); }, namesub => sub { 'id3v2.'.$_[0]{version} }, types => $id3v2_types, }, OGG => { addlist => [qw/description genre discnumber author metadata_block_picture/,''], default => [qw/title artist album tracknumber date description genre/], name => 'vorbis comment', types => $vorbis_types, lckeys => 1, }, APE=> { addlist => [qw/Title Subtitle Artist Album Genre Publisher Conductor Track Composer Comment Copyright Publicationright Year/,'Debut Album'], default => [qw/Title Artist Album Track Year Genre Comment/], infosub => sub { $_[0]->is_binary($_[1],$_[2]); }, name => 'APE tag', types => $ape_types, lckeys => 1, }, Lyrics3v2=>{ addlist => [qw/EAL EAR ETT INF AUT LYR/], default => [qw/EAL EAR ETT INF/], name => 'lyrics3v2 tag', types => $lyrics3v2_types, }, M4A => { addlist => ["\xA9cmt","\xA9wrt",qw/disk aART cpil ----/], default => ["\xA9nam","\xA9ART","\xA9alb",'trkn',"\xA9day","\xA9cmt","\xA9gen"], infosub => sub {Tag::M4A::get_field_info($_[1])}, name => 'ilst', types => $ilst_types, }, ); $tagprop{Flac}=$tagprop{OGG}; %DataType= ( t => ['EntrySimple'], #text T => ['EntrySimple'], #text M => ['EntryMultiLines'], #multi-line text #l => ['EntrySimple'], #3 letters language #unused, found only in multi-fields frames c => ['EntryNumber'], #counter C => ['EntryNumber',255], #1 byte integer (0-255) n => ['EntryNumber',65535], b => ['EntryBinary'], #binary u => ['EntryBinary'], #unknown -> binary f => ['EntryBoolean'], p => ['EntryCover'], L => ['EntryLyrics'], ); } sub new { my ($class,$tag,$option)=@_; my $tagtype=ref $tag; $tagtype=~s/^Tag:://i; unless ($tagprop{$tagtype}) {warn "unknown tag '$tagtype'\n"; return undef;} $tagtype=$tagprop{$tagtype}; my $self=bless Gtk2::VBox->new,$class; my $name=$tagtype->{name} || $tagtype->{namesub}($tag); $self->{title}=$name; $self->{tag}=$tag; $self->{tagtype}=$tagtype; my $sw=Gtk2::ScrolledWindow->new; #$sw->set_shadow_type('etched-in'); $sw->set_policy('automatic','automatic'); $self->{table}=my $table=Gtk2::Table->new(2,2,FALSE); $table->{row}=0; $table->{widgets}=[]; $sw->add_with_viewport($table); if ($option) { my $checkrm=Gtk2::CheckButton->new(_"Remove this tag"); $checkrm->signal_connect( toggled => sub { my $state=$_[0]->get_active; $table->{deleted}=$state; $table->set_sensitive(!$state); }); $self->pack_start($checkrm,FALSE,FALSE,2); } $self->add($sw); if (my $list=$tagtype->{addlist}) { my $addbut=::NewIconButton('gtk-add',_"add"); my $addlist=Gtk2::ComboBox->new_text; my $hbox=Gtk2::HBox->new(FALSE,8); $hbox->pack_start($_,FALSE,FALSE,0) for $addlist,$addbut; $self->pack_start($hbox,FALSE,FALSE,2); for my $key (@$list) { $key=lc$key if $tagtype->{lckeys}; my $name=($key ne '')? $tagtype->{types}{$key}[TAGNAME] : _"(other)"; $addlist->append_text($name); } $addlist->set_active(0); $addbut->signal_connect( clicked => sub { my $key=$list->[ $addlist->get_active ]; $self->addrow($key); Glib::Idle->add(\&scroll_to_bottom,$self); }); } my %toadd= map { $_=>undef } $tag->get_keys; my @default= @{$tagtype->{'default'}}; my $lc= $tagtype->{lckeys}; if ($lc) { my %lc; $lc{lc()}=1 for keys %toadd; @default= grep !$lc{lc()}, @default; } $toadd{$_}=undef for @default; for my $key (sort { ($tagtype->{types}{ ($lc? lc$a : $a) }[TAGORDER]||100) <=> ($tagtype->{types}{ ($lc? lc$b : $b) }[TAGORDER]||100) } keys %toadd) { my $nb=0; $self->addrow($key,$nb++,$_) for $tag->get_values($key); $self->addrow($key) if !$nb; } return $self; } sub scroll_to_bottom { my $self=shift; my $adj= $self->{table}->parent->get_vadjustment; $adj->clamp_page($adj->upper,$adj->upper); 0; #called from an idle => false to disconnect idle } sub addrow { my ($self,$key,$nb,$value)=@_; my $table=$self->{table}; my $row=$table->{row}++; my ($widget,@Todel); my $tagtype=$self->{tagtype}; my $typesref=$tagtype->{types}{($tagtype->{lckeys}? lc$key : $key)}; my ($name,$type,$realkey); if ($typesref) { $type=$typesref->[TAGTYPE]; $name=$typesref->[TAGNAME]; } if ($tagtype->{infosub}) { (my $type0,$realkey,my $fallbackname,my @extra)= $tagtype->{infosub}( $self->{tag}, $key, $nb ); $type||=$type0; $name||= $tagtype->{types}{$realkey}[TAGNAME] if $realkey; $name||= $fallbackname if $fallbackname; $value=[@extra, (ref $value ? @$value : $value)] if @extra; } $name||=$key; $type||='t'; if (length($type)>1) #frame with sub-frames { $value||=[]; $widget=EntryMulti->new($value,$key,$name,$type,$realkey); $table->attach($widget,1,3,$row,$row+1,['fill','expand'],'shrink',1,1); } else #simple case : 1 label -> 1 value { $value=$value->[0] if ref $value; $value='' unless defined $value; my $label; $type=$DataType{$type}[0] || 'EntrySimple'; my $param=$DataType{$type}[1]; if ($key eq '') { ($widget,$label)=EntryDouble->new($value); } else { $widget=$type->new($value,$param); $label=Gtk2::Label->new($name); $label->set_tooltip_text($key); } $table->attach($label,1,2,$row,$row+1,'shrink','shrink',1,1); $table->attach($widget,2,3,$row,$row+1,['fill','expand'],'shrink',1,1); @Todel=($label); } push @Todel,$widget; $widget->{key}=$key; $widget->{nb}=$nb; my $delbut=Gtk2::Button->new; $delbut->set_relief('none'); $delbut->add(Gtk2::Image->new_from_stock('gtk-remove','menu')); $table->attach($delbut,0,1,$row,$row+1,'shrink','shrink',1,1); $delbut->signal_connect( clicked => sub { $widget->{deleted}=1; $table->remove($_) for $_[0],@Todel; $table->{ondelete}($widget) if $table->{ondelete}; }); push @{ $table->{widgets} }, $widget; $table->show_all; } sub save { my $self=shift; my $table=$self->{table}; my $tag=$self->{tag}; if ($table->{deleted}) { $tag->removetag; warn "$tag removed\n" if $::debug; return 1; } my $modified; for my $w ( @{ $table->{widgets} } ) { if ($w->{deleted}) { next unless defined $w->{nb}; $tag->remove($w->{key},$w->{nb}); $modified=1; warn "$tag $w->{key} deleted\n" if $::debug; } else { my @v=$w->return_value; my $v= @v>1 ? \@v : $v[0]; next unless $w->{changed}; if (defined $w->{nb}) { $tag->edit($w->{key},$w->{nb},$v); } else { $tag->add( $w->{key},$v); } $modified=1; warn "$tag $w->{key} modified\n" if $::debug; } } return $modified; } package TagBox_id3v1; use base 'Gtk2::Box'; use constant { TRUE => 1, FALSE => 0 }; sub new { my ($class,$tag,$option)=@_; my $self=bless Gtk2::VBox->new, $class; $self->{title}=_"id3v1 tag"; $self->{tag}=$tag; $self->{table}=my $table=Gtk2::Table->new(2,2,FALSE); $table->{widgets}=[]; my $row=0; if ($option) { my $checkrm=Gtk2::CheckButton->new(_"Remove this tag"); $checkrm->signal_connect( toggled => sub { my $state=$_[0]->get_active; $table->{deleted}=$state; $_->set_sensitive(!$state) for grep $_ ne $_[0], $table->get_children; }); $table->attach($checkrm,0,2,$row,$row+1,'shrink','shrink',1,1); $row++; } $self->add($table); for my $aref ([_"Title",0,30],[_"Artist",1,30],[_"Album",2,30],[_"Year",3,4],[_"Comment",4,30],[_"Track",5,2]) { my $label=Gtk2::Label->new($aref->[0]); my $entry=EntrySimple->new( $tag->{ID3v1}[ $aref->[1] ], $aref->[2]); push @{ $table->{widgets} }, $entry; $table->attach($label,0,1,$row,$row+1,'shrink','shrink',1,1); $table->attach($entry,1,2,$row,$row+1,['fill','expand'],'shrink',1,1); $row++; } my $combo=EntryCombo->new($tag->{ID3v1}[6],\@Tag::MP3::Genres); push @{ $table->{widgets} }, $combo; $table->attach(Gtk2::Label->new(_"Genre"),0,1,$row,$row+1,'shrink','shrink',1,1); $table->attach($combo,1,2,$row,$row+1,['fill','expand'],'shrink',1,1); return $self; } sub save { my $self=shift; my $table=$self->{table}; my $filetag=$self->{tag}; if ($table->{deleted}) { $filetag->{ID3v1}=undef; return 1; } my $modified; my $wgts=$table->{widgets}; my $id3v1= $filetag->{ID3v1} || $filetag->new_ID3v1; for my $i (0..5) { $id3v1->[$i]=$wgts->[$i]->return_value; $modified=1 if $wgts->[$i]{changed}; } $id3v1->[6]= $wgts->[6]->return_value; $modified=1 if $wgts->[6]{changed}; return $modified; } package EntrySimple; use base 'Gtk2::Entry'; sub new { my ($class,$init,$len) = @_; my $self = bless Gtk2::Entry->new, $class; $self->set_text($init); $self->set_width_chars($len) if $len; $self->set_max_length($len) if $len; $self->{init}=$init; return $self; } sub return_value { my $self=shift; my $value=$self->get_text; #warn "$self '$value' '$self->{init}'" if $value ne $self->{init}; $self->{changed}=1 if $value ne $self->{init}; return $value; } package EntryMultiLines; use base 'Gtk2::ScrolledWindow'; sub new { my ($class,$init) = @_; my $self = bless Gtk2::ScrolledWindow->new, $class; $self->set_shadow_type('etched-in'); $self->set_policy('automatic','automatic'); my $textview= $self->{textview}= Gtk2::TextView->new; $textview->set_size_request(100,($textview->create_pango_layout("X")->get_pixel_size)[1]*4); #request 4 lines of height $self->add($textview); $self->set_text($init); $self->{init}=$self->get_text; return $self; } sub set_text { my $self=shift; $self->{textview}->get_buffer->set_text(shift); } sub get_text { my $self=shift; my $buffer=$self->{textview}->get_buffer; return $buffer->get_text( $buffer->get_bounds, 1); } sub return_value { my $self=shift; my $value=$self->get_text; $self->{changed}=1 if $value ne $self->{init}; return $value; } package EntryDouble; use base 'Gtk2::Entry'; sub new { my ($class,$init) = @_; my $self = bless Gtk2::Entry->new, $class; #$self->set_text($init); #$self->{init}=$init; $self->{keyEntry}=Gtk2::Entry->new; return $self,$self->{keyEntry}; } sub return_value { my $self=shift; my $value=$self->get_text; $self->{key}=$self->{keyEntry}->get_text; $self->{changed}=1 if ($self->{key} ne '' && $value ne ''); return $value; } package EntryNumber; use base 'Gtk2::SpinButton'; sub new { my ($class,$init,$max) = @_; my $self = bless Gtk2::SpinButton->new( Gtk2::Adjustment->new ($init||0, 0, $max||10000000, 1, 10, 0) ,10,0 ) , $class; $self->{init}=$self->get_value; return $self; } sub return_value { my $self=shift; my $value=$self->get_value; $self->{changed}=1 if $value ne $self->{init}; return $value; } package EntryBoolean; use base 'Gtk2::CheckButton'; sub new { my ($class,$init) = @_; my $self = bless Gtk2::CheckButton->new, $class; $self->set_active(1) if $init; $self->{init}=$init; return $self; } sub return_value { my $self=shift; my $value=$self->get_active; $self->{changed}=1 if ($value xor $self->{init}); return $value; } package EntryCombo; use base 'Gtk2::ComboBox'; sub new { my ($class,$init,$listref) = @_; my $self = bless Gtk2::ComboBox->new_text, $class; if ($init && $init=~m/\D/) { my $text=$init; $init=''; for my $i (0..$#$listref) { if ($listref->[$i] eq $text) {$init=$i;last} } } for my $text (@$listref) { $self->append_text($text); } $self->set_active($init) unless $init eq ''; $self->{init}=$init; return $self; } sub return_value { my $self=shift; my $value=$self->get_active; $value='' if $value==-1; $self->{changed}=1 if $value ne $self->{init}; return $value; } package EntryMulti; #for id3v2 frames containing multiple fields use base 'Gtk2::Frame'; my %SUBTAGPROP; our $PICTYPE; INIT { $PICTYPE=[_"other",_"32x32 PNG file icon",_"other file icon",_"front cover",_"back cover",_"leaflet page",_"media",_"lead artist",_"artist",_"conductor",_"band",_"composer",_"lyricist",_"recording location",_"during recording",_"during performance",_"movie/video screen capture",_"a bright coloured fish",_"illustration",_"band/artist logotype",_"Publisher/Studio logotype"]; %SUBTAGPROP= # [label,row,col_start,col_end,widget,extra_parameter] ( USLT => [ [_"Lang.",0,1,2,'EntrySimple',3], [_"Descr.",0,3,5], ['',1,0,5,'EntryLyrics'] ], COMM => [ [_"Lang",0,1,2,'EntrySimple',3], [_"Descr.",0,3,5], ['',1,0,5] ], APIC => [ [_"MIME type",0,1,5], [_"Picture Type",1,1,5,'EntryCombo',$PICTYPE], [_"Description",2,1,5], ['',3,0,5,'EntryCover'] ], GEOB => [ [_"MIME type",0,1,5], [_"Filename",1,1,5], [_"Description",2,1,5], ['',3,0,5,'EntryBinary'] #FIXME load & save & launch? ], TXXX => [ [_"Descr.",0,1,2], [_"Text",1,1,2] ], WXXX => [ [_"Descr.",0,1,2], [_"URL",1,1,2] #FIXME URL click ], POPM => [ [_"email",0,1,4], [_"Rating",1,1,2], [_"counter",1,3,4] ], USER => [ [_"Lang",0,1,2,'EntrySimple',3], [_"Terms of use",1,1,4] ], OWNE => [ [_"Price paid",0,1,2], [_"Date of purchase",1,1,2], [_"Seller",2,1,2], ], UFID => [ [_"Owner identifier",0,1,2], ['',1,0,2,'EntryBinary'] ], PRIV => [ [_"Owner identifier",0,1,2], ['',1,0,2,'EntryBinary'] ], '----' => [ [_"Application",0,1,2], [_"Name",1,1,2], ['',2,0,2], ], 'com.apple.iTunes----FMPS_Lyrics'=> [ [_"Application",0,1,2], [_"Name",1,1,2], ['',2,0,2,'EntryLyrics'], ], ); $SUBTAGPROP{metadata_block_picture}=$SUBTAGPROP{APIC}; #for vorbis pictures } sub new { my ($class,$values,$key,$name,$type,$realkey) = @_; my $self = bless Gtk2::Frame->new($name), $class; my $table=Gtk2::Table->new(1, 4, 0); $self->add($table); my $prop= $SUBTAGPROP{$key}; $prop||= $SUBTAGPROP{$realkey} if $realkey; my $row=0; my $subtag=0; for my $t (split //,$type) { my $val=$$values[$subtag]; $val='' unless defined $val; my ($name,$frow,$cols,$cole,$widget,$param)= ($prop) ? @{ $prop->[$subtag] } : (_"unknown",$row++,1,5,undef,undef); unless ($widget) { ($widget,$param)=@{ $DataType{$t} }; } $subtag++; if ($name ne '') { my $label=Gtk2::Label->new($name); $table->attach($label,$cols-1,$cols,$frow,$frow+1,'shrink','shrink',1,1); } $widget=$widget->new( $val,$param ); push @{ $self->{widgets} },$widget; $table->attach($widget,$cols,$cole,$frow,$frow+1,['fill','expand'],'shrink',1,1); } if ($key eq 'APIC') { $self->{widgets}[3]->set_mime_entry($self->{widgets}[0]); } return $self; } sub return_value { my $self=shift; my @values; for my $w ( @{ $self->{widgets} } ) { my @v=$w->return_value; $self->{changed}=1 if $w->{changed}; push @values,@v; } return @values; } package EntryBinary; use base 'Gtk2::Button'; sub new { my $class = shift; my $self = bless Gtk2::Button->new(_"View binary data ..."), $class; $self->{init}=$self->{value}=shift; $self->signal_connect(clicked => \&view); return $self; } sub return_value { my $self=shift; #$self->{changed}=1 if $self->{value} ne $self->{init}; return $self->{value}; } sub view { my $self=$_[0]; my $dialog = Gtk2::Dialog->new (_"View Binary", $self->get_toplevel, 'destroy-with-parent', 'gtk-close' => 'close'); $dialog->set_default_response ('close'); my $text; my $offset=0; while (my $b=substr $self->{value},$offset,16) { $text.=sprintf "%08x %-48s", $offset, join ' ',unpack '(H2)*',$b; $offset+=length $b; $b=~s/[^[:print:]]/./g; #replace non-printable with '.' $text.=" $b\n"; } my $textview=Gtk2::TextView->new; my $buffer=$textview->get_buffer; $buffer->set_text($text); $textview->modify_font(Gtk2::Pango::FontDescription->from_string('Monospace')); $textview->set_editable(0); my $sw=Gtk2::ScrolledWindow->new; $sw->set_shadow_type('etched-in'); $sw->set_policy('never', 'automatic'); $sw->add($textview); $dialog->vbox->add($sw); $dialog->set_default_size(100,100); $dialog->show_all; $dialog->signal_connect( response => sub { $_[0]->destroy; }); } package EntryCover; use base 'Gtk2::Box'; sub new { my $class = shift; my $self = bless Gtk2::HBox->new, $class; $self->{init}=$self->{value}=shift; my $img=$self->{img}=Gtk2::Image->new; my $vbox=Gtk2::VBox->new; my $eventbox=Gtk2::EventBox->new; $eventbox->add($img); $self->add($_) for $eventbox,$vbox; my $label=$self->{label}=Gtk2::Label->new; my $Bload=::NewIconButton('gtk-open',_"Replace..."); my $Bsave=::NewIconButton('gtk-save-as',_"Save as..."); $vbox->pack_start($_,0,0,2) for $label,$Bload,$Bsave; $Bload->signal_connect(clicked => \&load_cb); $Bsave->signal_connect(clicked => \&save_cb); $eventbox->signal_connect(button_press_event => \&GMB::Picture::pixbox_button_press_cb); $self->{Bsave}=$Bsave; ::set_drag($self, dest => [::DRAG_FILE,\&uri_dropped]); $self->set; return $self; } sub set_mime_entry { my $self=shift; $self->{mime_entry}=shift; $self->update_mime; } sub return_value { my $self=shift; $self->{changed}=1 if $self->{value} ne $self->{init} && length $self->{value}; return $self->{value}; } sub set { my $self=shift; my $label=$self->{label}; my $Bsave=$self->{Bsave}; my $length=length $self->{value}; unless ($length) { $label->set_text(_"empty"); $Bsave->set_sensitive(0); return; } my $loader= GMB::Picture::LoadPixData( $self->{value} ,'-150'); my $pixbuf; if (!$loader) { $label->set_text(_"error"); $Bsave->set_sensitive(0); ($self->{ext},$self->{mime})=('',''); } else { $pixbuf=$loader->get_pixbuf; $Bsave->set_sensitive(1); if ($Gtk2::VERSION >= 1.092) { my $h=$loader->get_format; $self->{ext} =$h->{extensions}[0]; $self->{mime}=$h->{mime_types}[0]; } else { ($self->{ext},$self->{mime})=_identify_pictype($self->{value}); } $label->set_text("$loader->{w} x $loader->{h} ($self->{ext} $length bytes)"); } my $img=$self->{img}; $img->set_from_pixbuf($pixbuf); $self->update_mime if $self->{mime_entry}; $img->parent->{pixdata}=$self->{value}; #for zoom on click } sub uri_dropped { my ($self,$type,$uri)=@_; if ($uri=~s#^file://##) { my $file=::decode_url($uri); $self->load_file($file); } #else #FIXME download http link } sub load_file { my ($self,$file)=@_; my $data= GMB::Picture::load_data($file); return unless $data; $self->{value}=$data; $self->set; } sub load_cb { my $self=::find_ancestor($_[0],__PACKAGE__); my $file=::ChoosePix(); $self->load_file($file) if defined $file; } sub save_cb { my $self=::find_ancestor($_[0],__PACKAGE__); return unless length $self->{value}; my $file=::ChooseSaveFile($self->{window},_"Save picture as",undef,'picture.'.$self->{ext}); return unless defined $file; open my$fh,'>',$file or return; print $fh $self->{value}; close $fh; } sub update_mime { my $self=shift; return unless $self->{mime}; $self->{mime_entry}->set_text($self->{mime}); } sub _identify_pictype #used only if $Gtk2::VERSION < 1.092 { $_[0]=~m/^\xff\xd8\xff\xe0..JFIF\x00/s && return ('jpg','image/jpeg'); $_[0]=~m/^\x89PNG\x0D\x0A\x1A\x0A/ && return ('png','image/png'); $_[0]=~m/^GIF8[79]a/ && return ('gif','image/gif'); $_[0]=~m/^BM/ && return ('bmp','image/bmp'); return ('',''); } package EntryLyrics; use base 'Gtk2::Button'; sub new { my $class = shift; my $self = bless Gtk2::Button->new(_"Edit Lyrics ..."), $class; $self->{init}=$self->{value}=shift; $self->signal_connect(clicked => \&edit); return $self; } sub return_value { my $self=shift; $self->{changed}=1 if $self->{value} ne $self->{init}; return $self->{value}; } sub edit { my $self=$_[0]; if ($self->{dialog}) { $self->{dialog}->force_present; return } $self->{dialog}= ::EditLyricsDialog( $self->get_toplevel, $self->{value},undef, sub { my $lyrics=shift; $self->{value}=$lyrics if defined $lyrics; $self->{dialog}=undef; }); } 1; gmusicbrowser-1.1.15~ds0.orig/oggheader.pm0000664000175000017500000004205212565212604017743 0ustar unit193unit193# Copyright (C) 2005-2009 Quentin Sculo # # 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 #http://xiph.org/vorbis/doc/framing.html #http://xiph.org/vorbis/doc/v-comment.html package Tag::OGG; use strict; use warnings; use Encode qw(decode encode); use MIME::Base64; use constant { PACKET_INFO => 1, PACKET_COMMENT => 3, PACKET_SETUP => 5, }; my @crc_lookup; my $digestcrc; INIT { eval { require Digest::CRC; $digestcrc=Digest::CRC->new(width=>32, init=>0, xorout=>0, poly=>0x04C11DB7, refin=>0, refout=>0); warn "oggheader.pm : using Digest::CRC\n" if $::debug; }; if ($@) { warn "oggheader.pm : Digest::CRC not found, using slow pure-perl replacement.\n" if $::debug; @crc_lookup= (0x00000000,0x04c11db7,0x09823b6e,0x0d4326d9, 0x130476dc,0x17c56b6b,0x1a864db2,0x1e475005, 0x2608edb8,0x22c9f00f,0x2f8ad6d6,0x2b4bcb61, 0x350c9b64,0x31cd86d3,0x3c8ea00a,0x384fbdbd, 0x4c11db70,0x48d0c6c7,0x4593e01e,0x4152fda9, 0x5f15adac,0x5bd4b01b,0x569796c2,0x52568b75, 0x6a1936c8,0x6ed82b7f,0x639b0da6,0x675a1011, 0x791d4014,0x7ddc5da3,0x709f7b7a,0x745e66cd, 0x9823b6e0,0x9ce2ab57,0x91a18d8e,0x95609039, 0x8b27c03c,0x8fe6dd8b,0x82a5fb52,0x8664e6e5, 0xbe2b5b58,0xbaea46ef,0xb7a96036,0xb3687d81, 0xad2f2d84,0xa9ee3033,0xa4ad16ea,0xa06c0b5d, 0xd4326d90,0xd0f37027,0xddb056fe,0xd9714b49, 0xc7361b4c,0xc3f706fb,0xceb42022,0xca753d95, 0xf23a8028,0xf6fb9d9f,0xfbb8bb46,0xff79a6f1, 0xe13ef6f4,0xe5ffeb43,0xe8bccd9a,0xec7dd02d, 0x34867077,0x30476dc0,0x3d044b19,0x39c556ae, 0x278206ab,0x23431b1c,0x2e003dc5,0x2ac12072, 0x128e9dcf,0x164f8078,0x1b0ca6a1,0x1fcdbb16, 0x018aeb13,0x054bf6a4,0x0808d07d,0x0cc9cdca, 0x7897ab07,0x7c56b6b0,0x71159069,0x75d48dde, 0x6b93dddb,0x6f52c06c,0x6211e6b5,0x66d0fb02, 0x5e9f46bf,0x5a5e5b08,0x571d7dd1,0x53dc6066, 0x4d9b3063,0x495a2dd4,0x44190b0d,0x40d816ba, 0xaca5c697,0xa864db20,0xa527fdf9,0xa1e6e04e, 0xbfa1b04b,0xbb60adfc,0xb6238b25,0xb2e29692, 0x8aad2b2f,0x8e6c3698,0x832f1041,0x87ee0df6, 0x99a95df3,0x9d684044,0x902b669d,0x94ea7b2a, 0xe0b41de7,0xe4750050,0xe9362689,0xedf73b3e, 0xf3b06b3b,0xf771768c,0xfa325055,0xfef34de2, 0xc6bcf05f,0xc27dede8,0xcf3ecb31,0xcbffd686, 0xd5b88683,0xd1799b34,0xdc3abded,0xd8fba05a, 0x690ce0ee,0x6dcdfd59,0x608edb80,0x644fc637, 0x7a089632,0x7ec98b85,0x738aad5c,0x774bb0eb, 0x4f040d56,0x4bc510e1,0x46863638,0x42472b8f, 0x5c007b8a,0x58c1663d,0x558240e4,0x51435d53, 0x251d3b9e,0x21dc2629,0x2c9f00f0,0x285e1d47, 0x36194d42,0x32d850f5,0x3f9b762c,0x3b5a6b9b, 0x0315d626,0x07d4cb91,0x0a97ed48,0x0e56f0ff, 0x1011a0fa,0x14d0bd4d,0x19939b94,0x1d528623, 0xf12f560e,0xf5ee4bb9,0xf8ad6d60,0xfc6c70d7, 0xe22b20d2,0xe6ea3d65,0xeba91bbc,0xef68060b, 0xd727bbb6,0xd3e6a601,0xdea580d8,0xda649d6f, 0xc423cd6a,0xc0e2d0dd,0xcda1f604,0xc960ebb3, 0xbd3e8d7e,0xb9ff90c9,0xb4bcb610,0xb07daba7, 0xae3afba2,0xaafbe615,0xa7b8c0cc,0xa379dd7b, 0x9b3660c6,0x9ff77d71,0x92b45ba8,0x9675461f, 0x8832161a,0x8cf30bad,0x81b02d74,0x857130c3, 0x5d8a9099,0x594b8d2e,0x5408abf7,0x50c9b640, 0x4e8ee645,0x4a4ffbf2,0x470cdd2b,0x43cdc09c, 0x7b827d21,0x7f436096,0x7200464f,0x76c15bf8, 0x68860bfd,0x6c47164a,0x61043093,0x65c52d24, 0x119b4be9,0x155a565e,0x18197087,0x1cd86d30, 0x029f3d35,0x065e2082,0x0b1d065b,0x0fdc1bec, 0x3793a651,0x3352bbe6,0x3e119d3f,0x3ad08088, 0x2497d08d,0x2056cd3a,0x2d15ebe3,0x29d4f654, 0xc5a92679,0xc1683bce,0xcc2b1d17,0xc8ea00a0, 0xd6ad50a5,0xd26c4d12,0xdf2f6bcb,0xdbee767c, 0xe3a1cbc1,0xe760d676,0xea23f0af,0xeee2ed18, 0xf0a5bd1d,0xf464a0aa,0xf9278673,0xfde69bc4, 0x89b8fd09,0x8d79e0be,0x803ac667,0x84fbdbd0, 0x9abc8bd5,0x9e7d9662,0x933eb0bb,0x97ffad0c, 0xafb010b1,0xab710d06,0xa6322bdf,0xa2f33668, 0xbcb4666d,0xb8757bda,0xb5365d03,0xb1f740b4 );} } #hash fields : # filename # fileHandle # serial serial number (binary 4 bytes) # seg_table segmentation table of last read page # granule granule of last read page # info -> hash containing : version channels rate bitrate_upper bitrate_nominal bitrate_lower seconds # comments -> hash of arrays (lowercase keys) # CommentsOrder -> list of keys (mixed-case keys) # commentpack_size # vorbis_string # stream_vers # end sub new { my ($class,$file)=@_; my $self=bless {}, $class; # check that the file exists unless (-e $file) { warn "File '$file' does not exist.\n"; return undef; } $self->{filename} = $file; $self->_open or return undef; { $self->{info}=_ReadInfo($self); last unless $self->{info}; $self->{comments}=_ReadComments($self); last unless $self->{comments}; $self->{end}=_skip_to_last_page($self); _read_packet($self,0) unless $self->{end}; warn "file truncated or corrupted.\n" unless $self->{end}; #calulate length last unless $self->{info}{rate};# && $self->{end}; my @granule=unpack 'C*',$self->{granule}; my $l=0; $l=$l*256+$_ for reverse @granule; $self->{info}{seconds}=my$s=$l/$self->{info}{rate}; } $self->_close; unless ($self->{info} && $self->{comments}) { warn "error, can't read file or not a valid ogg file\n"; return undef; } return $self; } sub _open { my $self=shift; my $file=$self->{filename}; open my$fh,'<',$file or warn "can't open $file : $!\n" and return undef; binmode $fh; $self->{fileHandle} = $fh; $self->{seg_table} = []; return $fh; } sub _openw { my ($self,$tmp)=@_; my $file=$self->{filename}; my $m='+<'; if ($tmp) {$file.='.TEMP';$m='>';} my $fh; until (open $fh,$m,$file) { my $err="Error opening '$file' for writing :\n$!"; warn $err."\n"; return undef unless $self->{errorsub} && $self->{errorsub}($!,'openwrite',$file) eq 'retry'; } binmode $fh; unless ($tmp) { $self->{fileHandle} = $fh; $self->{seg_table} = []; } return $fh; } sub _close { my $self=shift; $self->{seg_table} = undef; close delete($self->{fileHandle}); } sub write_file { my $self=shift; my $newcom_packref=_PackComments($self); #warn "old size $self->{commentpack_size}, need : ".length($$newcom_packref)."\n"; if ( $self->{commentpack_size} >= length $$newcom_packref) { warn "in place editing.\n"; my $left=length $$newcom_packref; my $offset2=0; my $fh=$self->_openw or return; _read_packet($self,PACKET_INFO); #skip first page while ($left) { my $pos=tell $fh; my ($pageref,$offset,$size)=_ReadPage($self); seek $fh,$pos,0; if ($left<$size) {$size=$left; $left=0;} else {$left-=$size} substr $$pageref,$offset,$size,substr($$newcom_packref,$offset2,$size); $offset2+=$size; _recompute_page_crc($pageref); print $fh $$pageref or warn $!; } $self->_close; return; } my $INfh=$self->_open or return; my $OUTfh=$self->_openw(1) or return; #open .TEMP file my $version=chr $self->{stream_vers}; my $serial=$self->{serial}; my $pageref=_ReadPage($self); #read the first page die unless $pageref; #FIXME check serial, OggS ... print $OUTfh $$pageref or warn $!; #write the first page unmodified my $pagenb=1; #skip the comment packet in the original file die unless _read_packet($self,PACKET_COMMENT); #concatenate newly generated comment packet and setup packet from the original file in $data, and compute the segments in @segments my $data; my @segments; for my $packref ( $newcom_packref , _read_packet($self,PACKET_SETUP) ) { $data.=$$packref; my $size=length $$packref; push @segments, (255)x int($size/255), $size%255; } #separate $data in pages and write them my $data_offset=0; my $continued=0; { my $size=0; my $segments; my $nbseg=0; my $seg; while ($size<4096) # make page of max 4095+255 bytes { last unless @segments; $seg=shift @segments; $size+=$seg; $segments.=chr $seg; $nbseg++; } #warn unpack('C*',$segments),"\n"; #warn "$size ",length($data)-$data_offset,"\n"; warn "writing page $pagenb\n" if $::debug; my $page=pack('a4aa x8 a4 V x4 C','OggS',$version,$continued,$serial,$pagenb++,$nbseg).$segments.substr($data,$data_offset,$size); _recompute_page_crc(\$page); print $OUTfh $page or warn $!; $data_offset+=$size; $continued=($seg==255)? "\x01" : "\x00"; redo if @segments; } # copy AUDIO data my $pos=tell $INfh; read $INfh,$data,27; seek $INfh,$pos,0; #warn "first audio data on page ".unpack('x18V',$data)."\n"; # fast raw copy by 1M chunks if page numbers haven't changed if ( substr($data,0,4) eq 'OggS' && unpack('x18V',$data) eq $pagenb) { my $buffer; print $OUTfh $buffer or warn $! while read $INfh,$buffer,1048576; } # __SLOW__ copy if page number must be changed -> and crc recomputed else { warn "must recompute crc for the whole file, this may take a while (install Digest::CRC to make it fast) ...\n" unless $digestcrc; while (my $pageref=_ReadPage($self)) # read each page { substr $$pageref,18,4,pack('V',$pagenb++); #replace page number _recompute_page_crc($pageref); #recompute crc print $OUTfh $$pageref or warn $!; #write page } } $self->_close; close $OUTfh; warn "replacing old file with new file.\n"; unlink $self->{filename} && rename $self->{filename}.'.TEMP',$self->{filename}; %$self=(); #destroy the object to make sure it is not reused as many of its data are now invalid return 1; } sub _ReadPage { my $self=shift; my $fh=$self->{fileHandle}; my $page; my $r=read $fh,$page,27; #read page header return undef unless $r==27 && substr($page,0,4) eq 'OggS'; my $segments=vec $page,26,8; $r=read $fh,$page,$segments,27; #read segment table return undef unless $r==$segments; my $size; #$size+=ord substr($page,$_,1) for (27..$segments+26); $size+=vec($page,$_,8) for (27..$segments+26); $r=read $fh,$page,$size,27+$segments; #read page data return undef unless $r==$size; return wantarray ? (\$page,27+$segments,$size) : \$page; } sub _ReadInfo { my $self=shift; #$self->{startaudio}=0; # 1) [vorbis_version] = read 32 bits as unsigned integer # 2) [audio_channels] = read 8 bit integer as unsigned # 3) [audio_sample_rate] = read 32 bits as unsigned integer # 4) [bitrate_maximum] = read 32 bits as signed integer # 5) [bitrate_nominal] = read 32 bits as signed integer # 6) [bitrate_minimum] = read 32 bits as signed integer # 7) [blocksize_0] = 2 exponent (read 4 bits as unsigned integer) # 8) [blocksize_1] = 2 exponent (read 4 bits as unsigned integer) # 9) [framing_flag] = read one bit if ( my $packref=_read_packet($self,PACKET_INFO) ) { my %info; @info{qw/version channels rate bitrate_upper bitrate_nominal bitrate_lower/}= unpack 'x7 VCV V3 C',$$packref; return \%info; } else { warn "Can't read info\n"; return undef; } } sub _ReadComments { my $self=$_[0]; if ( my $packref= _read_packet($self,PACKET_COMMENT) ) { $self->{commentpack_size}=length $$packref; my ($vstring,@comlist)=eval { unpack 'x7 V/a V/(V/a)',$$packref; }; if ($@) { warn "Comments corrupted\n"; return undef; } # Comments vendor strings I have found # 'Xiph.Org libVorbis I 20030909' : 1.0.1 # 'Xiph.Org libVorbis I 20020717' : 1.0 release of libvorbis # 'Xiphophorus libVorbis I 200xxxxx' : 1.0_beta1 to 1.0_rc3 # 'AO; aoTuV b3 [20041120] (based on Xiph.Org's libVorbis)' $self->{vorbis_string}=$vstring; if ($::debug && $vstring!~m/^Xiph.* libVorbis I (\d{8})/) { warn "unknown comments vendor string : $vstring\n"; } my %comments; my @order; $self->{CommentsOrder}=\@order; for my $kv (@comlist) { unless ($kv=~m/^([^=]+)=(.*)$/s) { warn "comment invalid - skipped\n"; next; } my $key=$1; my $val=decode('utf-8', $2); #warn "$key = $val\n"; push @{ $comments{lc$key} },$val; push @order, $key; } if (my $covers=$comments{coverart}) #upgrade old embedded pictures format to metadata_block_picture { @order= grep !m/^coverart/i, @order; for my $i (0..$#$covers) { my $data= $comments{"coverart"}[$i]; next unless $data; my @val= ( map( $comments{"coverart$_"}[$i], qw/mime type description/ ), decode_base64($data) ); push @{$comments{metadata_block_picture}}, \@val; push @order, 'METADATA_BLOCK_PICTURE'; } delete $comments{"coverart$_"} for qw/mime type description/,''; } return \%comments; } else { warn "Can't find comments\n"; return undef; } } sub _PackComments { my $self=$_[0]; my @comments; my %count; for my $key ( @{$self->{CommentsOrder}} ) { my $nb=$count{lc$key}++ || 0; my $val=$self->{comments}{lc$key}[$nb]; next unless defined $val; $key=encode('ascii',$key); $key=~tr/\x20-\x7D/?/c; $key=~tr/=/?/; #replace characters that are not allowed by '?' if (uc$key eq 'METADATA_BLOCK_PICTURE' && ref $val) { $val= Tag::Flac::_PackPicture($val); $val= encode_base64($$val); } push @comments,$key.'='.encode('utf8',$val); } my $packet=pack 'Ca6 V/a* V (V/a*)*',PACKET_COMMENT,'vorbis',$self->{vorbis_string},scalar @comments, @comments; $packet.="\x01"; #framing_flag return \$packet; } sub edit { my ($self,$key,$nb,$val)=@_; $nb||=0; my $aref=$self->{comments}{lc$key}; return unless $aref && @$aref >=$nb; $aref->[$nb]= $val; return 1; } sub add { my ($self,$key,$val)=@_; push @{ $self->{comments}{lc$key} }, $val; push @{$self->{CommentsOrder}}, $key; return 1; } sub insert #same as add but put it first (of its kind) { my ($self,$key,$val)=@_; unshift @{ $self->{comments}{lc$key} }, $val; push @{$self->{CommentsOrder}}, $key; return 1; } sub remove_all { my ($self,$key)=@_; return undef unless defined $key; $key=lc$key; $_=undef for @{ $self->{comments}{$key} }; return 1; } sub get_keys { keys %{ $_[0]{comments} }; } sub get_values { my ($self,$key)=($_[0],lc$_[1]); my $v= $self->{comments}{$key}; return () unless $v; if ($key eq 'metadata_block_picture') { for my $val (@$v) { next if ref $val or !defined $val; my $dec=decode_base64($val); $val= $dec ? Tag::Flac::_ReadPicture(\$dec) : undef; } } return grep defined, @$v; } sub remove { my ($self,$key,$nb)=@_; return undef unless defined $key and $nb=~m/^\d*$/; $nb||=0; $key=lc$key; my $val=$self->{comments}{$key}[$nb]; unless (defined $val) {warn "comment to delete not found\n"; return undef; } $self->{comments}{$key}[$nb]=undef; return 1; } sub _read_packet { my $self=shift; my $wantedtype=shift; #wanted type, 0 to read all packets until eof my $fh=$self->{fileHandle}; my $packet; do { my $lpacket=0; my $seg_table=$self->{seg_table}; my $lastseg; until ($lastseg) { my $size; unless ( @$seg_table ) { _read_page_header($self) || return undef } while (defined( my $byte=shift @$seg_table )) { $size+=$byte; unless ($byte==255) { $lastseg=1; last; } } next unless $size; my $read=read $fh,$packet,$size,$lpacket; return undef unless $size==$read; $lpacket+=$read; } } until ($wantedtype || $self->{end}); my ($type,$vorbis)=unpack 'Ca6',$packet; warn "read packet : $type $vorbis length=".length($packet)."\n" if $::debug; if ( $type==$wantedtype && $vorbis eq 'vorbis') { return \$packet; } else { return undef; } } sub _read_page_header { my $self=shift; my $fh=$self->{fileHandle}; my $buf; my $r=read $fh,$buf,27; return 0 unless $r==27; #http://www.xiph.org/ogg/vorbis/doc/framing.html # 'OggS' 4 bytes capture_pattern 0 # 0x00 1 byte stream_structure_version 1 # 1 byte header_type_flag 2 # 8 bytes absolute granule position 3 # 4 bytes stream serial number 4 # 4 bytes page sequence no 5 # 4 bytes page checksum 6 # 1 byte page_segments 7 # #warn "OggS : ".join(' ',unpack('a4CC a8 VVVC',$buf))."\n"; my ($captpat,$ver,$flags,$granule,$sn,$nbseg)=unpack 'a4CC a8 a4 x8 C',$buf; return undef unless $captpat eq 'OggS' and $ver eq 0; if ($self->{serial} && $self->{serial} ne $sn) {warn "corrupted page : serial number doesn't match\n";return undef} $self->{end}=$flags & 4; $self->{serial}=$sn; $self->{stream_vers}=$ver; $self->{granule}=$granule; return undef unless read($fh,$buf,$nbseg)==$nbseg; @{ $self->{seg_table} }=unpack 'C*',$buf; #warn " seg_table: ".join(' ',@{ $self->{seg_table} })."\n"; return 1; } sub _recompute_page_crc { my $pageref=$_[0]; #warn 'old crc : ',unpack('V',substr($$pageref,22,4)),"\n"; substr $$pageref,22,4,"\x00\x00\x00\x00"; my $crc=0; if ($digestcrc) { $digestcrc->add($$pageref); $crc=$digestcrc->digest; } else # pure-perl : SLOW { #$crc=($crc<<8)^vec($crc_lookup, ($crc>>24)^vec($$pageref,$_,8) ,32); # a bit slower #$crc=($crc<<8)^$crc_lookup[ ($crc>>24)^vec($$pageref,$_,8) ] #doesn't work if perl use 64bits $crc=(($crc<<8)&0xffffffff)^$crc_lookup[ ($crc>>24)^vec($$pageref,$_,8) ] for (0 .. length($$pageref)-1); } #warn "new crc : $crc\n"; substr $$pageref,22,4,pack('V',$crc); } sub _skip_to_last_page { my $self=shift; my $fh=$self->{fileHandle}; my $pos=tell $fh; seek $fh,-10000,2; read $fh,my$buf,10000; my $sn=$self->{serial}; my $granule; while ($buf=~m/OggS\x00(.)(.{8})(.{4})/gs) { #@_=unpack "a4CC a8 VVVC",$1; next unless $sn eq $3; #check serial number $granule=$2 unless $2 eq "\xff\xff\xff\xff\xff\xff\xff\xff"; #granule==-1 => no packets finish on this page next unless vec $1,2,1; #last page of logical bitstream last unless defined $granule; # found last page -> save granule $self->{granule}=$granule; return 1; } #didn't find last page seek $fh,$pos,0; return 0; } 1; gmusicbrowser-1.1.15~ds0.orig/COPYING0000664000175000017500000007733112565212604016523 0ustar unit193unit193 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS gmusicbrowser-1.1.15~ds0.orig/simple_http_wget.pm0000664000175000017500000001313612565212605021376 0ustar unit193unit193# Copyright (C) 2008-2011 Quentin Sculo # # 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 package Simple_http; use strict; use warnings; use POSIX ':sys_wait_h'; #for WNOHANG in waitpid use IO::Handle; my $UseCache= *GMB::Cache::add{CODE}; my $orig_proxy=$ENV{http_proxy}; my $gzip_ok; BEGIN { eval { require IO::Uncompress::Gunzip; $gzip_ok=1; }; } sub get_with_cb { my $self=bless {}; my %params=@_; $self->{params}=\%params; my ($callback,$url,$post)=@params{qw/cb url post/}; delete $params{cache} unless $UseCache; if (my $cached= $params{cache} && GMB::Cache::get($url)) { warn "cached result\n" if $::debug; Glib::Timeout->add(10,sub { $callback->( ${$cached->{data}}, type=>$cached->{type}, filename=>$cached->{filename}, ); 0}); return $self; } warn "simple_http_wget : fetching $url\n" if $::debug; my $proxy= $::Options{Simplehttp_Proxy} ? $::Options{Simplehttp_ProxyHost}.':'.($::Options{Simplehttp_ProxyPort}||3128) : $orig_proxy; $ENV{http_proxy}=$proxy; my $useragent= $params{user_agent} || 'Mozilla/5.0'; my $accept= $params{'accept'} || ''; my $gzip= $gzip_ok ? '--header=Accept-Encoding: gzip' : ''; my @cmd_and_args= (qw/wget --timeout=40 -S -O -/, $gzip, "--header=Accept: $accept", "--user-agent=$useragent"); push @cmd_and_args, "--referer=$params{referer}" if $params{referer}; push @cmd_and_args, '--post-data='.$post if $post; #FIXME not sure if I should escape something push @cmd_and_args, '--',$url; pipe my($content_fh),my$wfh; pipe my($error_fh),my$ewfh; my $pid=fork; if (!defined $pid) { warn "simple_http_wget : fork failed : $!\n"; Glib::Timeout->add(10,sub {$callback->(); 0}); return $self } elsif ($pid==0) #child { close $content_fh; close $error_fh; open my($olderr), ">&", \*STDERR; open \*STDOUT,'>&='.fileno $wfh; open \*STDERR,'>&='.fileno $ewfh; exec @cmd_and_args or print $olderr "launch failed (@cmd_and_args) : $!\n"; POSIX::_exit(1); } close $wfh; close $ewfh; $content_fh->blocking(0); #set non-blocking IO $error_fh->blocking(0); $self->{content_fh}=$content_fh; $self->{error_fh}=$error_fh; $self->{pid}=$pid; $self->{content}=$self->{ebuffer}=''; $self->{watch}= Glib::IO->add_watch(fileno($content_fh),[qw/hup err in/],\&receiving_cb,$self); $self->{ewatch}= Glib::IO->add_watch(fileno($error_fh), [qw/hup err in/],\&receiving_e_cb,$self); return $self; } sub receiving_e_cb { my $self=$_[2]; return 1 if read $self->{error_fh},$self->{ebuffer},1024,length($self->{ebuffer}); close $self->{error_fh}; while (waitpid(-1, WNOHANG)>0) {} #reap dead children return $self->{ewatch}=0; } sub receiving_cb { my $self=$_[2]; return 1 if read $self->{content_fh},$self->{content},1024,length($self->{content}); close $self->{content_fh}; while (waitpid(-1, WNOHANG)>0) {} #reap dead children $self->{pid}=$self->{sock}=$self->{watch}=undef; my $url= $self->{params}{url}; my $callback= $self->{params}{cb}; my $type; my $result=''; $url=$1 while $self->{ebuffer}=~m#^Location: (\w+://[^ ]+)#mg; $type=$1 while $self->{ebuffer}=~m#^ Content-Type: (.*)$#mg; ## $result=$1 while $self->{ebuffer}=~m#^ (HTTP/1\.\d+.*)$#mg; ## #warn $self->{ebuffer}; my $filename; while ($self->{ebuffer}=~m#^ Content-Disposition:\s*\w+\s*;\s*filename(\*)?=(.*)$#mgi) { $filename=$2; my $rfc5987=$1; #decode filename, not perfectly, but good enough (http://greenbytes.de/tech/tc2231/ is a good reference) $filename=~s#\\(.)#"\x00".ord($1)."\x00"#ge; my $enc='iso-8859-1'; if ($rfc5987 && $filename=~s#^([A-Za-z0-9_-]+)'\w*'##) {$enc=$1; $filename=::decode_url($filename)} #RFC5987 else { if ($filename=~s/^"(.*)"$/$1/) { $filename=~s#\x00(\d+)\x00#chr($1)#ge; $filename=~s#\\(.)#"\x00".ord($1)."\x00"#ge; } elsif ($filename=~m#[^A-Za-z0-9_.\x00-]#) {$filename=''} } $filename=~s#\x00(\d+)\x00#chr($1)#ge; $filename= eval {Encode::decode($enc,$filename)}; } my ($enc)= $self->{ebuffer}=~m#^ Content-Encoding:\s*(.*)#mg; if ($enc) { if ($enc eq 'gzip' && $gzip_ok) { my $gzipped= $self->{content}; IO::Uncompress::Gunzip::gunzip( \$gzipped, \$self->{content} ) or do {warn "simple_http_wget : gunzip failed: $IO::Uncompress::Gunzip::GunzipError\n"; $result='gunzip error';}; } else { warn "simple_http_wget : can't decode '$enc' encoding\n"; $result='encoded'; } } if ($result=~m#^HTTP/1\.\d+ 200 OK#) { my $response=\$self->{content}; if ($self->{params}{cache} && defined $$response) { GMB::Cache::add($url,{data=>$response,type=>$type,size=>length($$response),filename=>$filename}); } $callback->($$response,type=>$type,url=>$self->{params}{url},filename=>$filename); } else { warn "Error fetching $url : $result\n"; $callback->(undef,error=>$result); } return $self->{watch}=0; } sub progress { my $self=shift; my $length; $length=$1 while $self->{ebuffer}=~m/Content-Length:\s*(\d+)/ig; my $size= length $self->{content}; my $progress; if ($length && $size) { $progress= $size/$length; $progress=undef if $progress>1; } # $progress is undef or between 0 and 1 return $progress,$size; } sub abort { my $self=$_[0]; Glib::Source->remove($self->{watch}) if $self->{watch}; Glib::Source->remove($self->{ewatch}) if $self->{ewatch}; kill INT=>$self->{pid} if $self->{pid}; close $self->{content_fh} if defined $self->{content_fh}; close $self->{error_fh} if defined $self->{error_fh}; while (waitpid(-1, WNOHANG)>0) {} #reap dead children $self->{pid}=$self->{content_fh}=$self->{error_fh}=$self->{watch}=$self->{ewatch}=undef; } 1; gmusicbrowser-1.1.15~ds0.orig/flacheader.pm0000664000175000017500000001745012565212604020100 0ustar unit193unit193# Copyright (C) 2005-2009 Quentin Sculo # # 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 #http://flac.sourceforge.net/format.html package Tag::Flac; use strict; use warnings; use Encode qw(decode encode); use MIME::Base64; our @ISA=('Tag::OGG'); use constant { STREAMINFO => 0, PADDING => 1, APPLICATION => 2, SEEKTABLE => 3, VORBIS_COMMENT=> 4, CUESHEET => 5, PICTURE => 6, }; sub new { my ($class,$file)=@_; my $self=bless {}, $class; local $_; # check that the file exists unless (-e $file) { warn "File '$file' does not exist.\n"; return undef; } $self->{filename} = $file; $self->{startaudio}=0; #start of flac stream (in case an id3v2 tag is at the beginning of the file) my $fh=$self->_open or return undef; my $buffer; { last unless read($fh,$buffer,4)==4; if ($buffer=~m/^ID3/) { my $tag=Tag::ID3v2->new_from_file($self); $self->{startaudio}+=$tag->{size}; redo; } } unless ($buffer && $buffer eq 'fLaC') { warn "flac: Not a flac header\n"; $self->_close; return undef; } my $last; my @pictures; while ( !$last && read($fh,$buffer,4)==4 ) { $buffer=unpack 'N',$buffer; my $size=$buffer & 0xffffff; my $pos=tell $fh; my $type=($buffer >> 24) & 0x7f; $last=$buffer >>31; unless (read($fh,$buffer,$size)==$size) { warn "flac: Premature end of file\n"; $self->_close; return undef; } if ($type==STREAMINFO) {$self->{info}=_ReadInfo(\$buffer);} elsif ($type==VORBIS_COMMENT) {$self->{comments}=_UnpackComments($self,\$buffer); $self->{comment_offset}=$pos-4} elsif ($type==PICTURE) {my $pic=_ReadPicture(\$buffer); push @pictures,$pic if $pic;} } my $audiosize=(stat $fh)[7]-tell($fh); $self->_close; unless ($self->{info}) { warn "error, can't read file or not a valid flac file\n"; return undef; } $self->{info}{bitrate}= $self->{info}{seconds} ? $audiosize*8/$self->{info}{seconds} : 0; unless ($self->{comments}) { $self->{vorbis_string}='gmusicbrowser'; #FIXME $self->{CommentsOrder}=[]; $self->{comments}={}; } for my $pic (@pictures) { push @{ $self->{comments}{metadata_block_picture} }, $pic; push @{ $self->{CommentsOrder} }, 'metadata_block_picture', } return $self; } sub write_file { my $self=shift; local $_; my ($INfh,$OUTfh); my $pictures=''; if (my $list=$self->{comments}{metadata_block_picture}) { for my $pic (grep defined, @$list) { my $packet= _PackPicture($pic); my $head=pack 'N', (PICTURE<<24)+length $$packet; $pictures.= $head.$$packet; } @$list=(); #remove the pictures from vorbis comments } my $newcom_packref=_PackComments($self); my $fh=$self->_open or return undef; my $buffer; my $last; my $towrite='fLaC'; my $padding=0; seek $fh,$self->{startaudio} ,0; #skip extra tags return undef unless (read($fh,$buffer,4)==4 && $buffer eq 'fLaC'); while ( !$last && read($fh,$buffer,4)==4 ) { $buffer=unpack 'N',$buffer; my $size=$buffer & 0xffffff; my $type=($buffer >> 24) & 0x7f; $last=$buffer >>31; if ($type!=VORBIS_COMMENT && $type!=PADDING && $type!=PICTURE) { $buffer&=0x7fffffff; #set Last-metadata-block flag to 0 $towrite.=pack 'N',$buffer; unless (read($fh,$towrite,$size,length($towrite))==$size) { warn "flac: Premature end of file\n"; return undef; } } else {$padding+=$size+4; seek $fh,$size,1; } } $padding-= 4 + length($$newcom_packref) + length $pictures; my $header=VORBIS_COMMENT; my $inplace=($padding==0 || ($padding>3 && $padding<8192) ); if ($padding==0) {$header+=0x80;$padding='';} else { $padding=$inplace? $padding-4 : 256; $padding=pack "Nx$padding",((0x80+PADDING)<<24)+$padding; } $header=pack 'N',($header<<24)+length $$newcom_packref; if ($inplace) { $self->_close; $fh=$self->_openw or return undef; seek $fh,$self->{startaudio} ,0; print $fh $towrite.$pictures.$header.$$newcom_packref.$padding or warn $!; $self->_close; } else { my $tmpfh=$self->_openw(1) or return undef; if ($self->{startaudio}) { seek $fh,0,0; read($fh,$buffer,$self->{startaudio}); print $tmpfh $buffer or warn $!; } print $tmpfh $towrite.$pictures.$header.$$newcom_packref.$padding or warn $!; while (read($fh,$buffer,1048576)) { print $tmpfh $buffer or warn $!; } $self->_close; close $tmpfh; warn "replacing old file with new file.\n"; unlink $self->{filename} && rename $self->{filename}.'.TEMP',$self->{filename}; } %$self=(); #destroy the object to make sure it is not reused as many of its data are now invalid return 1; } sub _close { my $self=shift; close delete($self->{fileHandle}); } sub _ReadInfo { my $packref=$_[0]; my @v=unpack 'nn CnCn nCCN',$$packref; #A16 B16 C8 C16 D8 D16 E16 EEEEFFFG GGGGHHHH H32 I128 #A <16> The minimum block size (in samples) used in the stream #B <16> The maximum block size (in samples) used in the stream. (Minimum blocksize == maximum blocksize) implies a fixed-blocksize stream. #C <24> The minimum frame size (in bytes) used in the stream. May be 0 to imply the value is not known. #D <24> The maximum frame size (in bytes) used in the stream. May be 0 to imply the value is not known. #E <20> Sample rate in Hz. Though 20 bits are available, the maximum sample rate is limited by the structure of frame headers to 1048570Hz. Also, a value of 0 is invalid. #F <3> (number of channels)-1. FLAC supports from 1 to 8 channels #G <5> (bits per sample)-1. FLAC supports from 4 to 32 bits per sample. Currently the reference encoder and decoders only support up to 24 bits per sample #H <36> Total samples in stream. 'Samples' means inter-channel sample, i.e. one second of 44.1Khz audio will have 44100 samples regardless of the number of channels. A value of zero here means the number of total samples is unknown. #I <128> MD5 signature of the unencoded audio data my %info; $info{min_block_size}=$v[0]; $info{max_block_size}=$v[1]; $info{min_frame_size}=($v[2]<<16)+$v[3]; $info{max_frame_size}=($v[4]<<16)+$v[5]; $info{rate}=($v[6]<<4)+($v[7]>>4); $info{channels}=1+( ($v[7] & 0b1110)>>1 ); $info{bit_per_sample}=1+( ($v[7] & 0b1)<<5 )+( $v[8] >>4 ); $info{seconds}=( $v[9]+($v[8] & 0b1111)*2**32 )/$info{rate}; return \%info; } sub _ReadPicture { my $packref=$_[0]; my $ret; eval { my ($type,$mime,$desc,undef,undef,undef,undef,$data) =unpack 'N N/a N/a NNNN N/a',$$packref; $ret=[$mime,$type,$desc,$data]; }; if ($@) { warn "invalid picture block - skipped\n"; } return $ret; } sub _PackPicture { my $pic=shift; if (!ref $pic) { my $packet=decode_base64($pic); return \$packet; } my ($mime,$type,$desc,$data)=@$pic; my $packet= pack 'N N/a N/a NNNN N/a', ($type||0),$mime,$desc,0,0,0,0, $data; return \$packet; } sub _UnpackComments { my ($self,$packref)=@_; my ($vstring,@comlist)= eval { unpack 'V/a V/(V/a)',$$packref; }; if ($@) { warn "Comments corrupted\n"; return undef; } $self->{vorbis_string}=$vstring; my %comments; for my $kv (@comlist) { unless ($kv=~m/^([^=]+)=(.*)$/s) { warn "comment invalid - skipped\n"; next; } my $key=$1; my $val=decode('utf-8', $2); #warn "$key = $val\n"; push @{ $comments{lc$key} },$val; push @{$self->{CommentsOrder}}, $key; } return \%comments; } sub _PackComments { my $self=$_[0]; my @comments; my %count; for my $key ( @{$self->{CommentsOrder}} ) { my $nb=$count{lc$key}++ || 0; my $val=$self->{comments}{lc$key}[$nb]; next unless defined $val; $key=encode('ascii',$key); $key=~tr/\x20-\x7D/?/c; $key=~tr/=/?/; #replace characters that are not allowed by '?' push @comments,$key.'='.encode('utf8',$val); } my $packet=pack 'V/a* V (V/a*)*',$self->{vorbis_string},scalar @comments, @comments; #$packet.="\x01"; #framing_flag #gstreamer doesn't like it and not needed anyway return \$packet; } 1; gmusicbrowser-1.1.15~ds0.orig/simple_http_AE.pm0000664000175000017500000001014212565212605020707 0ustar unit193unit193# Copyright (C) 2010-2011 Quentin Sculo # # 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 package Simple_http; use strict; use warnings; use AnyEvent::HTTP; my $UseCache= *GMB::Cache::add{CODE}; my $gzip_ok; BEGIN { eval { require IO::Uncompress::Gunzip; $gzip_ok=1; }; } sub get_with_cb { my $self=bless {}; my %params=@_; $self->{params}=\%params; my ($callback,$url,$post)=@params{qw/cb url post/}; delete $params{cache} unless $UseCache; if (my $cached= $params{cache} && GMB::Cache::get($url)) { warn "cached result\n" if $::debug; Glib::Timeout->add(10,sub { $callback->( ${$cached->{data}}, type=>$cached->{type}, filename=>$cached->{filename}, ); 0}); return $self; } warn "simple_http_AE : fetching $url\n" if $::debug; my $proxy= $::Options{Simplehttp_Proxy} ? $::Options{Simplehttp_ProxyHost}.':'.($::Options{Simplehttp_ProxyPort}||3128) : $ENV{http_proxy}; AnyEvent::HTTP::set_proxy($proxy); my %headers; $headers{'Content-Type'}= 'application/x-www-form-urlencoded; charset=utf-8' if $post; $headers{'Referer'}= $params{referer} if $params{referer}; $headers{'User-Agent'}= $params{user_agent} || 'Mozilla/5.0'; $headers{Accept}= $params{'accept'} || ''; $headers{'Accept-Encoding'}= $gzip_ok ? 'gzip' : ''; my $method= $post ? 'POST' : 'GET'; my @args; push @args, body => $post if $post; if ($params{progress}) # enable progress info via progress() { push @args, on_header=> sub { $self->{content_length}=$_[0]{"content-length"}; $self->{content}=''; 1; }, on_body => sub { $self->{content}.= $_[0]; 1; }; } $self->{request}= http_request( $method, $url, @args, headers=>\%headers, sub { $self->finished(@_) } ); return $self; } sub finished { my ($self,$response,$headers)=@_; $response= $self->{content} if exists $self->{content}; my $url= $self->{params}{url}; my $callback= $self->{params}{cb}; delete $_[0]{request}; #warn "$_=>$headers->{$_}\n" for sort keys %$headers; my $filename; if ($headers->{'content-disposition'} && $headers->{'content-disposition'}=~m#^\s*\w+\s*;\s*filename(\*)?=(.*)$#mgi) { $filename=$2; my $rfc5987=$1; #decode filename, not perfectly, but good enough (http://greenbytes.de/tech/tc2231/ is a good reference) $filename=~s#\\(.)#"\x00".ord($1)."\x00"#ge; my $enc='iso-8859-1'; if ($rfc5987 && $filename=~s#^([A-Za-z0-9_-]+)'\w*'##) {$enc=$1; $filename=::decode_url($filename)} #RFC5987 else { if ($filename=~s/^"(.*)"$/$1/) { $filename=~s#\x00(\d+)\x00#chr($1)#ge; $filename=~s#\\(.)#"\x00".ord($1)."\x00"#ge; } elsif ($filename=~m#[^A-Za-z0-9_.\x00-]#) {$filename=''} } $filename=~s#\x00(\d+)\x00#chr($1)#ge; $filename= eval {Encode::decode($enc,$filename)}; } if (my $enc=$headers->{'content-encoding'}) { if ($enc eq 'gzip' && $gzip_ok) { my $gzipped= $response; IO::Uncompress::Gunzip::gunzip( \$gzipped, \$response ) or do {warn "simple_http : gunzip failed: $IO::Uncompress::Gunzip::GunzipError\n"; $headers->{Status}='gunzip error'; $headers->{Reason}='';}; } else { warn "simple_http : can't decode '$enc' encoding\n"; $headers->{Status}='encoded'; $headers->{Reason}=''; } } if ($headers->{Reason} eq 'OK') # and $headers->{Status} == 200 ? { my $type= $headers->{'content-type'}; if ($self->{params}{cache} && defined $response) { GMB::Cache::add($url,{data=>\$response,type=>$type,size=>length($response),filename=>$filename}); } $callback->($response,type=>$type,url=>$self->{params}{url},filename=>$filename); } else { my $error= $headers->{Status}.' '.$headers->{Reason}; warn "Error fetching $url : $error\n"; $callback->(undef,error=>$error); } } sub progress { my $self=shift; my $length= $self->{content_length}; return $length,0 unless exists $self->{content}; my $size= length $self->{content}; my $progress; if ($length && $size) { $progress= $size/$length; $progress=undef if $progress>1; } # $progress is undef or between 0 and 1 return $progress,$size; } sub abort { delete $_[0]{request}; } 1; gmusicbrowser-1.1.15~ds0.orig/plugins/0000775000175000017500000000000012565212605017137 5ustar unit193unit193gmusicbrowser-1.1.15~ds0.orig/plugins/albuminfo.pm0000664000175000017500000010620612565212605021456 0ustar unit193unit193# Copyright (C) 2011 Øystein Tråsdahl # # 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 =for gmbplugin ALBUMINFO name Albuminfo title Albuminfo plugin version 0.2 author Øystein Tråsdahl (based on the Artistinfo plugin) desc Retrieves album-relevant information (review etc.) from allmusic.com. =cut # TODO: # - Create local links in the review that can be used for contextmenus and filtering. # - Consider searching google instead, or add google search if amg fails. package GMB::Plugin::ALBUMINFO; use strict; use warnings; use utf8; require $::HTTP_module; use Gtk2::Gdk::Keysyms; use base 'Gtk2::Box'; use constant { OPT => 'PLUGIN_ALBUMINFO_', AMG_SEARCH_URL => 'http://www.allmusic.com/search/album/', AMG_ALBUM_URL => 'http://www.allmusic.com/album/', }; my @showfields = ( {short => 'rec_date', long => _"Recording date", active => 1, multi => 0, defaultshow => 1}, {short => 'rls_date', long => _"Release date", active => 1, multi => 0, defaultshow => 1}, {short => 'time', long => _"Running time", active => 0, multi => 0, defaultshow => 0}, {short => 'rating', long => _"Rating", active => 1, multi => 0, defaultshow => 1}, {short => 'genre', long => _"Genres", active => 1, multi => 1, defaultshow => 0}, {short => 'mood', long => _"Moods", active => 1, multi => 1, defaultshow => 0}, {short => 'style', long => _"Styles", active => 1, multi => 1, defaultshow => 0}, {short => 'theme', long => _"Themes", active => 1, multi => 1, defaultshow => 0}, ); ::SetDefaultOptions(OPT, PathFile => "~/.config/gmusicbrowser/review/%a - %l.txt", ShowCover => 1, CoverSize => 100, StyleAsGenre => 0, mass_download => 'missing', ); ::SetDefaultOptions(OPT, 'Show'.$_->{short} => $_->{defaultshow}) for (@showfields); delete $::Options{OPT.'Column'.$_} for 0..3; #remove old column options my $albuminfowidget = { class => __PACKAGE__, tabicon => 'plugin-albuminfo', tabtitle => _"Albuminfo", schange => sub { $_[0]->song_changed }, group => 'Play', autoadd_type => 'context page text', }; my %Columns= ( album => { name=> _"Album", storecol=>0, width=>130, }, artist => { name=> _"Artist", storecol=>1, width=>130, }, genre => { name=> _"Genre", storecol=>2, width=>110, }, year => { name=> _"Year", storecol=>3, width=>50, }, ); my @towrite; # Needed to avoid progress bar overflow in save_fields when called from mass_download my $save_fields_lock = 0; # Needed to avoid progress bar overflow in save_fields when called from mass_download sub Start { Layout::RegisterWidget(PluginAlbuminfo => $albuminfowidget); } sub Stop { Layout::RegisterWidget(PluginAlbuminfo => undef); } sub prefbox { my $frame_cover = Gtk2::Frame->new(' '._("Album cover").' '); my $spin_picsize = ::NewPrefSpinButton(OPT.'CoverSize',50,500, step=>5, page=>10, text=>_"Cover Size : ", cb=>sub { ::HasChanged('plugin_albuminfo_option_pic'); } ); my $chk_picshow = ::NewPrefCheckButton(OPT.'ShowCover'=>_"Show", widget => $spin_picsize, cb=>sub { ::HasChanged('plugin_albuminfo_option_pic'); }); # my $btn_amg = ::NewIconButton('plugin-artistinfo-allmusic',undef, sub {::main::openurl("http://www.allmusic.com/"); },'none',_"Open allmusic.com in your web browser"); # my $hbox_picsize = ::Hpack($spin_picsize, '-', $btn_amg); $frame_cover->add($chk_picshow); my $frame_review = Gtk2::Frame->new(_" Review "); my $entry_path = ::NewPrefEntry(OPT.'PathFile' => _"Save album info in:", width=>40); my $lbl_preview = Label::Preview->new(event=>'CurSong Option', noescape=>1, wrap=>1, preview=>sub { return '' unless defined $::SongID; my $t = ::pathfilefromformat( $::SongID, $::Options{OPT.'PathFile'}, undef,1); $t = ::filename_to_utf8displayname($t) if $t; $t = $t ? ::PangoEsc(_("example : ").$t) : "".::PangoEsc(_"invalid pattern").""; return ''.$t.''; }); my $chk_autosave = ::NewPrefCheckButton(OPT.'AutoSave'=>_"Auto-save positive finds", cb=>sub { ::HasChanged('plugin_albuminfo_option_save'); }); $frame_review->add(::Vpack($entry_path,$lbl_preview,$chk_autosave)); my $frame_fields = Gtk2::Frame->new(_" Fields "); my $chk_join = ::NewPrefCheckButton(OPT.'StyleAsGenre' => _"Include Styles in Genres", tip=>_"Allmusic uses both Genres and Styles to describe albums. If you use only Genres, you may want to include Styles in allmusic's list of Genres."); my @chk_fields; for my $field (qw(genre mood style theme)) { push(@chk_fields, ::NewPrefCheckButton(OPT.$field=>_(ucfirst($field)."s"), tip=>_"Note: inactive fields must be enabled by the user in the 'Fields' tab in Settings")); $chk_fields[-1]->set_sensitive(0) unless Songs::FieldEnabled($field); } my ($radio_add,$radio_rpl) = ::NewPrefRadio(OPT.'ReplaceFields',[_"Add to existing values",1, _"Replace existing values",0]); my $chk_saveflds = ::NewPrefCheckButton(OPT.'SaveFields'=>_"Auto-save fields with data from allmusic", widget=>::Vpack(\@chk_fields, $radio_add, $radio_rpl), tip=>_"Save selected fields for all tracks on the same album whenever album data is loaded from allmusic or from file."); $frame_fields->add(::Vpack($chk_join, $chk_saveflds)); my $frame_layout = Gtk2::Frame->new(_" Context pane layout "); my @chk_show = (); for my $f (@showfields) { push(@chk_show, ::NewPrefCheckButton(OPT.'Show'.$f->{short} => $f->{long})) if $f->{active}; } $frame_layout->add(::Hpack(@chk_show)); my $btn_download = Gtk2::Button->new(_"Download"); $btn_download->set_tooltip_text(_"Fields will be saved according to the settings above. Albuminfo files will be re-read if there are fields to be saved and you choose 'albums missing reviews' in the combo box."); my $cmb_download = ::NewPrefCombo(OPT.'mass_download', {all=>_"entire collection", missing=>_"albums missing reviews"}, text=>_"album information now for"); $btn_download->signal_connect(clicked => \&mass_download); return ::Vpack($frame_cover, $frame_review, $frame_fields, $frame_layout, [$btn_download,$cmb_download]); } sub mass_download { my $self = ::find_ancestor($_[0], __PACKAGE__); $self->{aIDs} = Songs::Get_all_gids('album'); $self->{end} = scalar(@{$self->{aIDs}}); $self->{progress} = 0; $self->{abort} = 0; ::Progress('albuminfo', end=>$self->{end}, aborthint=>_"Stop fetching albuminfo", title=>_"Fetching albuminfo", abortcb=>sub {$self->cancel}, bartext=>'$current / $end', ); ::IdleDo('9_download_one'.$self, undef, \&download_one, $self); } sub download_one { my ($self) = @_; ::Progress('albuminfo', current=>$self->{progress}, ); return if $self->{progress} >= $self->{end} || $self->{abort}; my $aID = $self->{aIDs}->[$self->{progress}++]; warn "Albuminfo: mass download in progress... $self->{progress}/$self->{end}\n" if $::debug; my $IDs = Songs::MakeFilterFromGID('album',$aID)->filter(); my $ID = $IDs->[0]; # Need a track (any track) from the album: pick the first in the list. my $album = Songs::Get($ID, 'album'); unless ($album) {::IdleDo('9_download_one'.$self, undef, \&download_one, $self); return} my $file = ::pathfilefromformat($ID, $::Options{OPT.'PathFile'}, undef, 1); if ($::Options{OPT.'mass_download'} ne 'all' && $file && -r $file) { if ($::Options{OPT.'SaveFields'}) { ::IdleDo('9_load_albuminfo'.$self,undef,\&load_file,$self,$ID,$file,1,\&download_one); } else { ::IdleDo('9_download_one'.$self, undef, \&download_one, $self); } } else { my $url = AMG_SEARCH_URL.::url_escapeall($album); warn "Albuminfo: fetching search results from $url\n" if $::debug; $self->{waiting} = Simple_http::get_with_cb(url=>$url, cache=>1, cb=>sub {$self->load_search_results($ID,1,\&download_one,@_)}); } } ##################################################################### # # Section: albuminfowidget (the Context pane tab). # ##################################################################### sub new { my ($class,$options) = @_; my $self = bless(Gtk2::VBox->new(0,0), $class); $self->{$_} = $options->{$_} for qw/group/; my $fontsize = $self->style->font_desc; $self->{fontsize} = $fontsize->get_size() / Gtk2::Pango->scale; # Heading: cover and album info. my $cover=Gtk2::HBox->new(0,0); my $group=$options->{group}; my $cover_create= sub { my $box=shift; $box->remove($_) for $box->get_children; return unless $::Options{OPT.'ShowCover'}; my $child = Layout::NewWidget("Cover", {group=>$group, forceratio=>1, maxsize=>$::Options{OPT.'CoverSize'}, xalign=>0, tip=>_"Click to show larger image", click1=>\&cover_popup, click3=>sub {::PopupAAContextMenu({self=>$_[0], field=>'album', ID=>$::SongID, gid=>Songs::Get_gid($::SongID,'album'), mode=>'P'});} }); $child->show_all; $box->add($child); }; ::Watch($cover, plugin_albuminfo_option_pic=> $cover_create); $cover_create->($cover); my $statbox = Gtk2::VBox->new(0,0); for my $name (qw/Ltitle Lstats/) { my $l = Gtk2::Label->new(''); $self->{$name} = $l; $l->set_justify('center'); if ($name eq 'Ltitle') { $l->set_line_wrap(1); $l->set_ellipsize('end'); } $statbox->pack_start($l,0,0,2); } $self->{ratingpic} = Gtk2::Image->new(); $statbox->pack_start($self->{ratingpic},0,0,2); # "Refresh", "save" and "search" buttons my $refreshbutton = ::NewIconButton('gtk-refresh', undef, sub { song_changed($_[0],'force'); }, "none", _"Refresh"); my $savebutton = ::NewIconButton('gtk-save', undef, sub {my $self=::find_ancestor($_[0],__PACKAGE__); save_review(::GetSelID($self),$self->{fields})}, "none", _"Save review"); $savebutton->show_all; $savebutton->set_no_show_all(1); my $update_savebutton_visible= sub { $_[0]->set_visible( !$::Options{OPT.'AutoSave'} ); }; ::Watch( $savebutton, plugin_albuminfo_option_save=> $update_savebutton_visible); $update_savebutton_visible->($savebutton); my $searchbutton = Gtk2::ToggleButton->new(); $searchbutton->set_relief('none'); $searchbutton->add(Gtk2::Image->new_from_stock('gtk-find','menu')); $searchbutton->signal_connect(clicked => sub {my $self=::find_ancestor($_[0],__PACKAGE__); if ($_[0]->get_active()) {$self->manual_search()} else {$self->song_changed()}}); my $buttonbox = Gtk2::HBox->new(); $buttonbox->pack_end($searchbutton,0,0,0); $buttonbox->pack_end($savebutton,0,0,0); $buttonbox->pack_end($refreshbutton,0,0,0); $statbox->pack_end($buttonbox,0,0,0); my $stateventbox = Gtk2::EventBox->new(); # To catch mouse events $stateventbox->add($statbox); $stateventbox->{group}= $options->{group}; $stateventbox->signal_connect(button_press_event => sub {my ($stateventbox, $event) = @_; return 0 unless $event->button == 3; my $ID = ::GetSelID($stateventbox); ::PopupAAContextMenu({ self=>$stateventbox, mode=>'P', field=>'album', ID=>$ID, gid=>Songs::Get_gid($ID,'album') }) if defined $ID; return 1; } ); my $coverstatbox = Gtk2::HBox->new(0,0); $coverstatbox->pack_start($cover,0,0,0); $coverstatbox->pack_end($stateventbox,1,1,5); # For the review: a TextView in a ScrolledWindow in a HBox my $textview = Gtk2::TextView->new(); $self->signal_connect(map => \&song_changed); $textview->set_cursor_visible(0); $textview->set_wrap_mode('word'); $textview->set_pixels_above_lines(2); $textview->set_editable(0); $textview->set_left_margin(5); $textview->set_has_tooltip(1); $textview->signal_connect(button_release_event => \&button_release_cb); $textview->signal_connect(motion_notify_event => \&update_cursor_cb); $textview->signal_connect(visibility_notify_event => \&update_cursor_cb); $textview->signal_connect(query_tooltip => \&update_cursor_cb); my $sw = Gtk2::ScrolledWindow->new(); $sw->add($textview); $sw->set_shadow_type('none'); $sw->set_policy('automatic','automatic'); my $infoview = Gtk2::HBox->new(); $infoview->set_spacing(0); $infoview->pack_start($sw,1,1,0); $infoview->show_all(); # Manual search layout my $searchview = Gtk2::VBox->new(); $self->{search} = my $search = Gtk2::Entry->new(); $search->set_tooltip_text(_"Enter album name"); my $Bsearch = ::NewIconButton('gtk-find', _"Search"); my $Bok = Gtk2::Button->new_from_stock('gtk-ok'); my $Bcancel = Gtk2::Button->new_from_stock('gtk-cancel'); $Bok ->set_size_request(80, -1); $Bcancel->set_size_request(80, -1); # Year is a 'Glib::String' to avoid printing "0" when year is missing. Caveat: will give wrong sort order for albums released before year 1000 or after year 9999 :) my $store = Gtk2::ListStore->new(('Glib::String')x5,'Glib::UInt'); # Album, Artist, Label, Year, URL, Sort order. my $treeview = Gtk2::TreeView->new($store); my %coladded; for my $col ( split(/\s+/,$::Options{OPT.'Columns'}||''), qw/album artist genre year/ ) { my $coldef= $Columns{$col}; next unless $coldef; next if $coladded{$col}++; #only add a column once my $colopt= $::Options{OPT.'Column_'.$col} || {}; my $column = Gtk2::TreeViewColumn->new_with_attributes($coldef->{name}, Gtk2::CellRendererText->new(), text=>$coldef->{storecol}); $column->{key}=$col; $column->set_sort_column_id($coldef->{storecol}); $column->set_expand(1); $column->set_resizable(1); $column->set_reorderable(1); $column->set_sizing('fixed'); $column->set_fixed_width( $colopt->{width}||$coldef->{width}||100 ); $column->set_visible(!$colopt->{hide}); $treeview->append_column($column); # Recreate the header label to be able to catch mouse clicks in column header: my $label = Gtk2::Label->new($coldef->{name}); $column->set_widget($label); $label->show; my $button = $label->get_ancestor('Gtk2::Button'); # The header label is attached to a button by Gtk $button->signal_connect(button_press_event => \&treeview_click_cb, $col) if $button; } $treeview->set_rules_hint(1); $treeview->signal_connect(row_activated => \&entry_selected_cb); my $scrwin = Gtk2::ScrolledWindow->new(); $scrwin->set_policy('automatic', 'automatic'); $scrwin->add($treeview); $searchview->add( ::Vpack(['_', $search, $Bsearch], '_', $scrwin, '-', ['-', $Bcancel, $Bok]) ); $search ->signal_connect(activate => \&new_search ); # Pressing Enter in the search entry. $Bsearch->signal_connect(clicked => \&new_search ); $Bok ->signal_connect(clicked => \&entry_selected_cb ); $Bcancel->signal_connect(clicked => \&song_changed ); $scrwin ->signal_connect(key_press_event => sub {entry_selected_cb($_[0]) if $_[1]->keyval == $Gtk2::Gdk::Keysyms{Return};}); $scrwin ->signal_connect(key_press_event => sub {song_changed($_[0]) if $_[1]->keyval == $Gtk2::Gdk::Keysyms{Escape};}); $searchview->show_all(); # Must call it once now before $searchview->set_no_show_all(1) disables it. $searchview->set_no_show_all(1); # GMB sometimes calls $plugin->show_all(). We then want only infoview to show. $searchview->hide(); # Pack it all into self (a VBox) $self->pack_start($coverstatbox,0,0,0); $self->pack_start($infoview,1,1,0); $self->pack_start($searchview,1,1,0); $searchview->signal_connect(show => sub {$searchbutton->set_active(1)}); $searchview->signal_connect(hide => sub {$searchbutton->set_active(0)}); $self->signal_connect(destroy => sub {$_[0]->cancel()}); # Save elements that will be needed in other methods. $self->{buffer} = $textview->get_buffer(); $self->{treeview} = $treeview; $self->{infoview} = $infoview; $self->{searchview} = $searchview; $self->{SaveOptions}= \&SaveOptions; #called when widget is removed or when saving options return $self; } sub SaveOptions { my $self = shift; my @cols= $self->{treeview}->get_columns; $::Options{OPT.'Columns'}= join ' ', map $_->{key}, @cols; for my $col (@cols) { my $colopt= $::Options{OPT.'Column_'.$col->{key}}= {}; $colopt->{width}= $col->get_width; $colopt->{hide}=1 if !$col->get_visible; } } # Called when headers in the results table in manual search are clicked sub treeview_click_cb { my ($button, $event, $colid) = @_; my $treeview = $button->parent; if ($event->button == 1) { my ($sortid,$order) = $treeview->get_model->get_sort_column_id(); my $storecol= $Columns{$colid}{storecol}; if ($sortid == $storecol && $order eq 'descending') { $treeview->get_model->set_sort_column_id(5,'ascending'); # After third click on column header: return to AMG sort order (default). return ::TRUE; } } elsif ($event->button == 3) { my $menu=::BuildChoiceMenu( { map { ($_=>$Columns{$_}{name}) } keys %Columns }, #hash of colkey => name reverse=>1, args => {treeview=>$treeview}, check=> sub { [map $_->{key}, grep $_->get_visible, $_[0]{treeview}->get_columns]; }, #list of visible columns code => sub { my ($args,$key)=@_; my @cols= $args->{treeview}->get_columns; my ($col)= grep $_->{key} eq $key, @cols; $col->set_visible( !$col->get_visible ) if $col; $cols[0]->set_visible(1) unless grep $_->get_visible, @cols; #make sure one column is visible }, ); $menu->show_all; $menu->popup(undef,undef,undef,undef,$event->button,$event->time); return ::TRUE; } return ::FALSE; # Let Gtk handle it } sub update_cursor_cb { my $textview = $_[0]; my (undef,$wx,$wy,undef) = $textview->window->get_pointer(); my ($x,$y) = $textview->window_to_buffer_coords('widget',$wx,$wy); my $iter = $textview->get_iter_at_location($x,$y); my $cursor = 'xterm'; for my $tag ($iter->get_tags()) { $cursor = 'hand2' if $tag->{tip}; $textview->set_tooltip_text($tag->{tip} || ''); } return if ($textview->{cursor} || '') eq $cursor; $textview->{cursor} = $cursor; $textview->get_window('text')->set_cursor(Gtk2::Gdk::Cursor->new($cursor)); } # Mouse button pressed in textview. If link: open url in browser. sub button_release_cb { my ($textview,$event) = @_; my $self = ::find_ancestor($textview,__PACKAGE__); return ::FALSE unless $event->button == 1; my ($x,$y) = $textview->window_to_buffer_coords('widget',$event->x, $event->y); my $iter = $textview->get_iter_at_location($x,$y); for my $tag ($iter->get_tags) { if ($tag->{url}) { ::main::openurl($tag->{url}); last; } elsif ($tag->{field}) { my $field= $tag->{field} eq 'year' ? 'year' : '+'.$tag->{field}; # prepend + for multi-value fields : Genre, Mood, Style, Theme my $aID = Songs::Get_gid(::GetSelID($self),'album'); Songs::Set(Songs::MakeFilterFromGID('album', $aID)->filter(), [$field => $tag->{val}]); } } return ::FALSE; } sub cover_popup { my ($self, $event) = @_; my $menu = Gtk2::Menu->new(); $menu->modify_bg('GTK_STATE_NORMAL',Gtk2::Gdk::Color->parse('black')); # black bg for the cover-popup my $picsize = 400; my $ID = ::GetSelID($self); my $aID = Songs::Get_gid($ID,'album'); if (my $img = AAPicture::newimg(album=>$aID,$picsize)) { my $apic = Gtk2::MenuItem->new(); $apic->modify_bg('GTK_STATE_SELECTED',Gtk2::Gdk::Color->parse('black')); $apic->add($img); $apic->show_all(); $menu->append($apic); $menu->popup (undef, undef, undef, undef, $event->button, $event->time); return 1; } else { return 0; } } # Print warnings in the text buffer sub print_warning { my ($self,$text) = @_; my $buffer = $self->{buffer}; $buffer->set_text(""); my $iter = $buffer->get_start_iter(); my $fontsize = $self->{fontsize}; my $tag_noresults = $buffer->create_tag(undef,justification=>'center',font=>$fontsize*2,foreground_gdk=>$self->style->text_aa("normal")); $buffer->insert_with_tags($iter,"\n$text",$tag_noresults); $buffer->set_modified(0); } # Print review (and additional data) in the text buffer sub print_review { my ($self) = @_; unless ($self->{fields}{url}) {$self->print_warning(_"No review found"); return} my $buffer = $self->{buffer}; my $fields = $self->{fields}; $buffer->set_text(""); my $fontsize = $self->{fontsize}; my $tag_h2 = $buffer->create_tag(undef, font=>$fontsize+1, weight=>Gtk2::Pango::PANGO_WEIGHT_BOLD); my $tag_b = $buffer->create_tag(undef, weight=>Gtk2::Pango::PANGO_WEIGHT_BOLD); my $tag_i = $buffer->create_tag(undef, style=>'italic'); my $iter = $buffer->get_start_iter(); for my $f (@showfields) { if ($fields->{$f->{short}} && $::Options{OPT.'Show'.$f->{short}} && $f->{active}) { $buffer->insert_with_tags($iter, "$f->{long}: ",$tag_b); if ($f->{multi}) { # genres, moods, styles and themes. my @old = Songs::Get_list(::GetSelID($self), $f->{short}); my @amg = @{$fields->{$f->{short}}}; my $i = 0; for my $val (@amg) { if (grep {lc($_) eq lc($val)} @old) { $buffer->insert($iter, $val); } else { # val doesn't exist in local db => create link to save it. my $tag = $buffer->create_tag(undef, foreground=>"#4ba3d2", underline=>'single'); $tag->{field} = $f->{short}; $tag->{val} = $val; $tag->{tip} = ::__x( _"Add {value} to {field} for all tracks on this album.", value=>$val, field=> lc($f->{long})); $buffer->insert_with_tags($iter, $val, $tag); } $buffer->insert($iter,", ") if ++$i < scalar(@amg); } } elsif ($f->{short} eq 'rls_date') { $fields->{rls_date} =~ m|(\d{4})|; if (defined $1 && $1 != Songs::Get(::GetSelID($self), 'year')) { # AMG year differs from local year => create link to correct. my $tag = $buffer->create_tag(undef, foreground=>"#4ba3d2", underline=>'single'); $tag->{field} = 'year'; $tag->{val} = $1; $tag->{tip} = ::__x( _"Set {year} as year for all tracks on this album.", year=>$1 ); $buffer->insert_with_tags($iter, $fields->{rls_date}, $tag); } else { $buffer->insert($iter,"$fields->{rls_date}"); } } elsif ($f->{short} eq 'rating') { $buffer->insert_pixbuf($iter, Songs::Stars($fields->{rating}, 'rating')); } else { $buffer->insert($iter,"$fields->{$f->{short}}"); } $buffer->insert($iter, "\n"); } } if ($fields->{review}) { $buffer->insert_with_tags($iter, "\n"._("Review")."\n", $tag_h2); $buffer->insert_with_tags($iter, ::__x(_"by {author}", author=>$fields->{author})."\n", $tag_i); $buffer->insert($iter,$fields->{review}); } else { $buffer->insert_with_tags($iter,"\n"._("No review written.")."\n",$tag_h2); } $buffer->insert($iter, "\n\n"); my $tag_a = $buffer->create_tag(undef, foreground=>"#4ba3d2", underline=>'single'); $tag_a->{url} = $fields->{url}; $tag_a->{tip} = $fields->{url}; $buffer->insert_with_tags($iter,_"Lookup at allmusic.com",$tag_a); $buffer->set_modified(0); } ##################################################################### # # Section: "Manual search" window. # ##################################################################### sub manual_search { my $self = shift; $self->{infoview}->hide(); $self->{searchview}->show(); my $gid = Songs::Get_gid(::GetSelID($self), 'album'); $self->{search}->set_text(Songs::Gid_to_Get('album', $gid)); $self->new_search(); } sub new_search { my $self = ::find_ancestor($_[0], __PACKAGE__); # $_[0] can be a button or gtk-entry. Ancestor is an albuminfo object. my $album = $self->{search}->get_text(); $album =~ s|^\s+||; $album =~ s|\s+$||; # remove leading and trailing spaces return if $album eq ''; my $url = AMG_SEARCH_URL.::url_escapeall($album); $self->cancel(); $self->{treeview}->get_model->clear; warn "Albuminfo: fetching search results from $url.\n" if $::debug; $self->{waiting} = Simple_http::get_with_cb(cb=>sub {$self->print_results(@_)},url=>$url, cache=>1); } sub print_results { my ($self,$html,%prop) = @_; delete $self->{waiting}; my $result = parse_amg_search_results($html, $prop{type}); # result is a ref to an array of hash refs my $store= $self->{treeview}->get_model; $store->set_sort_column_id(5, 'ascending'); for (@$result) { $store->set($store->append, 0,$_->{album}, 1,$_->{artist}, 2,$_->{genres}, 3,$_->{year}, 4,$_->{url}, 5,$_->{order}); } } sub entry_selected_cb { my $self = ::find_ancestor($_[0], __PACKAGE__); # $_[0] may be the TreeView or the 'OK' button. Ancestor is an albuminfo object. my ($path, $column) = $self->{treeview}->get_cursor(); unless (defined $path) {$self->{searchview}->hide(); $self->{infoview}->show(); return} # The user may click OK before selecting an album my $store = $self->{treeview}->get_model; my $url = $store->get($store->get_iter($path),4); warn "Albuminfo: fetching review from $url\n" if $::debug; $self->cancel(); $self->{waiting} = Simple_http::get_with_cb(cb=>sub {$self->{searchview}->hide(); $self->{infoview}->show(); $self->load_review(::GetSelID($self),0,undef,$url,@_)}, url=>$url, cache=>1); } ##################################################################### # # Section: loading and parsing review. Saving review and fields. # ##################################################################### sub song_changed { my ($widget,$force) = @_; my $self = ::find_ancestor($widget, __PACKAGE__); return unless $self->mapped() || $::Options{OPT.'SaveFields'}; $self->{infoview}->show(); $self->{searchview}->hide(); my $ID = ::GetSelID($self); return unless defined $ID; my $aID = Songs::Get_gid($ID,'album'); return unless $aID; if (!$self->{aID} || $aID != $self->{aID} || $force) { # Check if album has changed or a forced update is required. $self->{aID} = $aID; $self->{album} = Songs::Gid_to_Get("album",$aID); $self->album_changed($ID, $aID, $force); } else { # happens for example when song properties are edited, so we need to repaint the widget. ::IdleDo('9_refresh_albuminfo'.$self, undef, sub { $self->update_titlebox($aID); length($self->{album}) ? $self->print_review() : $self->print_warning(_"Unknown album") }); } } sub album_changed { my ($self,$ID,$aID,$force) = @_; $self->cancel(); $self->update_titlebox($aID); my $album = ::url_escapeall(Songs::Gid_to_Get("album",$aID)); unless (length($album)) {$self->print_warning(_"Unknown album"); return} my $url = AMG_SEARCH_URL.$album; $self->print_warning(_"Loading..."); unless ($force) { # Try loading from file. my $file = ::pathfilefromformat( ::GetSelID($self), $::Options{OPT.'PathFile'}, undef, 1 ); if ($file && -r $file) { ::IdleDo('9_load_albuminfo'.$self,undef,\&load_file,$self,$ID,$file,0,undef); return; } } warn "Albuminfo: fetching search results from $url\n" if $::debug; $self->{waiting} = Simple_http::get_with_cb(cb=>sub {$self->load_search_results($ID,0,undef,@_)}, url=>$url, cache=>1); } sub update_titlebox { my ($self,$aID) = @_; my $rating = AA::Get("rating:average",'album',$aID); $self->{rating} = int($rating+0.5); $self->{ratingrange} = AA::Get("rating:range", 'album',$aID); $self->{playcount} = AA::Get("playcount:sum",'album',$aID); my $tip = join("\n", _("Average rating:") .' '.$self->{rating}, _("Rating range:") .' '.$self->{ratingrange}, _("Total playcount:") .' '.$self->{playcount}); $self->{ratingpic}->set_from_pixbuf(Songs::Stars($self->{rating},'rating')); $self->{Ltitle}->set_markup( AA::ReplaceFields($aID,"%a","album",1) ); $self->{Lstats}->set_markup( AA::ReplaceFields($aID,'%b « %y\n%s, %l',"album",1) ); for my $name (qw/Ltitle Lstats/) { $self->{$name}->set_tooltip_text($tip); } } sub load_search_results { my ($self,$ID,$md,$cb,$html,%prop) = @_; # $md = 1 if mass_download, 0 otherwise. $cb = callback function if mass_download, undef otherwise. delete $self->{waiting}; my $result = parse_amg_search_results($html, $prop{type}); # $result[$i] = {url, album, artist, genres, year} my ($artist,$year) = ::Songs::Get($ID, qw/artist year/); my $url; for my $entry (@$result) { # Pick the first entry with the right artist and year, or if not: just the right artist. if (::superlc($entry->{artist}) eq ::superlc($artist)) { if (!$url || ($entry->{year} && $entry->{year} == $year)) { warn "Albuminfo: hit in search results: $entry->{album} by $entry->{artist} from $entry->{year} ($entry->{url})\n" if $::debug; $url = $entry->{url}; } last if $year && $entry->{year} && $entry->{year} == $year; } } if ($url) { warn "Albuminfo: fetching review from $url\n" if $::debug; $self->{waiting} = Simple_http::get_with_cb(cb=>sub {$self->load_review($ID,$md,$cb,$url,@_)}, url=>$url, cache=>1); } else { $self->{fields} = {}; warn "Albuminfo: album not found in search results\n" if $::debug; $self->print_warning(_"No review found") unless $md; ::IdleDo('9_download_one'.$self, undef, $cb, $self) if $cb; } } sub load_review { my ($self,$ID,$md,$cb,$url,$html,%prop) = @_; delete $self->{waiting}; $self->{fields} = parse_amg_album_page($url,$html,$prop{type}); $self->print_review() unless $md; save_review($ID, $self->{fields}) if $::Options{OPT.'AutoSave'} || $md; if ($::Options{OPT.'SaveFields'}) {push(@towrite, [$ID, %{$self->{fields}}]); save_fields()} ::IdleDo('9_download_one'.$self, undef, $cb, $self) if $cb; } sub parse_amg_search_results { my ($html,$type) = @_; $html = decode($html, $type); $html =~ s/\n/ /g; # Parsing the html yields (url, album, artist, year, genres) for each album my $i = 0; # Used to sort the hits in manual search my @result; for my $info (split /
]*>([^<]+)#i; next unless defined $url; my %hash=( order=>$i++, album=>$album, url=>$url); for my $field (qw/artist year genres/) { my ($value)= $info=~m#
]*>\s*(.*?)\s*
#i; next unless defined $value; $value=~s#<[^>]*>\s*##g; $hash{$field}=$value; } push @result, \%hash; # create an array of hash refs } return \@result; } sub parse_amg_album_page { my ($url,$html,$type) = @_; $html =~ s|\n||g; my %result; $result{url} = $url; $result{author} = $1 if $html =~ m|class="review-author headline">[^<]*by |i; if ($html =~ m|
(.*?)
|i){ $result{review} = $1; for ($result{review}) { s/^(?:

|\s+)*//i; # remove leading spaces/newlines s||\n|gi; # Replace newline tags by newlines. s||\n|gi; # Replace paragraph tags by newlines. s|\n\n+|\n|gi; # Never more than one empty line. s|<.*?>(.*?)|$1|g; # Remove the rest of the html tags. } } $result{rls_date} = $1 if $html =~ m|class="release-date">\s*Release Date\s*([^<]+)|i; $result{rec_date} = $1 if $html =~ m|class="recording-date">\s*Recording Date\s*([^<]+)|i; $result{time} = $1 if $html =~ m|class="duration">\s*Duration\s*([^<]+)|i; $result{amgid} = $1 if $html =~ m|AMG Pop ID.*?R\s*(\d+)\s*|i; $result{rating} = 10*$1 if $html =~ m|

\s*(\d+)\s*
|i; my ($genrehtml) = $html =~ m|class="genres?">\s*Genres?\s*
(.*?)
|i; (@{$result{genre}}) = $genrehtml =~ m|([^<]+)|ig if $genrehtml; my ($stylehtml) = $html =~ m|class="styles">\s*Styles\s*
(.*?)
|i; (@{$result{style}}) = $stylehtml =~ m|([^<]+)|ig if $stylehtml; my ($moodshtml) = $html =~ m|class="moods">\s*Album Moods\s*
(.*?)
|i; (@{$result{mood}}) = $moodshtml =~ m|([^<]+)|ig if $moodshtml; my ($themeshtml) = $html =~ m|class="themes">\s*Themes\s*
(.*?)
|i; (@{$result{theme}}) = $themeshtml =~ m|([^<]+)|ig if $themeshtml; #convert values from html for my $value (values %result){ if (ref $value) { @$value= map decode($_,$type), @$value; } else { $value=decode($value,$type); } } #DEBUG : print values #for (sort keys %result){ # next if $_ eq 'review'; # my $v=$result{$_}; # if (ref $v) {warn "$_ : ".join(" -- ",@$v)."\n"} # else {warn "$_ : $v\n"} #} if ($::Options{OPT.'StyleAsGenre'} && $result{style}) {@{$result{genre}} = ::uniq(@{$result{style}}, @{$result{genre}})} return \%result; } # Get right encoding of html and decode it sub decode { my ($html,$type) = @_; my $encoding; $encoding = lc($1) if ($type && $type =~ m|^text/.*; ?charset=([\w-]+)|); $encoding = 'utf-8' if ($html =~ m|xml version|); $encoding = lc($1) if ($html =~ m|{fields} = {}; if ( open(my$fh, '<', $file) ) { warn "Albuminfo: loading review from file $file\n" if $::debug; local $/ = undef; #slurp mode my $text = <$fh>; if (my $utf8 = Encode::decode_utf8($text)) {$text = $utf8} my (@tmp) = $text =~ m|<(.*?)>(.*)|gs; while (my ($key,$val) = splice(@tmp, 0, 2)) { if ($key && $val) { if ($key =~ m/genre|mood|style|theme/) { @{$self->{fields}{$key}} = split(', ', $val); } else { $self->{fields}{$key} = $val; } } } close $fh; if ($::Options{OPT.'StyleAsGenre'} && $self->{fields}->{style}) {@{$self->{fields}->{genre}} = ::uniq(@{$self->{fields}->{style}}, @{$self->{fields}->{genre}})} $self->print_review() unless $md; if ($::Options{OPT.'SaveFields'}) {push(@towrite, [$ID, %{$self->{fields}}]); save_fields()} # We may not have saved when first downloaded. } else { warn "Albuminfo: failed retrieving info from $file\n" if $::debug; $self->print_warning(_"No review found") unless $md; } ::IdleDo('9_download_one'.$self, undef, $cb, $self) if $cb; } # Save review to file. The format of the file is: values sub save_review { my ($ID,$fields) = @_; my $text = ""; for my $key (sort {lc $a cmp lc $b} keys %{$fields}) { # Sort fields alphabetically if ($key =~ m/genre|mood|style|theme/) { $text = $text . "<$key>".join(", ", @{$fields->{$key}})."\n"; } else { $text = $text . "<$key>".$fields->{$key}."\n"; } } my $format = $::Options{OPT.'PathFile'}; my ($path,$file) = ::pathfilefromformat( $ID, $format, undef, 1 ); unless ($path && $file) {::ErrorMessage(_("Error: invalid filename pattern")." : $format",$::MainWindow); return} return unless ::CreateDir($path,$::MainWindow,_"Error saving review") eq 'ok'; if ( open(my$fh, '>:utf8', $path.$file) ) { print $fh $text; close $fh; warn "Albuminfo: Saved review in ".$path.$file."\n" if $::debug; } else { ::ErrorMessage(::__x(_"Error saving review in '{file}' :\n{error}", file => $file, error => $!), $::MainWindow); } } # Save selected fields (moods, styles etc.) for all tracks in album sub save_fields { return if $save_fields_lock; return unless scalar(@towrite); my ($ID,%fields) = @{shift(@towrite)}; my $album = Songs::Gid_to_Get("album", Songs::Get_gid($ID,'album')); warn "Albuminfo: Saving tracks on $album (".scalar(@towrite)." album".(scalar(@towrite)!=1 ? "s" : "")." in queue).\n" if $::debug; my $IDs = Songs::MakeFilterFromGID('album', Songs::Get_gid($ID,'album'))->filter(); # Songs on this album my @updated_fields; for my $key (qw/genre mood style theme/) { if ($::Options{OPT.$key} && $fields{$key}) { if ( $::Options{OPT.'ReplaceFields'} ) { push(@updated_fields, $key, $fields{$key}); } else { push(@updated_fields, '+'.$key, $fields{$key}); } } } if (@updated_fields) { $save_fields_lock = 1; Songs::Set($IDs, \@updated_fields, callback_finish=>sub {$save_fields_lock = 0; save_fields();}); } else { save_fields(); # There may still be albums in @towrite } } # Cancel pending tasks, and abort possible http_wget in progress. sub cancel { my $self = shift; delete $::ToDo{'9_load_albuminfo'.$self}; delete $::ToDo{'9_refresh_albuminfo'.$self}; $self->{waiting}->abort() if $self->{waiting}; ::Progress('albuminfo', abort=>1); $self->{abort}=1; } 1 gmusicbrowser-1.1.15~ds0.orig/plugins/appindicator.pm0000664000175000017500000000556112565212605022161 0ustar unit193unit193 # Copyright (C) 2014 Quentin Sculo # # 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 =for gmbplugin AppIndicator name App indicator title App Indicator plugin desc Displays a panel indicator in some desktops req perl(Gtk2::AppIndicator, libgtk2-appindicator-perl perl-Gtk2-AppIndicator) =cut package GMB::Plugin::AppIndicator; use strict; use warnings; use constant { OPT => 'PLUGIN_AppIndicator_', }; use Gtk2::AppIndicator; ::SetDefaultOptions(OPT, MiddleClick=>'playpause'); my %mactions= # action when middle-clicking on icon, must correspond to an id in the tray menu (@::TrayMenu) ( playpause=> _"Play/Pause", showhide=> _"Show/Hide", next => _"Next", ); my ($indicator,$iconpath); sub Start { $indicator||=Gtk2::AppIndicator->new(::PROGRAM_NAME,'gmusicbrowser','application-status'); # events that requires updating the traymenu : ::Watch($indicator, $_=> \&QueueUpdate) for qw/Lock Playing Windows/; #::Watch($indicator, $_=> \&UpdateIcon) for qw/Playing Icons/; #FIXME needs initialization #deactivated because it can't work for now QueueUpdate(); } sub Stop { ::UnWatch_all($indicator); $indicator->get_menu->destroy; $indicator->set_passive; #can't find how to destroy it, so hide it and reuse it if plugin reactivated } sub prefbox { my $vbox=Gtk2::VBox->new(::FALSE, 2); my $middleclick= ::NewPrefCombo(OPT.'MiddleClick', \%mactions, text => _"Middle-click action :", cb=>\&Update); my $warning= Gtk2::Label->new_with_format("%s",_"(The middle-click action doesn't work correctly in some desktops)"); $vbox->pack_start($_,::FALSE,::FALSE,2) for $middleclick,$warning; return $vbox; } sub QueueUpdate { ::IdleDo('2_AppIndicator',500,\&Update); } sub Update { delete $::ToDo{'2_AppIndicator'}; return unless $indicator; my $menu= ::BuildMenu(\@::TrayMenu); $menu->show_all; $indicator->set_active; $indicator->set_menu($menu); my ($menuentry)= grep $_->{id} && $_->{id} eq $::Options{OPT.'MiddleClick'}, $menu->get_children; $indicator->set_secondary_activate_target($menuentry) if $menuentry; } #doesn't work, needs gmb to switch the standard icon system first sub UpdateIcon { my $state= !defined $::TogPlay ? 'default' : $::TogPlay ? 'play' : 'pause'; $state='default' unless $::TrayIcon{$state}; my $path= ::dirname($::TrayIcon{$state}); my $name= ::barename($::TrayIcon{$state}); $indicator->set_icon_theme_path($iconpath=$path) if $iconpath && $iconpath ne $path; $indicator->set_icon_name_active($name); } #patch for typo in Gtk2::AppIndicator package Gtk2::AppIndicator; sub set_secondary_activate_target { my $self=shift; my $widget=shift; $self->{secondary}=$widget; appindicator_set_secondary_activate_target($self->{ind},$widget); } 1; gmusicbrowser-1.1.15~ds0.orig/plugins/artistinfo.pm0000664000175000017500000007362412565212605021673 0ustar unit193unit193# Copyright (C) 2010-2011 Quentin Sculo and Simon Steinbeiß # # 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 =for gmbplugin ARTISTINFO name Artistinfo title Artistinfo plugin version 0.5 author Simon Steinbeiß author Pasi Lallinaho desc This plugin retrieves artist-relevant information (biography, upcoming events, similar artists) from last.fm. url http://gmusicbrowser.org/dokuwiki/doku.php?id=plugins:artistinfo =cut package GMB::Plugin::ARTISTINFO; use strict; use warnings; use utf8; require $::HTTP_module; use base 'Gtk2::Box'; use constant { OPT => 'PLUGIN_ARTISTINFO_', # MUST begin by PLUGIN_ followed by the plugin ID / package name SITEURL => 0, }; my %sites = ( biography => ['http://ws.audioscrobbler.com/2.0/?method=artist.getinfo&artist=%a&api_key=7aa688c2466dc17263847da16f297835&autocorrect=1',_"Biography",_"Show artist's biography"], events => ['http://ws.audioscrobbler.com/2.0/?method=artist.getevents&artist=%a&api_key=7aa688c2466dc17263847da16f297835&autocorrect=1',_"Events",_"Show artist's upcoming events"], similar => ['http://ws.audioscrobbler.com/2.0/?method=artist.getsimilar&artist=%a&api_key=7aa688c2466dc17263847da16f297835&autocorrect=1&limit=%l',_"Similar",_"Show similar artists"]); my @External= ( ['lastfm', "http://www.last.fm/music/%a", _"Show Artist page on last.fm"], ['wikipedia', "http://en.wikipedia.org/wiki/%a", _"Show Artist page on wikipedia"], ['youtube', "http://www.youtube.com/results?search_type=&aq=1&search_query=%a", _"Search for Artist on youtube"], ['amazon', "http://www.amazon.com/s/ref=nb_sb_noss?url=search-alias=aps&field-keywords=%a", _"Search amazon.com for Artist"], ['google', "http://www.google.com/search?q=%a", _"Search google for Artist" ], ['allmusic', "http://www.allmusic.com/search/artist/%a", _"Search allmusic for Artist" ], ['pitchfork', "http://pitchfork.com/search/?search_type=standard&query=%a", _"Search pitchfork for Artist" ], ['discogs', "http://www.discogs.com/artist/%a", _"Search discogs for Artist" ], ); my %menuitem= ( label => _"Search on the web", #label of the menu item submenu => sub { CreateSearchMenu( Songs::Gid_to_Get('artist',$_[0]{gid}) ); }, #when menu item selected test => sub {$_[0]{mainfield} eq 'artist'}, #the menu item is displayed if returns true ); my $nowplayingaID; my $queuewaiting; my %queuemode= ( order=>10, icon=>'gtk-refresh', short=> _"similar-artists", long=> _"Auto-fill queue with similar artists (from last.fm)", changed=>\&QAutofillSimilarArtists, keep=>1,save=>1,autofill=>1, ); =dop my @similarity= ( ['super', '0.9', '#ff0101'], ['very high', '0.7', '#e9c102'], ['high', '0.5', '#05bd4c'], ['medium', '0.3', '#453e45'], ['lower', '0.1', '#9a9a9a'], ); =cut # lastfm api key 7aa688c2466dc17263847da16f297835 # "secret" string: 18cdd008e76705eb5f942892d49a71e2 ::SetDefaultOptions(OPT,PathFile => "~/.config/gmusicbrowser/bio/%a", ArtistPicSize => 70, ArtistPicShow => 1, SimilarLimit => 15, SimilarRating => 20, SimilarLocal => 0, SimilarExcludeSeed => 0, Eventformat => '%title at %name
%startDate
%city (%country)

', Eventformat_history => ['%title
%startDate

','%title on %startDate

'], ); my $artistinfowidget= { class => __PACKAGE__, tabicon => 'plugin-artistinfo', # no icon by that name by default (yet) tabtitle => _"Artistinfo", saveoptions => 'site', schange => sub { $_[0]->SongChanged; }, group => 'Play', autoadd_type => 'context page text', }; sub Start { Layout::RegisterWidget(PluginArtistinfo => $artistinfowidget); push @::cMenuAA,\%menuitem; $::QActions{'autofill-similar-artists'} = \%queuemode; ::Update_QueueActionList(); } sub Stop { Layout::RegisterWidget(PluginArtistinfo => undef); @::cMenuAA= grep $_!=\%menuitem, @::SongCMenu; delete $::QActions{'autofill-similar-artists'}; ::Update_QueueActionList(); $queuewaiting->abort if $queuewaiting; $queuewaiting=undef; } sub new { my ($class,$options)=@_; my $self = bless Gtk2::VBox->new(0,0), $class; $self->{$_}=$options->{$_} for qw/site/; delete $self->{site} if $self->{site} && !$sites{$self->{site}}[SITEURL]; #reset selected site if no longer defined $self->{site} ||= 'biography'; # biography is the default site my $fontsize=$self->style->font_desc; $self->{fontsize} = $fontsize->get_size / Gtk2::Pango->scale; $self->{artist_esc} = ""; my $statbox=Gtk2::VBox->new(0,0); my $artistpic=Gtk2::HBox->new(0,0); for my $name (qw/Ltitle Lstats/) { my $l=Gtk2::Label->new(''); $self->{$name}=$l; $l->set_justify('center'); if ($name eq 'Ltitle') { $l->set_line_wrap(1);$l->set_ellipsize('end'); } $statbox->pack_start($l,0,0,2); } $self->{artistrating} = Gtk2::Image->new; $statbox->pack_start($self->{artistrating},0,0,0); my $stateventbox = Gtk2::EventBox->new; $stateventbox->add($statbox); $stateventbox->{group}= $options->{group}; $stateventbox->signal_connect(button_press_event => sub {my ($stateventbox, $event) = @_; return 0 unless $event->button == 3; my $ID=::GetSelID($stateventbox); ::ArtistContextMenu( Songs::Get_gid($ID,'artists'),{ ID=>$ID, self=> $stateventbox, mode => 'S'}) if defined $ID; return 1; } ); # FIXME: do a proper cm my $artistbox = Gtk2::HBox->new(0,0); $artistbox->pack_start($artistpic,0,1,0); $artistbox->pack_start($stateventbox,1,1,0); my $group= $options->{group}; my $artistpic_create= sub { my $box=shift; $box->remove($_) for $box->get_children; return unless $::Options{OPT.'ArtistPicShow'}; my $child = Layout::NewWidget("ArtistPic",{forceratio=>1,minsize=>$::Options{OPT.'ArtistPicSize'},click1=>\&apiczoom,xalign=>0,group=>$group,tip=>_"Click to show fullsize image"}); $child->show_all; $box->add($child); }; ::Watch($artistpic, plugin_artistinfo_option_pic=> $artistpic_create); $artistpic_create->($artistpic); my $textview=Gtk2::TextView->new; $self->signal_connect(map => \&SongChanged); $textview->set_cursor_visible(0); $textview->set_wrap_mode('word'); $textview->set_pixels_above_lines(2); $textview->set_editable(0); $textview->set_left_margin(5); $textview->set_has_tooltip(1); $textview->signal_connect(button_release_event => \&button_release_cb); $textview->signal_connect(motion_notify_event => \&update_cursor_cb); $textview->signal_connect(visibility_notify_event=>\&update_cursor_cb); $textview->signal_connect(query_tooltip => \&update_cursor_cb); my $store=Gtk2::ListStore->new('Glib::String','Glib::Double','Glib::String','Glib::UInt','Glib::String'); my $treeview=Gtk2::TreeView->new($store); my $tc_artist=Gtk2::TreeViewColumn->new_with_attributes( _"Artist",Gtk2::CellRendererText->new,markup=>0); $tc_artist->set_sort_column_id(0); $tc_artist->set_expand(1); $tc_artist->set_resizable(1); $treeview->append_column($tc_artist); $treeview->set_has_tooltip(1); $treeview->set_tooltip_text(_"Middle-click on local artists to set a filter on them, right-click non-local artists to search for them on the web."); my $renderer=Gtk2::CellRendererText->new; my $tc_similar=Gtk2::TreeViewColumn->new_with_attributes( "%",$renderer,text => 1); $tc_similar->set_cell_data_func($renderer, sub { my ($column, $cell, $model, $iter, $func_data) = @_; my $rating = $model->get($iter, 1); $cell->set( text => sprintf '%.1f', $rating ); }, undef); # limit similarity rating to one decimal $tc_similar->set_sort_column_id(1); $tc_similar->set_alignment(1.0); $tc_similar->set_min_width(10); $treeview->append_column($tc_similar); $treeview->set_rules_hint(1); $treeview->signal_connect(button_press_event => \&tv_contextmenu); $treeview->{store}=$store; my $toolbar=Gtk2::Toolbar->new; $toolbar->set_style( $options->{ToolbarStyle}||'both-horiz' ); $toolbar->set_icon_size( $options->{ToolbarSize}||'small-toolbar' ); #$toolbar->set_show_arrow(1); my $radiogroup; my $menugroup; foreach my $key (sort keys %sites) { my $item = $sites{$key}[1]; $item = Gtk2::RadioButton->new($radiogroup,$item); $item->{key} = $key; $item -> set_mode(0); # display as togglebutton $item -> set_relief("none"); $item -> set_tooltip_text($sites{$key}[2]); $item->set_active( $key eq $self->{site} ); $item->signal_connect(toggled => sub { my $self=::find_ancestor($_[0],__PACKAGE__); toggled_cb($self,$item,$textview); } ); $radiogroup = $item -> get_group; my $toolitem=Gtk2::ToolItem->new; $toolitem->add( $item ); $toolitem->set_expand(1); $toolbar->insert($toolitem,-1); # trying to make the radiobuttons overflowable, but no shared groups for radiobuttons and radiomenuitems (group doesn't seem to work for radiomenuitem at all) # my $menuitem=Gtk2::RadioMenuItem->new($menugroup,$sites{$key}[1]); # $menuitem->set_active( $key eq $self->{site} ); #$menuitem->set_group($menugroup); # $menuitem->set_draw_as_radio(1); # $menuitem->{key} = $key; # if ($menuitem->get_active) { warn $menuitem->{key}; } # $menuitem->signal_connect('toggled' => sub { &toggled_cb($self,$menuitem,$textview); } ); # $toolitem->set_proxy_menu_item($key,$menuitem); } for my $button ( [refresh => 'gtk-refresh', sub { my $self=::find_ancestor($_[0],__PACKAGE__); SongChanged($self,'force'); },_"Refresh", _"Refresh"], [save => 'gtk-save', \&Save_text, _"Save", _"Save artist biography",$::Options{OPT.'AutoSave'}], ) { my ($key,$stock,$cb,$label,$tip)=@$button; my $item=Gtk2::ToolButton->new_from_stock($stock); $item->signal_connect(clicked => $cb); $item->set_tooltip_text($tip) if $tip; my $menuitem = Gtk2::ImageMenuItem->new ($label); $menuitem->set_image( Gtk2::Image->new_from_stock($stock,'menu') ); $item->set_proxy_menu_item($key,$menuitem); $toolbar->insert($item,-1); if ($key eq 'save') { $item->show_all; $item->set_no_show_all(1); my $update= sub { $_[0]->set_visible(!$::Options{OPT.'AutoSave'}); }; ::Watch($item, plugin_artistinfo_option_save=> $update); $update->($item); } } my $artistinfobox = Gtk2::VBox->new(0,0); $artistinfobox->pack_start($artistbox,1,1,0); $artistinfobox->pack_start($toolbar,0,0,0); #$statbox->pack_start($toolbar,0,0,0); $self->{buffer}=$textview->get_buffer; $self->{store}=$store; my $infobox = Gtk2::HBox->new; $infobox->set_spacing(0); my $sw1=Gtk2::ScrolledWindow->new; my $sw2=Gtk2::ScrolledWindow->new; $sw1->add($textview); $sw2->add($treeview); for ($sw1,$sw2) { $_->set_shadow_type('none'); $_->set_policy('automatic','automatic'); $infobox->pack_start($_,1,1,0); } if ($self->{site} ne "similar") { $treeview->show; $sw2->set_no_show_all(1); } # only show the correct widget at startup else { $textview->show; $sw1->set_no_show_all(1); } $self->{sw1} = $sw1; $self->{sw2} = $sw2; $self->pack_start($artistinfobox,0,0,0); $self->pack_start($infobox,1,1,0); $self->signal_connect(destroy => \&destroy_event_cb); return $self; } sub toggled_cb { my ($self, $togglebutton,$textview) = @_; if ($togglebutton -> get_active) { $self->{site} = $togglebutton->{key}; $self->SongChanged; $textview->set_tooltip_text($self->{site}) if $self->{site} ne "events"; } } sub destroy_event_cb { my $self=shift; $self->cancel; } sub cancel { my $self=shift; delete $::ToDo{'8_artistinfo'.$self}; $self->{waiting}->abort if $self->{waiting}; } sub prefbox { my $vbox=Gtk2::VBox->new(0,2); my $entry=::NewPrefEntry(OPT.'PathFile' => _"Load/Save Artist Info in :", width=>50); my $preview= Label::Preview->new(preview => \&filename_preview, event => 'CurSong Option', noescape=>1,wrap=>1); my $autosave = ::NewPrefCheckButton(OPT.'AutoSave'=>_"Auto-save positive finds", tip=>_"only works when the artist-info tab is displayed", cb=>sub { ::HasChanged('plugin_artistinfo_option_save'); }); my $picsize=::NewPrefSpinButton(OPT.'ArtistPicSize',50,500, step=>5, page=>10, text =>_("Artist picture size : %d"), cb=>sub { ::HasChanged('plugin_artistinfo_option_pic'); }); my $picshow=::NewPrefCheckButton(OPT.'ArtistPicShow' => _"Show artist picture", widget => ::Vpack($picsize), cb=>sub { ::HasChanged('plugin_artistinfo_option_pic'); } ); my $eventformat=::NewPrefEntry(OPT.'Eventformat' => _"Enter custom event string :", expand=>1, tip => _"Use tags from last.fm's XML event pages with a leading % (e.g. %headliner), furthermore linebreaks '
' and any text you'd like to have in between. E.g. '%title taking place at %startDate
in %city, %country

'", history=>OPT.'Eventformat_history'); my $eventformat_reset=Gtk2::Button->new(_"reset format"); $eventformat_reset->{format_combo}=$eventformat; $eventformat_reset->signal_connect(clicked => sub { my $self = shift; my $prefentry = $self->{format_combo}; my ($combo) = grep $_->isa("Gtk2::ComboBoxEntry"), $prefentry->get_children; $combo->child->set_text('%title at %name
%startDate
%city (%country)

'); $::Options{OPT.'Eventformat'} = '%title at %name
%startDate
%city (%country)

'; }); my $similar_limit=::NewPrefSpinButton(OPT.'SimilarLimit',0,500, step=>1, page=>10, text1=>_"Limit similar artists to the first : ", tip=>_"0 means 'show all'"); my $similar_rating=::NewPrefSpinButton(OPT.'SimilarRating',0,100, step=>1, text1=>_"Limit similar artists to a rate of similarity : ", tip=>_"last.fm's similarity categories:\n>90 super\n>70 very high\n>50 high\n>30 medium\n>10 lower"); my $similar_local=::NewPrefCheckButton(OPT.'SimilarLocal' => _"Only show similar artists from local library", tip=>_"applied on reload"); my $similar_exclude_seed=::NewPrefCheckButton(OPT.'SimilarExcludeSeed' => _"Exclude 'seed'-artist from queue", tip=>_"The artists similar to the 'seed'-artist will be used to populate the queue, but you can decide to exclude the 'seed'-artist him/herself."); my $lastfm=::NewIconButton('plugin-artistinfo-lastfm',undef,sub { ::main::openurl("http://www.last.fm/music/"); },'none',_"Open last.fm website in your browser"); my $titlebox=Gtk2::HBox->new(0,0); $titlebox->pack_start($picshow,1,1,0); $titlebox->pack_start($lastfm,0,0,5); my $frame_bio=Gtk2::Frame->new(_"Biography"); $frame_bio->add(::Vpack($entry,$preview,$autosave)); my $frame_events=Gtk2::Frame->new(_"Events"); $frame_events->add(::Hpack('_',$eventformat,$eventformat_reset)); my $frame_similar=Gtk2::Frame->new(_"Similar Artists"); $frame_similar->add(::Vpack($similar_limit,$similar_rating,$similar_local,$similar_exclude_seed)); $vbox->pack_start($_,::FALSE,::FALSE,5) for $titlebox,$frame_bio,$frame_events,$frame_similar; return $vbox; } sub filename_preview { return '' unless defined $::SongID; my $t=::pathfilefromformat( $::SongID, $::Options{OPT.'PathFile'}, undef,1); $t= ::filename_to_utf8displayname($t) if $t; $t= $t ? ::PangoEsc(_("example : ").$t) : "".::PangoEsc(_"invalid pattern").""; return ''.$t.''; } sub set_buffer { my ($self,$text) = @_; $self->{buffer}->set_text(""); my $iter=$self->{buffer}->get_start_iter; my $fontsize=$self->{fontsize}; my $tag_noresults=$self->{buffer}->create_tag(undef,justification=>'center',font=>$fontsize*2,foreground_gdk=>$self->style->text_aa("normal")); $self->{buffer}->insert_with_tags($iter,"\n$text",$tag_noresults); $self->{buffer}->set_modified(0); } sub tv_contextmenu { my ($treeview, $event) = @_; return 0 unless $treeview; my ($path, $column) = $treeview->get_cursor; return unless defined $path; my $store=$treeview->{store}; my $iter=$store->get_iter($path); my $artist=$store->get( $store->get_iter($path),4); my $url=$store->get( $store->get_iter($path),2); my $aID=$store->get( $store->get_iter($path),3); if ($event->button == 2) { if ($url eq "local") { my $filter = Songs::MakeFilterFromGID('artists',$aID); ::SetFilter($treeview,$filter,1); } return 1; } elsif ($event->button == 3) { if ($url eq "local") { ::PopupAAContextMenu({gid=>$aID,self=>$treeview,field=>'artists',mode=>'S'}); return 0; } else { my $menu = CreateSearchMenu($artist,$url); my $title=Gtk2::MenuItem->new(_"Search for artist on:"); $menu->prepend($title); $menu->show_all; $menu->popup (undef, undef, undef, undef, $event->button, $event->time); } return 1; } } sub CreateSearchMenu { my($artist,$lastfm_url)=@_; $artist=::url_escapeall($artist); my $menu=Gtk2::Menu->new; for my $item (@External) { my ($key,$url,$text)=@$item; if ($key eq "lastfm" && $lastfm_url) { $url='http://'.$lastfm_url; } else { $url=~s/%a/$artist/; } my $menuitem = Gtk2::ImageMenuItem->new ($key); $menuitem->set_image( Gtk2::Image->new_from_stock('plugin-artistinfo-'.$key,'menu') ); $menuitem->signal_connect(activate => sub { ::main::openurl($url) if $url; return 0; }); $menu->append($menuitem); } return $menu; } sub apiczoom { my ($self, $event) = @_; my $ID = ::GetSelID($self); my $aID = Songs::Get_gid($ID,'artist'); my $picsize=250; my $img= AAPicture::newimg(artist=>$aID,$picsize); return 0 unless $img; my $menu=Gtk2::Menu->new; $menu->modify_bg('normal',Gtk2::Gdk::Color->parse('black')); # black bg for the artistpic-popup my $apic = Gtk2::MenuItem->new; $apic->modify_bg('selected',Gtk2::Gdk::Color->parse('black')); $apic->add($img); my $artist = Songs::Gid_to_Get("artist",$aID); my $item=Gtk2::MenuItem->new; my $label=Gtk2::Label->new; # use a label instead of a normal menu-item for formatted text $item->modify_bg('selected',Gtk2::Gdk::Color->parse('black')); $label->modify_fg($_,Gtk2::Gdk::Color->parse('white')) for qw/normal prelight/; $label->set_line_wrap(1); $label->set_justify('center'); $label->set_ellipsize('end'); $label->set_markup( "$artist" ); $item->add($label); $menu->append($apic); $menu->append($item); $menu->show_all; $menu->popup (undef, undef, undef, undef, $event->button, $event->time); return 1; } sub update_cursor_cb { my $textview=$_[0]; my (undef,$wx,$wy,undef)=$textview->window->get_pointer; my ($x,$y)=$textview->window_to_buffer_coords('widget',$wx,$wy); my $iter=$textview->get_iter_at_location($x,$y); my $cursor='xterm'; for my $tag ($iter->get_tags) { next unless $tag->{url}; $cursor='hand2'; $textview->set_tooltip_text($tag->{url}); last; } return if ($textview->{cursor}||'') eq $cursor; $textview->{cursor}=$cursor; $textview->get_window('text')->set_cursor(Gtk2::Gdk::Cursor->new($cursor)); } sub button_release_cb { my ($textview,$event) = @_; my $self=::find_ancestor($textview,__PACKAGE__); return ::FALSE unless $event->button == 1; my ($x,$y)=$textview->window_to_buffer_coords('widget',$event->x, $event->y); my $url=$self->url_at_coords($x,$y,$textview); ::main::openurl($url) if $url; return ::FALSE; } sub url_at_coords { my ($self,$x,$y,$textview)=@_; my $iter=$textview->get_iter_at_location($x,$y); for my $tag ($iter->get_tags) { next unless $tag->{url}; if ($tag->{url}=~m/^#(\d+)?/) { $self->scrollto($1) if defined $1; last } my $url= $tag->{url}; return $url; } } sub SongChanged { my ($widget,$force) = @_; my $self=::find_ancestor($widget,__PACKAGE__); my $ID = ::GetSelID($self); return unless defined $ID; $self -> ArtistChanged( Songs::Get_gid($ID,'artist'),Songs::Get_gid($ID,'album'),$force); } sub ArtistChanged { my ($self,$aID,$albumID,$force)=@_; return unless $self->mapped; return unless defined $aID; my $rating = AA::Get("rating:average",'artist',$aID); $self->{artistratingvalue}= int($rating+0.5); $self->{artistratingrange}=AA::Get("rating:range",'artist',$aID); $self->{artistplaycount}=AA::Get("playcount:sum",'artist',$aID); $self->{albumplaycount}=AA::Get("playcount:sum",'album',$albumID); my $tip = join "\n", _("Average rating:") .' '.$self->{artistratingvalue}, _("Rating range:") .' '.$self->{artistratingrange}, _("Artist playcount:") .' '.$self->{artistplaycount}, _("Album playcount:") .' '.$self->{albumplaycount}; $self->{artistrating}->set_from_pixbuf(Songs::Stars($self->{artistratingvalue},'rating')); $self->{Ltitle}->set_markup( AA::ReplaceFields($aID,"%a","artist",1) ); $self->{Lstats}->set_markup( AA::ReplaceFields($aID,'%X « %s'."\n%y","artist",1) ); for my $name (qw/Ltitle Lstats artistrating/) { $self->{$name}->set_tooltip_text($tip); } my $url = GetUrl($sites{$self->{site}}[SITEURL],$aID); return unless $url; if (!$self->{url} or ($url ne $self->{url}) or $force) { $self->cancel; $self->{url} = $url; if ($self->{site} eq "biography") { # check for local biography file before loading the page unless ($force) { my $file=::pathfilefromformat( ::GetSelID($self), $::Options{OPT.'PathFile'}, undef,1 ); if ($file && -r $file) { ::IdleDo('8_artistinfo'.$self,1000,\&load_file,$self,$file); $self->{sw2}->hide; $self->{sw1}->show; return; } } } ::IdleDo('8_artistinfo'.$self,1000,\&load_url,$self,$url); } } sub GetUrl { my ($url,$aID) = @_; my $artist = ::url_escapeall( Songs::Gid_to_Get("artist",$aID) ); return unless length $artist; $url=~s/%a/$artist/; my $limit = ""; if ($::Options{OPT.'SimilarLimit'} != "0") { $limit = $::Options{OPT.'SimilarLimit'}; } $url=~s/%l/$limit/; return $url; } sub load_url { my ($self,$url)=@_; $self->set_buffer(_"Loading..."); $self->cancel; warn "info : loading $url\n" if $::debug; $self->{url}=$url; $self->{sw2}->hide; $self->{sw1}->show; $self->{waiting}=Simple_http::get_with_cb(cb => sub {$self->loaded(@_)},url => $url, cache => 1); } sub loaded { my ($self,$data,%prop)=@_; delete $self->{waiting}; my $buffer=$self->{buffer}; my $type=$prop{type}; unless ($data) { $data=_("Loading failed.").qq( )._("retry").''; $type="text/html"; } $self->{url}=$prop{url} if $prop{url}; #for redirections $buffer->delete($buffer->get_bounds); my $encoding; if ($type && $type=~m#^text/.*; ?charset=([\w-]+)#) {$encoding=$1} if ($data=~m/xml version/) { $encoding='utf-8'; } $encoding=$1 if $data=~m#get_start_iter; my $fontsize = $self->{fontsize}; my $tag_warning = $buffer->create_tag(undef,foreground=>"#bf6161",justification=>'center',underline=>'single'); my $tag_extra = $buffer->create_tag(undef,foreground_gdk=>$self->style->text_aa("normal"),justification=>'left'); my $tag_noresults=$buffer->create_tag(undef,justification=>'center',font=>$fontsize*2,foreground_gdk=>$self->style->text_aa("normal")); my $tag_header = $buffer->create_tag(undef,justification=>'left',font=>$fontsize+1,weight=>Gtk2::Pango::PANGO_WEIGHT_BOLD); my $infoheader; if ($self->{site} eq "biography") { $infoheader = _"Artist Biography"; (my($lfm_artist,$url,$listeners,$playcount),$data)= $data =~ m|^.*?([^<]*).*?([^<]*).*?([^<]*).*?([^<]*).*?\W*<\!\[CDATA\[(.*?)\n.*\]\]>\W*|s; # last part of the regexp removes the license-notice (=last line) if (!defined $data) { $infoheader = "\n". _"No results found"; $tag_header = $tag_noresults; $buffer->insert_with_tags($iter,$infoheader."\n",$tag_header); } # fallback text if artist-info not found else { for ($data) { s/
|<\/p>/\n/gi; s/\n\n*/\n/g; # never more than one empty line s/<([^<]*)>//g; # strip tags } my $href = $buffer->create_tag(undef,justification=>'left',foreground=>"#4ba3d2",underline=>'single'); $href->{url}=$url.'/+wiki/edit'; my $aID = Songs::Get_gid($::SongID,'artist'); my $local_artist = Songs::Gid_to_Get("artist",$aID); my $warning; $warning= "Redirected to: ".$lfm_artist."\n" if $lfm_artist ne $local_artist; $buffer->insert_with_tags($iter,$warning,$tag_warning) if $warning; $buffer->insert_with_tags($iter,$infoheader."\n",$tag_header); $buffer->insert($iter,$data); $buffer->insert_with_tags($iter,"\n\n"._"Edit in the last.fm wiki",$href); $buffer->insert_with_tags($iter,"\n\n"._"Listeners: ".$listeners." | "._"Playcount: ".$playcount,$tag_extra); # only shown on fresh load, not saved to local info $self->{infoheader}=$infoheader; $self->{biography}=$data; $self->{lfm_url}=$url; $self->Save_text if $::Options{OPT.'AutoSave'}; } } elsif ($self->{site} eq "events") { if ($data =~ m#total=\"(.*?)\">#g) { if ( $1 == 0) { $self->set_buffer(_"No results found"); return; } else { $infoheader = ::__("%d Upcoming Event","%d Upcoming Events",$1)."\n\n"; } $buffer->insert_with_tags($iter,$infoheader,$tag_header) if $infoheader; } for my $event (split /<\/event>/, $data) { my %event; $event{$1} = ::decode_html($2) while $event=~ m#<(\w+)>([^<]*)#g; # FIXME: add workaround for description (which includes ) next unless $event{id}; # otherwise the last is also treated like an event $event{startDate} = substr($event{startDate},0,-9); # cut the useless time (hh:mm:ss) from the date my $format = $::Options{OPT.'Eventformat'}; $format =~ s/%(\w+)/$event{$1}/g; $format =~ s#
#\n#g; my $offset1 = $iter->get_offset; my $href = $buffer->create_tag(undef,justification=>'left'); $href->{url}=$event{url}; my ($first,$rest) = split /\n/,$format,2; $buffer->insert($iter,$first); my $offset2 = $iter->get_offset; $buffer->apply_tag($href,$buffer->get_iter_at_offset($offset1),$buffer->get_iter_at_offset($offset2)); $buffer->insert_with_tags($iter,"\n".$rest,$tag_extra) if $rest; } } elsif ($self->{site} eq "similar") { $self->{store}->clear; $self->{sw1}->hide; $self->{sw2}->show; for my $s_artist (split /<\/artist>/, $data) { my %s_artist; $s_artist{$1} = ::decode_html($2) while $s_artist=~ m#<(\w+)>([^<]*)#g; next unless $s_artist{name}; # otherwise the last is also treated like an artist if ($s_artist{match} >= $::Options{OPT.'SimilarRating'} / 100) { my $aID=Songs::Search_artistid($s_artist{name}); my $stats=''; my $color=$self->style->text_aa("normal")->to_string; my $fgcolor = substr($color,0,3).substr($color,5,2).substr($color,9,2); if ($aID) { $stats=AA::ReplaceFields($aID,' (%X « %s)',"artist",1); $s_artist{url} = "local"; $self->{store}->set($self->{store}->append,0,::PangoEsc($s_artist{name}).$stats,1,$s_artist{match} * 100,2,$s_artist{url},3,$aID,4,$s_artist{name}); } elsif ($::Options{OPT.'SimilarLocal'} == 0) { $self->{store}->set($self->{store}->append,0,::PangoEsc($s_artist{name}).$stats,1,$s_artist{match} * 100,2,$s_artist{url},3,$aID,4,$s_artist{name}); } } } } } sub load_file { my ($self,$file)=@_; my $buffer=$self->{buffer}; $buffer->delete($buffer->get_bounds); my $text=_("Loading failed."); if (open my$fh,'<',$file) { local $/=undef; #slurp mode $text=<$fh>; close $fh; if (my $utf8=Encode::decode_utf8($text)) {$text=$utf8} } my $fontsize=$self->{fontsize}; my $href = $buffer->create_tag(undef,justification=>'left',foreground=>"#4ba3d2",underline=>'single'); my $tag_header = $buffer->create_tag(undef,justification=>'left',font=>$fontsize+1,weight=>Gtk2::Pango::PANGO_WEIGHT_BOLD); my ($infoheader,$url); if ($text =~ m/(.*?)<\/title>(.*?)<url>(.*?)<\/url>/s) { $infoheader = $1; $url = $3; $text = $2; } else { $text =~ s/<title>(.*?)<\/title>\n?//i; $infoheader = $1 . "\n"; } my $iter=$buffer->get_start_iter; $buffer->insert_with_tags($iter,$infoheader,$tag_header); $buffer->insert($iter,$text); if ($url) { $href->{url}=$url; $buffer->insert_with_tags($iter,_"Edit in the last.fm wiki",$href); } $buffer->set_modified(0); } sub Save_text { my $self=::find_ancestor($_[0],__PACKAGE__); my $win=$self->get_toplevel; my $buffer=$self->{buffer}; my $text = "<title>".$self->{infoheader}."\n".$self->{biography}."\n\n".$self->{lfm_url}.""; my $format=$::Options{OPT.'PathFile'}; my ($path,$file)=::pathfilefromformat( ::GetSelID($self), $format, undef,1 ); unless ($path && $file) {::ErrorMessage(_("Error: invalid filename pattern")." : $format",$win); return} my $res=::CreateDir($path,$win,_"Error saving artistbio"); return unless $res eq 'ok'; if (open my$fh,'>:utf8',$path.$file) { print $fh $text; close $fh; $buffer->set_modified(0); warn "Saved artistbio in ".$path.$file."\n" if $::debug; } else {::ErrorMessage(::__x(_("Error saving artistbio in '{file}' :\n{error}"), file => $file, error => $!),$win);} } sub QAutofillSimilarArtists { $queuewaiting->abort if $queuewaiting; $queuewaiting=undef; return unless $::QueueAction eq 'autofill-similar-artists'; return if $::Options{MaxAutoFill}<=@$::Queue; return unless $::SongID; $nowplayingaID = Songs::Get_gid($::SongID,'artist'); return unless Songs::Gid_to_Get("artist",$nowplayingaID); my $url = GetUrl($sites{similar}[0],$nowplayingaID); return unless $url; $queuewaiting=Simple_http::get_with_cb(url => $url, cb => \&PopulateQueue ); } sub PopulateQueue { $queuewaiting=undef; if ( $nowplayingaID != Songs::Get_gid($::SongID,'artist')) { QAutofillSimilarArtists; return; } my $data = $_[0]; return unless $::QueueAction eq 'autofill-similar-artists'; # re-check queueaction and my $nb=$::Options{MaxAutoFill}-@$::Queue; return unless $nb>0; my @artist_gids; for my $s_artist (split /<\/artist>/, $data) { my %s_artist; $s_artist{$1} = ::decode_html($2) while $s_artist=~ m#<(\w+)>([^<]*)#g; next unless $s_artist{name}; if ($s_artist{match} >= $::Options{OPT.'SimilarRating'} / 100) { my $aID=Songs::Search_artistid($s_artist{name}); push (@artist_gids, $aID) if $aID; } } push (@artist_gids, Songs::Get_gid($::SongID,'artist')) unless $::Options{OPT.'SimilarExcludeSeed'}; # add currently playing artist as well my $filter= Filter->newadd(0, map Songs::MakeFilterFromGID("artist",$_), @artist_gids ); my $random= Random->new('random:',$filter->filter($::ListPlay)); my @IDs=$random->Draw($nb,[@$::Queue,$::SongID]); # add queue and current song to blacklist (won't draw) return unless @IDs; $::Queue->Push(\@IDs); } 1 gmusicbrowser-1.1.15~ds0.orig/plugins/rip.pm0000664000175000017500000000351312565212605020271 0ustar unit193unit193# Copyright (C) 2005-2007 Quentin Sculo # # 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 =for gmbplugin RIP name Rip title Rip plugin desc Add a button to rip a CD =cut package GMB::Plugin::RIP; use strict; use warnings; use constant { OPT => 'PLUGIN_RIP_',#used to identify the plugin and its options }; ::SetDefaultOptions(OPT, program => 'soundjuicer'); my %Programs= #id => [name,cmd] ( soundjuicer => ['sound-juicer','sound-juicer'], grip => ['grip','grip'], xcfa => ['xcfa','xcfa'], custom => [_"custom"], ); my %button_definition= ( class => 'Layout::Button', stock => 'plugin-rip', tip => _"Launch ripping program", activate=> \&Launch, autoadd_type => 'button main', ); sub Start { Layout::RegisterWidget(PluginRip=>\%button_definition); } sub Stop { Layout::RegisterWidget('PluginRip'); } sub prefbox { my $vbox=Gtk2::VBox->new(::FALSE, 2); my $sg1=Gtk2::SizeGroup->new('horizontal'); my $sg2=Gtk2::SizeGroup->new('horizontal'); my $entry=::NewPrefEntry(OPT.'custom', _"Custom command :", sizeg1=>$sg1,sizeg2=>$sg2, tip =>_('Command to launch when the button is pressed')); my %list= map {$_,$Programs{$_}[0]} keys %Programs; my $combo= ::NewPrefCombo ( OPT.'program', \%list, text=> _"Ripping software :", cb=> sub { $entry->set_sensitive( $::Options{OPT.'program'} eq 'custom' ) }, sizeg1=>$sg1, sizeg2=>$sg2 ); $entry->set_sensitive( $::Options{OPT.'program'} eq 'custom' ); $vbox->pack_start($_,::FALSE,::FALSE,2) for $combo,$entry; return $vbox; } sub Launch { my $program=$::Options{OPT.'program'}; my $cmd=$program eq 'custom' ? $::Options{OPT.'custom'} : $Programs{$program}[1]; ::forksystem($cmd) if $cmd; } 1; gmusicbrowser-1.1.15~ds0.orig/plugins/mpris2.pm0000664000175000017500000003264312565212605020721 0ustar unit193unit193# Copyright (C) 2011 Quentin Sculo # # 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 =for gmbplugin MPRIS2 name MPRIS v2 title MPRIS v2 support desc Allows controlling gmusicbrowser via DBus using the MPRIS v2.0 standard req perl(Net::DBus, libnet-dbus-perl perl-Net-DBus) =cut package GMB::Plugin::MPRIS2; use strict; use warnings; use constant { OPT => 'PLUGIN_MPRIS2_', }; use Net::DBus::Annotation 'dbus_call_async'; my $TEMPCOVERFILE= $::HomeDir.'temp_mpris2_cover'.$::DBus_suffix.'.jpg'; my $bus=$GMB::DBus::bus; die "Requires DBus support to be active\n" unless $bus; #only requires this to use the hack in gmusicbrowser_dbus.pm so that Net::DBus::GLib is not required, else could do just : use Net::DBus::GLib; $bus=Net::DBus::GLib->session; my @Objects; sub Start { my $service= $bus->export_service('org.mpris.MediaPlayer2.gmusicbrowser'); push @Objects, GMB::DBus::MPRIS2->new($service); unlink $TEMPCOVERFILE; } sub Stop { ::UnWatch_all($_) for @Objects; $_->disconnect for @Objects; @Objects=(); unlink $TEMPCOVERFILE; } sub prefbox { my $vbox=Gtk2::VBox->new(0,2); my $desc= Gtk2::Label->new(_"This plugin is needed for gmusicbrowser to appear in unity's sound menu."); $vbox->pack_start($desc,0,0,0); return $vbox; } package GMB::DBus::MPRIS2; use base 'Net::DBus::Object'; use Net::DBus::Exporter 'org.mpris.MediaPlayer2'; use Net::DBus ':typing'; our %PropChanged; # events watched by properties of org.mpris.MediaPlayer2.Player that send PropertiesChanged signal # the functions associated with these properties must bless the return value with dbus_string() and friends my %PropertiesWatch= ( PlaybackStatus => 'Playing', LoopStatus => 'Lock Repeat', Shuffle => 'Sort', Metadata => 'CurSong', Volume => 'Vol', CanGoNext => 'Playlist Sort Queue Repeat', CanGoPrevious => 'CurSongID', CanPlay => 'CurSongID', #CanSeek => 'CurSongID', # always true currently => no need to watch event ); sub new { my ($class,$service) = @_; my $self = $class->SUPER::new($service, '/org/mpris/MediaPlayer2'); bless $self, $class; ::Watch($self, Seek => \&Seeked); ::Watch($self, FullScreen=> sub { $self->emit_signal( PropertiesChanged => 'org.mpris.MediaPlayer2', {Fullscreen=> Fullscreen()} ,[] ); }); ::Watch($self, Quit=> sub { unlink $TEMPCOVERFILE; }); #watchers for properties of org.mpris.MediaPlayer2.Player that send PropertiesChanged signal my %events; for my $prop (sort keys %PropertiesWatch) { push @{ $events{$_} }, $prop for split / /, $PropertiesWatch{$prop}; } for my $event (keys %events) { my $props= $events{$event}; ::Watch($self, $event => sub { my $self=shift; $PropChanged{$_}=1 for @$props; ::IdleDo('2_MPRIS2_propchanged', 500, \&PropertiesChanged, $self); }); } return $self; } dbus_signal(PropertiesChanged => ['string',['dict','string',['variant']],['array','string']], 'org.freedesktop.DBus.Properties'); sub PropertiesChanged { my $self=shift; my %changed; for my $name (keys %PropChanged) { no strict "refs"; $changed{$name}= $name->(); } %PropChanged=(); $self->emit_signal( PropertiesChanged => 'org.mpris.MediaPlayer2.Player', \%changed,[] ); } dbus_method('Raise', [], [],{no_return=>1}); sub Raise { ::ShowHide(1); } dbus_method('Quit', [], [],{no_return=>1}); sub Quit { ::Quit(); } dbus_property('Fullscreen', 'bool', 'readwrite'); sub Fullscreen { if (defined $_[1]) { ::SetFullScreenMode($_[1]); } else { return dbus_boolean(!!$::FullscreenWindow) } } dbus_property('CanSetFullscreen', 'bool', 'read'); sub CanSetFullscreen {dbus_boolean(1)} dbus_property('CanQuit', 'bool', 'read'); sub CanQuit {dbus_boolean(1)} dbus_property('CanRaise', 'bool', 'read'); sub CanRaise {dbus_boolean(1)} dbus_property('HasTrackList', 'bool', 'read'); sub HasTrackList {dbus_boolean(0)} dbus_property('Identity', 'string', 'read'); sub Identity { 'gmusicbrowser' } dbus_property('DesktopEntry', 'string', 'read'); sub DesktopEntry { 'gmusicbrowser' } dbus_property('SupportedUriSchemes', ['array','string'], 'read'); sub SupportedUriSchemes { return ['file']; } dbus_property('SupportedMimeTypes', ['array','string'], 'read'); sub SupportedMimeTypes { return [qw(application/ogg audio/flac audio/mpeg audio/ogg audio/x-flac audio/x-m4a audio/x-musepack)]; } #FIXME dbus_method('Next', [], [], 'org.mpris.MediaPlayer2.Player', {no_return=>1}); sub Next { ::NextSong(); } dbus_method('Previous', [], [], 'org.mpris.MediaPlayer2.Player', {no_return=>1}); sub Previous { ::PrevSong(); } dbus_method('Pause', [], [], 'org.mpris.MediaPlayer2.Player', {no_return=>1}); sub Pause { ::Pause() if $::TogPlay; } dbus_method('PlayPause',[], [], 'org.mpris.MediaPlayer2.Player', {no_return=>1}); sub PlayPause { ::PlayPause(); } dbus_method('Stop', [], [], 'org.mpris.MediaPlayer2.Player', {no_return=>1}); sub Stop { ::Stop(); } dbus_method('Play', [], [], 'org.mpris.MediaPlayer2.Player', {no_return=>1}); sub Play { ::PlayPause() unless $::TogPlay; } dbus_method('Seek', ['int64'], [], 'org.mpris.MediaPlayer2.Player', {no_return=>1}); sub Seek { my $offset= $_[1]/1_000_000; #convert from microseconds my $sec= $::PlayTime || 0; return unless defined $::SongID; if ($offset>0) { $sec+=$offset; my $length= Songs::Get($::SongID,'length'); if ($sec>$length) { ::NextSong(); } else { ::SkipTo($sec) } } elsif ($offset<0) { $sec+=$offset; if ($sec<0) { ::PrevSong(); } else { ::SkipTo($sec) } } } dbus_method('SetPosition', ['objectpath','int64'], [], 'org.mpris.MediaPlayer2.Player', {no_return=>1}); sub SetPosition { my (undef,$ID,$position)= (@_); return unless $ID=~m#^/Song/(\d+)$#; $ID=$1; return unless defined $::SongID && $ID==$::SongID; $position/=1_000_000; my $length= Songs::Get($::SongID,'length'); return if $length<0 || $position>$length; ::SkipTo($position); } dbus_method('OpenUri', ['string'], [], 'org.mpris.MediaPlayer2.Player', {no_return=>1}); sub OpenUri { my $uri=$_[1]; my $IDs= ::Uris_to_IDs($uri); my $ID= $IDs->[0]; ::Select(song => $ID, play=>1) if defined $ID; } dbus_signal(Seeked => ['int64'], 'org.mpris.MediaPlayer2.Player'); sub Seeked { $_[0]->emit_signal( Seeked => $_[1]*1_000_000 ); } dbus_property('PlaybackStatus', 'string', 'read', 'org.mpris.MediaPlayer2.Player'); sub PlaybackStatus { my $status= $::TogPlay ? 'Playing' : defined $::TogPlay ? 'Paused' : 'Stopped'; return dbus_string($status); } dbus_property('LoopStatus', 'string', 'readwrite', 'org.mpris.MediaPlayer2.Player'); sub LoopStatus { if (defined $_[1]) { my $m=$_[1]; my $notrack; if ($m eq 'None') { ::SetRepeat(0); $notrack=1; } elsif ($m eq 'Track') { ::SetRepeat(1); ::ToggleLock('fullfilename',1); } elsif ($m eq 'Playlist'){ ::SetRepeat(1); $notrack=1; } if ($notrack && $::TogLock && $::TogLock eq 'fullfilename') { ::ToggleLock('fullfilename') } } else { my $r= !$::Options{Repeat} ? 'None' : ($::TogLock && $::TogLock eq 'fullfilename') ? 'Track' : 'Playlist'; return dbus_string($r); } } dbus_property('Rate', 'double', 'readwrite', 'org.mpris.MediaPlayer2.Player'); sub Rate {dbus_double(1)} dbus_property('MinimumRate', 'double', 'read', 'org.mpris.MediaPlayer2.Player'); sub MinimumRate {dbus_double(1)} dbus_property('MaximumRate', 'double', 'read', 'org.mpris.MediaPlayer2.Player'); sub MaximumRate {dbus_double(1)} dbus_property('Shuffle', 'bool', 'readwrite', 'org.mpris.MediaPlayer2.Player'); sub Shuffle { my $on= ($::RandomMode || $::Options{Sort}=~m/shuffle/) ? 1 : 0; return dbus_boolean($on) if !defined $_[1]; ::ToggleSort() if $_[1] xor $on; } dbus_property('Metadata', ['dict','string',['variant']], 'read', 'org.mpris.MediaPlayer2.Player'); sub Metadata { GetMetadata_from($::SongID); } dbus_property('Volume', 'double', 'readwrite', 'org.mpris.MediaPlayer2.Player'); sub Volume { if (defined $_[1]) { my $v=$_[1]; $v=0 if $v<0; ::ChangeVol(100*$v); } else { return dbus_double($::Volume/100); } } dbus_property('Position', 'int64', 'read', 'org.mpris.MediaPlayer2.Player'); sub Position { return dbus_int64( ($::PlayTime||0) *1_000_000 ); } dbus_property('CanGoNext', 'bool', 'read', 'org.mpris.MediaPlayer2.Player'); sub CanGoNext { return dbus_boolean(1) if !defined $::Position && @$::ListPlay; return dbus_boolean(1) if @$::Queue; return dbus_boolean(0) unless @$::ListPlay>1; return dbus_boolean(0) if !$::Options{Repeat} && $::Position==$#$::ListPlay; return dbus_boolean(1); } dbus_property('CanGoPrevious', 'bool', 'read', 'org.mpris.MediaPlayer2.Player'); sub CanGoPrevious { return dbus_boolean( @$::Recent > ($::RecentPos||0) ); } dbus_property('CanPlay', 'bool', 'read', 'org.mpris.MediaPlayer2.Player'); sub CanPlay { return dbus_boolean( defined $::SongID ); } dbus_property('CanPause', 'bool', 'read', 'org.mpris.MediaPlayer2.Player'); sub CanPause { return dbus_boolean( defined $::SongID ); } dbus_property('CanSeek', 'bool', 'read', 'org.mpris.MediaPlayer2.Player'); sub CanSeek { return dbus_boolean( defined $::SongID ); #will need to check if stream when supported } dbus_property('CanControl', 'bool', 'read', 'org.mpris.MediaPlayer2.Player'); sub CanControl {dbus_boolean(1)} # 'org.mpris.MediaPlayer2.Player','Metadata' sub GetMetadata_from { my $ID=shift; # Net::DBus support for properties is incomplete, the following use undocumented functions to force it to use the correct data types for the returned values my $type= [ &Net::DBus::Binding::Message::TYPE_DICT_ENTRY, [ &Net::DBus::Binding::Message::TYPE_STRING, [ &Net::DBus::Binding::Message::TYPE_VARIANT, [], ]]]; #my ($type)= Net::DBus::Binding::Introspector->_convert(['dict','string',['variant']]); #works too, not sure which one is best return Net::DBus::Binding::Value->new($type,{}) unless defined $ID; my %h; $h{$_}=Songs::Get($ID,$_) for qw/title album artist comment length track disc year album_artist uri album_picture rating bitrate samprate genre playcount/, grep Songs::FieldEnabled($_), qw/composer lyricist bpm/; my %r= #return values ( 'mpris:length' => dbus_int64($h{'length'}*1_000_000), 'mpris:trackid' => dbus_object_path("/Song/$ID"), #FIXME should contain a string that uniquely identifies the track within the scope of the playlist 'xesam:album' => dbus_string($h{album}), 'xesam:albumArtist' => dbus_array([ $h{album_artist} ]), 'xesam:artist' => dbus_array([ $h{artist} ]), 'xesam:comment' => ( $h{comment} ne '' ? dbus_array([$h{comment}]) : undef ), 'xesam:contentCreated' => ($h{year} ? dbus_string($h{year}) : undef), # ."-01-01T00:00Z" ? 'xesam:discNumber' => ($h{disc} ? dbus_int32($h{disc}) : undef), 'xesam:genre', => dbus_array([split /\x00/, $h{genre}]), 'xesam:lastUsed', => ($h{lastplay} ? dbus_string( ::strftime("%FT%RZ",gmtime($h{lastplay})) ) : undef), 'xesam:title', => dbus_string( $h{title} ), 'xesam:trackNumber' => ( $h{track} ? dbus_int32($h{track}) : undef), 'xesam:url' => dbus_string( $h{uri} ), 'xesam:useCount' => dbus_int32($h{playcount}), 'xesam:audioBPM' => ($h{bpm} ? dbus_int32($h{bpm}) : undef), 'xesam:composer' => ($h{composer}||'' ne '' ? dbus_array([ $h{composer} ]) : undef), 'xesam:lyricist', => ($h{lyricist}||'' ne '' ? dbus_array([ $h{lyricist} ]) : undef), ); my $rating=$h{rating}; if (defined $rating && length $rating) { $r{'xesam:userRating'}=dbus_double($rating/100); } unlink $TEMPCOVERFILE; if (my $pic= $h{album_picture}) #FIXME use ~album.picture.uri when available { if ($pic=~m/$::EmbImage_ext_re(?:\w+)?$/i) #embedded pictures { my $ok; { last unless defined($::SongID) && $ID==$::SongID; #only support embedded picture for current song my $data=FileTag::PixFromMusicFile($pic); last unless $data; my $fh; open($fh,'>',$TEMPCOVERFILE) && (print $fh $data) && close($fh) && ($ok=1); warn "mpris2 plugin, error writing temporary file '".$TEMPCOVERFILE."' for embedded cover: $!" unless $ok; } $pic= $ok ? $TEMPCOVERFILE : undef; } $r{'mpris:artUrl'}= dbus_string( 'file://'.::url_escape($pic) ) if $pic; } delete $r{$_} for grep !defined $r{$_}, keys %r; return Net::DBus::Binding::Value->new($type,\%r); } ### patched version of Net::DBus::Object::_dispatch_all_prop_read v1.0.0 to support properties of different types ### Net::DBus::Object::_dispatch_all_prop_read was added in Net::DBus v1.0.0 to support the org.freedesktop.DBus.Properties.GetAll method no warnings 'redefine'; sub Net::DBus::Object::_dispatch_all_prop_read { my $self = shift; my $connection = shift; my $message = shift; my $ins = $self->_introspector; if (!$ins) { return $connection->make_error_message($message, "org.freedesktop.DBus.Error.Failed", "no introspection data exported for properties"); } my ($pinterface) = $ins->decode($message, "methods", "Get", "params"); my %values = (); foreach my $pname ($ins->list_properties($pinterface)) { unless ($ins->is_property_readable($pinterface, $pname)) { next; # skip write-only properties } $values{$pname} = eval { $self->$pname; }; if ($@) { return $connection->make_error_message($message, "org.freedesktop.DBus.Error.Failed", "error reading '$pname' in interface '$pinterface': $@"); } } my $reply = $connection->make_method_return_message($message); ### patch : fix method name, which fix return type $self->_introspector->encode($reply, "methods", "GetAll", "returns", \%values); ### $self->_introspector->encode($reply, "methods", "Get", "returns", \%values); ### end of patch return $reply; } 1; gmusicbrowser-1.1.15~ds0.orig/plugins/mpris1.pm0000664000175000017500000001633612565212605020721 0ustar unit193unit193# Copyright (C) 2010 Quentin Sculo # # 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 =for gmbplugin MPRIS name MPRIS v1 title MPRIS v1 support desc Allows controlling gmusicbrowser via DBus using the MPRIS v1.0 standard req perl(Net::DBus, libnet-dbus-perl perl-Net-DBus) version 1.0 =cut package GMB::Plugin::MPRIS; use strict; use warnings; use constant { OPT => 'PLUGIN_MPRIS_', }; my $bus=$GMB::DBus::bus; die "Requires DBus support to be active\n" unless $bus; #only requires this to use the hack in gmusicbrowser_dbus.pm so that Net::DBus::GLib is not required, else could do just : use Net::DBus::GLib; $bus=Net::DBus::GLib->session; my @Objects; sub Start { my $service= $bus->export_service('org.mpris.gmusicbrowser'); push @Objects, GMB::DBus::MPRIS->new($service); #push @Objects, GMB::DBus::MPRIS::TrackList->new($Objects[0]); #push @Objects, GMB::DBus::MPRIS::Player->new($Objects[0]); push @Objects, GMB::DBus::MPRIS::TrackList->new($service); push @Objects, GMB::DBus::MPRIS::Player->new($service); #warn $_->get_object_path for @Objects; } sub Stop { ::UnWatch_all($_) for @Objects; $_->disconnect for @Objects; @Objects=(); } sub prefbox {} package GMB::DBus::MPRIS; use base 'Net::DBus::Object'; use Net::DBus::Exporter 'org.freedesktop.MediaPlayer'; sub new { my ($class,$service) = @_; my $self = $class->SUPER::new($service, '/'); bless $self, $class; return $self; } dbus_method('Identity', [], ['string']); sub Identity { return 'gmusicbrowser '.::VERSIONSTRING; } dbus_method('Quit', [], [],{no_return=>1}); sub Quit { ::Quit(); } dbus_method('MprisVersion', [], [['struct','uint16','uint16']]); sub MprisVersion { return [1,0]; } package GMB::DBus::MPRIS::TrackList; use base 'Net::DBus::Object'; use Net::DBus::Exporter 'org.freedesktop.MediaPlayer'; use Net::DBus ':typing'; #for dbus_uint32, dbus_double sub new { my ($class,$service) = @_; my $self = $class->SUPER::new($service, '/TrackList'); bless $self, $class; ::Watch($self,Playlist => \&TrackListChange); ::Watch($self,Sort => \&TrackListChange); return $self; } sub GetMetadata_from { my $ID=shift; return {} unless defined $ID; my %h; $h{$_}=Songs::Get($ID,$_) for qw/title album artist comment length track disc year album_artist uri album_picture rating bitrate samprate/; my $r=delete $h{rating}; if (defined $r && length $r) { ::setlocale(::LC_NUMERIC, 'C'); $h{rating}=dbus_double($r/20); ::setlocale(::LC_NUMERIC, ''); } if (my $pic= delete $h{album_picture}) #FIXME use ~album.picture.uri when available { $h{arturl}= 'file://'.::url_escape($pic); # ignore picture embedded in mp3/flac files ? } $h{mtime}= $h{'length'}*1000; $h{'time'}= delete $h{'length'}; $h{location}= delete $h{uri}; $h{tracknumber}= delete $h{track}; $h{'audio-samplerate'}= delete $h{samprate}; $h{'audio-bitrate'}= delete $h{bitrate}; $h{$_}=dbus_uint32($h{$_}) for qw/time mtime year audio-bitrate audio-samplerate disc/; return \%h; } dbus_method('GetMetadata', ['int32'], [['dict', 'string', ["variant"]]]); sub GetMetadata { my ($self,$pos)=@_; my $ID= $::ListPlay->[$pos]; GetMetadata_from($ID); } dbus_method('GetCurrentTrack', [], ['int32']); sub GetCurrentTrack { my $p=$::Position; $p=::FindPositionSong($::SongID,$::ListPlay) unless defined $p; #random mode or song not in playlist $p=0 unless defined $p; # fallback to 0, not really a good idea, but don't know what else to return if current song is not in playlist return $p; } dbus_method('GetLength', [], ['int32']); sub GetLength { return scalar @$::ListPlay; } dbus_method('AddTrack', ['string','bool'], ['int32']); sub AddTrack { my ($self,$uri,$playnow)=@_; $uri=~s/ /%20/g; #Uris_to_IDs split spaces FIXME Uris_to_IDs shouldn't do that my ($ID)= @{::Uris_to_IDs($uri)}; return 1 unless defined $ID; $::ListPlay->Push([$ID]); ::Select(song => $ID, play=>1) if $playnow; return 0; #success } dbus_method('DelTrack', ['int32'], [],{no_return=>1}); sub DelTrack { my ($self,$pos)=@_; $::ListPlay->Remove([$pos]); } dbus_method('SetLoop', ['bool'], [],{no_return=>1}); sub SetLoop { my ($self,$on)=@_; ::SetRepeat($on); } dbus_method('SetRandom', ['bool'], [],{no_return=>1}); sub SetRandom { my ($self,$on)=@_; my $israndom= ($::RandomMode || $::Options{Sort}=~m/shuffle/); ::ToggleSort() if $israndom xor $on; } dbus_signal(TrackListChange => ['int32']); sub TrackListChange { $_[0]->emit_signal( TrackListChange => scalar @$::ListPlay ); } package GMB::DBus::MPRIS::Player; use base 'Net::DBus::Object'; use Net::DBus::Exporter 'org.freedesktop.MediaPlayer'; sub new { my ($class,$service) = @_; my $self = $class->SUPER::new($service, '/Player'); bless $self, $class; ::Watch($self, PlayingSong => \&TrackChange); ::Watch($self, $_ => \&StatusChange) for qw/Sort Playing Lock Repeat/; ::Watch($self, $_ => \&CapsChange) for qw/Sort Repeat Playlist CurSongID/; return $self; } dbus_method('Next', [], [],{no_return=>1}); sub Next { ::NextSong(); } dbus_method('Prev', [], [],{no_return=>1}); sub Prev { ::PrevSong(); } dbus_method('Pause', [], [],{no_return=>1}); sub Pause { ::Pause(); } dbus_method('Stop', [], [],{no_return=>1}); sub Stop { ::Stop(); } dbus_method('Play', [], [],{no_return=>1}); sub Play { ::Play(); } dbus_method('Repeat', ['bool'], [],{no_return=>1}); sub Repeat { my ($self,$on)=@_; ::ToggleLock('fullfilename',$on); } dbus_method('GetStatus', [], [['struct','int32','int32','int32','int32']]); sub GetStatus { my $playstop= $::TogPlay ? 0 : defined $::TogPlay ? 1 : 2; #0 = Playing, 1 = Paused, 2 = Stopped my $israndom= ($::RandomMode || $::Options{Sort}=~m/shuffle/) ? 1 : 0; my $repeat= ($::TogLock && $::TogLock eq 'fullfilename') ? 1 : 0; my $loop= $::Options{Repeat} ? 1 : 0; return [$playstop, $israndom, $repeat, $loop]; } dbus_method('GetMetadata', [], [['dict', 'string', ["variant"]]]); sub GetMetadata { GMB::DBus::MPRIS::TrackList::GetMetadata_from($::SongID); } dbus_method('GetCaps', [], ['int32']); sub GetCaps { my $go_next= @$::ListPlay>1 && ( $::RandomMode || $::Options{Repeat} || $::Position<$#$::ListPlay); my $go_prev= @$::Recent && ($::RecentPos||0) < $#$::Recent; my $pause= defined $::SongID; my $play= defined $::SongID; my $seek=1; my $provide_metadata=1; #my $has_tracklist= $::RandomMode ? 0 : 1; my $has_tracklist=1; my $caps=my $i=0; for my $cap ($go_next, $go_prev, $pause, $play, $seek, $provide_metadata, $has_tracklist) { $caps+= 1<<$i if $cap; $i++; } return $caps; } dbus_method('VolumeSet', ['int32'], []); sub VolumeSet { ::UpdateVol($_[1]); } dbus_method('VolumeGet', [], ['int32']); sub VolumeGet { return ::GetVol(); } dbus_method('PositionSet', ['int32'], []); sub PositionSet { ::SkipTo($_[1]/1000) } dbus_method('PositionGet', [], ['int32']); sub PositionGet { return ($::PlayTime || 0)*1000; } dbus_signal(TrackChange => [['dict', 'string', ["variant"]]]); sub TrackChange { $_[0]->emit_signal( TrackChange => GetMetadata() ); } dbus_signal(StatusChange => [['struct','int32','int32','int32','int32']]); sub StatusChange { $_[0]->emit_signal( StatusChange => GetStatus() ); } dbus_signal(CapsChange => ['int32']); sub CapsChange { $_[0]->emit_signal( CapsChange => GetCaps() ); } 1 gmusicbrowser-1.1.15~ds0.orig/plugins/lullaby.pm0000664000175000017500000000604212565212605021143 0ustar unit193unit193# Copyright (C) 2009 Quentin Sculo # # 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 =for gmbplugin LULLABY name Lullaby title Lullaby plugin desc Allow for scheduling fade-out and stop =cut #TODO : #- configure what to do at the end #- visual feedback #- way to abort package GMB::Plugin::LULLABY; use strict; use warnings; use constant { OPT => 'PLUGIN_LULLABY_', }; ::SetDefaultOptions(OPT, timespan => 30); my @dayname= (_"Sunday", _"Monday", _"Tuesday", _"Wednesday", _"Thursday", _"Friday", _"Saturday"); my $handle; my $alarm; my $StartingVolume; sub Start { $::Command{FadeOut}=[\&start_fadeout,_"Fade-out then stop",_"Timespan of the fade-out in seconds"]; update_alarm(); } sub Stop { Glib::Source->remove($handle) if $handle; $handle=undef; Glib::Source->remove($alarm) if $alarm; $alarm=undef; delete $::Command{FadeOut}; } sub prefbox { my $vbox=Gtk2::VBox->new; my $spin=::NewPrefSpinButton(OPT.'timespan', 1,60*60*24, step=>1, text=>_"Fade-out in %d seconds"); my $button=Gtk2::Button->new(_"Fade-out"); $button->signal_connect(clicked => \&start_fadeout); my $sg=Gtk2::SizeGroup->new('horizontal'); my @hours; for my $wd (0..6) { my $min= ::NewPrefSpinButton(OPT."day${wd}m", 0,59, cb=>\&update_alarm, step=>1, page=>5, wrap=>1); my $hour=::NewPrefSpinButton(OPT."day${wd}h", 0,23, cb=>\&update_alarm, step=>1, page=>4, wrap=>1); my $timeentry=::Hpack($hour,Gtk2::Label->new(':'),$min); my $check=::NewPrefCheckButton(OPT."day$wd",$dayname[$wd], cb=>\&update_alarm, widget=>$timeentry, horizontal=>1, sizegroup=>$sg); push @hours,$check; } $vbox->pack_start($_,::FALSE,::FALSE,2) for $spin,@hours,$button; return $vbox; } sub update_alarm { Glib::Source->remove($alarm) if $alarm; $alarm=undef; my $now=time; my (undef,undef,undef,$mday0,$mon,$year,$wday0,$yday,$isdst)= localtime($now); my $next=0; for my $wd (0..6) { next unless $::Options{OPT."day$wd"}; my $mday=$mday0+($wd-$wday0+7)%7; my $m=$::Options{OPT."day${wd}m"}; my $h=$::Options{OPT."day${wd}h"}; my $time=::mktime(0,$m,$h,$mday,$mon,$year); #warn "$wd $time<$now\n"; $time=::mktime(0,$m,$h,$mday+7,$mon,$year) if $time<=$now; if ($next) {$next=$time if $time<$next} else {$next=$time} #warn "$wd $time next=$next\n"; } return unless $next;#warn ($next-$now); $alarm=Glib::Timeout->add(($next-$now)*1000,\&alarm); } sub alarm { start_fadeout(); update_alarm(); } sub start_fadeout { return if $handle; my $timespan=$::Options{OPT.'timespan'}; $timespan=$_[1] if $_[1] and $_[1]=~m/^\d+$/; $StartingVolume= ::GetVol(); $handle=Glib::Timeout->add($timespan*1000/100,\&fade,$StartingVolume/100); } sub fade { my $dec=$_[0]; my $new= ::GetVol() - $dec; $new=0 if $new<0; ::UpdateVol($new); if ($new==0) { warn "fade-out finished\n" if $::debug; $handle=undef; ::Stop(); ::UpdateVol($StartingVolume); #::TurnOff(); } return $handle; #false when finished } 1 gmusicbrowser-1.1.15~ds0.orig/plugins/nowplaying.pm0000664000175000017500000000650412565212605021671 0ustar unit193unit193# Copyright (C) 2005-2009 Quentin Sculo # # 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 # the plugin file must have the following block before the first non-comment line, # it must be of the format : # =for gmbplugin PID # name short name # title long name, the short name is used if empty # desc description, may be multiple lines # =cut =for gmbplugin NOWPLAYING name Now playing title NowPlaying plugin desc Run a command when playing a song =cut # the plugin package must be named GMB::Plugin::PID (replace PID), and must have these sub : # Start : called when the plugin is activated # Stop : called when the plugin is de-activated # prefbox : returns a Gtk2::Widget used to describe the plugin and set its options package GMB::Plugin::NOWPLAYING; use strict; use warnings; use constant { OPT => 'PLUGIN_NOWPLAYING_', }; my $handle; sub Start { $handle={}; #the handle to the Watch function must be a hash ref, if it is a Gtk2::Widget, UnWatch will be called when the widget is destroyed ::Watch($handle, PlayingSong => \&Changed); ::Watch($handle, Playing => \&PlayStop); } sub Stop { ::UnWatch($handle,'PlayingSong'); ::UnWatch($handle,'Playing'); } sub prefbox { my $vbox=Gtk2::VBox->new(::FALSE, 2); my $sg1=Gtk2::SizeGroup->new('horizontal'); my $entry=::NewPrefEntry(OPT.'CMD',_"Command when playing song changed :", expand=>1,sizeg1=>$sg1); my $entry2=::NewPrefEntry(OPT.'StoppedCMD',_"Command when stopped :", expand=>1,sizeg1=>$sg1); my $preview= Label::Preview->new(preview => \&command_preview, event => 'CurSong Option', noescape=>1); my $check=::NewPrefCheckButton(OPT.'SENDSTDINPUT',_"Send Title/Artist/Album in standard input"); my $replacetable=::MakeReplaceTable('talydnfc'); $vbox->pack_start($_,::FALSE,::FALSE,2) for $replacetable,$entry,$preview,$entry2,$check; return $vbox; } sub command_preview { my $ID=$::SongID; my $cmd= $::Options{OPT.'CMD'}; return '' unless defined $ID && defined $cmd; my @cmd= ::split_with_quotes($cmd); return '' unless @cmd; $_= ::PangoEsc( ::ReplaceFields($ID,$_) ) for @cmd; splice @cmd,$_,0, ::MarkupFormat("\n%s", ::__x(_"argument {n} :",n=>$_)) for reverse 1..$#cmd; unshift @cmd, ::MarkupFormat('%s', _"command :"); my $t= join(' ',@cmd); return ''.$t.''; } sub PlayStop { return if defined $::TogPlay; #TogPlay is undef when Stopped, 0 when Paused, 1 when Playing my $cmd=$::Options{OPT.'StoppedCMD'}; return unless $cmd; ::forksystem(::split_with_quotes($cmd)); } # FIXME should simply call run_system_cmd($cmd,[$::SongID],0); but the SENDSTDINPUT option make that difficult sub Changed { my $ID=$::SongID; my $cmd= $::Options{OPT.'CMD'}; return unless defined $cmd && $cmd=~m/\S/; $cmd=Encode::encode("utf8", $cmd); my $quotesub= sub { my $s=$_[0]; if (utf8::is_utf8($s)) { $s=Encode::encode("utf8",$s); } $s; }; my @cmd= ::split_with_quotes($cmd); $_=::ReplaceFields($ID,$_,$quotesub) for @cmd; if ($::Options{OPT.'SENDSTDINPUT'}) { my $string=::ReplaceFields($ID,"Title=%t\nArtist=%a\nAlbum=%l\nLength=%m\nYear=%y\nTrack=%n\n"); open my$out,'|-:utf8',@cmd; print $out $string; close $out; } else { ::forksystem(@cmd); } } 1 #the file must return true gmusicbrowser-1.1.15~ds0.orig/plugins/autosave.pm0000664000175000017500000000207712565212605021332 0ustar unit193unit193# Copyright (C) 2005-2007 Quentin Sculo # # 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 =for gmbplugin AUTOSAVE name Autosave title Autosave plugin =cut package GMB::Plugin::AUTOSAVE; use strict; use warnings; use constant { OPT => 'PLUGIN_AUTOSAVE_', }; my $savesub=$::Command{Save}[0]; my $handle; ::SetDefaultOptions(OPT, minutes => 15); sub Start { $handle=Glib::Timeout->add($::Options{OPT.'minutes'}*60000,sub {$savesub->(); 1}); } sub Stop { Glib::Source->remove($handle); $handle=undef; } sub prefbox { my $vbox=Gtk2::VBox->new(::FALSE, 2); my $spin=::NewPrefSpinButton(OPT.'minutes', 1,60*24, step=>1, page=>15, cb=>sub { Stop() if $handle; Start(); }, text=>_"Save tags/settings every %d minutes"); my $button=Gtk2::Button->new(_"Save now"); $button->signal_connect(clicked => $savesub); $vbox->pack_start($_,::FALSE,::FALSE,2) for $spin,$button; return $vbox; } 1 gmusicbrowser-1.1.15~ds0.orig/plugins/desktopwidget.pm0000664000175000017500000002117012565212605022353 0ustar unit193unit193# Copyright (C) 2010 Quentin Sculo # # 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 =for gmbplugin DesktopWidgets name Desktop widgets title Desktop widgets plugin desc Open special layouts as desktop widgets =cut package GMB::Plugin::DesktopWidgets; use strict; use warnings; use constant { OPT => 'PLUGIN_DesktopWidgets_', }; my $DWlist= $::Options{OPT.'list'} ||= {}; my %Displayed; my ($OptionsBox,$Treeview,$LayoutCombo); sub Start { Glib::Idle->add( sub { CreateWindow($_) for sort keys %$DWlist; 0; }); } sub Stop { $_->close_window for grep $_, values %Displayed; %Displayed=(); } sub prefbox { my $vbox=Gtk2::VBox->new(::FALSE, 2); $LayoutCombo= TextCombo::Tree->new( sub {Layout::get_layout_list('D')}, undef, undef, event=>'Layouts', ); my $layoutlabel= Gtk2::Label->new(_"Layout :"); my $add= ::NewIconButton('gtk-add',_"Add", sub { New($LayoutCombo->get_value); }); my $store=Gtk2::ListStore->new('Glib::String','Glib::Boolean','Glib::String'); $Treeview=Gtk2::TreeView->new($store); $Treeview->set_size_request(100,($Treeview->create_pango_layout("X")->get_pixel_size)[1]*5.5); #request 5.5 lines of height (not counting row spacing) $Treeview->set_headers_visible(::FALSE); my $togglerenderer=Gtk2::CellRendererToggle->new; $togglerenderer->signal_connect(toggled => \&toggled); $Treeview->append_column( Gtk2::TreeViewColumn->new_with_attributes( 'active', $togglerenderer, active => 1 )); my $renderer=Gtk2::CellRendererText->new; $Treeview->append_column( Gtk2::TreeViewColumn->new_with_attributes( 'name', $renderer, text => 2 )); my $sw=Gtk2::ScrolledWindow->new; $sw->set_shadow_type('etched-in'); $sw->set_policy('automatic','automatic'); $sw->add($Treeview); $Treeview->get_selection->signal_connect(changed => \&selchanged_cb); $OptionsBox=Gtk2::VBox->new(::FALSE, 2); $vbox->pack_start($_,::FALSE,::FALSE,2) for ::Hpack( $layoutlabel,$LayoutCombo,$add ),$sw,$OptionsBox; ::weaken($OptionsBox); ::weaken($Treeview); ::weaken($LayoutCombo); Fill(); return $vbox; } sub Fill { my $selected=shift; return unless $Treeview; my $store=$Treeview->get_model; $store->clear; for my $key (sort keys %$DWlist) { my $layout= $DWlist->{$key}{layout}; my $name= Layout::get_layout_name($layout); my $iter=$store->append; $store->set($iter, 0,$key, 1,!$DWlist->{$key}{dw_inactive}, 2,$name); $Treeview->get_selection->select_iter($iter) if $selected && $key eq $selected; } } sub Remove { my $key=shift; return unless $key; delete $DWlist->{$key}; my $window= delete $Displayed{$key}; $window->close_window if $window; Fill(); } sub selchanged_cb { my $treesel=shift; $OptionsBox->remove($_) for $OptionsBox->get_children; my $iter=$treesel->get_selected; return unless $iter; my $key=$Treeview->get_model->get($iter,0); FillOptions($key); } sub toggled { my ($cell, $path_string)=@_; my $store=$Treeview->get_model; my $iter=$store->get_iter_from_string($path_string); my $key= $store->get($iter,0); my $state= $DWlist->{$key}{dw_inactive}^=1; $store->set($iter,1,!$state); if ($state) { my $window= delete $Displayed{$key}; $window->close_window if $window; } else { CreateWindow($key); } } sub New { my $layout=shift; return unless defined $layout; my $layoutdef=$Layout::Layouts{$layout}; return unless $layoutdef; my $key='DesktopWidget000'; $key++ while defined $DWlist->{$key}; my $default_window_opt= ::ParseOptions( $layoutdef->{Window}||'' ); my $size= $default_window_opt->{size} || '1x1'; my ($w,$h)= $size=~m/(\d+)x(\d+)/; my %opt= ( layout => $layout, DefaultFontColor=> $layoutdef->{DefaultFontColor} || 'white', DefaultFont => $layoutdef->{DefaultFont} || 'Sans 12', monitor => 0, below => 1, opacity => 1, x => 0, y => 0, w => $w||1, h => $h||1, ); $DWlist->{$key}= \%opt; CreateWindow($key); Fill($key); } sub FillOptions { my $key=shift; return unless $OptionsBox; my $opt= $DWlist->{$key}; my $layout= $opt->{layout}; my $label= Gtk2::Label->new; my $remove= ::NewIconButton('gtk-remove',_"Remove this widget", sub { Remove($key) }); if ($Layout::Layouts{$layout}) { my $name= $Layout::Layouts{$layout}{Name} || $layout; my $author=$Layout::Layouts{$layout}{Author}; my $markup= "%s"; if (defined $author) { $author= _("by").' '.$author; $markup.= "\n%s"; } $label->set_markup_with_format($markup,$name, $author||() ); } else { $label->set_markup_with_format("%s",_"The layout for this desktop widget is missing."); my $vbox= ::Hpack($label, '-',$remove); $OptionsBox->pack_start($vbox,::FALSE,::FALSE,2); $OptionsBox->show_all; return; } my $textcolor= Gtk2::ColorButton->new_with_color( Gtk2::Gdk::Color->parse($opt->{DefaultFontColor}) ); $textcolor->signal_connect(color_set=>sub { $opt->{DefaultFontColor}= $_[0]->get_color->to_string; CreateWindow($key); }); #my $set_textcolor= ::NewPrefCheckButton(OPT.'set_textcolor',_"Change default text color", cb=>\&init, widget=>$textcolor, horizontal=>1); my $textfont= Gtk2::FontButton->new_with_font( $opt->{DefaultFont} ); $textfont->signal_connect(font_set=>sub { $opt->{DefaultFont}= $_[0]->get_font_name; CreateWindow($key); }); my $adjx=Gtk2::Adjustment->new($opt->{x},0,100,1,10,0); my $adjy=Gtk2::Adjustment->new($opt->{y},0,100,1,10,0); #my $spinx=Gtk2::SpinButton->new($adjx,0,0); #my $spiny=Gtk2::SpinButton->new($adjy,0,0); my $spinx=Gtk2::HScale->new($adjx); my $spiny=Gtk2::HScale->new($adjy); $spinx->set_digits(0); $spiny->set_digits(0); $adjx->signal_connect(value_changed => sub { $opt->{x}=$_[0]->get_value; MoveWindow($key); }); $adjy->signal_connect(value_changed => sub { $opt->{y}=$_[0]->get_value; MoveWindow($key); }); my $screen= Gtk2::Gdk::Screen->get_default; my $monitors= $screen->get_n_monitors; my $adj_mon=Gtk2::Adjustment->new($opt->{monitor},0,$monitors-1,1,2,0); my $spin_mon= Gtk2::SpinButton->new($adj_mon,0,0); $spin_mon->set_sensitive(0) if $monitors<2; $adj_mon->signal_connect(value_changed => sub { $opt->{monitor}=$_[0]->get_value; MoveWindow($key); }); my $adjo=Gtk2::Adjustment->new($opt->{opacity}*100,0,100,1,10,0); my $spino=Gtk2::SpinButton->new($adjo,0,0); $adjo->signal_connect(value_changed => sub { $opt->{opacity}=$_[0]->get_value/100; my $win=$Displayed{$key}; $win->set_opacity($opt->{opacity}) if $win; }); my $ontop=Gtk2::CheckButton->new(_"On top of other windows instead of below"); $ontop->set_active($opt->{ontop}); $ontop->signal_connect(toggled=> sub { my $on=$_[0]->get_active; $opt->{ontop}=$on; $opt->{below}=!$on; CreateWindow($key); }); my $adjw=Gtk2::Adjustment->new($opt->{w},1,::max(2000,$opt->{w},Gtk2::Gdk::Screen->get_default->get_width),10,50,0); my $adjh=Gtk2::Adjustment->new($opt->{h},1,::max(2000,$opt->{h},Gtk2::Gdk::Screen->get_default->get_height),10,50,0); my $spinw=Gtk2::SpinButton->new($adjw,0,0); my $spinh=Gtk2::SpinButton->new($adjh,0,0); $adjw->signal_connect(value_changed => sub { $opt->{w}=$_[0]->get_value; ResizeWindow($key); }); $adjh->signal_connect(value_changed => sub { $opt->{h}=$_[0]->get_value; ResizeWindow($key); }); my $vbox= ::Vpack( [ $label, '-',$remove ], [ Gtk2::Label->new(_"Default text color"), $textcolor], [ Gtk2::Label->new(_"Default text font"), $textfont], [ Gtk2::Label->new(_"Centered on"), '_',$spinx, Gtk2::Label->new('% x'), '_',$spiny, Gtk2::Label->new('%') ], [ Gtk2::Label->new(_"Monitor"), $spin_mon, ], [ Gtk2::Label->new(_"Minimum size"), $spinw, Gtk2::Label->new('x'), $spinh ], [ Gtk2::Label->new(_"Opacity"), $spino, Gtk2::Label->new('%') ], $ontop); $OptionsBox->pack_start($vbox,::FALSE,::FALSE,2); $OptionsBox->show_all; } sub CreateWindow { my $key=shift; my $opt= $DWlist->{$key}; return unless $opt; return if $opt->{dw_inactive}; delete $opt->{dw_inactive}; $opt->{monitor}||=0; my $pos= $opt->{monitor}.'@'.$opt->{x}.'%x'.$opt->{y}.'%'; my $size= $opt->{w}.'x'.$opt->{h}; $Displayed{$key}= Layout::Window->new($opt->{layout}, %$opt, 'pos'=>$pos, size=>$size, uniqueid=>$key, ifexist=>'replace', fallback=> 'NONE', nodecoration=>1, skippager=>1, skiptaskbar=>1, sticky=>1, typehint=>'dock', ); } sub MoveWindow { my $key=shift; my $win=$Displayed{$key}; return unless $win; my $opt= $DWlist->{$key}; $win->{'pos'}= $opt->{monitor}.'@'.$opt->{x}.'%x'.$opt->{y}.'%'; my ($x,$y)=$win->Position; $win->move($x,$y); } sub ResizeWindow { my $key=shift; my $win=$Displayed{$key}; return unless $win; CreateWindow($key); #$win->resize($DWlist->{$key}{w},$DWlist->{$key}{h}); #better, but doesn't work well with Cover widget } 1 gmusicbrowser-1.1.15~ds0.orig/plugins/export.pm0000664000175000017500000001444312565212605021024 0ustar unit193unit193# Copyright (C) 2005-2008 Quentin Sculo # # 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 =for gmbplugin Export name Export title Export plugin desc Adds menu entries to song contextual menu =cut package GMB::Plugin::Export; use strict; use warnings; use constant { OPT => 'PLUGIN_Export_', }; my $ON; my %menuentry= (topath => { label => _"Copy to portable player", #label of the menu entry code => \&Copy, #when menu entry selected test => \&checkvalidpath, #the menu entry is displayed if returns true notempty => 'IDs', #display only if at least one song }, tom3u => { label => _"Export to .m3u file", code => \&ToM3U, notempty => 'IDs', }, toCSV => { label => _"Export song properties to a .csv file", code => \&ToCSV, notempty => 'IDs', }, tocmd => { label => sub { ($::Options{OPT.'tocmd_label'} || _"Unnamed custom command") }, code => \&RunCommand, test => sub {my $c=$::Options{OPT.'tocmd_cmd'}; defined $c && $c ne '';}, notempty => 'IDs', } ); my %FLmenuentry= (topath => { label => _"Copy to portable player", code => \&Copy, test => \&checkvalidpath, isdefined => 'filter', }, tom3u => { label => _"Export to .m3u file", code => \&ToM3U, isdefined => 'filter', }, ); my %exportbutton= ( class => 'Layout::Button', stock => 'gtk-save', tip => _"Export to .m3u file", activate=> sub { my $array=::GetSongArray($_[0]); ToM3U({IDs=>$array}) if $array; }, #autoadd_type => 'button songs', ); sub Start { $ON=1; updatemenu(); Layout::RegisterWidget(PluginM3UExport=>\%exportbutton); } sub Stop { $ON=0; updatemenu(); Layout::RegisterWidget(PluginM3UExport=>undef); } sub prefbox { my $vbox=Gtk2::VBox->new(::FALSE, 2); my $sg1=Gtk2::SizeGroup->new('horizontal'); my $sg2=Gtk2::SizeGroup->new('horizontal'); my $entry1=::NewPrefFileEntry(OPT.'path',_"Player mounted on :",folder=>1, sizeg1=>$sg1,sizeg2=>$sg2); my $entry2=::NewPrefEntry(OPT.'folderformat',_"Folder format :", sizeg1=>$sg1,sizeg2=>$sg2, tip =>_("These fields can be used :")."\n".::MakeReplaceText('talydnAY')); my $entry3=::NewPrefEntry(OPT.'filenameformat',_"Filename format :", sizeg1=>$sg1,sizeg2=>$sg2, tip =>_("These fields can be used :")."\n".::MakeReplaceText('talydnAYo')); my $check1=::NewPrefCheckButton(OPT.'topath',_"Copy to mounted portable player", cb=>\&updatemenu, widget=> ::Vpack($entry1,$entry2,$entry3) ); my $entry4=::NewPrefEntry(OPT.'tocmd_label',_"Menu entry name", sizeg1=> $sg1,sizeg2=>$sg2, tip => _("Name under which the command will appear in the menu")); my $entry5=::NewPrefEntry(OPT.'tocmd_cmd',_"System command :", sizeg1=> $sg1,sizeg2=>$sg2, tip => _("These fields can be used :")."\n".::MakeReplaceText('ftalydnAY')."\n". _("In this case one command by file will be run\n\n"). _('Or you can use the field $files which will be replaced by the list of files, and only one command will be run')); my $check2=::NewPrefCheckButton(OPT.'tocmd',_"Execute custom command on selected files", cb=>\&updatemenu, widget=> ::Vpack($entry4,$entry5) ); my $check3=::NewPrefCheckButton(OPT.'tom3u',_"Export to .m3u file", cb=>\&updatemenu); my $check4=::NewPrefCheckButton(OPT.'toCSV',_"Export song properties to a .csv file", cb=>\&updatemenu); $vbox->pack_start($_,::FALSE,::FALSE,2) for $check1,$check2,$check3,$check4; return $vbox; } sub updatemenu { my $removeall=!$ON; for my $eid (keys %menuentry) { my $menu=\@::SongCMenu; my $entry=$menuentry{$eid}; if (!$removeall && $::Options{OPT.$eid}) { push @$menu,$entry unless (grep $_==$entry, @$menu); } else { @$menu =grep $_!=$entry, @$menu; } } for my $eid (keys %FLmenuentry) { my $menu=\@FilterList::FLcMenu; my $entry=$FLmenuentry{$eid}; if (!$removeall && $::Options{OPT.$eid}) { push @$menu,$entry unless (grep $_==$entry, @$menu); } else { @$menu =grep $_!=$entry, @$menu; } } } sub checkvalidpath { my $p= ::decode_url($::Options{OPT.'path'}); $p && (-d $p || $p=~m/[%\$]/); } sub Copy { my $IDs=$_[0]{IDs} || $_[0]{filter}->filter; my $path= ::decode_url( $::Options{OPT.'path'} ); ::CopyMoveFiles($IDs,copy=>1, basedir=>$path, dirformat => $::Options{OPT.'folderformat'}, filenameformat => $::Options{OPT.'filenameformat'}, ); } sub ToM3U { my $IDs=$_[0]{IDs} || $_[0]{filter}->filter; return unless @$IDs; my $file=::ChooseSaveFile(undef,_"Write filenames to ...", Songs::Get($IDs->[0],'path'), 'list.m3u'); return unless defined $file; my $content="#EXTM3U\n"; for my $ID (@$IDs) { my ($file,$length,$artist,$title)= Songs::Get($ID,qw/fullfilename length artist title/); ::_utf8_on($file); # for file, so it doesn't get converted in utf8 $content.= "\n#EXTINF:$length,$artist - $title\n$file\n"; } { my $fh; open($fh,'>:utf8',$file) && (print $fh $content) && close($fh) && last; my $res= ::Retry_Dialog($!,_"Error writing .m3u file", details=> ::__x( _"file: {filename}", filename=>$file)); redo if $res eq 'retry'; } } sub ToCSV { my $IDs=$_[0]{IDs} || $_[0]{filter}->filter; my $check=::NewPrefCheckButton(OPT.'toCSV_notitlerow',_"Do not add a title row"); my $file=::ChooseSaveFile(undef,_"Write filenames to ...",undef,'songs.csv',$check); return unless defined $file; my @fields= (qw/file path/,sort grep !m/^file$|^path$/, (Songs::PropertyFields())); #make sure file and path are in position 0 and 1 my $retry; { if ($retry) { my $res= ::Retry_Dialog($!,_"Error writing .csv file", details=> ::__x( _"file: {filename}", filename=>$file)); last unless $res eq 'retry'; } $retry=1; open my$fh,'>:utf8',$file or redo; unless ($::Options{OPT.'toCSV_notitlerow'}) #print a title row { print $fh join(',',map Songs::FieldName($_), @fields)."\n" or redo; } for my $ID (@$IDs) { my @val; push @val, Songs::Get($ID,@fields); s/\x00/\t/g for @val; #for genres and labels s/"/""/g for @val; ::_utf8_on($val[0]); # for file and path, so it doesn't get converted in utf8 ::_utf8_on($val[1]); # FIXME find a cleaner way to do that #print STDERR join(',',@val)."\n"; print $fh join(',',map '"'.$_.'"', @val)."\n" or redo; } close $fh or redo; } } sub RunCommand { my $IDs=$_[0]{IDs} || $_[0]{filter}->filter; my $cmd= $::Options{OPT.'tocmd_cmd'}; ::run_system_cmd($cmd,$IDs,0); } 1 gmusicbrowser-1.1.15~ds0.orig/plugins/webcontext.pm0000664000175000017500000006551412565212605021672 0ustar unit193unit193# Copyright (C) 2005-2009 Quentin Sculo # # 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 =for gmbplugin WebContext name Web context title Web context plugin desc Provides context views using MozEmbed or WebKit desc wikipedia, lyrics, and custom webpages =cut my ($OKMoz,$OKWebKit,$CrashMoz); BEGIN { { last unless (grep -f $_.'/Gtk2/MozEmbed.pm',@INC); # test if mozembed is working system(q(GNOME_DISABLE_CRASH_DIALOG=1 perl -e 'use Gtk2 "-init"; use Gtk2::MozEmbed;$w=Gtk2::Window->new;my $e=Gtk2::MozEmbed->new; $w->add($e);$w->show_all; exit 0')); #this segfault when mozembed doesn't find its libs #if (($? & 127) ==11) {die "Error : mozembed libraries not found. You need to add the mozilla path in /etc/ld.so.conf and run ldconfig (as root) or add the mozilla libraries path to the LD_LIBRARY_PATH environment variable.\n"} if ($?) { warn "Gtk2::MozEmbed found but not working.\n"; $CrashMoz=1; last; } #crash or fail to load $OKMoz=1; } $OKWebKit=1 if grep -f $_.'/Gtk2/WebKit.pm',@INC; } use strict; use warnings; use utf8; package GMB::Plugin::WebContext::MozEmbed; #use Gtk2::MozEmbed; our $Embed; sub init { Gtk2::MozEmbed->set_profile_path ($::HomeDir,'mozilla_profile'); if ($Gtk2::MozEmbed::VERSION>=0.06) {Gtk2::MozEmbed->push_startup} else {$Embed||=Gtk2::MozEmbed->new;} #needed to keep a Gtk2::MozEmbed to prevent xpcom from shutting down with the last gtkmozembed } sub new_embed { my $embed=Gtk2::MozEmbed->new; $embed->signal_connect(link_message => \&link_message_cb); $embed->signal_connect(net_stop => \&net_startstop_cb,0); $embed->signal_connect(net_start => \&net_startstop_cb,1); $embed->signal_connect(open_uri => \&about_to_load_cb); #called before loading a new uri, must return false return $embed; } sub link_message_cb { my $embed=$_[0]; my $self=::find_ancestor($embed,__PACKAGE__); $self->link_message( $embed->get_link_message ); } sub net_startstop_cb { my ($embed,$loading)=@_; my $self=::find_ancestor($embed,__PACKAGE__); $self->{BStop}->set_sensitive( $loading ); $self->{BBack}->set_sensitive( $embed->can_go_back ); $self->{BNext}->set_sensitive( $embed->can_go_forward ); my $cursor= $loading ? Gtk2::Gdk::Cursor->new('watch') : undef; $embed->window->set_cursor($cursor) if $embed->window; } sub about_to_load_cb #called before loading a new uri, { my ($embed,$uri)=@_; my $self=::find_ancestor($embed,__PACKAGE__); $self->{Entry}->set_text($uri); #update location entry my $http= $uri=~m#^https?://# ? 1 : 0 ; $self->{BOpen}->set_sensitive($http); 0; #must return false, else won't be loaded } sub loaded { my ($self,$data,%prop)=@_; my $embed=$self->{embed}; $embed->render_data($data,$self->{url},$prop{type}); # $embed->open_stream ($self->{url}, $type); # $embed->append_data ($data); # $embed->close_stream; } sub go_back { $_[0]{embed}->go_back } sub go_forward { $_[0]{embed}->go_forward } sub stop_load { $_[0]{embed}->stop_load } sub get_location{ $_[0]{embed}->get_location } sub Open { $_[0]{embed}->load_url($_[1]); } sub set_stripped_wiki #FIXME use print version of the wikipedia page instead ? { my $stripped=$_[0]; my $content=''; if ($stripped) { $content="/*@-moz-document domain(wikipedia.org) { */ .portlet {display: none !important;} #f-list {display: none !important;} #footer {display: none !important;} #content {margin: 0 0 0 0 !important;} /* } */ "; } open my $fh,'>',join(::SLASH,$::HomeDir,'mozilla_profile','chrome','userContent.css') or return; print $fh $content; close $fh; } package GMB::Plugin::WebContext::WebKit; #use Gtk2::WebKit; sub new_embed { my $embed=Gtk2::WebKit::WebView->new; $embed->signal_connect(hovering_over_link => \&link_message_cb); $embed->signal_connect(load_finished => \&net_startstop_cb,0); $embed->signal_connect(load_committed => \&net_startstop_cb,1); $embed->signal_connect(navigation_policy_decision_requested=> sub { if ($_[3]->get_button==2) #middle-click on a link { my $nb=::find_ancestor($_[0],'Layout::NoteBook'); #only works if inside a NB/TabbedLists/Context widget $nb->newtab('PluginWebPage',1,{url=>$_[2]->get_uri}) if $nb; #open the link in a new tab return 1 if $nb; } return 0; }); $embed->signal_connect('notify::title'=> sub { my $embed=shift; my $self=::find_ancestor($embed,'GMB::Plugin::WebContext'); $self->set_title($embed->get('title')) if $self; }); my $sw= Gtk2::ScrolledWindow->new; $sw->set_shadow_type('etched-in'); $sw->set_policy('automatic','automatic'); $sw->add($embed); return $embed,$sw; } sub link_message_cb { my ($embed,undef,$msg)=@_; my $self=::find_ancestor($embed,__PACKAGE__); $self->link_message($msg); } sub net_startstop_cb { my ($embed,$frame,$loading)=@_; my $self=::find_ancestor($embed,__PACKAGE__); $self->{BStop}->set_sensitive( $loading ); $self->{BBack}->set_sensitive( $embed->can_go_back ); $self->{BNext}->set_sensitive( $embed->can_go_forward ); my $cursor= $loading ? Gtk2::Gdk::Cursor->new('watch') : undef; $embed->window->set_cursor($cursor) if $embed->window; my $uri=$frame->get_uri; $self->{Entry}->set_text($uri) if $loading; $self->set_title($uri) if $loading; $uri= $uri=~m#^https?://# ? 1 : 0 ; $self->{BOpen}->set_sensitive($uri); } sub set_title {} #ignored unless overridden by the class sub loaded { my ($self,$data,%prop)=@_; my $embed=$self->{embed}; $embed->load_html_string($data,$self->{url}); #FIXME doesn't check type ($prop{type}) } sub go_back { $_[0]{embed}->go_back } sub go_forward { $_[0]{embed}->go_forward } sub stop_load { $_[0]{embed}->stop_loading } sub get_location{ $_[0]{embed}->get_focused_frame->get_uri } sub Open { $_[0]{embed}->load_uri($_[1]); } sub set_stripped_wiki {} #FIXME use print version of the wikipedia page instead ? # $::Options{OPT.'StrippedWiki'} package GMB::Plugin::WebContext; our @ISA; BEGIN {push @ISA,'GMB::Context';} use base 'Gtk2::VBox'; use constant { OPT => 'PLUGIN_WebContext_', }; our %Predefined = ( google => { tabtitle => 'google', baseurl => 'http://www.google.com/search?q="%a"+"%t"', }, amgartist=>{ tabtitle => 'amg artist', baseurl => 'http://www.allmusic.com/search/artist/%a', }, amgalbum=> { tabtitle => 'amg album', baseurl => 'http://www.allmusic.com/search/album/%l', }, lastfm => { tabtitle => 'last.fm', baseurl => 'http://www.last.fm/music/%a', }, discogs => { tabtitle => 'discogs', baseurl => 'http://www.discogs.com/artist/%a', }, youtube => { tabtitle => 'youtube', baseurl => 'http://www.youtube.com/results?search_query="%a"', }, pollstar=> { tabtitle => 'pollstar', baseurl => 'http://www.pollstar.com/eventSearch.aspx?SearchBy=%a', }, songfacts=>{ tabtitle => 'songfacts', baseurl => 'http://www.songfacts.com/search_fact.php?title=%t', }, rateyourmusic=>{ tabtitle => 'rateyourmusic',baseurl=> 'http://rateyourmusic.com/search?searchterm=%a&searchtype=a', }, ); our %Widgets= ( PluginWebLyrics => { class => 'GMB::Plugin::WebContext::Lyrics', tabicon => 'gmb-lyrics', # no icon by that name by default tabtitle => _"Lyrics", schange => \&Update, group => 'Play', autoadd_type => 'context page lyrics html', saveoptions => 'follow urientry statusbar', }, PluginWikipedia => { class => 'GMB::Plugin::WebContext::Wikipedia', tabicon => 'plugin-wikipedia', tabtitle => _"Wikipedia", schange => \&Update, group => 'Play', autoadd_type => 'context page wikipedia html', saveoptions => 'follow urientry statusbar', }, PluginWebCustom => { class => 'GMB::Plugin::WebContext::Custom', tabtitle => _"Untitled", schange => \&Update, group => 'Play', saveoptions => 'follow urientry statusbar', }, PluginWebPage => { class => 'GMB::Plugin::WebContext::Page', tabtitle => _"Untitled", saveoptions => 'url title urientry statusbar', options => 'url title', #load/save the title because page is only loaded when mapped, so we can ask it its title until tab is selected }, ); our @default_options= (follow=>1, urientry=>1, statusbar=>0, ); our @contextmenu= ( { label=> _"Show/hide URI entry", toggleoption=> 'self/urientry', code => sub { my $w=$_[0]{self}{Entry}; if ($_[0]{self}{urientry}) { $w->set_no_show_all(0); $w->show_all; } else { $w->hide; } }, }, { label=> _"Show/hide status bar", toggleoption=> 'self/statusbar', code => sub { my $w=$_[0]{self}{Status}; if ($_[0]{self}{statusbar}) { $w->set_no_show_all(0); $w->show_all; } else { $w->hide; } }, }, ); my $active; ::SetDefaultOptions(OPT, StrippedWiki => 1, Custom => { map {$_=>{ %{$Predefined{$_}} } } qw/lastfm amgartist youtube/ }); UpdateBackend(); UpdateCustom($_) for sort keys %{ $::Options{OPT.'Custom'} }; sub UpdateBackend { my $backend= $::Options{OPT.'Backend'} || ''; $backend='' if !$OKMoz && $backend eq 'MozEmbed' || !$OKWebKit && $backend eq 'WebKit'; unless ($backend) { if ($OKWebKit) {$backend='WebKit'} elsif ($OKMoz) {$backend='MozEmbed'} } $::Options{OPT.'Backend'}=$backend; my $was_active=$active; Stop() if $active; if ($OKMoz && $backend eq 'MozEmbed') { require Gtk2::MozEmbed; Gtk2::MozEmbed->import; GMB::Plugin::WebContext::MozEmbed::init(); @ISA= grep $_ ne 'GMB::Plugin::WebContext::WebKit', @ISA; push @ISA, 'GMB::Plugin::WebContext::MozEmbed'; } elsif ($OKWebKit) { require Gtk2::WebKit; Gtk2::WebKit->import; @ISA= grep $_ ne 'GMB::Plugin::WebContext::MozEmbed', @ISA; push @ISA, 'GMB::Plugin::WebContext::WebKit'; } Start() if $was_active; } sub Start { return unless $OKMoz or $OKWebKit; $active=1; Layout::RegisterWidget($_ => $Widgets{$_}) for keys %Widgets; &set_stripped_wiki; } sub Stop { $active=0; Layout::RegisterWidget($_ => undef) for keys %Widgets; } sub UpdateCustom { my ($id,$hash)=@_; if (!defined $id) # new custom page { return unless $hash; $id= $hash->{tabtitle}||'' unless defined $id; $id=~tr/a-zA-Z0-9//cd; $id='custom' if $id eq ''; ::IncSuffix($id) while $Widgets{'PluginWebCustom_'.$id.'_'}; #find a new name } elsif ($active) { Layout::RegisterWidget('PluginWebCustom_'.$id.'_' => undef); } if ($hash) # edit existing custom page { $::Options{OPT.'Custom'}{$id}{$_}= $hash->{$_} for keys %$hash; } $hash=$::Options{OPT.'Custom'}{$id}; return unless $hash; my %widget= ( %{ $Widgets{PluginWebCustom} }, autoadd_type => 'context page custom html', %$hash ); #base the widget on the PluginWebCustom widget $widget{tabtitle}||= _"Untitled"; my $name='PluginWebCustom_'.$id.'_'; #'_' is appended because gmb widget names cannot end with numbers $Widgets{$name}= \%widget; if ($active) { Layout::RegisterWidget($name => $Widgets{$name}); } return $id; } sub RemoveCustom { my $id=shift; delete $::Options{OPT.'Custom'}{$id}; my $name='PluginWebCustom_'.$id.'_'; delete $Widgets{$name}; Layout::RegisterWidget($name => undef); } sub new { my ($class,$opt)=@_; my $self = bless Gtk2::VBox->new(0,0), $class; %$opt=( @default_options, %$opt ); $self->{$_}=$opt->{$_} for qw/follow group urientry statusbar baseurl/; my $toolbar=Gtk2::Toolbar->new; $toolbar->set_style( $opt->{ToolbarStyle}||'both-horiz' ); $toolbar->set_icon_size( $opt->{ToolbarSize}||'small-toolbar' ); my $status=$self->{Status}=Gtk2::Statusbar->new; $status->{id}=$status->get_context_id('link'); ($self->{embed},my $container)= $self->new_embed; $container||=$self->{embed}; $self->{DefaultFocus}=$self->{embed}; $self->{embed}->signal_connect(button_press_event=> \&button_press_cb); my $entry=$self->{Entry}=Gtk2::Entry->new; my $back= $self->{BBack}=Gtk2::ToolButton->new_from_stock('gtk-go-back'); my $next= $self->{BNext}=Gtk2::ToolButton->new_from_stock('gtk-go-forward'); my $stop= $self->{BStop}=Gtk2::ToolButton->new_from_stock('gtk-stop'); my $open= $self->{BOpen}=Gtk2::ToolButton->new_from_stock('gtk-open'); $open->set_tooltip_text(_"Open this page in the web browser"); #$open->set_use_drag_window(1); #::set_drag($open,source=>[::DRAG_FILE,sub {$embed->get_location;}]); $self->{$_}->set_sensitive(0) for qw/BBack BNext BStop BOpen/; my $entryitem=Gtk2::ToolItem->new; $entryitem->add($entry); $entryitem->set_expand(1); # create follow toggle button, function from GMB::Context my $follow=$self->new_follow_toolitem; $toolbar->insert($_,-1) for $back,$next,$stop,$follow,$open,$entryitem,$self->addtoolbar; $self->pack_start($toolbar,::FALSE,::FALSE,1); $self->add( $container ); $self->pack_end($status,::FALSE,::FALSE,1); $entry->set_no_show_all(!$self->{urientry}); $status->set_no_show_all(!$self->{statusbar}); $self->signal_connect(map => \&Update); $entry->signal_connect(activate => sub { ::find_ancestor($_[0],__PACKAGE__)->load_url($_[0]->get_text); }); $back->signal_connect(clicked => sub { ::find_ancestor($_[0],__PACKAGE__)->go_back }); $next->signal_connect(clicked => sub { ::find_ancestor($_[0],__PACKAGE__)->go_forward }); $stop->signal_connect(clicked => sub { ::find_ancestor($_[0],__PACKAGE__)->stop_load }); $open->signal_connect(clicked => sub { my $url=::find_ancestor($_[0],__PACKAGE__)->get_location; ::openurl($url) if $url=~m#^https?://# }); $toolbar->signal_connect('popup-context-menu' => \&popup_toolbar_menu ); return $self; } sub button_press_cb { my ($embed,$event)=@_; my $button= $event->button; my $self= ::find_ancestor($embed,__PACKAGE__); if ($button==8) { $self->go_back; } elsif ($button==9) { $self->go_forward; } else { return 0; } return 1; } sub addtoolbar #default method, overridden by packages that add extra items to the toolbar { return (); } sub prefbox { my $vbox=Gtk2::VBox->new(::FALSE, 2); #my $combo=::NewPrefCombo(OPT.'Site',[sort keys %sites],'site : ',sub {$ID=undef;&Changed;}); my $check=::NewPrefCheckButton(OPT.'StrippedWiki',_"Strip wikipedia pages", cb=>\&set_stripped_wiki, tip=>_"Remove header, footer and left column from wikipedia pages"); my $Bopen=Gtk2::Button->new(_"open context window"); $Bopen->signal_connect(clicked => sub { ::ContextWindow; }); my ($radio_wk,$radio_moz)= ::NewPrefRadio( OPT.'Backend', [_"Use WebKit", 'WebKit', _"Use MozEmbed", 'MozEmbed', ], cb=> sub { $check->set_sensitive($::Options{OPT.'Backend'} eq 'MozEmbed'); UpdateBackend(); }); my $label_wk= $OKWebKit ? '' : _"Not found"; my $label_moz= $OKMoz ? '' : $CrashMoz ? _"Found but not working" : _"Not found"; $radio_wk ->set_tooltip_text($label_wk) if $label_wk; $radio_moz->set_tooltip_text($label_moz) if $label_moz; $radio_wk->set_sensitive($OKWebKit); $radio_moz->set_sensitive($OKMoz); $check->set_sensitive($::Options{OPT.'Backend'} eq 'MozEmbed'); $vbox->pack_start($_,::FALSE,::FALSE,1) for $radio_wk,$radio_moz,Gtk2::VSeparator->new,$check,$Bopen; $vbox->pack_start( GMB::Plugin::WebContext::Custom::Edition->new, ::TRUE,::TRUE,8 ); $vbox->set_sensitive( $OKMoz || $OKWebKit ); return $vbox; } sub load_url { my ($self,$url,$post)=@_; $url='http://'.$url unless $url=~m#^\w+://#;# || $url=~m#^about:#; $self->{url}=$url; $self->{post}=$post; if ($post) { Simple_http::get_with_cb(cb => sub {$self->loaded(@_)},url => $url,post => $post); } else {$self->Open($url);} } sub link_message { my ($self,$msg)=@_; $msg='' unless defined $msg; my $statusbar=$self->{Status}; $statusbar->pop( $statusbar->{id} ); $statusbar->push( $statusbar->{id}, $msg ); } sub set_stripped_wiki { GMB::Plugin::WebContext::MozEmbed::set_stripped_wiki( $::Options{OPT.'StrippedWiki'} ); } #FIXME sub popup_toolbar_menu { my ($toolbar,$x,$y,$button)=@_; my $args= { self=> ::find_ancestor($toolbar,__PACKAGE__), }; my $menu=::BuildMenu(\@contextmenu,$args); $menu->show_all; $menu->popup(undef,undef,sub {$x,$y},undef,$button,0); } sub Update { $_[0]->SongChanged( ::GetSelID($_[0]) ) if $_[0]->mapped; } ################################################################################# package GMB::Plugin::WebContext::Page; our @ISA=('GMB::Plugin::WebContext'); #only called once, when mapped sub SongChanged { $_[0]->load_url($_[0]{url}) if $_[0]{url}; } sub DynamicTitle #called by Layout::NoteBook when tab is created { my ($self,$default)=@_; my $title=$self->{title}; $title=$default unless length $title; my $label=Gtk2::Label->new($title); $label->set_ellipsize('end'); $label->set_max_width_chars(20); $self->{titlelabel}=$label; return $label; } sub set_title { my ($self,$title)=@_; ($title)= $self->{url}=~m#^https?://(?:www\.)?([\w.]+)# unless length $title; $self->{title}=$title; $self->{titlelabel}->set_text($title); } package GMB::Plugin::WebContext::Lyrics; our @ISA=('GMB::Plugin::WebContext'); use constant { OPT => GMB::Plugin::WebContext::OPT, #FIXME }; my %sites= ( lyrc => ['lyrc','http://lyrc.com.ar/en/tema1en.php?artist=%a&songname=%s'], #leoslyrics => ['leolyrics','http://api.leoslyrics.com/api_search.php?artist=%a&songtitle=%s'], google => ['google','http://www.google.com/search?q="%a"+"%s"'], lyriki => ['lyriki','http://lyriki.com/index.php?title=%a:%s'], lyricwiki => [lyricwiki => 'http://lyrics.wikia.com/%a:%s'], lyricsplugin => [lyricsplugin => 'http://www.lyricsplugin.com/winamp03/plugin/?title=%s&artist=%a'], lyricscom => [ 'lyrics.com' => 'http://www.lyrics.com/search.php?keyword=%s+%a&what=all' ], ); $::Options{OPT.'LyricSite'}=undef if $::Options{OPT.'LyricSite'} && !$sites{$::Options{OPT.'LyricSite'}}; ::SetDefaultOptions(OPT, LyricSite => 'google'); sub addtoolbar { #my $self=$_[0]; my %h= map {$_=>$sites{$_}[0]} keys %sites; my $cb=sub { my $self=::find_ancestor($_[0],__PACKAGE__); $self->SongChanged($self->{ID},1); }; my $combo=::NewPrefCombo( OPT.'LyricSite', \%h, cb => $cb, toolitem => _"Lyrics source"); return $combo; } sub SongChanged { my ($self,$ID,$force)=@_; return unless defined $ID; return if defined $self->{ID} && !$force && ($ID==$self->{ID} || !$self->{follow}); $self->{ID}=$ID; my ($title,$artist)= map ::url_escapeall($_), Songs::Get($ID,qw/title artist/); return if $title eq ''; my (undef,$url,$post)=@{$sites{$::Options{OPT.'LyricSite'}}}; for ($url,$post) { next unless defined $_; s/%a/$artist/; s/%s/$title/; } ::IdleDo('8_mozlyrics'.$self,1000,sub {$self->load_url($url,$post)}); } package GMB::Plugin::WebContext::Wikipedia; our @ISA=('GMB::Plugin::WebContext'); use constant { OPT => GMB::Plugin::WebContext::OPT, #FIXME }; my %locales= ( en => 'English', fr => 'Français', de => 'Deutsch', pl => 'Polski', nl => 'Nederlands', sv => 'Svenska', it => 'Italiano', pt => 'Português', es => 'Español', # ja => "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e\x0a", ); #::_utf8_on( $locales{ja} ); ::SetDefaultOptions(OPT, WikiLocale => 'en'); sub addtoolbar { #my $self=$_[0]; my $cb=sub { my $self=::find_ancestor($_[0],__PACKAGE__); $self->SongChanged($self->{ID},1); }; my $combo=::NewPrefCombo( OPT.'WikiLocale', \%locales, cb => $cb, toolitem => _"Wikipedia Locale"); return $combo; } sub SongChanged { my ($self,$ID,$force)=@_; return unless defined $ID; $self->{ID}=$ID; my $artist=Songs::Get($ID,'first_artist'); #FIXME add a way to choose artist ? return if $artist eq ''; return if defined $self->{Artist} && !$force && ($artist eq $self->{Artist} || !$self->{follow}); $self->{Artist}=$artist; $artist=::url_escapeall($artist); my $url='http://'.$::Options{OPT.'WikiLocale'}.'.wikipedia.org/wiki/'.$artist; #my $url='http://'.$::Options{OPT.'WikiLocale'}.'.wikipedia.org/w/index.php?title='.$artist.'&action=render'; ::IdleDo('8_mozpedia'.$self,1000,sub {$self->load_url($url)}); #::IdleDo('8_mozpedia'.$self,1000,sub {$self->wikiload}); } sub wikiload #not used for now { my $self=$_[0]; my $url=::url_escapeall($self->{Artist}); $url='http://'.$::Options{OPT.'WikiLocale'}.'.wikipedia.org/wiki/'.$url; #$url='http://google.com/search?q='.$url; $self->{url}=$url; Simple_http::get_with_cb(cb => sub { my $cb=sub { $self->wikifilter(@_) }; if (!$_[0] || $_[0]=~m/No page with that title exists/) { Simple_http::get_with_cb(cb => $cb, url => $url); } else { $self->{url}.='_(band)'; &$cb } },url => $url.'_(band)'); } sub wikifilter #not used for now { my ($self,$data,%prop)=@_; return unless $data; #FIXME #$data=''.$data; $self->loaded($data,%prop); } package GMB::Plugin::WebContext::Custom; our @ISA=('GMB::Plugin::WebContext'); sub SongChanged { my ($self,$ID,$force)=@_; return unless defined $ID; $self->{ID}=$ID; my $url= $self->{baseurl}; unless ($url) { warn "no baseurl defined for custom webcontext $self->{name}\n"; return } $url= ::ReplaceFields($ID,$url, \&::url_escapeall); return if $self->{url} && !$force && ($url eq $self->{url} || !$self->{follow}); warn "loading $url\n"; ::IdleDo('8_mozcustom'.$self,1000,sub {$self->load_url($url)}); } package GMB::Plugin::WebContext::Custom::Edition; use base 'Gtk2::Box'; my $CustomPages= $::Options{GMB::Plugin::WebContext::OPT.'Custom'}; sub new { my $class=shift; my $self=bless Gtk2::VBox->new, $class; my $store=Gtk2::ListStore->new('Glib::String','Glib::String'); my $treeview=Gtk2::TreeView->new($store); my $renderer=Gtk2::CellRendererText->new; $renderer->set(editable => 1); $renderer->signal_connect_swapped(edited => \&rename_cb,$store); $treeview->append_column( Gtk2::TreeViewColumn->new_with_attributes( '', $renderer, text => 1 )); $treeview->set_headers_visible(::FALSE); $treeview->get_selection->signal_connect(changed => \&selchanged_cb); my $sw=Gtk2::ScrolledWindow->new; $sw->set_shadow_type('etched-in'); $sw->set_policy('automatic','automatic'); $sw->add($treeview); my $hbox=Gtk2::HBox->new; my $editbox=Gtk2::VBox->new; $self->{editbox}=$editbox; $self->{store}=$store; $self->{treeview}=$treeview; $hbox->pack_start($sw,::FALSE,::FALSE,2); $hbox->add($editbox); my $label=Gtk2::Label->new; $label->set_markup_with_format('%s',_"Custom context pages :"); $label->set_alignment(0,.5); $self->pack_start($label,::FALSE,::FALSE,2); $self->add($hbox); #buttons my $new= ::NewIconButton('gtk-new', _"New"); my $save= ::NewIconButton('gtk-save', _"Save"); my $remove=::NewIconButton('gtk-remove',_"Remove"); my $preset=::NewIconButton('gtk-add', _"Pre-set"); $preset->child->add(Gtk2::Arrow->new('down','none')); $new ->signal_connect( clicked=> sub { my $self=::find_ancestor($_[0],__PACKAGE__); $self->fill_editbox; }); $save ->signal_connect( clicked=> \&save_cb); $remove ->signal_connect( clicked=> \&remove_cb); $preset ->signal_connect(button_press_event=>\&preset_menu_cb); my $bbox=Gtk2::HButtonBox->new; $bbox->set_layout('start'); $bbox->add($_) for $remove, $new, $preset, $save; $self->pack_end($bbox,::FALSE,::FALSE,0); $self->{button_save}=$save; $self->{button_remove}=$remove; my $sg=Gtk2::SizeGroup->new('horizontal'); $sg->add_widget($_) for $sw, $remove, $new, $preset, $save; fill_list($self->{store}); $self->fill_editbox; return $self; } sub fill_list { my $store=shift; $store->clear; for my $id ( ::sorted_keys($CustomPages,'tabtitle') ) { $store->set($store->append, 0,$id, 1,$CustomPages->{$id}{tabtitle}); } } sub fill_editbox #if $id => fill entries with existing properties, if $hash => fill entries with content of hash, if neither => empty entries { my ($self,$id,$hash)=@_; my $editbox= $self->{editbox}; $editbox->remove($_) for $editbox->get_children; $self->{button_save} ->set_sensitive(defined $hash); $self->{button_remove}->set_sensitive(defined $id); $editbox->{entry_title}= my $entry_title=Gtk2::Entry->new; $editbox->{entry_url}= my $entry_url= Gtk2::Entry->new; $editbox->{id}=$id; my $preview=Label::Preview->new ( entry => $entry_url, format => ::MarkupFormat('%s', _"example : %s"), event => 'CurSong', preview => sub { my $url=shift; defined $::SongID && $url ? ::ReplaceFields($::SongID,$url, \&::url_escapeall) : undef }, wrap=>1, ); $preview->set_selectable(1); $hash= $CustomPages->{$id} if defined $id; if ($hash) { $hash ||= $CustomPages->{$id}; $entry_title->set_text($hash->{tabtitle}); $entry_url ->set_text($hash->{baseurl}); } my $sg=Gtk2::SizeGroup->new('horizontal'); my $label_title=Gtk2::Label->new(_"Title"); my $label_url=Gtk2::Label->new(_"url"); $sg->add_widget($_) for $label_title, $label_url; my $box= ::Vpack( [$label_title,'_',$entry_title], [$label_url,'_',$entry_url], $preview ); $_->signal_connect(changed=> \&entry_changed_cb) for $entry_title, $entry_url; $entry_title->signal_connect(changed=> \&update_selection); $editbox->pack_start($box,::FALSE,::FALSE,2); $editbox->show_all; } sub entry_changed_cb { my $self= ::find_ancestor($_[0],__PACKAGE__); my $editbox= $self->{editbox}; $self->{button_save}->set_sensitive( $editbox->{entry_title}->get_text ne '' && $editbox->{entry_url}->get_text ne '' ); } sub update_selection { my $self=::find_ancestor($_[0],__PACKAGE__); my $editbox= $self->{editbox}; my $title= $editbox->{entry_title}->get_text; my $newid; for my $id (keys %$CustomPages) { next unless $CustomPages->{$id}{tabtitle} eq $title; $newid=$id; last; } $editbox->{id}=$newid; my $treesel=$self->{treeview}->get_selection; $self->{button_remove}->set_sensitive(defined $newid); $editbox->{busy}=1; $treesel->unselect_all; if (defined $newid) #select current id { my $store=$self->{store}; my $iter=$store->get_iter_first; while ($iter) { if ( $store->get($iter,0) eq $newid ) { $treesel->select_iter($iter); last; } $iter=$store->iter_next($iter); } } $editbox->{busy}=0; } sub selchanged_cb { my $treesel=shift; my $treeview=$treesel->get_tree_view; my $self=::find_ancestor($treeview,__PACKAGE__); return if $self->{editbox}{busy}; my $iter=$treesel->get_selected; my $id; $id=$treeview->get_model->get($iter,0) if $iter; $self->fill_editbox($id); } sub rename_cb { my ($store, $path_string, $newvalue) = @_; my $iter=$store->get_iter_from_string($path_string); my $id=$store->get($iter,0); return if $newvalue eq ''; return if $CustomPages->{$id}{tabtitle} eq $newvalue; GMB::Plugin::WebContext::UpdateCustom($id=> {tabtitle=>$newvalue}); fill_list($store); } sub remove_cb { my $self=::find_ancestor($_[0],__PACKAGE__); my $editbox= $self->{editbox}; my $id=$editbox->{id}; return unless defined $id; GMB::Plugin::WebContext::RemoveCustom($id); fill_list($self->{store}); } sub save_cb { my $button=shift; $button->set_sensitive(0); my $self=::find_ancestor($button,__PACKAGE__); my $editbox= $self->{editbox}; my $hash= { tabtitle=> $editbox->{entry_title}->get_text, baseurl=> $editbox->{entry_url}->get_text, }; my $id=$editbox->{id}; GMB::Plugin::WebContext::UpdateCustom($id => $hash); # if $id is undef, create a new page, else edit existing one $editbox->{busy}=1; fill_list($self->{store}); $editbox->{busy}=0; $self->update_selection; } sub preset_menu_cb { my ($button,$event)=@_; my $self=::find_ancestor($button,__PACKAGE__); my $menu=Gtk2::Menu->new; my $predef= \%GMB::Plugin::WebContext::Predefined; my $menu_cb= sub { my $preid=$_[1]; $self->fill_editbox(undef,$predef->{$preid}); $self->update_selection; }; for my $preid ( ::sorted_keys($predef,'tabtitle') ) { my $item=Gtk2::MenuItem->new( $predef->{$preid}{tabtitle} ); $item->signal_connect(activate => $menu_cb,$preid); $menu->append($item); } ::PopupMenu($menu); } 1 gmusicbrowser-1.1.15~ds0.orig/plugins/lyrics.pm0000664000175000017500000006441412565212605021013 0ustar unit193unit193# Copyright (C) 2005-2009 Quentin Sculo # # 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 =for gmbplugin LYRICS name Lyrics title Lyrics plugin desc Search and display lyrics =cut package GMB::Plugin::LYRICS; use strict; use warnings; use utf8; require $::HTTP_module; our @ISA; BEGIN {push @ISA,'GMB::Context';} use base 'Gtk2::VBox'; use constant { OPT => 'PLUGIN_LYRICS_', # MUST begin by PLUGIN_ followed by the plugin ID / package name }; my $notfound=_"No lyrics found"; my @justification= ( left => _"Left aligned", center => _"Centered", right => _"Right aligned", fill => _"Justified", ); my @ContextMenuAppend= ( { label => _"Scroll with song", check => sub { $_[0]{self}{AutoScroll} }, code => sub { $::Options{OPT.'AutoScroll'}= $_[1]; $_[0]{self}->SetAutoScroll; }, }, { label => _"Hide toolbar", check => sub { $_[0]{self}{HideToolbar} }, code => sub { $_[0]{self}->SetToolbarHide($_[1]); }, }, { label => _"Choose font...", code => sub { $_[0]{self}->ChooseFont; }, }, { label => _"Lyrics alignement", check => sub { $_[0]{self}{justification} }, submenu => \@justification, submenu_reverse => 1, submenu_ordered_hash=>1, code => sub { $_[0]{self}{textview}->set_justification( $_[0]{self}{justification}=$_[1] ); }, }, ); my %Sites= # id => [name,url,?post?,function] if the function return 1 => lyrics can be saved ( #lyrc => ['lyrc','http://lyrc.com.ar/en/tema1en.php','artist=%a&songname=%t'], #lyrc => ['lyrc','http://lyrc.com.ar/en/tema1en.php?artist=%a&songname=%t',undef,sub # { local $_=$_[0]; # return -1 if m#]+add[^>]+>(?:[^<]*#i; # return 1 if s#]+badsong[^>]+>BADSONG##i; # return 0; # }], #leoslyrics => ['leolyrics','http://api.leoslyrics.com/api_search.php?artist=%a&songtitle=%t'], #google => ['google','http://www.google.com/search?q="%a"+"%t"'], lyriki => ['lyriki','http://lyriki.com/index.php?title=%a:%t',undef, sub { my $no= $_[0]=~m/
/s; $_[0]=~s/^.*(.*?).*$/$1/s && !$no; }], #lyricsplugin => [lyricsplugin => 'http://www.lyricsplugin.com/winamp03/plugin/?title=%t&artist=%a',undef, # sub { my $ok=$_[0]=~m#
.*\w\n.*\w.*
#s; $_[0]=~s/
['lyrics-songs',sub { ::ReplaceFields($_[0], "http://letras.terra.com.br/winamp.php?musica=%t&artista=%a", sub {::url_escapeall(::superlc($_[0]));}) },undef, sub { my $is_suggestion= $_[0]=~m#

Provável música

#i; my $l=html_extract($_[0],div=>'letra'); $l=~s#
.*?
##s if $l; #remove header with title and artist links my $ref=\$_[0]; $$ref= $l ? $l : $notfound; return $l && !$is_suggestion; }], lyricwiki => [lyricwiki => 'http://lyrics.wikia.com/%a:%t',undef, sub { return 0,'http://lyrics.wikia.com/'.$1 if $_[0]=~m#.*?((?:&\#\d+;|
|){5,}).*!$1!s; #keep only the "lyric box" return 0 if $_[0]=~m![...](?:
)*!; # truncated lyrics : "[...]" followed by italic explanation => not auto-saved return !!$1; }], #lyricwikiapi => [lyricwiki => 'http://lyricwiki.org/api.php?artist=%a&song=%t&fmt=html',undef, # sub { $_[0]!~m#
\W*Not found\W*
#s }], #azlyrics => [ azlyrics => 'http://search.azlyrics.com/cgi-bin/azseek.cgi?q="%a"+"%t"'], #Lyricsfly ? AUTO => [_"Auto",], #special mode that search multiple sources ); $::Options{OPT.'Font'} ||= delete $::Options{OPT.'FontSize'}; #for versions <1.1.6 if (my $site=$::Options{OPT.'LyricSite'}) { delete $::Options{OPT.'LyricSite'} unless exists $Sites{$site} } #reset selected site if no longer defined ::SetDefaultOptions(OPT, Font => 10, PathFile => "~/.lyrics/%a/%t.lyric", LyricSite => 'lyricssongs', PreferEmbeddedLyrics=>0); my $lyricswidget= { class => __PACKAGE__, tabicon => 'gmb-lyrics', # no icon by that name by default tabtitle => _"Lyrics", saveoptions => 'HideToolbar font follow justification edit', schange => sub { $_[0]->SongChanged($_[1]); }, #$_[1] is new ID group => 'Play', autoadd_type => 'context page lyrics text', justification => 'left', edit => 0, }; sub Start { Layout::RegisterWidget(PluginLyrics => $lyricswidget); } sub Stop { Layout::RegisterWidget(PluginLyrics => undef); } sub new { my ($class,$options)=@_; my $self = bless Gtk2::VBox->new(0,0), $class; $options->{follow}=1 if not exists $options->{follow}; $self->{$_}=$options->{$_} for qw/HideToolbar follow group font justification edit/; my $textview=Gtk2::TextView->new; $self->signal_connect(map => sub { $_[0]->SongChanged( ::GetSelID($_[0]) ); }); $self->signal_connect_after(key_press_event => \&key_pressed_cb); $textview->signal_connect(button_release_event => \&button_release_cb); $textview->signal_connect(motion_notify_event => \&update_cursor_cb); $textview->signal_connect(visibility_notify_event=>\&update_cursor_cb); $textview->signal_connect(scroll_event => \&scroll_cb); $textview->signal_connect(populate_popup => \&populate_popup_cb); $textview->set_wrap_mode('word'); $textview->set_justification( $self->{justification} ); $textview->set_left_margin(5); $textview->set_right_margin(5); if (my $color= $options->{color} || $options->{DefaultFontColor}) { $textview->modify_text('normal', Gtk2::Gdk::Color->parse($color) ); } $self->{buffer}=$textview->get_buffer; $self->{textview}=$textview; $self->{DefaultFocus}=$textview; my $sw=Gtk2::ScrolledWindow->new; $sw->set_shadow_type( $options->{shadow} || 'etched-in'); $sw->set_policy('automatic','automatic'); $sw->add($textview); my $toolbar=Gtk2::Toolbar->new; $toolbar->set_style( $options->{ToolbarStyle}||'both-horiz' ); $toolbar->set_icon_size( $options->{ToolbarSize}||'small-toolbar' ); for my $aref ( [backb => 'gtk-go-back',\&Back_cb, _"Previous page"], [saveb => 'gtk-save', \&Save_text, _"Save", _"Save lyrics"], [undef, 'gtk-refresh', \&Refresh_cb, _"Refresh"], ) { my ($key,$stock,$cb,$label,$tip)=@$aref; my $item=Gtk2::ToolButton->new_from_stock($stock); $item->set_label($label); $item->signal_connect(clicked => $cb); $item->set_tooltip_text($tip) if $tip; $toolbar->insert($item,-1); ::weaken( $self->{$key}=$item ) if $key; } $self->{saveb}->set_is_important(1); # create follow toggle button, function from GMB::Context my $follow=$self->new_follow_toolitem; $self->{editb}=my $editmode= Gtk2::ToggleToolButton->new_from_stock('gtk-edit'); $editmode->signal_connect(toggled=> sub { SetEditable($_[0],$_[0]->get_active); }); $editmode->set_tooltip_text(_"Edit mode"); my $adj= $self->{fontsize_adj}= Gtk2::Adjustment->new(10,4,80,1,5,0); my $zoom=Gtk2::ToolItem->new; my $zoom_spin=Gtk2::SpinButton->new($adj,1,0); $zoom->add($zoom_spin); $zoom->set_tooltip_text(_"Font size"); my $source=::NewPrefCombo( OPT.'LyricSite', { map {$_=>$Sites{$_}[0]} keys %Sites} ,cb => \&Refresh_cb, toolitem=> _"Lyrics source"); my $scroll=::NewPrefCheckButton( OPT.'AutoScroll', _"Auto-scroll", cb=>\&SetAutoScroll, tip=>_"Scroll with the song", toolitem=>1); $toolbar->insert($_,-1) for $editmode,$follow,$zoom,$scroll,$source; $self->pack_start($toolbar,0,0,0); $self->add($sw); $self->{toolbar}=$toolbar; $self->signal_connect(destroy => \&destroy_event_cb); $self->{buffer}->signal_connect(modified_changed => sub {$_[1]->set_sensitive($_[0]->get_modified);}, $self->{saveb}); $self->{backb}->set_sensitive(0); $self->SetFont; $zoom_spin->signal_connect(value_changed=> sub { my $self=::find_ancestor($_[0],__PACKAGE__); $self->SetFont($_[0]->get_value) }); $self->SetToolbarHide($self->{HideToolbar}); $self->SetAutoScroll; $self->SetEditable($self->{edit}); return $self; } sub destroy_event_cb { my $self=shift; $self->cancel; } sub cancel { my $self=shift; delete $::ToDo{'8_lyrics'.$self}; $self->{waiting}->abort if $self->{waiting}; $self->{waiting}=$self->{pixtoload}=undef; } sub prefbox { my $vbox=Gtk2::VBox->new(::FALSE, 2); my $entry=::NewPrefEntry(OPT.'PathFile' => _"Lyrics file :", width=>30, tip=> _"Lyrics file name format" ); my $preview= Label::Preview->new(preview => \&filename_preview, event => 'CurSong Option', noescape=>1,wrap=>1); my $autosave=::NewPrefCheckButton(OPT.'AutoSave' => _"Auto-save positive finds", tip=>_"only works with some lyrics source and when the lyrics tab is active"); my $embed=::NewPrefCombo(OPT.'PreferEmbeddedLyrics', { 0=> _"lyrics file", 1=> _"file tag"}, text=>_"Prefered place to load and save lyrics :"); my $alwaysload=::NewPrefCheckButton(OPT.'AlwaysLoad' => _"Load lyrics even if lyrics panel is hidden"); my $Bopen=Gtk2::Button->new(_"open context window"); $Bopen->signal_connect(clicked => sub { ::ContextWindow; }); $vbox->pack_start($_,::FALSE,::FALSE,1) for $embed,$entry,$preview,$autosave,$alwaysload,$Bopen; return $vbox; } sub filename_preview { return '' unless defined $::SongID; my $t=::pathfilefromformat( $::SongID, $::Options{OPT.'PathFile'}, undef,1); $t= ::filename_to_utf8displayname($t) if $t; $t= $t ? ::PangoEsc(_("example : ").$t) : "".::PangoEsc(_"invalid pattern").""; return ''.$t.''; } sub SetToolbarHide { my ($self,$hide)=@_; $self->{HideToolbar}=$hide; my $toolbar=$self->{toolbar}; if ($self->{HideToolbar}) {$toolbar->set_no_show_all(1); $toolbar->hide} else {$toolbar->set_no_show_all(0); $toolbar->show_all} } sub SetEditable { my $self=::find_ancestor($_[0],__PACKAGE__); my $on=$_[1]; $self->{edit}=$on; my $view=$self->{textview}; $view->set_editable($on); $view->set_cursor_visible($on); $self->{editb}->set_active($on) if $self->{editb}->get_active xor $on; } sub SetAutoScroll { my $self=::find_ancestor($_[0],__PACKAGE__); if ($self->{AutoScroll}=$::Options{OPT.'AutoScroll'}) { ::Watch($self,Time => \&TimeChanged); } else { ::UnWatch($self,'Time'); }; } sub SetFont { my ($self,$newfont)=@_; return if $self->{busy}; $self->{busy}=1; my $textview=$self->{textview}; my $font= $self->{font} || $::Options{OPT.'Font'}; my $size; if ($newfont) { if ($newfont=~m/\D/ || !$font) { $font=$newfont } else { $size=$newfont } } my $fontdesc=Gtk2::Pango::FontDescription->from_string($font); $fontdesc->set_size( $size * Gtk2::Pango->scale ) if $size; # update spin button $size= $fontdesc->get_size / Gtk2::Pango->scale; my $adj=$self->{fontsize_adj}; $adj->set_value( $size ); $::Options{OPT.'Font'}= $self->{font}= $fontdesc->to_string; $textview->modify_font($fontdesc); delete $self->{busy}; } sub ChooseFont { my $self=shift; my $dialog=Gtk2::FontSelectionDialog->new(_"Choose font for lyrics"); $dialog->set_font_name( $self->{font} ); my $response= $dialog->run; if ($response eq 'ok') { $self->SetFont( $dialog->get_font_name ); } $dialog->destroy; } sub Set_message { my ($self,$text) = @_; $self->{buffer}->set_text(""); my $iter=$self->{buffer}->get_start_iter; my $fontsize=$self->style->font_desc; my $tag_noresults=$self->{buffer}->create_tag(undef,justification=>'center',font=>$fontsize*2,foreground_gdk=>$self->style->text_aa("normal")); $self->{buffer}->insert_with_tags($iter,"\n$text",$tag_noresults); $self->{buffer}->set_modified(0); } sub Back_cb { my $self=::find_ancestor($_[0],__PACKAGE__); my $url=pop @{$self->{history}}; $_[0]->set_sensitive(0) unless @{$self->{history}}; $self->{lastokurl}=undef; $self->load_url($url) if $url; } sub Refresh_cb { my $self=::find_ancestor($_[0],__PACKAGE__); $self->SongChanged($self->{ID},'force'); } sub SongChanged { my ($self,$ID,$force)=@_; return unless $self->mapped || $::Options{OPT.'AlwaysLoad'}; return unless defined $ID; return if defined $self->{ID} && !$force && ( $ID==$self->{ID} || !$self->{follow} ); $self->cancel; #cancel any lyrics operation in progress on an a previous song $self->{ID}=$ID; $self->{time}=undef; if (!$force) { ::IdleDo('8_lyrics'.$self,1000,\&load_from_file,$self); } else { $self->load_from_web; } } sub load_from_web { my ($self,$next)=@_; my $site; if ($next) { $site = shift @{$self->{trynext}}; if (!$site) { $self->Set_message(_"No lyrics found"); return; } } else { $site = $::Options{OPT.'LyricSite'}; delete $self->{trynext}; if ($site eq 'AUTO') { ($site,@{$self->{trynext}})= qw/lyricssongs lyricwiki/; #FIXME make it configurable } } return unless $site; my (undef,$url,$post,$check)=@{$Sites{$site}}; my $ID= $self->{ID}; for my $val ($url,$post) { next unless defined $val; if (ref $val) { $val= $val->($ID); } else { $val= ::ReplaceFields($ID, $val, \&::url_escapeall); } } return unless $url; ::IdleDo('8_lyrics'.$self,1000,\&load_url,$self,$url,$post,$check); } sub TimeChanged #scroll the text { my $self=$_[0]; return unless defined $::SongID && defined $self->{ID} && $self->{ID} eq $::SongID; return unless defined $::PlayTime; my $adj=($self->get_children)[1]->get_vadjustment; my $range=($adj->upper - $adj->lower - $adj->page_size); return unless $range >0; return if $adj->get_value > $adj->upper - $adj->page_size; my $delta=$::PlayTime - ($self->{time} || 0); return if abs($delta) <1; my $inc=$delta / Songs::Get($::SongID,'length'); $self->{time}=$::PlayTime; $adj->set_value($adj->get_value+$inc*$range); } sub populate_popup_cb { my ($textview,$menu)=@_; my $self=::find_ancestor($textview,__PACKAGE__); # add menu items for links my ($x,$y)=$textview->window_to_buffer_coords('widget',$textview->get_pointer); if (my $url=$self->url_at_coords($x,$y)) { my $item2=Gtk2::MenuItem->new(_"Open link in Browser"); $item2->signal_connect(activate => sub { ::openurl($url); }); my $item3=Gtk2::MenuItem->new(_"Copy link address"); $item3->signal_connect(activate => sub { my $url=$_[1]; my $clipboard=$_[0]->get_clipboard(Gtk2::Gdk::Atom->new('CLIPBOARD',1)); $clipboard->set_text($url); },$url); $menu->prepend($_) for Gtk2::SeparatorMenuItem->new, $item3,$item2; } $menu->append(Gtk2::SeparatorMenuItem->new); ::BuildMenu( \@ContextMenuAppend, { self=>$self, }, $menu ); $menu->show_all; } sub html_extract { my ($data,$tag,$id)=@_; my $re=qr/<\Q$tag\E [^>]*id="(\w+)"[^>]*>|<(\/)?\Q$tag\E>/i; my ($start,$depth); while ($data=~m/$re/g) { if ($2) #closing { if ($depth) { $depth--; return substr $data,$start,$+[0]-$start unless $depth; } } elsif ($depth) { $depth++ } elsif (defined $1 && $1 eq $id) { $start=$-[0]; $depth=1; } } } sub load_url { my ($self,$url,$post,$check)=@_; $self->Set_message(_"Loading..."); $self->cancel; warn "lyrics : loading $url\n";# if $::debug; $self->{url}=$url; $self->{post}=$post; $self->{check}=$check; # function to check if lyrics found $self->{waiting}=Simple_http::get_with_cb(cb => sub {$self->loaded(@_)},url => $url,post => $post); } sub loaded #_very_ crude html to gtktextview renderer { my ($self,$data,%data_prop)=@_; delete $self->{waiting}; my $type=$data_prop{type}; my $buffer=$self->{buffer}; unless ($data) { $data=_("Loading failed.").qq(
)._("retry").''; $type="text/html"; } $self->{url}=$data_prop{url} if $data_prop{url}; #for redirections $buffer->delete($buffer->get_bounds); my $encoding; if ($type && $type=~m#^text/.*; ?charset=([\w-]+)#) {$encoding=$1} if ($type && $type!~m#^text/html#) { if ($type=~m#^text/#) {$buffer->set_text($data);} elsif ($type=~m#^image/#) { my $loader= GMB::Picture::LoadPixData($data); if (my $p=$loader->get_pixbuf) {$buffer->insert_pixbuf(0,$p);} } return; } $encoding=$1 if $data=~m#{check}) { ($oklyrics,my $redirect)= $check->($data,$self->{ID}); if ($redirect) { $self->load_url($redirect,undef,$check); return; } if ($self->{trynext} && !($oklyrics && $oklyrics>0)) { $self->load_from_web('trynext'); return } } if ($self->{lastokurl}) { my $history=$self->{history}||=[]; push @$history,$self->{lastokurl}; $#$history=20 if $#$history>20; $self->{backb}->set_sensitive(1) if @$history==1; } $self->{lastokurl}=$self->{url}; for ($data) {s///gs; s###gsi; #added to remove warnings from lyrc.com.ar, maybe should be restricted to lyrc.com.ar ? } my (%prop,$ul,@urls,@pixbufs,$title,%namedanchors,$li); my $iter=$buffer->get_start_iter; my @l=split /(<[^>]*>)/s, $data; while (defined($_=shift @l)) { s/[\r\n]//g; s/\s+/ /g; next if $_ eq ' '; s# # #gi; s##\n#gi; if ($_ eq '
') {$_="\n".shift(@l)."\n"}
		if (m/^[^<]/) # insert text
		{	if ($ul && $li)
			{	$buffer->insert($iter, (' 'x$ul).' - ');
				$li=0;
			}
			my $text=::decode_html($_);
			if (keys %prop)
			{ my $tag=$buffer->create_tag(undef,%prop);
			  $buffer->insert_with_tags($iter, $text, $tag);
			}
			else	{$buffer->insert($iter, $text);}
		}
		elsif (m#^<(script|style)[ >]#i) {shift @l while @l && $l[0] ne ""}
		elsif (m#^]#i) {$buffer->insert($iter,"\n");$ul++}
		elsif (m#^]#i) {$li=1}
		elsif (m#^]#i) {$buffer->insert($iter,"\n");}
		elsif (my ($tag,$p)=m#^<(\w+) (.*)>$#)
		{	my %p;
			#$p{$1}=$3 while $p=~m/(\w+)=(["'])(.+?)\2/sg;
			$p{$1}=$3 while $p=~m/\G\s*(\w+)=(["'])(.*?)\2/gc || $p=~m/\G\s*(\w+)()=(\S+)/gc;
			if ($tag eq 'a')
			{ if	(exists $p{href}) { push @urls,$iter->get_offset,$p{href}; }
			  elsif	(exists $p{name}) { $namedanchors{$p{name}}=$iter->get_offset; }
			}
			elsif ($tag eq 'font' && 0)
				{ if (my $s=$p{size})
				  {	if ($s=~m/^\d+$/) {$prop{scale}=$s*.33}
					elsif ($s=~m/^(-?\d+)$/) {$prop{scale}||=1;$prop{scale}+=$1*.33}
					else {delete $prop{scale};}
					warn "$s => ".($prop{scale}||"")."\n";
				  }
				  else {delete $prop{scale};}
				}
			elsif ($tag=~m/^h(\d)/i)
				{ $prop{scale}=(3,2.5,2,1.5,1.2,1,.66)[$1];
				  $prop{weight}=Gtk2::Pango::PANGO_WEIGHT_BOLD if $1 eq '1';
				}
			elsif ($tag eq 'table') {$buffer->insert($iter,"\n");}
			elsif ($tag eq 'img' && exists $p{src})
			{	my $mark=$buffer->create_mark(undef,$iter,1);
				push @pixbufs,[$mark, $p{src}, (@urls%3==2 ? $urls[-1] : ())];
			}
			if (exists $p{id}) {$namedanchors{$p{id}}=$iter->get_offset;}
		}
		elsif (m#^$#)
		{	if    ($1 eq 'a')	{push @urls,$iter->get_offset if @urls%3==2;}
			elsif ($1 eq 'u')	{delete $prop{underline};}
			elsif ($1 eq 'b')	{delete $prop{weight};}
			elsif ($1 eq 'i')	{delete $prop{style};}
			elsif ($1 eq 'font')	{delete $prop{scale};}
			elsif ($1=~m/^h(\d)/i)	{delete $prop{scale};delete $prop{weight};$buffer->insert($iter,"\n");}
			elsif ($1 eq 'ul')	{$buffer->insert($iter,"\n");$ul--;}
			elsif ($1 eq 'tr')	{$buffer->insert($iter,"\n");}
			elsif ($1 eq 'li')	{$buffer->insert($iter,"\n");}
			elsif ($1 eq 'p')	{$buffer->insert($iter,"\n");}
			elsif ($1 eq 'div')	{$buffer->insert($iter,"\n");}
		}
		elsif (m#^<(\w+)>$#)
		{	if    ($1 eq 'u')	{$prop{underline}='single';}
			elsif ($1 eq 'b')	{$prop{weight}=Gtk2::Pango::PANGO_WEIGHT_BOLD;}
			elsif ($1 eq 'i')	{$prop{style}='italic';}
			elsif ($1 eq 'title')	{$title=shift @l while $l[0] ne ""}
		}
	}
	while (@urls)
	{	my ($offs1,$url,$offs2)=splice @urls,0,3;
		my $tag=$buffer->create_tag(undef,foreground => 'blue',underline => 'single');
		if ($url=~m/^#(.*)/)
		{	if (exists $namedanchors{$1}) {$url='#'.$namedanchors{$1};}
			else {next}
		}
		$tag->{url}=$url;
		$buffer->apply_tag($tag,
			$buffer->get_iter_at_offset($offs1),
			$buffer->get_iter_at_offset($offs2));
	}
	for my $url (map $_->[2], grep @$_>2, @pixbufs)
	{	next unless $url=~m/^#(.*)/;
		if (exists $namedanchors{$1}) {$url='#'.$namedanchors{$1};}
		else {$url=undef}
	}

	if (0)
	{	$self->{pixtoload}=\@pixbufs;
		::IdleDo('8_FetchPix'.$self,100,\&load_pixbuf,$self) if @pixbufs;
	}
	if ($oklyrics && $oklyrics>0)
	{	$self->Save_text if $::Options{OPT.'AutoSave'};
	}
	#else { $buffer->set_modified(0); }
}

sub load_pixbuf
{	my $self=shift;
	my $ref=shift @{ $self->{pixtoload} };
	return 0 unless $ref;
	my ($mark,$url,$link)=@$ref;
	$self->{waiting}=Simple_http::get_with_cb(url => $self->full_url($url), cache=>1, cb=>
	sub
	{	$self->{waiting}=undef;
		my $loader;
		$loader= GMB::Picture::LoadPixData($_[0]) if $_[0];
		if ($loader)
		{	my $buffer=$self->{buffer};
			my $iter=$buffer->get_iter_at_mark($mark);
			$buffer->delete_mark($mark);
			my $offset=$iter->get_offset;
			$buffer->insert_pixbuf($iter,$loader->get_pixbuf);
			if ($link)
			{	my $tag=$buffer->create_tag(undef,foreground => 'blue');
				$tag->{url}=$link;
				$buffer->apply_tag($tag,$buffer->get_iter_at_offset($offset),$iter);
			}
		}
		::IdleDo('8_FetchPix'.$self,100,\&load_pixbuf,$self); #load next
	});
#::IdleDo('8_FetchPix'.$self,100,\&load_pixbuf,$self) unless $self->{waiting};
}

#sub loaded_old #old method, more crude :)
#{	my $self=shift;
#	my $buffer=$self->{buffer};
#	unless ($_[0]) {$buffer->delete($buffer->get_bounds);$buffer->set_text(_"Loading failed.");return;}
#	local $_=$_[0];
#	s/[\r\n]//g;
#	s#.*?##;
#	s#