File Coverage

blib/lib/Wx/Perl/IconDepot.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Wx::Perl::IconDepot;
2              
3             =head1 NAME
4              
5             Wx::Perl::IconDepot - Use icon libraries quick & easy
6              
7             =cut
8              
9 1     1   46732 use strict;
  1         2  
  1         24  
10 1     1   5 use warnings;
  1         2  
  1         26  
11              
12 1     1   4 use vars qw($VERSION);
  1         2  
  1         48  
13             our $VERSION = '0.02';
14              
15 1     1   128 use Wx qw( :image );
  0            
  0            
16             use File::Basename;
17             use Module::Load::Conditional qw( can_load );
18              
19             my %imgext = (
20             '.jpg' => wxBITMAP_TYPE_JPEG,
21             '.jpeg' => wxBITMAP_TYPE_JPEG,
22             '.png' => wxBITMAP_TYPE_PNG,
23             '.gif' => wxBITMAP_TYPE_GIF,
24             '.bmp' => wxBITMAP_TYPE_BMP,
25             '.xpm' => wxBITMAP_TYPE_XPM,
26             );
27             my @extensions = (keys %imgext);
28              
29             if (can_load(modules => {'Image::LibRSVG' => '0.07'})) {
30             push @extensions, '.svg';
31             use IO::Scalar;
32             }
33              
34              
35             my @iconpath = ();
36             if ($^O eq 'MSWin32') {
37             push @iconpath, $ENV{ALLUSERSPROFILE} . '\Icons'
38             } else {
39             push @iconpath, $ENV{HOME} . '/.local/share/icons',
40             push @iconpath, '/usr/share/icons',
41             }
42              
43             ###############################################################################
44             =head1 SYNOPSIS
45              
46             =over 4
47              
48             my $depot = new Wx::Perl::IconDepot(\@pathnames);
49             $depot->SetThemes($theme1, $theme2, $theme3);
50             my $wxbitmap = $depot->GetBitmap($name, $size, $context)
51             my $wxicon = $depot->GetIcon($name, $size, $context)
52             my $wximage = $depot->GetImage($name, $size, $context)
53              
54             =back
55              
56             =head1 DESCRIPTION
57              
58             This module allows B easy access to icon libraries used in desktops
59             like KDE and GNOME.
60              
61             It supports libraries containing scalable vector graphics like Breeze if
62             B is installed. If not you are confined to bitmapped libraries
63             like Oxygen or Adwaita.
64              
65             On Windows you have to install icon libraries yourself in C:\ProgramData\Icons.
66             You will find plenty of them on Github. Extract an icon set and copy the main
67             folder of the theme (the one that contains the file 'index.theme') to
68             C:\ProgramData\Icons. On Linux you will probably find some icon themes
69             in /usr/share/icons.
70              
71             The constructor takes a reference to a list of folders where it finds the icons
72             libraries. If you specify nothing, it will assign default values for:
73              
74             Windows: $ENV{ALLUSERSPROFILE} . '\Icons'. IconDepot will not create
75             the folder if it does not exist.
76              
77             Others: $ENV{HOME} . '/.local/share/icons', '/usr/share/icons'
78              
79             =cut
80              
81             sub new {
82             my $proto = shift;
83             my $class = ref($proto) || $proto;
84              
85             my $self = {};
86             bless ($self, $class);
87              
88             my $pathlist = shift;
89             unless (defined $pathlist) { $pathlist = \@iconpath };
90              
91             $self->{ACTIVE} = [];
92             $self->{ACTIVENAMES} = [];
93             $self->{DEFAULTSIZE} = 22;
94             $self->{FORCEIMAGE} = 1;
95             $self->{INDEX} = undef;
96             $self->{ICONPATH} = $pathlist;
97             $self->{MISSINGIMAGE} = $self->FindINC('Wx/Perl/IconDepot/image_missing.png');
98             $self->{THEMEPOOL} = {};
99             $self->{THEMES} = $self->CollectThemes;
100             Wx::InitAllImageHandlers();
101             return $self;
102             }
103              
104             =head1 PUBLIC METHODS
105              
106             =over 4
107              
108             =cut
109              
110             ###############################################################################
111             =item BI<($theme, >[ I<$name, $size> ] I<);>
112              
113             =over 4
114              
115             Returns a list of available contexts. If you set $name to undef if will return
116             all contexts of size $size. If you set $size to undef it will return all
117             contexts associated with icon $name. If you set $name and $size to undef it
118             will return all known contexts in the theme. out $size it returns a list
119             of all contexts found in $theme.
120              
121             =back
122              
123             =cut
124              
125             sub AvailableContexts {
126             my ($self, $theme, $name, $size) = @_;
127             my $t = $self->GetTheme($theme);
128             my %found = ();
129             if ((not defined $name) and (not defined $size)) {
130             my @names = keys %$t;
131             for (@names) {
132             my $si = $t->{$_};
133             my @sizes = keys %$si;
134             for (@sizes) {
135             my $ci = $si->{$_};
136             for (keys %$ci) {
137             $found{$_} = 1;
138             }
139             }
140             }
141             } elsif ((defined $name) and (not defined $size)) {
142             if (exists $t->{$name}) {
143             my $si = $t->{$name};
144             my @sizes = keys %$si;
145             for (@sizes) {
146             my $ci = $si->{$_};
147             for (keys %$ci) {
148             $found{$_} = 1;
149             }
150             }
151             }
152             } elsif ((not defined $name) and (defined $size)) {
153             my @names = keys %$t;
154             for (@names) {
155             if (exists $t->{$_}->{$size}) {
156             my $ci = $t->{$_}->{$size};
157             for (keys %$ci) {
158             $found{$_} = 1;
159             }
160             }
161             }
162             } else {
163             if (exists $t->{$name}) {
164             my $si = $t->{$name};
165             if (exists $si->{$size}) {
166             my $ci = $si->{$size};
167             %found = %$ci
168             }
169             }
170             }
171             return sort keys %found
172             }
173              
174             ###############################################################################
175             =item BI<($theme, >[ I<$size, $context> ] I<);>
176              
177             =over 4
178              
179             Returns a list of available icons. If you set $size to undef the list will
180             contain names it found in all sizes. If you set $context to undef it will return
181             names it found in all contexts. If you leave out both then
182             you get a list of all available icons. Watch out, it might be pretty long.
183              
184             =back
185              
186             =cut
187              
188             sub AvailableIcons {
189             my ($self, $theme, $size, $context) = @_;
190             my $t = $self->GetTheme($theme);
191              
192             my @names = keys %$t;
193             my @matches = ();
194             if ((not defined $size) and (not defined $context)) {
195             @matches = @names
196             } elsif ((defined $size) and (not defined $context)) {
197             for (@names) {
198             if (exists $t->{$_}->{$size}) { push @matches, $_ }
199             }
200             } elsif ((not defined $size) and (defined $context)) {
201             for (@names) {
202             my $name = $_;
203             my $si = $t->{$name};
204             my @sizes = keys %$si;
205             for (@sizes) {
206             if (exists $t->{$name}->{$_}->{$context}) { push @matches, $name }
207             }
208             }
209             } else {
210             for (@names) {
211             if (exists $t->{$_}->{$size}) {
212             my $c = $t->{$_}->{$size};
213             if (exists $c->{$context}) {
214             push @matches, $_
215             }
216             }
217             }
218             }
219             return sort @matches
220             }
221              
222             ###############################################################################
223             =item BI<($theme, >[ I<$name, $context> ] I<);>
224              
225             =over 4
226              
227             Returns a list of available contexts. If you leave out $size it returns a list
228             of all contexts found in $theme.
229              
230             =back
231              
232             =cut
233              
234             sub AvailableSizes {
235             my ($self, $theme, $name, $context) = @_;
236             my $t = $self->GetTheme($theme);
237              
238             my %found = ();
239             if ((not defined $name) and (not defined $context)) {
240             my @names = keys %$t;
241             for (@names) {
242             my $si = $t->{$_};
243             my @sizes = keys %$si;
244             for (@sizes) {
245             $found{$_} = 1
246             }
247             }
248             } elsif ((defined $name) and (not defined $context)) {
249             if (exists $t->{$name}) {
250             my $si = $t->{$name};
251             %found = %$si;
252             }
253             } elsif ((not defined $name) and (defined $context)) {
254             my @names = keys %$t;
255             for (@names) {
256             my $n = $_;
257             my $si = $t->{$n};
258             my @sizes = keys %$si;
259             for (@sizes) {
260             if (exists $t->{$n}->{$_}->{$context}) {
261             $found{$_} = 1
262             }
263             }
264             }
265             } else {
266             if (exists $t->{$name}) {
267             my $si = $t->{$name};
268             my @sizes = keys %$si;
269             for (@sizes) {
270             if (exists $t->{$name}->{$_}->{$context}) {
271             $found{$_} = 1
272             }
273             }
274             }
275             }
276             return sort {$a <=> $b} keys %found
277             }
278              
279             ###############################################################################
280             =item B
281              
282             =over 4
283              
284             Returns a list of available themes it found while initiating the module.
285              
286             =back
287              
288             =cut
289              
290             sub AvailableThemes {
291             my $self = shift;
292             my $k = $self->{THEMES};
293             return sort keys %$k
294             }
295              
296             ###############################################################################
297             =item BI<($name, >[ I<$size, $context, \$resize> ] I<);>
298              
299             =over 4
300              
301             Returns the filename of an image in the library. Finds the best suitable
302             version of the image in the library according to $size and $context. If it
303             eventually returns an image of another size, it sets $resize to 1. This gives
304             the opportunity to scale the image to the requested icon size. All parameters
305             except $name are optional.
306              
307             =back
308              
309             =cut
310              
311             sub FindImage {
312             my ($self, $name, $size, $context, $resize) = @_;
313             unless (defined $size) { $size = 'unknown' }
314             unless (defined $context) { $context = 'unknown' }
315             my $active = $self->{ACTIVE};
316             for (@$active) {
317             my $index = $_;
318             if (exists $index->{$name}) {
319             return $self->FindImageS($index->{$name}, $size, $context, $resize);
320             }
321             }
322             return undef;
323             }
324              
325             ###############################################################################
326             =item B
327              
328             =over 4
329              
330             Returns a list of active themes. Primary theme first then the fallback themes.
331              
332             =back
333              
334             =cut
335              
336             sub GetActiveThemes {
337             my $self = shift;
338             my $a = $self->{ACTIVENAMES};
339             return @$a
340             }
341              
342             ###############################################################################
343             =item BI<($name>, [ I<$size, $context, $force> ] I<);>
344              
345             =over 4
346              
347             Returns a Wx::Bitmap object. If you do not specify I<$size> or the icon does
348             not exist in the specified size, it will return the largest possible icon.
349             I<$force> can be 0 or 1. It is 0 by default. If you set it to 1 a missing icon
350             image is returned instead of undef when the icon cannot be found.
351              
352             =back
353              
354             =cut
355              
356             sub GetBitmap {
357             my $self = shift;
358             return Wx::Bitmap->new($self->GetImage(@_))
359             }
360              
361             ###############################################################################
362             =item BI<($name>, [ I<$size, $context, $force> ] I<);>
363              
364             =over 4
365              
366             Returns a Wx::Icon object. If you do not specify I<$size> or the icon does not
367             exist in the specified size, it will return the largest possible icon.
368             I<$force> can be 0 or 1. It is 0 by default. If you set it to 1 a missing icon
369             image is returned instead of undef when the icon cannot be found.
370              
371             =back
372              
373             =cut
374              
375             sub GetIcon {
376             my $self = shift;
377             my $bmp = $self->GetBitmap(@_);
378             my $icon = Wx::Icon->new();
379             $icon->CopyFromBitmap($bmp);
380             return $icon
381             }
382              
383             ###############################################################################
384             =item BI<($name>, [ I<$size, $context, $force> ] I<);>
385              
386             =over 4
387              
388             Returns a Wx::Image object. If you do not specify I<$size> or the icon does
389             not exist in the specified size, it will find the largest possible icon and
390             scale it to the requested size. I<$force> can be 0 or 1. It is 0 by default.
391             If you set it to 1 a missing icon image is returned instead of undef when the
392             icon cannot be found.
393              
394             =back
395              
396             =cut
397              
398             sub GetImage {
399             my ($self, $name, $size, $context, $force) = @_;
400             unless (defined $force) { $force = 0 }
401             my $resize = 0;
402             my $file = $self->FindImage($name, $size, $context, \$resize);
403             if (defined $file) {
404             my $img = $self->LoadImage($file, $size);
405             if ($img->IsOk) {
406             if ($resize) {
407             return $img->Scale($size, $size);
408             }
409             return $img
410             } else {
411             return undef
412             }
413             } elsif ($force and (defined $size) and ($size =~ /^\d+$/)) { #size must be defined and numeric
414             return $self->GetMissingImage($size)
415             }
416             return undef
417             }
418              
419             ###############################################################################
420             =item BI<($theme)>
421              
422             =over 4
423              
424             Returns the full path to the folder containing I<$theme>
425              
426             =back
427              
428             =cut
429              
430             sub GetThemePath {
431             my ($self, $theme) = @_;
432             my $t = $self->{THEMES};
433             if (exists $t->{$theme}) {
434             return $t->{$theme}->{path}
435             } else {
436             warn "Icon theme $theme not found"
437             }
438             }
439              
440             ###############################################################################
441             =item BI<($file)>
442              
443             =over 4
444              
445             Returns true if I<$file> is an image. Otherwise returns false.
446              
447             =back
448              
449             =cut
450              
451             sub IsImageFile {
452             my ($self, $file) = @_;
453             unless (-f $file) { return 0 } #It must be a file
454             my ($d, $f, $e) = fileparse(lc($file), @extensions);
455             if ($e ne '') { return 1 }
456             return 0
457             }
458              
459             ###############################################################################
460             =item BI<($file)>
461              
462             =over 4
463              
464             Loads image I<$file> and returns it as a Wx::Image object.
465              
466             =back
467              
468             =cut
469              
470             sub LoadImage {
471             my ($self, $file, $size) = @_;
472             if (-e $file) {
473             my ($name,$path,$suffix) = fileparse(lc($file), @extensions);
474             if (exists $imgext{$suffix}) {
475             my $type = $imgext{$suffix};
476             my $img = Wx::Image->new($file, $type);
477             if ($img->IsOk) {
478             return $img
479             }
480             } elsif ($suffix eq '.svg') {
481             my $renderer = Image::LibRSVG->new;
482             $renderer->loadFromFileAtSize($file, $size, $size);
483             my $png = $renderer->getImageBitmap("png", 100);
484             my $img = Wx::Image->newStreamType(IO::Scalar->new(\$png), wxBITMAP_TYPE_PNG);
485             if ($img->IsOk) {
486             return $img
487             }
488             } else {
489             warn "could not define image type for file $file"
490             }
491             } else {
492             warn "image file $file not found \n";
493             }
494             return undef
495             }
496              
497             ###############################################################################
498             =item BI<($theme1, >[ I<$theme2, $theme3> ] I<);>
499              
500             =over 4
501              
502             Initializes themes. I<$theme1> is the primary theme. The rest are subsequent
503             fallback themes. I suggest to use your favourite theme as the first one and
504             the theme that has the most icons as the last one.
505              
506             =back
507              
508             =cut
509              
510             sub SetThemes {
511             my $self = shift;
512             my @activenames = ();
513             my @active = ();
514             for (@_) {
515             push @activenames, $_;
516             push @active, $self->GetTheme($_);
517             }
518             $self->{ACTIVENAMES} = \@activenames;
519             $self->{ACTIVE} = \@active;
520             }
521              
522             ###############################################################################
523             =back
524              
525             =head1 PRIVATE METHODS
526              
527             =over 4
528              
529             =cut
530              
531             ###############################################################################
532             =item B
533              
534             Called during initialization. It scans the folders the constructor receives for
535             icon libraries. It loads their index files and stores the info.
536              
537             =over 4
538              
539             =back
540              
541             =cut
542              
543             sub CollectThemes {
544             my $self = shift;
545             my %themes = ();
546             my $iconpath = $self->{ICONPATH};
547             for (@$iconpath) {
548             my $dir = $_;
549             if (opendir DIR, $dir) {
550             while (my $entry = readdir(DIR)) {
551             my $fullname = "$dir/$entry";
552             if (-d $fullname) {
553             if (-e "$fullname/index.theme") {
554             my $index = $self->LoadThemeFile($fullname);
555             my $main = delete $index->{general};
556             if (%$index) {
557             my $name = $entry;
558             if (exists $main->{Name}) {
559             $name = $main->{Name}
560             }
561             $themes{$name} = {
562             path => $fullname,
563             general => $main,
564             folders => $index,
565             }
566             }
567             }
568             }
569             }
570             closedir DIR;
571             }
572             }
573             return \%themes
574             }
575              
576             ###############################################################################
577             =item BI<($themeindex)>
578              
579             =over 4
580              
581             Creates a searchable index from a loaded theme index file. Returns a reference
582             to a hash.
583              
584             =back
585              
586             =cut
587              
588             sub CreateIndex {
589             my ($self, $tindex) = @_;
590             my %index = ();
591             my $base = $tindex->{path};
592             my $folders = $tindex->{folders};
593             foreach my $dir (keys %$folders) {
594             my @raw = <"$base/$dir/*">;
595             foreach my $file (@raw) {
596             if ($self->IsImageFile($file)) {
597             my ($name, $d, $e) = fileparse($file, @extensions);
598             unless (exists $index{$name}) {
599             $index{$name} = {}
600             }
601             my $size = $folders->{$dir}->{Size};
602             unless (defined $size) {
603             $size = 'unknown';
604             }
605             unless (exists $index{$name}->{$size}) {
606             $index{$name}->{$size} = {}
607             }
608             my $context = $folders->{$dir}->{Context};
609             unless (defined $context) {
610             $context = 'unknown';
611             }
612             $index{$name}->{$size}->{$context} = $file;
613             }
614             }
615             }
616             return \%index;
617             }
618              
619             ###############################################################################
620             =item BI<($sizeindex, $context)>
621              
622             =over 4
623              
624             Looks for an icon in $context for a given size index (a portion of a searchable
625             index). If it can not find it, it looks for another version in all other
626             contexts. Returns the first one it finds.
627              
628             =back
629              
630             =cut
631              
632             sub FindImageC {
633             my ($self, $si, $context) = @_;
634             if (exists $si->{$context}) {
635             return $si->{$context}
636             } else {
637             my @contexts = sort keys %$si;
638             if (@contexts) {
639             return $si->{$contexts[0]};
640             }
641             }
642             return undef
643             }
644              
645             ###############################################################################
646             =item BI<($nameindex, $size, $context, \$resize)>
647              
648             =over 4
649              
650             Looks for an icon of $size for a given name index (a portion of a searchable
651             index). If it can not find it it looks for another version in all other sizes.
652             In this case it returns the biggest one it finds and sets $resize to 1.
653              
654             =back
655              
656             =cut
657              
658             sub FindImageS {
659             my ($self, $nindex, $size, $context, $resize) = @_;
660             if (exists $nindex->{$size}) {
661             my $file = $self->FindImageC($nindex->{$size}, $context);
662             if (defined $file) { return $file }
663             } else {
664             if (defined $resize) { $$resize = 1 }
665             my @sizes = reverse sort keys %$nindex;
666             for (@sizes) {
667             my $si = $nindex->{$_};
668             my $file = $self->FindImageC($si, $context);
669             if (defined $file) { return $file }
670             }
671             }
672             return undef
673             }
674              
675             ###############################################################################
676             =item BI<($file)>
677              
678             =over 4
679              
680             Looks for a file in @INC. if found returns the full pathname.
681              
682             =back
683              
684             =cut
685              
686             sub FindINC {
687             my ($self, $file) = @_;
688             for (@INC) {
689             my $f = $_ . "/$file";
690             if (-e $f) {
691             return $f;
692             }
693             }
694             return undef;
695             }
696              
697             ###############################################################################
698             =item BI<($size)>
699              
700             =over 4
701              
702             Returns a Wx::Image object of the missing image symbal on the requested size.
703              
704             =back
705              
706             =cut
707              
708             sub GetMissingImage {
709             my ($self, $size) = @_;
710             my $tmp = Wx::Image->new($self->{MISSINGIMAGE}, wxBITMAP_TYPE_PNG, );
711             return $tmp->Scale($size, $size, wxIMAGE_QUALITY_HIGH)
712             }
713              
714             ###############################################################################
715             =item BI<($themename)>
716              
717             =over 4
718              
719             Looks for a searchable index of the theme. If it is not yet created it will
720             be created first and stored in the index pool.
721              
722             =back
723              
724             =cut
725              
726             sub GetTheme {
727             my ($self, $theme) = @_;
728             my $pool = $self->{THEMEPOOL};
729             if (exists $pool->{$theme}) {
730             return $pool->{$theme}
731             } else {
732             my $themindex = $self->{THEMES}->{$theme};
733             if (defined $themindex) {
734             my $index = $self->CreateIndex($themindex);
735             $pool->{$theme} = $index;
736             return $index
737             } else {
738             warn "Setting theme '$theme' failed"
739             }
740             }
741             }
742              
743             ###############################################################################
744             =item BI<($file)>
745              
746             =over 4
747              
748             Loads a theme index file and returns the information in it in a hash.
749             It returns a reference to this hash.
750              
751             =back
752              
753             =cut
754              
755             sub LoadThemeFile {
756             my ($self, $file) = @_;
757             if (defined $file) {
758             $file = "$file/index.theme";
759             if (open(OFILE, "<", $file)) {
760             my %index = ();
761             my $section;
762             my %inf = ();
763             my $firstline = ;
764             unless ($firstline =~ /^\[.+\]$/) {
765             warn "Illegal file format $file";
766             } else {
767             while () {
768             my $line = $_;
769             chomp $line;
770             if ($line =~ /^\[([^\]]+)\]/) { #new section
771             if (defined $section) {
772             $index{$section} = { %inf }
773             } else {
774             $index{general} = { %inf }
775             }
776             $section = $1;
777             %inf = ();
778             } elsif ($line =~ s/^([^=]+)=//) { #new key
779             $inf{$1} = $line;
780             }
781             }
782             if (defined $section) {
783             $index{$section} = { %inf }
784             }
785             close OFILE;
786             }
787             return \%index;
788             } else {
789             warn "Cannot open theme index file: $file"
790             }
791             }
792             }
793              
794             ###############################################################################
795              
796             =back
797              
798             =head1 AUTHOR
799              
800             Hans Jeuken (hansjeuken at xs4all dot nl)
801              
802             =head1 BUGS
803              
804             If you find any, please contact the author.
805              
806             Icon libararies that depend on .svg images show up in the list of
807             B when no support for scalable vector graphics is available.
808              
809             =head1 TODO
810              
811             =cut
812              
813             1;
814             __END__