summaryrefslogtreecommitdiff
path: root/lib/gcstar/GCItemsLists
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gcstar/GCItemsLists')
-rw-r--r--lib/gcstar/GCItemsLists/GCImageListComponents.pm848
-rw-r--r--lib/gcstar/GCItemsLists/GCImageLists.pm2028
-rw-r--r--lib/gcstar/GCItemsLists/GCListOptions.pm496
-rw-r--r--lib/gcstar/GCItemsLists/GCTextLists.pm2101
4 files changed, 5473 insertions, 0 deletions
diff --git a/lib/gcstar/GCItemsLists/GCImageListComponents.pm b/lib/gcstar/GCItemsLists/GCImageListComponents.pm
new file mode 100644
index 0000000..aa2bf2b
--- /dev/null
+++ b/lib/gcstar/GCItemsLists/GCImageListComponents.pm
@@ -0,0 +1,848 @@
+package GCImageListComponents;
+
+###################################################
+#
+# Copyright 2005-2011 Christian Jodar
+#
+# This file is part of GCstar.
+#
+# GCstar is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# GCstar is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GCstar; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+#
+###################################################
+
+use strict;
+
+{
+ package GCImageListItem;
+
+ use GCUtils;
+ use GCStyle;
+ use base "Gtk2::EventBox";
+ use File::Temp qw/ tempfile /;
+
+ @GCImageListItem::ISA = ('Gtk2::EventBox');
+
+ sub new
+ {
+ my ($proto, $container, $info) = @_;
+ my $class = ref($proto) || $proto;
+ my $self = $class->SUPER::new;
+ bless ($self, $class);
+
+ # Some information that we'll need later
+ $self->{info} = $info;
+ $self->{container} = $container;
+ $self->{style} = $container->{style};
+ $self->{tooltips} = $container->{tooltips};
+ $self->{file} = $container->{parent}->{options}->file;
+ $self->{collectionDir} = $container->{collectionDir};
+ $self->{model} = $container->{parent}->{model};
+ $self->{imageCache} = $container->{imageCache};
+ $self->{dataManager} = $container->{parent}->{items};
+
+ $self->can_focus(1);
+ my $image = new Gtk2::Image;
+ $self->add($image);
+ $self->refreshInfo($info);
+ $self->set_size_request($container->{style}->{vboxWidth}, $container->{style}->{vboxHeight});
+ $self->show_all;
+
+ return $self;
+ }
+
+ sub setInfo
+ {
+ my ($self, $info) = @_;
+
+ $self->{info} = $info;
+ }
+
+ sub refreshInfo
+ {
+ my ($self, $info, $cacheRefresh) = @_;
+
+ $self->setInfo($info);
+
+ $self->refreshPopup;
+
+ delete $self->{zoomedPixbufCache};
+
+ {
+ my $pixbuf = $self->createPixbuf($info, $cacheRefresh);
+ if (! $self->{style}->{withImage})
+ {
+ $self->modify_bg('normal', $self->{style}->{inactiveBg});
+ }
+ $self->{previousPixbuf} = $pixbuf->copy;
+ $self->child->set_from_pixbuf($pixbuf);
+ }
+ if ($self->{selected})
+ {
+ $self->{selected} = 0;
+ $self->highlight;
+ $self->{selected} = 1;
+ }
+ }
+
+ sub refreshPopup
+ {
+ my $self = shift;
+ # Old versions of Gtk2 don't support set_tooltip_markup
+ eval {
+ $self->set_tooltip_markup($self->{dataManager}->getSummary($self->{info}->{idx}));
+ };
+ if ($@)
+ {
+ print "$@\n";
+ # So we do it the old way for them
+ $self->{tooltips}->set_tip($self, $self->{info}->{title}, '');
+ }
+ }
+
+ sub savePicture
+ {
+ my $self = shift;
+ $self->{previousPixbuf} = $self->child->get_pixbuf->copy
+ if $self->child;
+ }
+
+ sub restorePicture
+ {
+ my $self = shift;
+ $self->child->set_from_pixbuf($self->{previousPixbuf})
+ if $self->{previousPixbuf} && $self->child;
+ }
+
+ sub startZoomAnimation
+ {
+ my $self = shift;
+ $self->{currentZoom} = 1.01;
+ my $pixbuf = $self->createPixbuf($self->{info}, 0, 1.01);
+ $self->child->set_from_pixbuf($pixbuf);
+ $self->{zoomTimeout} = Glib::Timeout->add(20 , sub {
+ my $widget = shift;
+ $widget->{currentZoom} += 0.02;
+ if ($widget->{currentZoom} > 1.06)
+ {
+ $widget->{zoomTimeout} = undef;
+ return 0;
+ }
+ my $pixbuf = $widget->createPixbuf($self->{info}, 0, $widget->{currentZoom});
+ $widget->child->set_from_pixbuf($pixbuf)
+ if $widget->child;
+ return 1;
+ }, $self);
+ }
+
+ sub stopZoomAnimation
+ {
+ my $self = shift;
+ Glib::Source->remove($self->{zoomTimeout})
+ if $self->{zoomTimeout};
+ }
+
+ # This method sets all the event callbacks
+ sub prepareHandlers
+ {
+ my ($self, $idx, $info) = @_;
+ $self->{idx} = $idx;
+ $self->{info} = $info;
+
+ $self->signal_handler_disconnect($self->{mouseHandler})
+ if $self->{mouseHandler};
+ $self->{mouseHandler} = $self->signal_connect('button_press_event' => sub {
+ my ($widget, $event) = @_;
+
+ if (($event->type ne '2button-press') && !(($event->button eq 3) && ($widget->{selected})))
+ {
+ my $state = $event->get_state;
+ my $keepPrevious = 0;
+ if ($state =~ /control-mask/)
+ {
+ $widget->{container}->select($widget->{idx}, 0, 1);
+ }
+ elsif ($state =~ /shift-mask/)
+ {
+ $widget->{container}->restorePrevious;
+ $widget->{container}->selectMany($widget->{idx});
+ }
+ else
+ {
+ $widget->{container}->select($widget->{idx});
+ }
+ $widget->{container}->setPreviousSelectedDisplayed($widget->{idx});
+
+ #$self->{parent}->display($widget->{idx}) unless $event->type eq '2button-press';
+ $widget->{container}->displayDetails(0, keys %{$widget->{container}->{selectedIndexes}});
+ }
+
+ $widget->{container}->displayDetails(1, $widget->{idx}) if $event->type eq '2button-press';
+ $widget->{container}->showPopupMenu($event->button, $event->time) if ($event->button eq 3);
+ $widget->grab_focus;
+ });
+
+ if ($self->{style}->{withAnimation})
+ {
+ $self->signal_handler_disconnect($self->{enterHandler})
+ if $self->{enterHandler};
+ $self->{enterHandler} = $self->signal_connect('enter_notify_event' => sub {
+ my ($widget, $event) = @_;
+ if (!$widget->{selected})
+ {
+ $widget->startZoomAnimation;
+ }
+ });
+
+ $self->signal_handler_disconnect($self->{leaveHandler})
+ if $self->{leaveHandler};
+ $self->{leaveHandler} = $self->signal_connect('leave_notify_event' => sub {
+ my ($widget, $event) = @_;
+ if (!$widget->{selected})
+ {
+ $widget->stopZoomAnimation;
+ $widget->restorePicture;
+ }
+ });
+ }
+
+
+ $self->signal_handler_disconnect($self->{keyHandler})
+ if $self->{keyHandler};
+
+ $self->{keyHandler} = $self->signal_connect('key-press-event' => sub {
+ my ($widget, $event) = @_;
+ my $displayed = $self->{container}->convertIdxToDisplayed($widget->{idx});
+ my $key = Gtk2::Gdk->keyval_name($event->keyval);
+ if ($key eq 'Delete')
+ {
+ $widget->{container}->{parent}->deleteCurrentItem;
+ return 1;
+ }
+ if (($key eq 'Return') || ($key eq 'space'))
+ {
+ $widget->{container}->displayDetails(1, $widget->{idx});
+ return 1;
+ }
+ my $unicode = Gtk2::Gdk->keyval_to_unicode($event->keyval);
+ if ($unicode)
+ {
+ $self->{container}->showSearch(pack('U',$unicode));
+ }
+ else
+ {
+ my $columns = $widget->{container}->getColumnsNumber;
+
+ ($key eq 'Right') ? $displayed++ :
+ ($key eq 'Left') ? $displayed-- :
+ ($key eq 'Down') ? $displayed += $columns :
+ ($key eq 'Up') ? $displayed -= $columns :
+ ($key eq 'Page_Down') ? $displayed += ($widget->{style}->{pageCount} * $columns):
+ ($key eq 'Page_Up') ? $displayed -= ($widget->{style}->{pageCount} * $columns):
+ ($key eq 'Home') ? $displayed = 0 :
+ ($key eq 'End') ? $displayed = $widget->{container}->getNbItems - 1 :
+ return 1;
+
+ return 1 if ($displayed < 0) || ($displayed >= $widget->{container}->getNbItems);
+ my $column = $displayed % $columns;
+ my $valueIdx = $widget->{container}->convertDisplayedToIdx($displayed);
+# my $keepPrevious = 0;
+ my $state = $event->get_state;
+ if ($state =~ /control-mask/)
+ {
+ $widget->{container}->select($valueIdx, 0, 1);
+ $widget->{container}->unsetPreviousSelectedDisplayed;
+ }
+ elsif ($state =~ /shift-mask/)
+ {
+ $widget->{container}->setPreviousSelectedDisplayed($widget->{idx});
+ $widget->{container}->restorePrevious;
+ $widget->{container}->selectMany($valueIdx);
+ }
+ else
+ {
+ $widget->{container}->select($valueIdx);
+ $widget->{container}->unsetPreviousSelectedDisplayed;
+ }
+ $widget->{container}->displayDetails(0, $valueIdx);
+ $widget->{container}->grab_focus;
+ $widget->{container}->showCurrent unless (($key eq 'Left') && ($column != ($columns - 1)))
+ || (($key eq 'Right') && ($column != 0));
+ }
+ return 1;
+
+ });
+
+ }
+
+ sub highlight
+ {
+ my ($self, $keepPrevious) = @_;
+ return if $self->{selected};
+ $self->{selected} = 1;
+ if (! $self->{style}->{withImage})
+ {
+ $self->modify_bg('normal', $self->{style}->{activeBg});
+ }
+# $self->savePicture
+# unless $keepPrevious;
+
+ my $pixbuf = $self->createPixbuf($self->{info}, 0, 1.1);
+
+ $pixbuf->saturate_and_pixelate($pixbuf, 1.5, 0);
+ $pixbuf = $pixbuf->composite_color_simple ($pixbuf->get_width, $pixbuf->get_height, 'nearest',220, 128, $self->{style}->{activeBgValue}, $self->{style}->{activeBgValue});
+ $self->child->set_from_pixbuf($pixbuf);
+ }
+
+ sub unhighlight
+ {
+ my ($self) = @_;
+
+ $self->modify_bg('normal', $self->{style}->{inactiveBg})
+ if (! $self->{style}->{withImage});
+ $self->restorePicture;
+ $self->{selected} = 0;
+ }
+
+ sub createPixbuf
+ {
+ my ($self, $info, $cacheRefresh, $zoom) = @_;
+
+ my $displayedImage = $info->{picture};
+ my $pixbuf = undef;
+
+ my $borrower = $info->{borrower};
+ my $favourite = $info->{favourite};
+
+ # Item has a picture assigned
+ if ($cacheRefresh)
+ {
+ $self->{imageCache}->forceCacheUpdateForNextUse;
+ }
+
+ if ($zoom)
+ {
+ if (! exists $self->{zoomedPixbufCache}->{$zoom})
+ {
+ $self->{zoomedPixbufCache}->{$zoom} = $self->{imageCache}->getPixbuf($info, $zoom);
+ }
+ $pixbuf = $self->{zoomedPixbufCache}->{$zoom};
+ }
+ else
+ {
+ $pixbuf = $self->{imageCache}->getPixbuf($info, $zoom);
+ }
+
+ my $width;
+ my $height;
+ my $boxWidth = $self->{style}->{imgWidth};
+ my $boxHeight = $self->{style}->{imgHeight};
+
+ my $overlay;
+ my $imgWidth;
+ my $imgHeight;
+ my $targetOverlayHeight;
+ my $targetOverlayWidth;
+ my $pixbufTempHeight;
+ my $pixbufTempWidth;
+ my $alpha = 1;
+ if ($self->{style}->{useOverlays})
+ {
+ # Need to call this to get the overlay padding
+ ($imgWidth, $imgHeight, $overlay) = $self->{imageCache}->getDestinationImgSize($pixbuf->get_width,
+ $pixbuf->get_height);
+ }
+ $width = $pixbuf->get_width;
+ $height = $pixbuf->get_height;
+
+ # Do the composition
+
+ if ($self->{style}->{useOverlays})
+ {
+ if ($self->{style}->{withImage})
+ {
+ # Using background, so center accordingly
+ my $offsetX = (($self->{style}->{offsetX} / 2) * $self->{style}->{factor}) + (($boxWidth - ($width + $overlay->{paddingLeft} + $overlay->{paddingRight})) / 2);
+ my $offsetY = 15 * $self->{style}->{factor} + ($boxHeight - ($height + $overlay->{paddingTop} + $overlay->{paddingBottom}));
+
+ # Make an empty pixbuf to work within
+ my $tempPixbuf =Gtk2::Gdk::Pixbuf->new('rgb', 1, 8,
+ $self->{style}->{backgroundPixbuf}->get_width,
+ $self->{style}->{backgroundPixbuf}->get_height);
+ $tempPixbuf->fill(0x00000000);
+
+ # Place cover in pixbuf
+ $pixbuf->composite($tempPixbuf,
+ $offsetX + $overlay->{paddingLeft}, $offsetY + $overlay->{paddingTop},
+ $width , $height,
+ $offsetX + $overlay->{paddingLeft}, $offsetY + $overlay->{paddingTop},
+ 1, 1,
+ 'nearest', 255);
+ $pixbuf = $tempPixbuf;
+
+ # Composite overlay picture
+ $self->{style}->{overlayPixbuf}->composite($pixbuf,
+ $offsetX, $offsetY,
+ $width + $overlay->{paddingLeft} + $overlay->{paddingRight},
+ $height + $overlay->{paddingTop} + $overlay->{paddingBottom},
+ $offsetX, $offsetY,
+ ($width + $overlay->{paddingLeft} + $overlay->{paddingRight}) / $self->{style}->{overlayPixbuf}->get_width,
+ ($height + $overlay->{paddingTop} + $overlay->{paddingBottom}) / $self->{style}->{overlayPixbuf}->get_height,
+ 'nearest', 255);
+
+ # Overlay borrower image if required
+ if ($borrower && ($borrower ne 'none'))
+ {
+ # De-saturate borrowed items
+ $pixbuf->saturate_and_pixelate($pixbuf, .1, 0);
+ $self->{style}->{lendPixbuf}->composite($pixbuf,
+ $pixbuf->get_width - $self->{style}->{lendPixbuf}->get_width - $offsetX,
+ $offsetY + $height + $overlay->{paddingTop} + $overlay->{paddingBottom} - $self->{style}->{lendPixbuf}->get_height,
+ $self->{style}->{lendPixbuf}->get_width, $self->{style}->{lendPixbuf}->get_height,
+ $pixbuf->get_width - $self->{style}->{lendPixbuf}->get_width - $offsetX,
+ $offsetY + $height + $overlay->{paddingTop} + $overlay->{paddingBottom} - $self->{style}->{lendPixbuf}->get_height,
+ 1, 1,
+ 'nearest', 255);
+ }
+
+ # Overlay favourite image if required
+ if ($favourite)
+ {
+ $self->{style}->{favPixbuf}->composite($pixbuf,
+ $pixbuf->get_width - $self->{style}->{favPixbuf}->get_width - $offsetX,
+ $offsetY,
+ $self->{style}->{favPixbuf}->get_width, $self->{style}->{favPixbuf}->get_height,
+ $pixbuf->get_width - $self->{style}->{favPixbuf}->get_width - $offsetX,
+ $offsetY,
+ 1, 1,
+ 'nearest', 255);
+ }
+
+ # Create and apply reflection if required
+ if ($self->{style}->{withReflect})
+ {
+ my $reflect;
+ $reflect = $pixbuf->flip(0);
+ $reflect->composite(
+ $pixbuf,
+ 0, 2 * ($offsetY + $height + $overlay->{paddingTop} + $overlay->{paddingBottom}) - $pixbuf->get_height,
+ $pixbuf->get_width,
+ 2 * ($pixbuf->get_height - $height - $offsetY - $overlay->{paddingTop} - $overlay->{paddingBottom}) - (10 * $self->{style}->{factor}),
+ 0, 2 * ($offsetY + $height + $overlay->{paddingTop} + $overlay->{paddingBottom}) - $pixbuf->get_height,
+ 1, 1,
+ 'nearest', 100
+ );
+
+ # Apply foreground fading
+ $self->{style}->{foregroundPixbuf}->composite(
+ $pixbuf,
+ 0, 0,
+ $pixbuf->get_width, $pixbuf->get_height,
+ 0, 0,
+ 1, 1,
+ 'nearest', 255
+ );
+ }
+
+ # Heft created pixbuf onto background
+ my $bgPixbuf = $self->{style}->{backgroundPixbuf}->copy;
+ $pixbuf->composite($bgPixbuf,
+ 0,0,
+ $pixbuf->get_width , $pixbuf->get_height,
+ 0,0,
+ 1, 1,
+ 'nearest', 255);
+ $pixbuf = $bgPixbuf;
+
+ }
+ else
+ {
+ # Not using background, so we need to make an empty pixbuf which is right size for overlay first
+ my $tempPixbuf =Gtk2::Gdk::Pixbuf->new('rgb', 1, 8,
+ $width + $overlay->{paddingLeft} + $overlay->{paddingRight},
+ $height + $overlay->{paddingTop} + $overlay->{paddingBottom});
+ $tempPixbuf->fill(0x00000000);
+
+ # Now, place list image inside empty pixbuf
+ $pixbuf->composite($tempPixbuf,
+ $overlay->{paddingLeft}, $overlay->{paddingTop},
+ $width , $height,
+ $overlay->{paddingLeft}, $overlay->{paddingTop},
+ 1, 1,
+ 'nearest', 255 * $alpha);
+ $pixbuf = $tempPixbuf;
+
+ # Place overlay on top of pixbuf
+ $self->{style}->{overlayPixbuf}->composite($pixbuf,
+ 0, 0,
+ $width + $overlay->{paddingLeft} + $overlay->{paddingRight},
+ $height + $overlay->{paddingTop} + $overlay->{paddingBottom},
+ 0, 0,
+ ($width + $overlay->{paddingLeft} + $overlay->{paddingRight}) / $self->{style}->{overlayPixbuf}->get_width,
+ ($height + $overlay->{paddingTop} + $overlay->{paddingBottom}) / $self->{style}->{overlayPixbuf}->get_height,
+ 'nearest', 255 * $alpha);
+
+ # Overlay borrower image if required
+ if ($borrower && ($borrower ne 'none'))
+ {
+ # De-saturate borrowed items
+ $pixbuf->saturate_and_pixelate($pixbuf, .1, 0);
+
+ $self->{style}->{lendPixbuf}->composite($pixbuf,
+ $pixbuf->get_width - $self->{style}->{lendPixbuf}->get_width,
+ $pixbuf->get_height - $self->{style}->{lendPixbuf}->get_height,
+ $self->{style}->{lendPixbuf}->get_width, $self->{style}->{lendPixbuf}->get_height,
+ $pixbuf->get_width - $self->{style}->{lendPixbuf}->get_width,
+ $pixbuf->get_height - $self->{style}->{lendPixbuf}->get_height,
+ 1, 1,
+ 'nearest', 255);
+ }
+
+ # Overlay favourite image if required
+ if ($favourite)
+ {
+ $self->{style}->{favPixbuf}->composite($pixbuf,
+ $pixbuf->get_width - $self->{style}->{favPixbuf}->get_width,
+ 0,
+ $self->{style}->{favPixbuf}->get_width, $self->{style}->{favPixbuf}->get_height,
+ $pixbuf->get_width - $self->{style}->{favPixbuf}->get_width,
+ 0,
+ 1, 1,
+ 'nearest', 255);
+ }
+
+ }
+ }
+ else
+ {
+ # No overlays, nice and simple
+
+ # Overlay borrower image if required
+ if ($borrower && ($borrower ne 'none'))
+ {
+ # De-saturate borrowed items
+ $pixbuf->saturate_and_pixelate($pixbuf, .1, 0);
+ $self->{style}->{lendPixbuf}->composite($pixbuf,
+ $width - $self->{style}->{lendPixbuf}->get_width - $self->{style}->{factor},
+ $height - $self->{style}->{lendPixbuf}->get_height - $self->{style}->{factor},
+ $self->{style}->{lendPixbuf}->get_width, $self->{style}->{lendPixbuf}->get_height,
+ $width - $self->{style}->{lendPixbuf}->get_width - $self->{style}->{factor},
+ $height - $self->{style}->{lendPixbuf}->get_height - $self->{style}->{factor},
+ 1, 1,
+ 'nearest', 255);
+ }
+
+ # Overlay favourite image if required
+ if ($favourite)
+ {
+ $self->{style}->{favPixbuf}->composite($pixbuf,
+ $width - $self->{style}->{favPixbuf}->get_width - $self->{style}->{factor},
+ $self->{style}->{factor},
+ $self->{style}->{favPixbuf}->get_width, $self->{style}->{favPixbuf}->get_height,
+ $width - $self->{style}->{favPixbuf}->get_width - $self->{style}->{factor},
+ $self->{style}->{factor},
+ 1, 1,
+ 'nearest', 255);
+ }
+
+ my $reflect;
+ $reflect = $pixbuf->flip(0)
+ if $self->{style}->{withReflect};
+
+ my $offsetX = (($self->{style}->{offsetX} / 2) * $self->{style}->{factor}) + (($boxWidth - $width) / 2);
+ my $offsetY = 15 * $self->{style}->{factor} + ($boxHeight - $height);
+ if ($self->{style}->{withImage})
+ {
+ my $bgPixbuf = $self->{style}->{backgroundPixbuf}->copy;
+ $pixbuf->composite($bgPixbuf,
+ $offsetX, $offsetY,
+ $width, $height,
+ $offsetX, $offsetY,
+ 1, 1,
+ 'nearest', 255);
+ $pixbuf = $bgPixbuf;
+ }
+
+ if ($self->{style}->{withReflect})
+ {
+ $reflect->composite(
+ $pixbuf,
+ $offsetX, $height + $offsetY,
+ $width, $pixbuf->get_height - $height - $offsetY - (10 * $self->{style}->{factor}),
+ $offsetX, $height + $offsetY,
+ 1, 1,
+ 'nearest', 100
+ );
+
+ # Apply foreground fading
+ $self->{style}->{foregroundPixbuf}->composite(
+ $pixbuf,
+ 0, 0,
+ $pixbuf->get_width, $pixbuf->get_height,
+ 0, 0,
+ 1, 1,
+ 'nearest', 255
+ );
+ }
+ }
+ return $pixbuf;
+ }
+
+
+
+}
+
+{
+ package GCImageCache;
+
+ use File::Path;
+ use File::Copy;
+ use List::Util qw/min/;
+
+ sub new
+ {
+ my ($proto, $imagesDir, $imageSize, $style, $defaultImage) = @_;
+ my $class = ref($proto) || $proto;
+ my $self = {
+ imagesDir => $imagesDir,
+ imageSize => $imageSize,
+ style => $style,
+ cacheDir => $imagesDir.'/.cache/',
+ oldCacheDir => $imagesDir,
+ defaultImage => $defaultImage,
+ forceUpdate => 0,
+ };
+ # Make sure destination directory exists
+ if ( ! -d $self->{cacheDir})
+ {
+ mkpath $self->{cacheDir};
+ }
+ bless ($self, $class);
+
+ $self->clearOldCache;
+
+ return $self;
+ }
+
+ # This method removes images cached by previous versions
+ sub clearOldCache
+ {
+ my $self = shift;
+ my $trashDir = $self->{imagesDir}.'.trash';
+ mkpath $trashDir;
+ foreach (glob $self->{oldCacheDir}.'/*')
+ {
+ if (/\.cache\.[0-4](\.|$)/)
+ {
+ move $_, $trashDir;
+ }
+ }
+ }
+
+ sub forceCacheUpdateForNextUse
+ {
+ my ($self) = @_;
+ $self->{forceUpdate} = 1;
+ }
+
+ sub getPixbuf
+ {
+ my ($self, $info, $zoom) = @_;
+ my $fileName;
+ my $pixbuf = undef;
+ if (!$zoom)
+ {
+ $fileName = $self->getCachedFileName($info);
+ if ($self->{forceUpdate} || (! -e $fileName))
+ {
+ $self->createImageCache($info);
+ }
+ $self->{forceUpdate} = 0;
+ eval {
+ $pixbuf = Gtk2::Gdk::Pixbuf->new_from_file($fileName);
+ };
+ }
+ else
+ {
+ # When a zoom is requested, we have to generate the picture
+ $fileName = $self->getCachedFileName($info);
+ # Get picture size from cached file to avoid re-computing everything
+ my ($picFormat, $picWidth, $picHeight) = Gtk2::Gdk::Pixbuf->get_file_info($fileName);
+ # Then open the original file
+ my $origFileName = $info->{picture};
+ if (! -f $origFileName)
+ {
+ $origFileName = $self->{defaultImage};
+ }
+ if (!$self->{style}->{useOverlays})
+ {
+ $zoom -= 0.01;
+ }
+
+ eval {
+ $pixbuf = Gtk2::Gdk::Pixbuf->new_from_file($origFileName);
+ my $newWidth = int($picWidth * $zoom);
+ my $newHeight = int($picHeight * $zoom);
+ $pixbuf = GCUtils::scaleMaxPixbuf($pixbuf, $newWidth, $newHeight, 1, 0);
+ };
+ }
+
+ return $pixbuf;
+ }
+
+ sub getCachedFileName
+ {
+ my ($self, $info, $size) = @_;
+
+ my $gcsautoid = $info->{autoid};
+ my $title = $info->{title};
+
+ $title =~ s/[^a-zA-Z0-9]*//g;
+ my $cacheFilename = $self->{cacheDir};
+ if ($info->{picture})
+ {
+ $cacheFilename .= $gcsautoid
+ ."."
+ .$title;
+ }
+ else
+ {
+ $cacheFilename .= 'GCSDefaultImage';
+ }
+ $cacheFilename .= (defined $size ? $size : $self->{imageSize});
+ $cacheFilename .= ".overlay"
+ if $self->{style}->{useOverlays};
+
+ return $cacheFilename;
+ }
+
+ # Resizes artwork to required sizes and saves copies of the images, for fast loading
+ sub createImageCache
+ {
+ my ($self, $info) = @_;
+
+ my $srcImage = $info->{picture};
+ if (! -f $srcImage)
+ {
+ $srcImage = $self->{defaultImage};
+ $info->{picture} = "";
+ }
+
+ # Load in the original source image
+ my $origPixbuf = Gtk2::Gdk::Pixbuf->new_from_file($srcImage);
+
+ my $gcsautoid = $info->{autoid};
+ my $title = $info->{title};
+ $title =~ s/[^a-zA-Z0-9]*//g;
+ # Get original picture format
+ my ($picFormat, $picWidth, $picHeight) = Gtk2::Gdk::Pixbuf->get_file_info($srcImage);
+
+ # Loop through possible sizes
+ for (my $size = 0; $size < 5; $size++) {
+ my $imgWidth;
+ my $imgHeight;
+ my $overlay;
+
+ my $cacheFilename = $self->getCachedFileName($info, $size);
+
+ # Get size for cached image
+ ($imgWidth, $imgHeight, $overlay) = $self->getDestinationImgSize($picWidth,
+ $picHeight,
+ $size);
+
+ # Scale pixbuf and save
+ my $scaledPixbuf = GCUtils::scaleMaxPixbuf($origPixbuf, $imgWidth, $imgHeight, 0, 0);
+ if ($picFormat->{name} eq 'jpeg')
+ {
+ $scaledPixbuf->save ($cacheFilename, 'jpeg', quality => '99');
+ }
+ else
+ {
+ $scaledPixbuf->save ($cacheFilename, 'png');
+ }
+ }
+ }
+
+ # Calculates height and width of list image
+ sub getDestinationImgSize
+ {
+ my ($self, $origWidth, $origHeight, $size) = @_;
+
+ $size = $self->{imageSize}
+ if (!defined $size);
+
+ my $imgWidth;
+ my $imgHeight;
+ my $overlay;
+
+ # No overlays
+ $imgWidth = $self->{style}->{imgWidth} / $self->{style}->{factor};
+ $imgHeight = $self->{style}->{imgHeight} / $self->{style}->{factor};
+
+ if ($self->{style}->{useOverlays})
+ {
+ # Overlays
+
+ # Calculate size of list image with proportional size of overlay padding added
+ my $pixbufTempHeight = (($self->{style}->{overlayPaddingTop} + $self->{style}->{overlayPaddingBottom})/$self->{style}->{overlayPixbuf}->get_height + 1) * $origHeight;
+ my $pixbufTempWidth = (($self->{style}->{overlayPaddingLeft} + $self->{style}->{overlayPaddingRight})/$self->{style}->{overlayPixbuf}->get_width + 1) * $origWidth;
+
+ # Find out target size of overlay, keeping the same ratio as the size calculated above (ie, list image + relative padding)
+ my $ratio = $pixbufTempHeight / $pixbufTempWidth;
+ my $targetOverlayHeight;
+ my $targetOverlayWidth;
+ if (($pixbufTempWidth > $imgWidth) || ($pixbufTempHeight > $imgHeight))
+ {
+ if (($pixbufTempWidth * $imgHeight/$pixbufTempHeight) < $imgHeight )
+ {
+ $targetOverlayHeight = $imgHeight;
+ $targetOverlayWidth = int($imgHeight / $ratio);
+ }
+ else
+ {
+ $targetOverlayHeight = int( $imgWidth * $ratio);
+ $targetOverlayWidth = $imgWidth;
+ }
+ }
+ else
+ {
+ # Special case when image is small enough and doesn't need to be resized
+ $targetOverlayHeight = $pixbufTempHeight;
+ $targetOverlayWidth = $pixbufTempWidth;
+ }
+
+ # Calculate final offset amounts for target size of overlay
+ $overlay->{paddingLeft} = int($self->{style}->{overlayPaddingLeft} * $targetOverlayWidth / $self->{style}->{overlayPixbuf}->get_width);
+ $overlay->{paddingRight} = int($self->{style}->{overlayPaddingRight} * $targetOverlayWidth / $self->{style}->{overlayPixbuf}->get_width);
+ $overlay->{paddingTop} = int($self->{style}->{overlayPaddingTop} * $targetOverlayHeight / $self->{style}->{overlayPixbuf}->get_height);
+ $overlay->{paddingBottom} = int($self->{style}->{overlayPaddingBottom} * $targetOverlayHeight / $self->{style}->{overlayPixbuf}->get_height);
+
+ $imgWidth = $imgWidth - $overlay->{paddingLeft} - $overlay->{paddingRight};
+ $imgHeight = $imgHeight - $overlay->{paddingTop} - $overlay->{paddingBottom};
+ }
+
+ my $factor = ($size == 0) ? 0.5
+ : ($size == 1) ? 0.8
+ : ($size == 3) ? 1.5
+ : ($size == 4) ? 2
+ : 1;
+ $imgWidth *= $factor;
+ $imgHeight *= $factor;
+
+ return ($imgWidth, $imgHeight, $overlay);
+ }
+
+}
+
+1;
diff --git a/lib/gcstar/GCItemsLists/GCImageLists.pm b/lib/gcstar/GCItemsLists/GCImageLists.pm
new file mode 100644
index 0000000..2250bcb
--- /dev/null
+++ b/lib/gcstar/GCItemsLists/GCImageLists.pm
@@ -0,0 +1,2028 @@
+package GCImageLists;
+
+###################################################
+#
+# Copyright 2005-2010 Christian Jodar
+#
+# This file is part of GCstar.
+#
+# GCstar is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# GCstar is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GCstar; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+#
+###################################################
+
+use strict;
+use locale;
+
+# Number of ms to wait before enhancing the next picture
+my $timeOutBetweenEnhancements = 50;
+
+{
+ package GCBaseImageList;
+
+ use File::Basename;
+ use GCItemsLists::GCImageListComponents;
+ use GCUtils;
+ use GCStyle;
+ use base "Gtk2::VBox";
+ use File::Temp qw/ tempfile /;
+
+ sub new
+ {
+ my ($proto, $container, $columns) = @_;
+ my $class = ref($proto) || $proto;
+ my $self = $class->SUPER::new(0,0);
+ bless ($self, $class);
+
+ my $parent = $container->{parent};
+
+ $self->{preferences} = $parent->{model}->{preferences};
+ $self->{imagesDir} = $parent->getImagesDir();
+ $self->{coverField} = $parent->{model}->{commonFields}->{cover};
+ $self->{titleField} = $parent->{model}->{commonFields}->{title};
+ $self->{idField} = $parent->{model}->{commonFields}->{id};
+ $self->{borrowerField} = $parent->{model}->{commonFields}->{borrower}->{name};
+ # Sort field
+ $self->{sortField} = $self->{preferences}->secondarySort
+ || $self->{titleField};
+ $self->{fileIdx} = "";
+ $self->{selectedIndexes} = {};
+ $self->{previousSelectedDisplayed} = 0;
+ $self->{displayedToItemsArray} = {};
+ $self->{container} = $container;
+ $self->{scroll} = $container->{scroll};
+ $self->{searchEntry} = $container->{searchEntry};
+
+
+ $self->{preferences}->sortOrder(1)
+ if ! $self->{preferences}->exists('sortOrder');
+
+ $self->{parent} = $container->{parent};
+
+ $self->{tooltips} = Gtk2::Tooltips->new();
+
+ $self->{columns} = $columns;
+ $self->{dynamicSize} = ($columns == 0);
+ $self->clearCache;
+
+
+ $self->set_border_width(0);
+
+ $self->signal_connect('button_press_event' => sub {
+ my ($widget, $event) = @_;
+ if ($event->button eq 3)
+ {
+ $self->{parent}->{context}->popup(undef, undef, undef, undef, $event->button, $event->time)
+ }
+ });
+
+ $self->can_focus(1);
+
+ $self->{imageCache} = new GCImageCache($self->{imagesDir},
+ $self->{preferences}->listImgSize,
+ $container->{style},
+ $self->{parent}->{defaultImage});
+
+ return $self;
+ }
+
+ sub couldExpandAll
+ {
+ my $self = shift;
+
+ return 0;
+ }
+
+ sub getCurrentIdx
+ {
+ my $self = shift;
+ return $self->{displayedToIdx}->{$self->{current}};
+ }
+
+ sub getCurrentItems
+ {
+ my $self = shift;
+ my @indexes = keys %{$self->{selectedIndexes}};
+ return \@indexes;
+ }
+
+ sub isSelected
+ {
+ my ($self, $idx) = @_;
+ foreach (keys %{$self->{selectedIndexes}})
+ {
+ return 1 if $_ == $idx;
+ }
+ return 0;
+ }
+
+ sub DESTROY
+ {
+ my $self = shift;
+
+ #unlink $self->{style}->{tmpBgPixmap};
+ $self->SUPER::DESTROY;
+ }
+
+ sub isUsingDate
+ {
+ my ($self) = @_;
+ return 0;
+ }
+
+ sub setSortOrder
+ {
+ my ($self, $order) = @_;
+ $order = 0 if !defined $order;
+ $self->{currentOrder} = ($order == -1) ? (1 - $self->{currentOrder})
+ : $self->{preferences}->sortOrder;
+
+ if ($self->{itemsArray})
+ {
+ if ($order == -1)
+ {
+ @{$self->{itemsArray}} = reverse @{$self->{itemsArray}};
+ }
+ else
+ {
+ sub compare
+ {
+ return (
+ GCUtils::gccmpe($a->{sortValue}, $b->{sortValue})
+ );
+ }
+ if ($self->{currentOrder} == 1)
+ {
+ @{$self->{itemsArray}} = sort compare @{$self->{itemsArray}};
+ }
+ else
+ {
+ @{$self->{itemsArray}} = reverse sort compare @{$self->{itemsArray}};
+ }
+ }
+ }
+ $self->refresh if ! $self->{initializing};
+ $self->{initializing} = 0;
+ }
+
+ sub setFilter
+ {
+ my ($self, $filter, $items, $refresh, $splash) = @_;
+ $self->{displayedNumber} = 0;
+ $self->{filter} = $filter;
+ $self->{displayedToItemsArray} = {};
+ my $current = $self->{current};
+ $self->restorePrevious;
+ my $i = 0;
+ foreach (@{$self->{itemsArray}})
+ {
+ $_->{displayed} = $filter->test($items->[$_->{idx}]);
+ if ($_->{displayed})
+ {
+ $self->{displayedToItemsArray}->{$self->{displayedNumber}} = $i;
+ $self->{displayedNumber}++;
+ }
+ $self->{container}->setDisplayed($_->{idx}, $_->{displayed});
+ $i++;
+ }
+ my $newIdx = $self->getFirstVisibleIdx($current);
+ my $conversionNeeded = 0;
+ $conversionNeeded = 1 if ! exists $self->{boxes}->[$current];
+
+ if ($refresh)
+ {
+ $self->refresh(1, $splash);
+ $self->show_all;
+ }
+
+ $self->{initializing} = 0;
+ return $self->displayedToItemsArrayIdx($newIdx)
+ if $conversionNeeded;
+ return $newIdx;
+ }
+
+ sub getFirstVisibleIdx
+ {
+ my ($self, $displayed) = @_;
+ return $displayed if ! exists $self->{boxes}->[$displayed];
+ my $currentIdx = $self->{boxes}->[$displayed]->{info}->{idx};
+ my $info = $self->{boxes}->[$displayed]->{info};
+
+ return $currentIdx if (! exists $self->{boxes}->[$displayed])
+ || ($self->{boxes}->[$displayed]->{info}->{displayed});
+ my $previous = -1;
+ my $after = 0;
+ foreach my $item(@{$self->{itemsArray}})
+ {
+ $after = 1 if $item->{idx} == $currentIdx;
+ if ($after)
+ {
+ return $item->{idx} if $item->{displayed};
+ }
+ else
+ {
+ $previous = $item->{idx} if $item->{displayed};
+ }
+ }
+ return $previous;
+ }
+
+ sub refresh
+ {
+ my ($self, $forceClear, $splash) = @_;
+ return if $self->{columns} == 0;
+
+ # Store current item index
+ my $currentIdx = $self->{displayedToIdx}->{$self->{current}};
+ $self->{boxes} = [];
+ $self->{displayedToIdx} = {};
+ $self->{idxToDisplayed} = {};
+
+ $self->clearView if (! $self->{initializing}) || $forceClear;
+ $self->{number} = 0;
+ my $idx = 0;
+ $self->{collectionDir} = dirname($self->{parent}->{options}->file);
+ foreach (@{$self->{itemsArray}})
+ {
+ $splash->setProgressForItemsSort($idx++) if $splash;
+ next if ! $_->{displayed};
+ $self->addDisplayedItem($_);
+ }
+ delete $self->{collectionDir};
+ # Determine new current displayed
+ $self->{current} = $self->{idxToDisplayed}->{$currentIdx};
+ if ($self->{toBeSelectedLater})
+ {
+ $self->{parent}->display($self->select(-1));
+ $self->{toBeSelectedLater} = 0;
+ }
+ #$self->show_all;
+ }
+
+ sub getNbItems
+ {
+ my $self = shift;
+ return $self->{displayedNumber};
+ }
+
+ sub clearCache
+ {
+ my $self = shift;
+
+ if ($self->{cache})
+ {
+ foreach (@{$self->{cache}})
+ {
+ $_->{imageBox}->destroy
+ if $_->{imageBox};
+ }
+ }
+ $self->{cache} = [];
+ }
+
+ sub reset
+ {
+ my $self = shift;
+ #Restore current picture if modified
+ $self->restorePrevious;
+
+ $self->{itemsArray} = [];
+ $self->{boxes} = [];
+ $self->{number} = 0;
+ $self->{count} = 0;
+ $self->{displayedNumber} = 0;
+ $self->{current} = 0;
+ $self->{previous} = 0;
+ $self->clearView;
+ }
+
+ sub clearView
+ {
+ my $self = shift;
+
+ # TODO : This will be different with many lists
+ #my $parent = $self->parent;
+ #$self->parent->remove($self)
+ # if $parent;
+
+ my @children = $self->get_children;
+ foreach (@children)
+ {
+ my @children2 = $_->get_children;
+ foreach my $child(@children2)
+ {
+ $_->remove($child);
+ }
+ $self->remove($_);
+ $_->destroy;
+ }
+ $self->{rowContainers} = [];
+ $self->{enhanceInformation} = [];
+
+ # TODO : This will be different with many lists
+ #$self->{scroll}->add_with_viewport($self)
+ # if $parent;
+
+ $self->{initializing} = 1;
+ }
+
+ sub done
+ {
+ my ($self, $splash, $refresh) = @_;
+ if ($refresh)
+ {
+ $self->refresh(0, $splash);
+ }
+ $self->{initializing} = 0;
+ }
+
+ sub setColumnsNumber
+ {
+ my ($self, $columns, $refresh) = @_;
+ $self->{columns} = $columns;
+ my $init = $self->{initializing};
+ $self->{initializing} = 1;
+ $self->refresh($refresh) if $refresh;
+ $self->show_all;
+ $self->{initializing} = $init;
+ }
+
+ sub getColumnsNumber
+ {
+ my $self = shift;
+ return $self->{columns};
+ }
+
+ sub createImageBox
+ {
+ my ($self, $info) = @_;
+
+ my $imageBox = new GCImageListItem($self, $info);
+ return $imageBox;
+ }
+
+ sub getFromCache
+ {
+ my ($self, $info) = @_;
+ if (! $self->{cache}->[$info->{idx}])
+ {
+ my $item = {};
+ $item->{imageBox} = $self->createImageBox($info);
+ $self->{cache}->[$info->{idx}] = $item;
+ }
+ return $self->{cache}->[$info->{idx}];
+ }
+
+ sub findPlace
+ {
+ my ($self, $item, $sortvalue) = @_;
+ my $refSortValue = $sortvalue || $item->{sortValue};
+ $refSortValue = uc($refSortValue);
+
+ # First search where it should be inserted
+ my $place = 0;
+ my $itemsIdx = 0;
+ if ($self->{currentOrder} == 1)
+ {
+ foreach my $followingItem(@{$self->{itemsArray}})
+ {
+ my $testSortValue = uc($followingItem->{sortValue});
+ my $cmp = GCUtils::gccmpe($testSortValue, $refSortValue);
+ $itemsIdx++ if ! ($cmp > 0);
+
+ next if !$followingItem->{displayed};
+ last if ($cmp > 0);
+ $place++;
+ }
+ }
+ else
+ {
+ foreach my $followingItem(@{$self->{itemsArray}})
+ {
+ my $testSortValue = uc($followingItem->{sortValue});
+ my $cmp = GCUtils::gccmpe($refSortValue, $testSortValue);
+ $itemsIdx++ if ! ($cmp > 0);
+ next if !$followingItem->{displayed};
+ last if ($cmp > 0);
+ $place++;
+ }
+ }
+ return ($place, $itemsIdx) if wantarray;
+ return $place;
+ }
+
+ sub createItemInfo
+ {
+ my ($self, $idx, $info) = @_;
+ my $displayedImage = GCUtils::getDisplayedImage($info->{$self->{coverField}},
+ undef,
+ $self->{parent}->{options}->file,
+ $self->{collectionDir});
+ my $item = {
+ idx => $idx,
+ title => $self->{parent}->transformTitle($info->{$self->{titleField}}),
+ picture => $displayedImage,
+ borrower => $info->{$self->{borrowerField}},
+ sortValue => $self->{sortField} eq $self->{titleField}
+ ? $self->{parent}->transformTitle($info->{$self->{titleField}})
+ : $info->{$self->{sortField}},
+ favourite => $info->{favourite},
+ displayed => 1,
+ autoid => $info->{$self->{idField}}
+ };
+ return $item;
+ }
+
+ sub addItem
+ {
+ my ($self, $info, $immediate, $idx, $keepConversionTables) = @_;
+
+ my $item = $self->createItemInfo($idx, $info);
+
+ if ($immediate)
+ {
+ # When the flag is set, that means we modified an item and that it had
+ # to be added to that group. In this case, we don't want to de-select
+ # the current one.
+ if (!$keepConversionTables)
+ {
+ $self->restorePrevious;
+ # To force the selection
+ $self->{current} = -1;
+ }
+ # First search where it should be inserted
+ my ($place, $itemsArrayIdx) = $self->findPlace($item);
+ # Prepare the conversions displayed <-> index
+ if (!$keepConversionTables)
+ {
+ $self->{displayedToIdx}->{$place} = $idx;
+ $self->{idxToDisplayed}->{$idx} = $place;
+ }
+ # Then we insert it at correct position
+ $self->addDisplayedItem($item, $place);
+ splice @{$self->{itemsArray}}, $itemsArrayIdx, 0, $item;
+ }
+ else
+ {
+ # Here we know it will be sorted after
+ push @{$self->{itemsArray}}, $item;
+ }
+
+ $self->{count}++;
+ $self->{displayedNumber}++;
+ $self->{header}->show_all if $self->{header} && $self->{displayedNumber} > 0;
+ }
+
+ # Params:
+ # $info: Information already formated for this class
+ # $place: Optional value to indicate where it should be inserted
+ sub addDisplayedItem
+ {
+ # info is an iternal info generated
+ my ($self, $info, $place) = @_;
+ return if ! $self->{columns};
+ my $item = $self->getFromCache($info);
+ my $imageBox = $item->{imageBox};
+ my $i = $info->{idx};
+ if (!defined $place)
+ {
+ $self->{displayedToIdx}->{$self->{number}} = $i;
+ $self->{idxToDisplayed}->{$i} = $self->{number};
+ }
+ $imageBox->prepareHandlers($i, $info);
+
+ if (($self->{number} % $self->{columns}) == 0)
+ {
+ #New row begin
+ $self->{currentRow} = new Gtk2::HBox(0,0);
+ push @{$self->{rowContainers}}, $self->{currentRow};
+ $self->pack_start($self->{currentRow},0,0,0);
+ $self->{currentRow}->show_all if ! $self->{initializing};
+ }
+
+ if (defined($place))
+ {
+ # Get the row and col where it should be inserted
+ my $itemLine = int $place / $self->{columns};
+ my $itemCol = $place % $self->{columns};
+ # Insert it at correct place
+ $self->{rowContainers}->[$itemLine]->pack_start($imageBox,0,0,0);
+ $self->{rowContainers}->[$itemLine]->reorder_child($imageBox, $itemCol);
+ $imageBox->show_all;
+ $self->shiftItems($place, 1, 0, scalar @{$self->{boxes}});
+ splice @{$self->{boxes}}, $place, 0, $imageBox;
+ $self->initConversionTables;
+ }
+ else
+ {
+ $self->{currentRow}->pack_start($imageBox,0,0,0);
+ $self->{idxToDisplayed}->{$i} = $self->{number};
+ push @{$self->{boxes}}, $imageBox;
+ }
+
+ $self->{number}++;
+ }
+
+ sub grab_focus
+ {
+ my $self = shift;
+ $self->SUPER::grab_focus;
+ $self->{boxes}->[$self->{current}]->grab_focus;
+ }
+
+ sub displayedToItemsArrayIdx
+ {
+ my ($self, $displayed) = @_;
+ return 0 if ! exists $self->{boxes}->[$displayed];
+ # If we have nothing, that means we have no filter. So displayed and idx are the same
+ return $displayed if ! exists $self->{displayedToItemsArray}->{$displayed};
+ return $self->{displayedToItemsArray}->{$displayed};
+ }
+
+ sub shiftItems
+ {
+ my ($self, $place, $direction, $justFromView, $maxPlace) = @_;
+ my $idx = $self->{displayedToIdx}->{$place};
+ my $itemLine = int $place / $self->{columns};
+ my $itemCol = $place % $self->{columns};
+ # Did we already remove or add the item ?
+ my $alreadyChanged = ($direction < 0) || (defined $maxPlace);
+ # Useful to always have the same comparison a few lines below
+ # Should be >= for $direction == 1
+ # This difference is because we didn't added it yet while it has
+ # already been removed in the other direction
+ #$itemCol-- if ! (defined $maxPlace);
+ $itemCol++ if ($direction < 0);
+ # Same here
+ $idx-- if $alreadyChanged;
+ my $newDisplayed = 0;
+ my $currentLine = 0;
+ my $currentCol;
+ my $shifting = 0;
+ # Limit indicates which value for column should make use take action
+ # For backward, it's the 1st one. For forward, the last one
+ my $limit = 0;
+ $limit = ($self->{columns} - 1) if $direction > 0;
+ foreach my $item(@{$self->{itemsArray}})
+ {
+ if (!$item->{displayed})
+ {
+ $item->{idx} += $direction if ((!defined $maxPlace) && ($item->{idx} > $idx));
+ next;
+ }
+ $currentLine = int $newDisplayed / $self->{columns};
+ $currentCol = $newDisplayed % $self->{columns};
+ $shifting = $direction if (!$shifting)
+ && (
+ ($currentLine > $itemLine)
+ || (($currentLine == $itemLine)
+ && ($currentCol >= $itemCol))
+ );
+ $shifting = 0 if (defined $maxPlace) && ($newDisplayed > $maxPlace);
+ # When using maxPlace, we are only moving in view
+ if ((!defined $maxPlace) && ($item->{idx} > $idx))
+ {
+ $item->{idx} += $direction;
+ $self->{cache}->[$item->{idx}]->{imageBox}->{idx} = $item->{idx}
+ if ($item->{idx} > 0) && $self->{cache}->[$item->{idx}];
+ }
+ if ($shifting)
+ {
+ # Is this the first/last one in the line?
+ if ($currentCol == $limit)
+ {
+ $self->{rowContainers}->[$currentLine]->remove(
+ $self->{cache}->[$item->{idx}]->{imageBox}
+ );
+ $self->{rowContainers}->[$currentLine + $direction]->pack_start(
+ $self->{cache}->[$item->{idx}]->{imageBox},
+ 0,0,0
+ );
+ # We can't directly insert on the beginning.
+ # So we need a little adjustement here
+ if ($direction > 0)
+ {
+ $self->{rowContainers}->[$currentLine + $direction]->reorder_child(
+ $self->{cache}->[$item->{idx}]->{imageBox},
+ 0
+ );
+ }
+ }
+ }
+ $newDisplayed++;
+ }
+ }
+
+ sub shiftIndexes
+ {
+ my ($self, $indexes) = @_;
+ my $nbIndexes = scalar @$indexes;
+ my $nbLower;
+ my $currentIdx;
+ my @cache;
+ foreach my $box(@{$self->{boxes}})
+ {
+ # Find how many are lowers in our indexes
+ # We suppose they are sorted
+ $nbLower = 0;
+ $currentIdx = $box->{info}->{idx};
+ foreach (@$indexes)
+ {
+ last if $_ > $currentIdx;
+ $nbLower++;
+ }
+ $box->{info}->{idx} -= $nbLower;
+ $cache[$box->{info}->{idx}] = $self->{cache}->[$box->{info}->{idx} + $nbLower];
+ }
+ $self->{cache} = \@cache;
+ }
+
+ sub initConversionTables
+ {
+ my $self = shift;
+ my $displayed = 0;
+ $self->{displayedToIdx} = {};
+ $self->{idxToDisplayed} = {};
+ foreach (@{$self->{boxes}})
+ {
+ $self->{displayedToIdx}->{$displayed} = $_->{info}->{idx};
+ $self->{idxToDisplayed}->{$_->{info}->{idx}} = $displayed;
+ $_->{idx} = $_->{info}->{idx};
+ $displayed++;
+ }
+ }
+
+ sub convertIdxToDisplayed
+ {
+ my ($self, $idx) = @_;
+ return $self->{idxToDisplayed}->{$idx};
+ }
+
+ sub convertDisplayedToIdx
+ {
+ my ($self, $displayed) = @_;
+ return $self->{displayedToIdx}->{$displayed};
+ }
+
+ sub removeItem
+ {
+ my ($self, $idx, $justFromView) = @_;
+ $self->{count}--;
+ $self->{displayedNumber}--;
+ # Fix to remove header only when items are grouped
+ $self->{header}->hide if $self->{container}->{groupItems} && $self->{displayedNumber} <= 0;
+ my $displayed = $self->{idxToDisplayed}->{$idx};
+ my $itemLine = int $displayed / $self->{columns};
+ #my $itemCol = $displayed % $self->{columns};
+ $self->{rowContainers}->[$itemLine]->remove(
+ $self->{cache}->[$idx]->{imageBox}
+ );
+
+ # Remove event box from cache
+ my $itemsArrayIdx = $self->displayedToItemsArrayIdx($displayed);
+
+ $self->{cache}->[$idx]->{imageBox}->destroy;
+ $self->{cache}->[$idx]->{imageBox} = 0;
+
+ splice @{$self->{cache}}, $idx, 1 if !$justFromView;
+ splice @{$self->{boxes}}, $self->{idxToDisplayed}->{$idx}, 1;
+
+ if ($justFromView)
+ {
+ $self->shiftItems($displayed, -1, 0, scalar @{$self->{boxes}});
+ }
+ else
+ {
+ $self->shiftItems($displayed, -1);
+ }
+ $self->initConversionTables;
+
+ splice @{$self->{itemsArray}}, $itemsArrayIdx, 1;
+ my $next = $self->{displayedToIdx}->{$displayed};
+ if ($displayed >= (scalar(@{$self->{boxes}})))
+ {
+ $next = $self->{displayedToIdx}->{--$displayed}
+ }
+ $self->{current} = $displayed;
+
+ my $last = scalar @{$self->{itemsArray}};
+ delete $self->{displayedToIdx}->{$last};
+ # To be sure we still have consistent data, we re-initialize the other hash by swapping keys and values.
+ $self->{idxToDisplayed} = {};
+ my ($k,$v);
+ $self->{idxToDisplayed}->{$v} = $k while (($k,$v) = each %{$self->{displayedToIdx}});
+
+ # Fix to remove items from "displayed" list on delete
+ my $numDisplayed = scalar(keys %{$self->{container}->{displayed}});
+ delete $self->{container}->{displayed}->{$numDisplayed-1};
+
+ $self->{number}--;
+ return $next;
+ }
+
+ sub removeCurrentItems
+ {
+ my ($self) = @_;
+ my @indexes = sort {$a <=> $b} keys %{$self->{selectedIndexes}};
+ my $nbRemoved = 0;
+ $self->restorePrevious;
+ my $next;
+ foreach my $idx(@indexes)
+ {
+ $next = $self->removeItem($idx - $nbRemoved);
+ $nbRemoved++;
+ }
+ $self->{selectedIndexes} = {};
+ $self->select($next, 1);
+
+ return $next;
+ }
+
+ sub restoreItem
+ {
+ my ($self, $idx) = @_;
+
+ my $previous = $self->{idxToDisplayed}->{$idx};
+ next if ($previous == -1) || (!defined $previous) || (!$self->{boxes}->[$previous]);
+
+ $self->{boxes}->[$previous]->unhighlight;
+ delete $self->{selectedIndexes}->{$idx};
+ }
+
+ sub restorePrevious
+ {
+ my ($self, $fromContainer) = @_;
+ foreach my $idx(keys %{$self->{selectedIndexes}})
+ {
+ $self->restoreItem($idx);
+ }
+ $self->{container}->clearSelected($self) if !$fromContainer;
+ }
+
+ sub selectAll
+ {
+ my $self = shift;
+
+ $self->restorePrevious;
+ $self->select($self->{displayedToIdx}->{0}, 1, 0);
+ foreach my $displayed(1..scalar(@{$self->{boxes}}) - 1)
+ {
+ $self->select($self->{displayedToIdx}->{$displayed}, 0, 1);
+ }
+ $self->{parent}->display(keys %{$self->{selectedIndexes}});
+ }
+
+ sub selectMany
+ {
+ my ($self, $lastSelected) = @_;
+
+ my ($min, $max);
+ if ($self->{previousSelectedDisplayed} > $self->{idxToDisplayed}->{$lastSelected})
+ {
+ $min = $self->{idxToDisplayed}->{$lastSelected};
+ $max = $self->{previousSelectedDisplayed};
+ }
+ else
+ {
+ $min = $self->{previousSelectedDisplayed};
+ $max = $self->{idxToDisplayed}->{$lastSelected};
+ }
+ foreach my $displayed($min..$max)
+ {
+ $self->select($self->{displayedToIdx}->{$displayed}, 0, 1);
+ }
+
+ }
+
+ sub select
+ {
+ my ($self, $idx, $init, $keepPrevious) = @_;
+ $self->{container}->setCurrentList($self);
+ $idx = $self->{displayedToIdx}->{0} if $idx == -1;
+ my $displayed = $self->{idxToDisplayed}->{$idx};
+ if (! $self->{columns})
+ {
+ $self->{toBeSelectedLater} = 1;
+ return $idx;
+ }
+ my @boxes = @{$self->{boxes}};
+
+ return $idx if ! scalar(@boxes);
+ my $alreadySelected = 0;
+ $alreadySelected = $boxes[$displayed]->{selected}
+ if exists $boxes[$displayed];
+ my $nbSelected = scalar keys %{$self->{selectedIndexes}};
+
+ return $idx if $alreadySelected && ($nbSelected < 2) && (!$init);
+ if ($keepPrevious)
+ {
+ if (($alreadySelected) && ($nbSelected > 1))
+ {
+
+ $self->restoreItem($idx);
+ # Special case where user has deselect items, so now only one item is left selected
+ # and menus need to be updated to reflect that
+ $self->updateMenus(1)
+ if $nbSelected <= 2;
+
+ return $idx;
+ }
+ $self->{selectedIndexes}->{$idx} = 1;
+ }
+ else
+ {
+ $self->restorePrevious;
+ $self->{selectedIndexes} = {$idx => 1};
+ }
+
+ $self->{current} = $displayed;
+
+ $boxes[$displayed]->highlight
+ if exists $boxes[$displayed];
+
+ $self->grab_focus;
+ $self->{container}->setCurrentList($self)
+ if $self->{container};
+
+ # Update menu items to reflect number of items selected
+ $self->updateMenus(scalar keys %{$self->{selectedIndexes}});
+ return $idx;
+ }
+
+ sub displayDetails
+ {
+ my ($self, $createWindow, @idx) = @_;
+ if ($createWindow)
+ {
+ $self->{parent}->displayInWindow($idx[0]);
+ }
+ else
+ {
+ $self->{parent}->display(@idx);
+ }
+ }
+
+ sub showPopupMenu
+ {
+ my ($self, $button, $time) = @_;
+
+ $self->{parent}->{context}->popup(undef, undef, undef, undef, $button, $time);
+ }
+
+ sub setPreviousSelectedDisplayed
+ {
+ my ($self, $idx) = @_;
+ $self->{previousSelectedDisplayed} = $self->{idxToDisplayed}->{$idx}
+ if !exists $self->{previousSelectedDisplayed};
+ }
+
+ sub unsetPreviousSelectedDisplayed
+ {
+ my ($self, $idx) = @_;
+ delete $self->{previousSelectedDisplayed};
+ }
+
+ sub updateMenus
+ {
+ # Update menu items to reflect number of items selected
+ my ($self, $nbSelected) = @_;
+ foreach (
+ # Menu labels
+ [$self->{parent}->{menubar}, 'duplicateItem', 'MenuDuplicate'],
+ [$self->{parent}->{menubar}, 'deleteCurrentItem', 'MenuEditDeleteCurrent'],
+ # Context menu labels
+ [$self->{parent}, 'contextNewWindow', 'MenuNewWindow'],
+ [$self->{parent}, 'contextDuplicateItem', 'MenuDuplicate'],
+ [$self->{parent}, 'contextItemDelete', 'MenuEditDeleteCurrent'],
+ )
+ {
+ $self->{parent}->{menubar}->updateItem(
+ $_->[0]->{$_->[1]},
+ $_->[2].(($nbSelected > 1) ? 'Plural' : ''));
+ }
+ }
+
+ sub setHeader
+ {
+ my ($self, $header) = @_;
+ $self->{header} = $header;
+ }
+
+ sub showCurrent
+ {
+ my $self = shift;
+ return if ! $self->{columns};
+ if ($self->{initializing})
+ {
+ Glib::Timeout->add(100 ,\&showCurrent, $self);
+ return;
+ }
+
+ my $adj = $self->{scroll}->get_vadjustment;
+ my $totalRows = int $self->{number} / $self->{columns};
+ my $row = (int $self->{current} / $self->{columns});
+
+ my $ypos = 0;
+ if ($self->{header})
+ {
+ $ypos = $self->{header}->allocation->y;
+ # We scroll also the size of the header.
+ # But we don't do that for the 1st row to have it displayed then.
+ $ypos += $self->{header}->allocation->height
+ if $row;
+ }
+ # Add the items before
+ $ypos += (($row - 1) * $self->{style}->{vboxHeight});
+
+ $adj->set_value($ypos);
+ return 0;
+ }
+
+ sub changeItem
+ {
+ my ($self, $idx, $previous, $new, $withSelect) = @_;
+ return $self->changeCurrent($previous, $new, $idx, 0);
+ }
+
+ sub changeCurrent
+ {
+ my ($self, $previous, $new, $idx, $wantSelect) = @_;
+ my $forceSelect = 0;
+ #To ease comparison, do some modifications.
+ #empty borrower is equivalent to 'none'.
+ $previous->{$self->{borrowerField}} = 'none' if $previous->{$self->{borrowerField}} eq '';
+ $new->{$self->{borrowerField}} = 'none' if $new->{$self->{borrowerField}} eq '';
+ my $previousDisplayed = $self->{idxToDisplayed}->{$idx};
+ my $newDisplayed = $previousDisplayed;
+ if ($new->{$self->{sortField}} ne $previous->{$self->{sortField}})
+ {
+ # Adjust title
+ my $newTitle = $self->{parent}->transformTitle($new->{$self->{titleField}});
+ my $newSort = $self->{sortField} eq $self->{titleField} ? $newTitle : $new->{$self->{sortField}};
+
+ $self->{boxes}->[$previousDisplayed]->{info}->{title} = $newTitle;
+ $self->{tooltips}->set_tip($self->{boxes}->[$previousDisplayed], $newTitle, '');
+ my $newItemsArrayIdx;
+ ($newDisplayed, $newItemsArrayIdx) = $self->findPlace(undef, $newSort);
+ # We adjust the index as we'll remove an item
+ $newDisplayed-- if $newDisplayed > $previousDisplayed;
+ if ($previousDisplayed != $newDisplayed)
+ {
+ #$self->restorePrevious;
+ my $itemPreviousLine = int $previousDisplayed / $self->{columns};
+ my $itemNewLine = int $newDisplayed / $self->{columns};
+ my $itemNewCol = $newDisplayed % $self->{columns};
+ my ($direction, $origin, $limit);
+ if ($previousDisplayed > $newDisplayed)
+ {
+ $direction = 1;
+ $origin = $newDisplayed;
+ $limit = $previousDisplayed - 1;
+ }
+ else
+ {
+ $direction = -1;
+ $origin = $previousDisplayed;
+ $limit = $newDisplayed;
+ $itemNewCol++ if ($itemNewLine > $itemPreviousLine) && ($itemNewCol != 0)
+ }
+ my $box = $self->{cache}->[$idx]->{imageBox};
+ my $previousItemsArrayIdx = $self->displayedToItemsArrayIdx($previousDisplayed);
+ $self->{rowContainers}->[$itemPreviousLine]->remove($box);
+ splice @{$self->{boxes}}, $previousDisplayed, 1;
+ $self->{rowContainers}->[$itemNewLine]->pack_start($box,0,0,0);
+ $self->{rowContainers}->[$itemNewLine]->reorder_child($box, $itemNewCol);
+
+ $self->shiftItems($origin, $direction, 0, $limit);
+ my $item = splice @{$self->{itemsArray}}, $previousItemsArrayIdx, 1;
+ $newItemsArrayIdx-- if $previousItemsArrayIdx < $newItemsArrayIdx;
+ splice @{$self->{itemsArray}}, $newItemsArrayIdx, 0, $item;
+ splice @{$self->{boxes}}, $newDisplayed, 0, $box;
+ $self->initConversionTables;
+ }
+ }
+
+ my @boxes = @{$self->{boxes}};
+ my $item = $self->createItemInfo($idx, $new);
+ if (($previous->{$self->{coverField}} ne $new->{$self->{coverField}})
+ || ($previous->{$self->{borrowerField}} ne $new->{$self->{borrowerField}})
+ || ($previous->{favourite} ne $new->{favourite}))
+ {
+ $boxes[$newDisplayed]->refreshInfo($item, 1);
+ $forceSelect = 1;
+ $wantSelect = 1 if $wantSelect ne '';
+ }
+ else
+ {
+ # Popup is refreshed by previous call.
+ # So we just need to explicitely do it here
+ if ($boxes[$newDisplayed])
+ {
+ $boxes[$newDisplayed]->setInfo($item);
+ $boxes[$newDisplayed]->refreshPopup;
+ }
+ }
+ if ($self->{filter})
+ {
+ # Test visibility
+ my $previouslyVisible = $self->{filter}->test($previous);
+ my $visible = $self->{filter}->test($new);
+ if ($previouslyVisible && ! $visible)
+ {
+ $self->{displayedNumber}--;
+ $self->restorePrevious if $wantSelect;
+ my $itemLine = int $newDisplayed / $self->{columns};
+
+ $self->{rowContainers}->[$itemLine]->remove(
+ $self->{cache}->[$idx]->{imageBox}
+ );
+ my $info = $self->{boxes}->[$newDisplayed]->{info};
+ splice @{$self->{boxes}}, $newDisplayed, 1;
+ $self->shiftItems($newDisplayed, -1, 0, scalar @{$self->{boxes}});
+ $self->initConversionTables;
+ $info->{displayed} = $visible;
+ $idx = $self->getFirstVisibleIdx($newDisplayed);
+ $wantSelect = 0 if ! scalar @{$self->{boxes}}
+ }
+ }
+ $self->select($idx, $forceSelect) if $wantSelect;
+ return $idx;
+ }
+
+ sub showSearch
+ {
+ my ($self, $char) = @_;
+ $self->{searchEntry}->set_text($char);
+ $self->{searchEntry}->show_all;
+ $self->activateSearch;
+ $self->{container}->{searchTimeOut} = Glib::Timeout->add(4000, sub {
+ $self->hideSearch;
+ $self->{searchTimeOut} = 0;
+ return 0;
+ });
+ }
+
+ sub activateSearch
+ {
+ my ($self) = @_;
+ $self->{searchEntry}->grab_focus;
+ $self->{searchEntry}->select_region(length($self->{searchEntry}->get_text), -1);
+ }
+
+ sub hideSearch
+ {
+ my $self = shift;
+ $self->{searchEntry}->set_text('');
+ $self->{searchEntry}->hide;
+ $self->grab_focus;
+ $self->{previousSearch} = '';
+ }
+
+ sub internalSearch
+ {
+ my $self = shift;
+
+ my $query = $self->{searchEntry}->get_text;
+ return if !$query;
+ my $newDisplayed = -1;
+
+ my $current = 0;
+ my $length = length($query);
+ if ($self->{currentOrder})
+ {
+ if (($length > 1) && ($length > length($self->{previousSearch})))
+ {
+ $current = $self->{idxToDisplayed}->{$self->{itemsArray}->[$self->{current}]->{idx}};
+ }
+ foreach(@{$self->{itemsArray}}[$current..$self->{count} - 1])
+ {
+ next if !$_->{displayed};
+ if ($_->{title} ge $query)
+ {
+ $newDisplayed = $self->{idxToDisplayed}->{$_->{idx}};
+ last;
+ }
+ }
+ }
+ else
+ {
+ foreach(@{$self->{itemsArray}}[$current..$self->{count} - 1])
+ {
+ next if !$_->{displayed};
+ if (($_->{title} =~ m/^\Q$query\E/i) || ($_->{title} lt $query))
+ {
+ $newDisplayed = $self->{idxToDisplayed}->{$_->{idx}};
+ last;
+ }
+ }
+ }
+
+ if ($newDisplayed != -1)
+ {
+ my $valueIdx = $self->{displayedToIdx}->{$newDisplayed};
+ $self->select($valueIdx);
+ $self->{parent}->display($valueIdx);
+ $self->{boxes}->[$newDisplayed]->grab_focus;
+ $self->showCurrent;
+ $self->activateSearch;
+ }
+ $self->{previousSearch} = $query;
+ }
+
+}
+
+{
+ package GCImageList;
+
+ use base "Gtk2::VBox";
+ use File::Temp qw/ tempfile /;
+
+ my $defaultGroup = 'GCMAINDEFAULTGROUP';
+
+ sub new
+ {
+ my ($proto, $parent, $columns) = @_;
+ my $class = ref($proto) || $proto;
+ my $self = $class->SUPER::new(0,0);
+ bless ($self, $class);
+
+ $self->{preferences} = $parent->{model}->{preferences};
+ $self->{parent} = $parent;
+ $self->{columns} = $columns;
+
+ $self->{borrowerField} = $parent->{model}->{commonFields}->{borrower}->{name};
+
+ $self->{scroll} = new Gtk2::ScrolledWindow;
+ $self->{scroll}->set_policy ('automatic', 'automatic');
+ $self->{scroll}->set_shadow_type('none');
+
+ $self->{searchEntry} = new Gtk2::Entry;
+ #$self->{list} = new GCBaseImageList($self, $columns);
+
+ $self->{orderSet} = 0;
+ $self->{sortButton} = Gtk2::Button->new;
+ $self->setSortButton($self->{preferences}->sortOrder);
+ $self->{searchEntry}->signal_connect('changed' => sub {
+ return if ! $self->{searchEntry}->get_text;
+ $self->internalSearch;
+ });
+ $self->{searchEntry}->signal_connect('key-press-event' => sub {
+ my ($widget, $event) = @_;
+ Glib::Source->remove($self->{searchTimeOut})
+ if $self->{searchTimeOut};
+ return if ! $self->{searchEntry}->get_text;
+ my $key = Gtk2::Gdk->keyval_name($event->keyval);
+ if ($key eq 'Escape')
+ {
+ $self->hideSearch;
+ return 1;
+ }
+ $self->{searchTimeOut} = Glib::Timeout->add(4000, sub {
+ $self->hideSearch;
+ $self->{searchTimeOut} = 0;
+ return 0;
+ });
+
+ return 0;
+ });
+
+ #$self->{scroll}->add_with_viewport($self->{list});
+ $self->{mainList} = new Gtk2::VBox(0,0);
+ $self->{scroll}->add_with_viewport($self->{mainList});
+ #$self->{list}->initPixmaps;
+
+ $self->pack_start($self->{sortButton},0,0,0);
+ $self->pack_start($self->{scroll},1,1,0);
+ $self->pack_start($self->{searchEntry},0,0,0);
+
+ $self->{sortButton}->signal_connect('clicked' => sub {
+ $self->setSortOrder(-1);
+ $self->setSortButton;
+ });
+
+ $self->initStyle;
+ $self->setGroupingInformation;
+ $self->{empty} = 1;
+ $self->{orderedLists} = [];
+ $self->{displayed} = {};
+ return $self;
+ }
+
+ sub setSortButton
+ {
+ my ($self, $order) = @_;
+ $order = $self->{currentOrder}
+ if !defined $order;
+ my $image = Gtk2::Image->new_from_stock($order
+ ? 'gtk-sort-descending'
+ : 'gtk-sort-ascending',
+ 'button');
+ my $stockItem = Gtk2::Stock->lookup($order
+ ? 'gtk-sort-ascending'
+ : 'gtk-sort-descending');
+ $stockItem->{label} =~ s/_//g;
+ $self->{sortButton}->set_label($stockItem->{label});
+ $self->{sortButton}->set_image($image);
+
+ }
+
+ sub show_all
+ {
+ my $self = shift;
+ $self->SUPER::show_all;
+ $self->{mainList}->show_all;
+ $self->{searchEntry}->hide;
+ }
+
+ sub done
+ {
+ my $self = shift;
+ foreach (values %{$self->{lists}})
+ {
+ $_->done;
+# $self->{style}->{vboxWidth} = $_->{style}->{vboxWidth}
+# if !exists $self->{style}->{vboxWidth};
+ }
+ # We set a number of ms to wait before enhancing the pictures
+ my $offset = 0;
+ foreach (@{$self->{orderedLists}})
+ {
+ $self->{lists}->{$_}->{offset} = $offset;
+ $offset += $timeOutBetweenEnhancements * ($self->{lists}->{$_}->{displayedNumber} + 1);
+ }
+ if ($self->{columns} == 0)
+ {
+ $self->signal_connect('size-allocate' => sub {
+ $self->computeAllocation;
+ });
+ $self->computeAllocation;
+ }
+ else
+ {
+ foreach (values %{$self->{lists}})
+ {
+ $_->setColumnsNumber($self->{columns}, 0);
+ }
+ }
+ }
+
+ sub computeAllocation
+ {
+ my $self = shift;
+ return if !$self->{style}->{vboxWidth};
+ my $width = $self->{scroll}->child->allocation->width - 15;
+ return if $width < 0;
+ if (($self->{scroll}->get_hscrollbar->visible)
+ || ($width > (($self->{columns} + 1) * $self->{style}->{vboxWidth})))
+ {
+ my $columns = int ($width / $self->{style}->{vboxWidth});
+ if ($columns)
+ {
+ return if $columns == $self->{columns};
+ $self->{columns} = $columns;
+ foreach (values %{$self->{lists}})
+ {
+ $_->setColumnsNumber($columns, 1);
+ }
+ # TODO : We should maybe select an item here
+ #$self->{parent}->display($self->select(-1, 1))
+ # if !$self->{current};
+ }
+ else
+ {
+ $self->{columns} = 1;
+ }
+ }
+
+ }
+
+ sub initStyle
+ {
+ my $self = shift;
+ my $parent = $self->{parent};
+
+ my $size = $self->{preferences}->listImgSize;
+ $self->{style}->{withAnimation} = $self->{preferences}->animateImgList;
+ $self->{style}->{withImage} = $self->{preferences}->listBgPicture;
+ $self->{style}->{useOverlays} = ($self->{preferences}->useOverlays) && ($parent->{model}->{collection}->{options}->{overlay}->{image});
+ $self->{preferences}->listImgSkin($GCStyle::defaultList) if ! $self->{preferences}->exists('listImgSkin');
+ $self->{style}->{skin} = $self->{preferences}->listImgSkin;
+ # Reflect setting can be enabled using "withReflect=1" in the listbg style file
+ $self->{style}->{withReflect} = 0;
+ $self->{preferences}->listImgSize(2) if ! $self->{preferences}->exists('listImgSize');
+
+ my $bgdir;
+ # Load in extra settings from the style file
+ if ($self->{style}->{withImage})
+ {
+ $bgdir = $ENV{GCS_SHARE_DIR}.'/list_bg/'.$self->{style}->{skin};
+ if (open STYLE, $bgdir.'/style')
+ {
+ while (<STYLE>)
+ {
+ chomp;
+ next if !$_;
+ m/^(.*?)\s*=\s*(.*)$/;
+ my $item = $1;
+ (my $value = $2) =~ s/^"(.*?)"$/$1/;
+ $self->{style}->{$item} = $value;
+ }
+ close STYLE;
+ }
+ }
+
+ # Sets image width/height (for size = 2), getting value from the collection model or setting to
+ # default values of 120, 160 if not specified in model file
+ $self->{style}->{imgWidth} = (exists $parent->{model}->{collection}->{options}->{defaults}->{listImageWidth})
+ ? $parent->{model}->{collection}->{options}->{defaults}->{listImageWidth}
+ : 120;
+ $self->{style}->{imgHeight} = (exists $parent->{model}->{collection}->{options}->{defaults}->{listImageHeight})
+ ? $parent->{model}->{collection}->{options}->{defaults}->{listImageHeight}
+ : 160;
+
+ $self->{style}->{factor} = ($size == 0) ? 0.5
+ : ($size == 1) ? 0.8
+ : ($size == 3) ? 1.5
+ : ($size == 4) ? 2
+ : 1;
+ $self->{style}->{imgWidth} *= $self->{style}->{factor};
+ $self->{style}->{imgHeight} *= $self->{style}->{factor};
+ $self->{style}->{offsetX} = 11;
+ if ($self->{style}->{withImage})
+ {
+ if (! $self->{style}->{useOverlays})
+ {
+ $self->{style}->{offsetX} = 26;
+ }
+ }
+ else
+ {
+ $self->{style}->{offsetX} = 22;
+ }
+
+ $self->{style}->{vboxWidth} = $self->{style}->{imgWidth} + ($self->{style}->{offsetX} * $self->{style}->{factor});
+
+ $self->{style}->{vboxHeight} = $self->{style}->{imgHeight} + (10 * $self->{style}->{factor});
+ $self->{style}->{vboxHeight} += (20 * $self->{style}->{factor}) if $self->{style}->{withImage};
+ $self->{style}->{vboxHeight} += (30 * $self->{style}->{factor}) if $self->{style}->{withReflect};
+ $self->{style}->{pageCount} = int 5 / $self->{style}->{factor};
+
+ # Pixbuf for lending icon
+ my $lendImageFile = $ENV{GCS_SHARE_DIR}.'/overlays/lend_';
+ $lendImageFile .= ($size < 1) ? 'verysmall'
+ : ($size < 2) ? 'small'
+ : ($size < 3) ? 'med'
+ : ($size < 4) ? 'large'
+ : 'xlarge';
+ $self->{style}->{lendPixbuf} = Gtk2::Gdk::Pixbuf->new_from_file($lendImageFile.'.png');
+
+ # Pixbuf for favourite icon
+ my $favImageFile = $ENV{GCS_SHARE_DIR}.'/overlays/favourite_';
+ $favImageFile .= ($size < 1) ? 'verysmall'
+ : ($size < 2) ? 'small'
+ : ($size < 3) ? 'med'
+ : ($size < 4) ? 'large'
+ : 'xlarge';
+ $self->{style}->{favPixbuf} = Gtk2::Gdk::Pixbuf->new_from_file($favImageFile.'.png');
+
+ if ($self->{style}->{useOverlays})
+ {
+ $self->{style}->{overlayImage} = $ENV{GCS_SHARE_DIR}.'/overlays/'.$parent->{model}->{collection}->{options}->{overlay}->{image};
+ $self->{style}->{overlayPixbuf} = Gtk2::Gdk::Pixbuf->new_from_file($self->{style}->{overlayImage});
+
+ $self->{style}->{overlayPaddingLeft} = $parent->{model}->{collection}->{options}->{overlay}->{paddingLeft};
+ $self->{style}->{overlayPaddingRight} = $parent->{model}->{collection}->{options}->{overlay}->{paddingRight};
+ $self->{style}->{overlayPaddingTop} = $parent->{model}->{collection}->{options}->{overlay}->{paddingTop};
+ $self->{style}->{overlayPaddingBottom} = $parent->{model}->{collection}->{options}->{overlay}->{paddingBottom};
+ }
+
+ # Default value for align
+ $self->{style}->{groupAlign} = 'center';
+
+ if ($self->{style}->{withImage})
+ {
+ $self->{style}->{bgPixmap} = $bgdir.'/list_bg.png';
+
+ my $tmpPixbuf = Gtk2::Gdk::Pixbuf->new_from_file($self->{style}->{bgPixmap});
+ $tmpPixbuf = GCUtils::scaleMaxPixbuf($tmpPixbuf,
+ $self->{style}->{vboxWidth},
+ $self->{style}->{vboxHeight},
+ 1);
+ (my $fh, $self->{style}->{tmpBgPixmapFile}) = tempfile(UNLINK => 1);
+ close $fh;
+ if ($^O =~ /win32/i)
+ {
+ # It looks like Win32 version only supports JPEG pictures for background
+ $tmpPixbuf->save($self->{style}->{tmpBgPixmap}, 'jpeg', quality => '100');
+ }
+ else
+ {
+ $tmpPixbuf->save($self->{style}->{tmpBgPixmapFile}, 'png');
+ }
+
+ #($self->{style}->{tmpBgPixmap}, $self->{style}->{tmpBgMask}) = $tmpPixbuf->render_pixmap_and_mask(255);
+
+ GCUtils::setWidgetPixmap($self->{mainList}->parent, $self->{style}->{tmpBgPixmapFile});
+
+ $self->{style}->{backgroundPixbuf} = Gtk2::Gdk::Pixbuf->new_from_file($self->{style}->{bgPixmap});
+ $self->{style}->{backgroundPixbuf} = GCUtils::scaleMaxPixbuf($self->{style}->{backgroundPixbuf},
+ $self->{style}->{vboxWidth},
+ $self->{style}->{vboxHeight},
+ 1);
+ my @colors = split m/,/, $self->{preferences}->listFgColor;
+ ($colors[0], $colors[1], $colors[2]) = (65535, 65535, 65535) if !@colors;
+ my $red = int($colors[0] / 257);
+ my $green = int($colors[1] / 257);
+ my $blue = int($colors[2] / 257);
+ $self->{style}->{activeBgValue} = ($red << 16) + ($green << 8) + $blue;
+
+ if ($self->{style}->{withReflect})
+ {
+ $self->{style}->{foregroundPixbuf} = Gtk2::Gdk::Pixbuf->new_from_file($bgdir.'/list_fg.png');
+ $self->{style}->{foregroundPixbuf} = GCUtils::scaleMaxPixbuf($self->{style}->{foregroundPixbuf},
+ $self->{style}->{vboxWidth},
+ $self->{style}->{vboxHeight},
+ 1);
+ }
+
+ $self->{groupBgFile} = $bgdir.'/group.png';
+ }
+ else
+ {
+ my @colors = split m/,/, $self->{preferences}->listBgColor;
+ ($colors[0], $colors[1], $colors[2]) = (65535, 65535, 65535) if !@colors;
+ $self->{style}->{inactiveBg} = new Gtk2::Gdk::Color($colors[0], $colors[1], $colors[2]);
+ @colors = split m/,/, $self->{preferences}->listFgColor;
+ ($colors[0], $colors[1], $colors[2]) = (0, 0, 0) if !@colors;
+ $self->{style}->{activeBg} = new Gtk2::Gdk::Color($colors[0], $colors[1], $colors[2]);
+ $self->{mainList}->parent->modify_bg('normal', $self->{style}->{inactiveBg});
+ $self->{mainList}->parent->modify_bg('active', $self->{style}->{inactiveBg});
+ $self->{mainList}->parent->modify_bg('prelight', $self->{style}->{inactiveBg});
+ $self->{mainList}->parent->modify_bg('selected', $self->{style}->{inactiveBg});
+ $self->{mainList}->parent->modify_bg('insensitive', $self->{style}->{inactiveBg});
+ }
+ }
+
+ sub initListStyle
+ {
+ my ($self, $list) = @_;
+ $list->{style} = $self->{style};
+ if ($self->{style}->{withImage})
+ {
+ GCUtils::setWidgetPixmap($list->parent, $self->{style}->{tmpBgPixmapFile});
+ }
+ else
+ {
+ $self->set_border_width(5);
+ $list->parent->modify_bg('normal', $self->{style}->{inactiveBg});
+ $list->parent->modify_bg('active', $self->{style}->{inactiveBg});
+ $list->parent->modify_bg('prelight', $self->{style}->{inactiveBg});
+ $list->parent->modify_bg('selected', $self->{style}->{inactiveBg});
+ $list->parent->modify_bg('insensitive', $self->{style}->{inactiveBg});
+ }
+ }
+
+ sub setCurrentList
+ {
+ my ($self, $list) = @_;
+ $self->{currentList} = $list;
+ }
+
+ sub setGroupingInformation
+ {
+ my $self = shift;
+
+ $self->{collectionField} = $self->{preferences}->groupBy;
+ $self->{groupItems} = ($self->{collectionField} ne '');
+ if (!$self->{groupItems})
+ {
+ $self->addGroup($defaultGroup, uc $defaultGroup, 1)
+ if !$self->{currentList};
+ }
+ }
+
+ sub getGroups
+ {
+ my ($self, $info) = @_;
+
+ my $field = $self->{collectionField};
+ my $value = $info->{$field};
+ my $type = '';
+ $type = $self->{parent}->{model}->{fieldsInfo}->{$field}->{type}
+ if defined $self->{parent}->{model}->{fieldsInfo}->{$field}->{type};
+
+ $value = $self->{parent}->transformValue($value, $field, 1);
+
+ if (ref($value) eq 'ARRAY')
+ {
+ if (!scalar (@$value))
+ {
+ $value = [$defaultGroup];
+ }
+ }
+ else
+ {
+ $value = $defaultGroup
+ if ($type =~ /text$/) && ($value eq '');
+ my @array = ($value);
+ $value = \@array;
+ }
+
+
+ return $value;
+ }
+
+ sub sortAndFind
+ {
+ my ($self, $group) = @_;
+
+ # We insert it in the list
+ my @tmpList = @{$self->{orderedLists}};
+ #push @tmpList, $group;
+ # We sort it
+ if ($self->{currentOrder} == 0)
+ {
+ @tmpList = reverse sort {GCUtils::gccmpe($a, $b)} @tmpList;
+ }
+ else
+ {
+ @tmpList = sort {GCUtils::gccmpe($a, $b)} @tmpList;
+ }
+
+ # And now we find back its position
+ $self->{orderedLists} = \@tmpList;
+ return GCUtils::inArray($group, @tmpList);
+ }
+
+ sub getNbItems
+ {
+ my $self = shift;
+
+ # We count the number of items in displayed hash where value is 1
+ return scalar grep {$_ == 1} values %{$self->{displayed}};
+ }
+
+ sub createHeader
+ {
+ my ($self, $title) = @_;
+ my $label;
+ my $fixedTitle = $title;
+ $fixedTitle =~ s/&/&amp;/;
+ $fixedTitle =~ s/</&lt;/;
+ $fixedTitle =~ s/>/&gt;/;
+
+ if ($self->{style}->{withImage})
+ {
+ $label = new GCColorLabel(Gtk2::Gdk::Color->parse('#000000'));
+ $label->set_markup('<span '.$self->{style}->{groupStyle}.">$fixedTitle</span>");
+ GCUtils::setWidgetPixmap($label, $self->{groupBgFile});
+ }
+ else
+ {
+ $label = new GCColorLabel($self->{style}->{activeBg});
+ $label->set_markup('<span weight="bold" color="'.$self->{style}->{inactiveBg}->to_string."\">$fixedTitle</span>");
+ }
+ $label->set_justify($self->{style}->{groupAlign});
+ $label->set_padding($GCUtils::halfMargin, $GCUtils::halfMargin);
+ return $label;
+ return new Gtk2::Label($title);
+ }
+
+ sub addGroup
+ {
+ my ($self, $group, $refGroup, $immediate) = @_;
+
+ my $listBox = new Gtk2::VBox(0,0);
+ my $list = new GCBaseImageList($self, $self->{columns});
+ if ($self->{groupItems})
+ {
+ my $label;
+ if ($refGroup eq $defaultGroup)
+ {
+ $label = $self->createHeader('');
+ }
+ else
+ {
+ $label = $self->createHeader($group);
+ }
+ $listBox->pack_start($label, 0, 0, 0);
+ $list->setHeader($label);
+ $list->{refGroup} = $refGroup;
+ $label->show_all;
+ }
+ my $eventBox = new Gtk2::EventBox;
+ $eventBox->add($list);
+ $listBox->pack_start($eventBox, 0, 0, 0);
+ $self->{mainList}->pack_start($listBox, 0, 0, 0);
+
+ push @{$self->{orderedLists}}, $refGroup
+ if ($refGroup ne $defaultGroup);
+
+ if ($immediate && $self->{groupItems})
+ {
+ my $place = $self->sortAndFind($refGroup);
+ $self->{mainList}->reorder_child($listBox, $place);
+ }
+
+ $listBox->show_all;
+ $self->initListStyle($list);
+ $self->{lists}->{$refGroup} = $list;
+ $self->{listBoxes}->{$refGroup} = $listBox;
+ $self->{currentList} = $list if ! $self->{currentList};
+ $list->done(undef, 1) if $immediate;
+ return $list;
+ }
+
+ sub addItem
+ {
+ my ($self, $info, $immediate) = @_;
+ my $groups = [];
+ if ($self->{groupItems})
+ {
+ $groups = $self->getGroups($info);
+ }
+ else
+ {
+ $groups = [$defaultGroup];
+ }
+ foreach my $group(@$groups)
+ {
+ my $refGroup = uc($group);
+ if (! exists $self->{lists}->{$refGroup})
+ {
+ $self->addGroup($group, $refGroup, $immediate);
+ }
+ $self->{currentList} = $self->{lists}->{$refGroup} if $immediate;
+ $self->{lists}->{$refGroup}->addItem($info, $immediate, $self->{count}, 0);
+ # Storing conversion from index to the actual list
+ $self->{idxToList}->{$self->{count}} = $self->{lists}->{$refGroup};
+ }
+ # Default is to display it. It will maybe be filtered later
+ $self->{displayed}->{$self->{count}} = 1;
+ $self->{count}++;
+ }
+
+ sub couldExpandAll
+ {
+ my $self = shift;
+
+ return $self->{groupItems};
+ }
+
+ sub showCurrent
+ {
+ my $self = shift;
+ # TODO:
+ $self->{currentList}->showCurrent
+ if $self->{currentList};
+ }
+
+ sub clearSelected
+ {
+ my ($self, $current) = @_;
+ foreach (values %{$self->{lists}})
+ {
+ next if $_ == $current;
+ $_->restorePrevious(1);
+ }
+
+ }
+
+ sub reset
+ {
+ my $self = shift;
+ foreach (values %{$self->{lists}})
+ {
+ $_->reset;
+ }
+ $self->{count} = 0;
+ $self->{idxToList} = {};
+ }
+
+ sub clearCache
+ {
+ my ($self) = @_;
+ foreach (values %{$self->{lists}})
+ {
+ $_->clearCache;
+ }
+ #$self->{vboxWidth} = 1;
+ }
+
+ sub setSortOrder
+ {
+ my ($self, $order) = @_;
+ $self->{orderSet} = 1;
+
+ if ($self->{groupItems})
+ {
+ my $first = 1;
+ foreach (values %{$self->{lists}})
+ {
+ $_->setSortOrder($order);
+ # We get it computed by the first internal list
+ $self->{currentOrder} = $_->{currentOrder}
+ if $first;
+ $first = 0;
+ }
+ # Now the internal lists are ordered, we need to order them
+ my @tmpList = @{$self->{orderedLists}};
+
+ # We sort the list, using gccmpe to handle sorting of numeric values and dates
+ if ($self->{currentOrder} == 0)
+ {
+ @tmpList = reverse sort {GCUtils::gccmpe($a, $b)} @{$self->{orderedLists}};
+ }
+ else
+ {
+ @tmpList = sort {GCUtils::gccmpe($a, $b)} @{$self->{orderedLists}};
+ }
+
+ # Clear the current view
+ my @children = $self->{mainList}->get_children;
+ foreach my $child(@children)
+ {
+ $self->{mainList}->remove($child);
+ }
+ # And fill it again with the current order
+ foreach my $refGroup(@tmpList, $defaultGroup)
+ {
+ next if !$self->{listBoxes}->{$refGroup};
+ $self->{mainList}->pack_start($self->{listBoxes}->{$refGroup}, 0, 0, 0);
+ $self->{listBoxes}->{$refGroup}->show_all;
+ }
+
+ # Save the new order
+ $self->{orderedLists} = \@tmpList;
+ }
+ else
+ {
+ $self->{currentList}->setSortOrder($order);
+ $self->{currentList}->show_all;
+ # We get it computed by the first internal list
+ $self->{currentOrder} = $self->{currentList}->{currentOrder};
+ }
+ }
+
+ sub setFilter
+ {
+ my ($self, $filter, $items, $refresh, $splash) = @_;
+ shift;
+ my $current;
+ my $result = -1;
+ my $list;
+ $self->{displayed} = {};
+ foreach (keys %{$self->{lists}})
+ {
+ $list = $self->{lists}->{$_};
+ $current = $list->setFilter(@_);
+ $result = $current if $list == $self->{currentList};
+ if ($list->{displayedNumber})
+ {
+ $self->{listBoxes}->{$_}->show_all;
+ }
+ else
+ {
+ $self->{listBoxes}->{$_}->hide;
+ }
+ }
+ $result = -1 if !defined $result;
+ return $result;
+ }
+
+ sub setDisplayed
+ {
+ my ($self, $idx, $displayed) = @_;
+ $self->{displayed}->{$idx} = $displayed;
+ }
+
+ sub select
+ {
+ my ($self, $idx, $init, $keepPrevious) = @_;
+ my $list;
+ if ($self->{groupItems})
+ {
+ if (($idx == -1) || (!defined $idx))
+ {
+ if (defined $self->{orderedLists}->[0])
+ {
+ $list = $self->{lists}->{$self->{orderedLists}->[0]};
+ }
+ else
+ {
+ $list = $self->{lists}->{$defaultGroup};
+ }
+ }
+ else
+ {
+ $list = $self->{idxToList}->{$idx};
+ }
+ }
+ else
+ {
+ $list = $self->{currentList};
+ }
+ $list->select($idx, $init, $keepPrevious)
+ if $list;
+ }
+
+ sub savePreferences
+ {
+ my ($self, $preferences) = @_;
+ return if !$self->{orderSet};
+ $preferences->sortField($self->{titleField});
+ $preferences->sortOrder($self->{currentOrder});
+ }
+
+ sub getCurrentIdx
+ {
+ my $self = shift;
+ return 0 if !$self->{currentList};
+ return $self->{currentList}->getCurrentIdx;
+ }
+
+ sub removeCurrentItems
+ {
+ my $self = shift;
+ # TODO : This doesn't work if there are items selected in many lists
+
+ my @indexes = sort @{$self->getCurrentItems};
+ my $selected;
+ my @listWhereAlreadyRemoved;
+ # Find other lists where they were
+ foreach my $list(values %{$self->{lists}})
+ {
+ next if $list == $self->{currentList};
+ foreach my $idx(@indexes)
+ {
+ my $nbRemoved = 0;
+ if (exists $list->{idxToDisplayed}->{$idx - $nbRemoved})
+ {
+ $list->removeItem($idx - $nbRemoved);
+ push @listWhereAlreadyRemoved, 0 + $list;
+ $nbRemoved++;
+ }
+ #splice @{$list->{cache}}, $idx - $nbRemoved, 1;
+ delete $self->{displayed}->{$idx};
+ }
+ }
+ # Adjust the total number of items according to what we removed
+ $self->{count} -= scalar @indexes;
+
+ $selected = $self->{currentList}->removeCurrentItems;
+ push @listWhereAlreadyRemoved, $self->{currentList};
+
+ # Now we have to adjust all of the indexes in other lists
+ foreach my $list(values %{$self->{lists}})
+ {
+ # We don't perform the switch if we already removed the item
+ my $found = 0;
+ foreach my $listRm(@listWhereAlreadyRemoved)
+ {
+ if ($listRm == $list)
+ {
+ # Found a list where we removed it
+ $found = 1;
+ last;
+ }
+ }
+ next if $found;
+ $list->shiftIndexes(\@indexes);
+ $list->initConversionTables;
+ }
+
+ # If we removed all the items in the current group, we are looking for the 1st one
+ # of the next group (fallback on previous if last one)
+ if (!defined $selected)
+ {
+ my $nextList;
+ foreach my $i(0 .. $#{$self->{orderedLists}})
+ {
+ if ($self->{orderedLists}->[$i] eq $self->{currentList}->{refGroup})
+ {
+ if ($i < $#{$self->{orderedLists}})
+ {
+ $nextList = $self->{orderedLists}->[$i+1];
+ last;
+ }
+ else
+ {
+ $nextList = $self->{orderedLists}->[$i-1]
+ if $i > 0;
+ last;
+ }
+ }
+ }
+ if ($nextList)
+ {
+ my $currentList = $self->{lists}->{$nextList};
+ $selected = $currentList->{displayedToIdx}->{0};
+ $currentList->select($selected);
+ $self->{currentList} = $currentList;
+ }
+ }
+ return $selected;
+ }
+
+ sub getCurrentItems
+ {
+ my $self = shift;
+ # TODO : This doesn't work if there are items selected in many lists
+ return $self->{currentList}->getCurrentItems;
+ }
+
+ sub changeCurrent
+ {
+ my ($self, $previous, $new, $idx, $wantSelect) = @_;
+ if ($self->{groupItems})
+ {
+ # Will be set to a true value if the 1st added item should be selected
+ my $shouldBeSelected = 0;
+ # Get the list where it was
+ my @prevGroups = sort @{$self->getGroups($previous)};
+
+ # And the one where it should be
+ my @newGroups = sort @{$self->getGroups($new)};
+
+ my ($found, $place);
+ # First look for previous ones
+ foreach my $pg(@prevGroups)
+ {
+ my $pg = uc $pg;
+ ($found, $place) = (0, 0);
+ # Try to find it in the new groups
+ foreach my $ng (@newGroups)
+ {
+ my $refGroup = uc($ng);
+ $found = 1 if $refGroup eq $pg;
+ # As it is sorted, we can stop when we find a greater one
+ last if $refGroup ge $pg;
+ $place++;
+ }
+ # If found, we just change it
+ if ($found)
+ {
+ $self->{lists}->{$pg}->changeCurrent($previous, $new, $idx, $wantSelect);
+ # And we remove it from the list
+ splice @newGroups, $place, 1;
+ }
+ # Otherwise, it means it was removed from this group
+ else
+ {
+ $shouldBeSelected = 1
+ if $self->{lists}->{$pg}->isSelected($idx);
+ $self->{lists}->{$pg}->removeItem($idx,1);
+ }
+ }
+ # Now we should have a list whith just the new groups
+ foreach my $ng(@newGroups)
+ {
+ my $refGroup = uc $ng;
+ # We should create the list if it doesn't exist
+ if (! exists $self->{lists}->{$refGroup})
+ {
+ my $list = $self->addGroup($ng, $refGroup, 1);
+ }
+
+ # 2nd parameter means it should be added immediately
+ # 4th one is that we should not change the conversion tables because it's not
+ # a new item
+ $self->{lists}->{$refGroup}->addItem($new, 1, $idx, 1);
+ if ($shouldBeSelected)
+ {
+ $self->{lists}->{$refGroup}->select($idx, 0, 1);
+ $shouldBeSelected = 0;
+ }
+ }
+ # TODO It should return something else if filtered
+ return $idx;
+ }
+ else
+ {
+ return $self->{currentList}->changeCurrent($previous, $new, $idx, $wantSelect);
+ }
+ }
+
+ sub AUTOLOAD
+ {
+ return if our $AUTOLOAD =~ /::DESTROY$/;
+ (my $name = $AUTOLOAD) =~ s/.*?::(.*)/$1/;
+ my $self = shift;
+ #GCUtils::printStack(6);
+ #print "CALLING $name\n";
+ return $self->{currentList}->$name(@_);
+ }
+}
+
+1;
diff --git a/lib/gcstar/GCItemsLists/GCListOptions.pm b/lib/gcstar/GCItemsLists/GCListOptions.pm
new file mode 100644
index 0000000..c41084b
--- /dev/null
+++ b/lib/gcstar/GCItemsLists/GCListOptions.pm
@@ -0,0 +1,496 @@
+package GCListOptions;
+
+###################################################
+#
+# Copyright 2005-2011 Christian Jodar
+#
+# This file is part of GCstar.
+#
+# GCstar is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# GCstar is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GCstar; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+#
+###################################################
+
+use strict;
+use Gtk2;
+
+{
+ package GCListOptionsPanel;
+ use base "Gtk2::Frame";
+
+ sub setView
+ {
+ my ($self, $view) = @_;
+ if ((!exists $self->{currentView}) || ($view != $self->{currentView}))
+ {
+ $self->{currentView} = $view;
+ if ($self->{panel})
+ {
+ $self->{scroll}->remove($self->{scroll}->get_child);
+ $self->{panel}->destroy;
+ }
+
+ $self->{cancel}->set_sensitive(1);
+ $self->{apply}->set_sensitive(1);
+
+ if ($view == 0)
+ {
+ $self->{panel} = new GCEmptyOptionsPanel($self->{parent}->{model}->{preferences}, $self->{parent});
+ $self->{cancel}->set_sensitive(0);
+ $self->{apply}->set_sensitive(0);
+ }
+ elsif ($view == 1)
+ {
+ $self->{panel} = new GCImagesOptionsPanel($self->{parent}->{model}->{preferences}, $self->{parent});
+ }
+ else
+ {
+ $self->{panel} = new GCDetailedOptionsPanel($self->{parent}->{model}->{preferences}, $self->{parent});
+ }
+ $self->{scroll}->add_with_viewport($self->{panel});
+ $self->{scroll}->child->set_shadow_type('none');
+ }
+ $self->{panel}->initValues;
+ }
+
+ sub new
+ {
+ my ($proto, $optionsManager, $parent) = @_;
+ my $class = ref($proto) || $proto;
+
+ my $self = $class->SUPER::new;
+ $self->{optionsManager} = $optionsManager;
+ $self->{parent} = $parent;
+ $self->{scroll} = new Gtk2::ScrolledWindow;
+ $self->{scroll}->set_policy ('automatic', 'automatic');
+ $self->{scroll}->set_shadow_type('none');
+ $self->{hboxButtons} = new Gtk2::HBox(0,0);
+ $self->{cancel} = GCButton->newFromStock('gtk-clear');
+ $self->{apply} = GCButton->newFromStock('gtk-apply');
+
+ $self->{vbox} = new Gtk2::VBox(0,0);
+
+ $self->{hboxButtons}->pack_end($self->{apply}, 0, 0, $GCUtils::halfMargin);
+ $self->{hboxButtons}->pack_end($self->{cancel}, 0, 0, $GCUtils::halfMargin);
+ $self->add($self->{vbox});
+
+ $self->{vbox}->pack_start($self->{scroll}, 1, 1, $GCUtils::quarterMargin);
+ $self->{vbox}->pack_start($self->{hboxButtons}, 0, 0, $GCUtils::quarterMargin);
+
+ $self->{cancel}->signal_connect('clicked' => sub {
+ $self->{panel}->initValues
+ if $self->{panel};
+ });
+ $self->{apply}->signal_connect('clicked' => sub {
+ if ($self->{panel})
+ {
+ $self->{panel}->saveValues;
+ $self->{parent}->setItemsList(0, 1);
+ }
+ });
+
+
+ bless $self, $class;
+ return $self;
+ }
+}
+
+{
+ # Class used when there is no option
+ package GCEmptyOptionsPanel;
+ use base "Gtk2::VBox";
+ use GCStyle;
+
+ sub initValues
+ {
+ my $self = shift;
+ }
+ sub saveValues
+ {
+ my $self = shift;
+ }
+ sub new
+ {
+ my ($proto, $optionsManager, $parent) = @_;
+ my $class = ref($proto) || $proto;
+
+ my $self = $class->SUPER::new(0,0);
+ bless $self, $class;
+ # TODO to be replaced with a translatable label
+ my $label = new GCLabel("No option");
+ $self->pack_start($label,1,1,0);
+ $self->show_all;
+ return $self;
+ }
+}
+
+{
+ # Class used to let user select images options
+ package GCImagesOptionsPanel;
+ use base "Gtk2::Table";
+ #use base "Gtk2::VBox";
+ use GCStyle;
+
+ sub initValues
+ {
+ my $self = shift;
+ $self->{resizeImgList}->set_active($self->{optionsManager}->resizeImgList);
+ $self->{animateImgList}->set_active($self->{optionsManager}->animateImgList);
+ $self->{columns}->set_value($self->{optionsManager}->columns);
+ $self->{imgSizeOption}->setValue($self->{optionsManager}->listImgSize);
+ $self->{optionStyle}->setValue($self->{optionsManager}->listImgSkin);
+ $self->{useOverlays}->set_active($self->{optionsManager}->useOverlays);
+ $self->{listBgPicture}->set_active($self->{optionsManager}->listBgPicture);
+ $self->activateColors(! $self->{optionsManager}->listBgPicture);
+ $self->{mlbg} = $self->{optionsManager}->listBgColor;
+ $self->{mlfg} = $self->{optionsManager}->listFgColor;
+ $self->{groupByOption}->setValue($self->{optionsManager}->groupBy);
+ $self->{secondarySortOption}->setValue($self->{optionsManager}->secondarySort);
+ }
+
+ sub saveValues
+ {
+ my $self = shift;
+
+ $self->{optionsManager}->resizeImgList(($self->{resizeImgList}->get_active) ? 1 : 0);
+ $self->{optionsManager}->animateImgList(($self->{animateImgList}->get_active) ? 1 : 0);
+ $self->{optionsManager}->columns($self->{columns}->get_value);
+ $self->{optionsManager}->listImgSize($self->{imgSizeOption}->getValue);
+ $self->{optionsManager}->listImgSkin($self->{optionStyle}->getValue);
+ $self->{optionsManager}->listBgColor($self->{mlbg});
+ $self->{optionsManager}->listFgColor($self->{mlfg});
+ $self->{optionsManager}->listBgPicture(($self->{listBgPicture}->get_active) ? 1 : 0);
+ $self->{optionsManager}->useOverlays(($self->{useOverlays}->get_active) ? 1 : 0);
+ $self->{optionsManager}->groupBy($self->{groupByOption}->getValue);
+ $self->{optionsManager}->secondarySort( $self->{secondarySortOption}->getValue);
+ }
+
+ sub changeColor
+ {
+ my ($self, $type) = @_;
+
+ my $dialog = new Gtk2::ColorSelectionDialog($self->{lang}->{ImagesOptionsSelectColor});
+ my $vboxPicture = new Gtk2::VBox(0,0);
+ my @colors = split m/,/, $self->{'ml'.$type};
+ my $previous = new Gtk2::Gdk::Color($colors[0], $colors[1], $colors[2]);
+ $dialog->colorsel->set_current_color($previous) if $previous;
+ my $response = $dialog->run;
+ if ($response eq 'ok')
+ {
+ my $color = $dialog->colorsel->get_current_color;
+ $self->{'ml'.$type} = join ',',$color->red, $color->green, $color->blue;
+ }
+ $dialog->destroy;
+ }
+
+ sub activateColors
+ {
+ my ($self, $value) = @_;
+
+ $self->{labelStyle}->set_sensitive(!$value);
+ $self->{optionStyle}->set_sensitive(!$value);
+ $self->{labelBg}->set_sensitive($value);
+ $self->{buttonBg}->set_sensitive($value);
+ }
+
+ sub new
+ {
+ my ($proto, $optionsManager, $parent) = @_;
+ my $class = ref($proto) || $proto;
+
+ my $self = $class->SUPER::new(14,4);
+ #my $self = $class->SUPER::new(0,0);
+
+ $self->{optionsManager} = $optionsManager;
+ $self->{lang} = $parent->{lang};
+
+# $self->set_row_spacings($GCUtils::halfMargin);
+ $self->set_col_spacings($GCUtils::margin);
+ $self->set_border_width($GCUtils::margin);
+
+ $self->{labelColumns} = new GCLabel($self->{lang}->{OptionsColumns});
+ my $adj = Gtk2::Adjustment->new(0, 1, 20, 1, 1, 0) ;
+ $self->{columns} = Gtk2::SpinButton->new($adj, 0, 0);
+
+ $self->{resizeImgList} = new Gtk2::CheckButton($self->{lang}->{ImagesOptionsResizeImgList});
+ $self->{resizeImgList}->signal_connect('clicked' => sub {
+ $self->{columns}->set_sensitive(! $self->{resizeImgList}->get_active);
+ });
+ $self->{resizeImgList}->set_active($self->{resizeImgList});
+
+ $self->{animateImgList} = new Gtk2::CheckButton($self->{lang}->{ImagesOptionsAnimateImgList});
+ $self->{animateImgList}->signal_connect('clicked' => sub {
+ $self->{columns}->set_sensitive(! $self->{animateImgList}->get_active);
+ });
+ $self->{animateImgList}->set_active($self->{animateImgList});
+
+ $self->{imgSizeLabel} = new GCLabel($self->{lang}->{ImagesOptionsSizeLabel});
+ $self->{imgSizeOption} = new GCMenuList;
+ my %imgSizes = %{$self->{lang}->{ImagesOptionsSizeList}};
+ my @imgValues = map {{value => $_, displayed => $imgSizes{$_}}}
+ (sort keys %imgSizes);
+ $self->{imgSizeOption}->setValues(\@imgValues);
+
+ $self->{useOverlays} = new Gtk2::CheckButton($self->{lang}->{ImagesOptionsUseOverlays});
+ $self->{useOverlays}->set_active($self->{useOverlays});
+
+ $self->{listBgPicture} = new Gtk2::CheckButton($self->{lang}->{ImagesOptionsBgPicture});
+ $self->{listBgPicture}->signal_connect('clicked' => sub {
+ $self->activateColors(! $self->{listBgPicture}->get_active);
+ });
+
+ $self->{labelStyle} = new GCLabel($self->{lang}->{OptionsStyle});
+ $self->{optionStyle} = new GCMenuList;
+ my @styleValues;
+ foreach (@GCStyle::lists)
+ {
+ (my $displayed = $_) =~ s/_/ /g;
+ push @styleValues, {value => $_, displayed => $displayed};
+ }
+ $self->{optionStyle}->setValues(\@styleValues);
+
+ $self->{labelBg} = new GCLabel($self->{lang}->{ImagesOptionsBg});
+ $self->{buttonBg} = new Gtk2::Button($self->{lang}->{ImagesOptionsSelectColor});
+ $parent->{tooltips}->set_tip($self->{buttonBg},
+ $self->{lang}->{ImagesOptionsBgTooltip});
+ $self->{buttonBg}->signal_connect('clicked' => sub {
+ $self->changeColor('bg');
+ });
+
+ $self->{labelFg} = new GCLabel($self->{lang}->{ImagesOptionsFg});
+ $self->{buttonFg} = new Gtk2::Button($self->{lang}->{ImagesOptionsSelectColor});
+ $self->{buttonFg}->signal_connect('clicked' => sub {
+ $self->changeColor('fg');
+ });
+ $parent->{tooltips}->set_tip($self->{buttonFg},
+ $self->{lang}->{ImagesOptionsFgTooltip});
+
+ $self->{groupItems} = new GCLabel($self->{lang}->{DetailedOptionsGroupItems});
+ $self->{groupByOption} = new GCFieldSelector(0, undef, 1);
+ $self->{groupByOption}->setModel($parent->{model});
+
+ $self->{secondarySort} = new GCLabel($self->{lang}->{DetailedOptionsSecondarySort});
+ $self->{secondarySortOption} = new GCFieldSelector(0, undef, 1);
+ $self->{secondarySortOption}->setModel($parent->{model});
+
+# my $tableDisplay = new Gtk2::Table(5,2);
+# $tableDisplay->set_row_spacings($GCUtils::halfMargin);
+# $tableDisplay->set_col_spacings($GCUtils::margin);
+# $tableDisplay->set_border_width($GCUtils::margin);
+ my $imagesDisplayExpander = new GCExpander($self->{lang}->{OptionsImagesDisplayGroup}, 1);
+ $self->attach($imagesDisplayExpander, 0, 4, 0, 1, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+ $self->attach($self->{resizeImgList}, 2, 4, 1, 2, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+ $self->attach($self->{animateImgList}, 2, 4, 2, 3, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+ $self->attach($self->{labelColumns}, 2, 3, 3, 4, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+ $self->attach($self->{columns}, 3, 4, 3, 4, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+ $self->attach($self->{imgSizeLabel}, 2, 3, 4, 5, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+ $self->attach($self->{imgSizeOption}, 3, 4, 4, 5, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+ $self->attach($self->{groupItems}, 2, 3, 5, 6, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+ $self->attach($self->{groupByOption}, 3, 4, 5, 6, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+ $self->attach($self->{secondarySort}, 2, 3, 6, 7, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+ $self->attach($self->{secondarySortOption}, 3, 4, 6, 7, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+
+ my $imagesStyleExpander = new GCExpander($self->{lang}->{OptionsImagesStyleGroup}, 1);
+ $self->attach($imagesStyleExpander, 0, 4, 8, 9, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+ $self->attach($self->{useOverlays}, 2, 4, 9, 10, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+ $self->attach($self->{listBgPicture}, 2, 4, 10, 11, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+ $self->attach($self->{labelStyle}, 2, 3, 11, 12, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+ $self->attach($self->{optionStyle}, 3, 4, 11, 12, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+ $self->attach($self->{labelBg}, 2, 3, 12, 13, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+ $self->attach($self->{buttonBg}, 3, 4, 12, 13, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+ $self->attach($self->{labelFg}, 2, 3, 13, 14, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+ $self->attach($self->{buttonFg}, 3, 4, 13, 14, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+
+ $imagesDisplayExpander->signal_connect('activate' => sub {
+ if (!$imagesDisplayExpander->get_expanded)
+ {
+ $self->{resizeImgList}->show_all;
+ $self->{labelColumns}->show_all;
+ $self->{columns}->show_all;
+ $self->{imgSizeLabel}->show_all;
+ $self->{imgSizeOption}->show_all;
+ $self->{groupItems}->show_all;
+ $self->{groupByOption}->show_all;
+ $self->{secondarySort}->show_all;
+ $self->{secondarySortOption}->show_all;
+ }
+ else
+ {
+ $self->{resizeImgList}->hide_all;
+ $self->{labelColumns}->hide_all;
+ $self->{columns}->hide_all;
+ $self->{imgSizeLabel}->hide_all;
+ $self->{imgSizeOption}->hide_all;
+ $self->{groupItems}->hide_all;
+ $self->{groupByOption}->hide_all;
+ $self->{secondarySort}->hide_all;
+ $self->{secondarySortOption}->hide_all;
+ }
+ });
+ $imagesStyleExpander->signal_connect('activate' => sub {
+ if (!$imagesStyleExpander->get_expanded)
+ {
+ $self->{useOverlays}->show_all;
+ $self->{listBgPicture}->show_all;
+ $self->{labelStyle}->show_all;
+ $self->{optionStyle}->show_all;
+ $self->{labelBg}->show_all;
+ $self->{buttonBg}->show_all;
+ $self->{labelFg}->show_all;
+ $self->{buttonFg}->show_all;
+ }
+ else
+ {
+ $self->{useOverlays}->hide_all;
+ $self->{listBgPicture}->hide_all;
+ $self->{labelStyle}->hide_all;
+ $self->{optionStyle}->hide_all;
+ $self->{labelBg}->hide_all;
+ $self->{buttonBg}->hide_all;
+ $self->{labelFg}->hide_all;
+ $self->{buttonFg}->hide_all;
+ }
+ });
+
+ $self->show_all;
+ $imagesDisplayExpander->set_expanded(1);
+ $imagesStyleExpander->set_expanded(1);
+
+ bless ($self, $class);
+ return $self;
+ }
+}
+
+
+
+{
+ # Class used to let user select detailed options
+ package GCDetailedOptionsPanel;
+ use base "Gtk2::VBox";
+
+
+ sub initValues
+ {
+ my $self = shift;
+
+ $self->{imgSizeOption}->setValue($self->{optionsManager}->detailImgSize);
+ $self->{groupByOption}->setValue($self->{optionsManager}->groupBy);
+ $self->{secondarySortOption}->setValue($self->{optionsManager}->secondarySort);
+ $self->{groupedFirst}->setValue($self->{optionsManager}->groupedFirst);
+ $self->{addCount}->setValue($self->{optionsManager}->addCount);
+
+ my @tmpFieldsArray = split m/\|/, $self->{optionsManager}->details;
+ $self->{fieldsSelection}->setListFromIds(\@tmpFieldsArray);
+
+ }
+
+ sub saveValues
+ {
+ my $self = shift;
+ $self->{optionsManager}->detailImgSize($self->{imgSizeOption}->getValue);
+ $self->{optionsManager}->groupBy($self->{groupByOption}->getValue);
+ $self->{optionsManager}->secondarySort($self->{secondarySortOption}->getValue);
+ $self->{optionsManager}->groupedFirst($self->{groupedFirst}->getValue);
+ $self->{optionsManager}->addCount($self->{addCount}->getValue);
+ my $details = join('|', @{$self->{fieldsSelection}->getSelectedIds});
+ $self->{optionsManager}->details($details);
+ }
+
+ sub hideExtra
+ {
+ my $self = shift;
+
+ }
+
+ sub show
+ {
+ my $self = shift;
+
+ $self->initValues;
+
+ my $code = $self->SUPER::show;
+ if ($code eq 'ok')
+ {
+ $self->saveValues;
+ }
+ $self->hide;
+ }
+
+ sub new
+ {
+ my ($proto, $optionsManager, $parent, $preIdList) = @_;
+ my $class = ref($proto) || $proto;
+ my $self = $class->SUPER::new(0,0);
+
+ $self->{lang} = $parent->{lang};
+ $self->{optionsManager} = $optionsManager;
+
+ bless ($self, $class);
+
+ $self->set_border_width($GCUtils::margin);
+
+ $self->{groupItems} = new GCLabel($self->{lang}->{DetailedOptionsGroupItems});
+ $self->{groupByOption} = new GCFieldSelector(0, undef, 1);
+ $self->{groupByOption}->setModel($parent->{model});
+
+ $self->{secondarySort} = new GCLabel($self->{lang}->{DetailedOptionsSecondarySort});
+ $self->{secondarySortOption} = new GCFieldSelector(0, undef, 1);
+ $self->{secondarySortOption}->setModel($parent->{model});
+
+ $self->{groupedFirst} = new GCCheckBox($self->{lang}->{DetailedOptionsGroupedFirst});
+ $self->{addCount} = new GCCheckBox($self->{lang}->{DetailedOptionsAddCount});
+
+ $self->{imgSizeLabel} = new GCLabel($self->{lang}->{DetailedOptionsImageSize});
+ $self->{imgSizeOption} = new GCMenuList;
+ my %imgSizes = %{$self->{lang}->{ImagesOptionsSizeList}};
+ my @imgValues = map {{value => $_, displayed => $imgSizes{$_}}}
+ (sort keys %imgSizes);
+ $self->{imgSizeOption}->setValues(\@imgValues);
+
+ my $preferencesExpander = new GCExpander($self->{lang}->{OptionsDetailedPreferencesGroup}, 1);
+ $self->pack_start($preferencesExpander, 0, 0, $GCUtils::quarterMargin);
+
+ my $tablePreferences = new Gtk2::Table(4, 5);
+ $tablePreferences->set_col_spacings($GCUtils::margin);
+
+ $tablePreferences->attach($self->{imgSizeLabel}, 2, 3, 1, 2, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+ $tablePreferences->attach($self->{imgSizeOption}, 3, 4, 1, 2, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+
+ $tablePreferences->attach($self->{groupItems}, 2, 3, 2, 3, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+ $tablePreferences->attach($self->{groupByOption}, 3, 4, 2, 3, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+ $tablePreferences->attach($self->{secondarySort}, 2, 3, 3, 4, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+ $tablePreferences->attach($self->{secondarySortOption}, 3, 4, 3, 4, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+
+ $tablePreferences->attach($self->{groupedFirst}, 2, 4, 4, 5, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+ $tablePreferences->attach($self->{addCount}, 2, 4, 5, 6, 'fill', 'fill', 0, $GCUtils::quarterMargin);
+
+ $preferencesExpander->add($tablePreferences);
+
+ my $fieldsExpander = new GCExpander($self->{lang}->{DetailedOptionsFields}, 1);
+ my @tmpFieldsArray = split m/\|/, $optionsManager->details;
+ $self->{fieldsSelection} = new GCFieldsSelectionWidget($parent, \@tmpFieldsArray, 1);
+
+ $self->pack_start($fieldsExpander, 0, 0, $GCUtils::quarterMargin);
+ $fieldsExpander->add($self->{fieldsSelection});
+ $self->{fieldsSelection}->set_border_width($GCUtils::margin);
+
+ $preferencesExpander->set_expanded(1);
+ $fieldsExpander->set_expanded(1);
+
+ $self->show_all;
+ return $self;
+ }
+}
+
+1;
diff --git a/lib/gcstar/GCItemsLists/GCTextLists.pm b/lib/gcstar/GCItemsLists/GCTextLists.pm
new file mode 100644
index 0000000..aaa080b
--- /dev/null
+++ b/lib/gcstar/GCItemsLists/GCTextLists.pm
@@ -0,0 +1,2101 @@
+package GCTextLists;
+
+###################################################
+#
+# Copyright 2005-2010 Christian Jodar
+#
+# This file is part of GCstar.
+#
+# GCstar is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# GCstar is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GCstar; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+#
+###################################################
+
+#
+# This file handle the left list part, in text Mode and Detailed Mode
+#
+
+use strict;
+use locale;
+
+{
+ package GCBaseTextList;
+ use base "Gtk2::ScrolledWindow";
+ use GCUtils;
+
+ sub new
+ {
+ my ($proto, $parent) = @_;
+ my $class = ref($proto) || $proto;
+ my $self = $class->SUPER::new;
+ bless ($self, $class);
+ $self->{parent} = $parent;
+ $self->{preferences} = $parent->{model}->{preferences};
+ $self->{preferences}->sortOrder(1)
+ if ! $self->{preferences}->exists('sortOrder');
+ $self->{count} = 0;
+ return $self;
+ }
+
+ sub getNbItems
+ {
+ my $self = shift;
+ return $self->{count};
+ }
+
+ sub convertIterToChildIter
+ {
+ my ($self, $iter) = @_;
+ my $result = $self->{completeModel}->convert_iter_to_child_iter($iter);
+ $result = $self->{subModel}->convert_iter_to_child_iter($result);
+ return $result;
+ }
+
+ sub convertChildIterToIter
+ {
+ my ($self, $iter) = @_;
+ my $result = $iter;
+ $result = $self->{subModel}->convert_child_iter_to_iter($result);
+ $result = $self->{completeModel}->convert_child_iter_to_iter($result);
+ return $result;
+ }
+
+ sub convertIterToString
+ {
+ my ($self, $iter) = @_;
+ return '' if ! $iter;
+
+ return $self->{completeModel}->get_string_from_iter($iter);
+ }
+
+ sub convertIdxToIter
+ {
+ my ($self, $idx) = @_;
+ $self->{completeModel}->foreach(sub {
+ my ($model, $path, $iter) = @_;
+ if (($self->convertIterToIdx($iter) == $idx)
+ && (!$model->iter_n_children($iter)))
+ {
+ $self->{currentIterString} = $model->get_path($iter)->to_string;
+ return 1;
+ }
+ return 0;
+ });
+ }
+
+ sub selectAll
+ {
+ my $self = shift;
+
+ $self->{list}->get_selection->select_all;
+ }
+
+ sub selectIter
+ {
+ my ($self, $iter, $deactivateUpdate) = @_;
+ $self->{deactivateUpdate} = $deactivateUpdate;
+ $self->{list}->get_selection->unselect_all;
+ $self->{list}->get_selection->select_iter($iter);
+ $self->{deactivateUpdate} = 0;
+ }
+
+ sub getCurrentIter
+ {
+ my $self = shift;
+ my $iter = undef;
+ #my $iter = $self->{list}->get_selection->get_selected;
+ my @rows = $self->{list}->get_selection->get_selected_rows;
+ $iter = $self->{list}->get_model->get_iter($rows[0]) if $rows[0];
+ return $iter;
+ }
+
+ sub getCurrentItems
+ {
+ my $self = shift;
+ my @indexes;
+ my @iterators;
+ my @rows = $self->{list}->get_selection->get_selected_rows;
+ foreach (@rows)
+ {
+ my $iter = $self->{list}->get_model->get_iter($_);
+ push @iterators, $iter;
+ push @indexes, $self->convertIterToIdx($iter);
+ }
+ @indexes = sort @indexes;
+ return (\@indexes, \@iterators) if wantarray;
+ return \@indexes;
+ }
+
+ sub getCurrentIterFromString
+ {
+ my $self = shift;
+ return ($self->{currentIterString})
+ ? $self->{completeModel}->get_iter_from_string($self->{currentIterString})
+ : $self->{completeModel}->get_iter_first;
+ }
+
+ sub removeCurrentItems
+ {
+ my ($self) = @_;
+ my ($indexes, $iterators) = $self->getCurrentItems;
+
+ $self->{deactivateSortCache} = 1;
+
+ my ($nextIter, $newIdx) = $self->getNextIter($iterators->[-1], $indexes);
+ my $realIter = $self->convertIterToChildIter($nextIter);
+ my $nextPath = $self->{model}->get_path($realIter)->to_string;
+
+ my $count = scalar @$indexes;
+
+ $self->{count} -= $count;
+ $self->{nextItemIdx} -= $count;
+
+ $self->{list}->expand_to_path($self->{completeModel}->get_path($nextIter))
+ if $self->{currentIterString} =~ /:/;
+ $self->selectIter($nextIter)
+ if ($nextIter);
+
+ my @toBeRemoved;
+ my %pathToChange;
+ foreach my $number(@$indexes)
+ {
+ #Shift backward all following items.
+ $self->{model}->foreach(sub {
+ my ($model, $path, $iter) = @_;
+ return 0 if $model->iter_has_child($iter);
+ my $currentIdx = ($model->get($iter))[$self->{idxColumn}];
+ if ($currentIdx > $number)
+ {
+ $pathToChange{$path->to_string}++;
+ }
+ elsif ($currentIdx == $number)
+ {
+ # We store them for future removal
+ push @toBeRemoved, new Gtk2::TreeRowReference($model, $path);
+ }
+ return 0;
+ });
+ }
+
+ # Perform the actual shift
+ my $offset = 0;
+ foreach my $path(keys %pathToChange)
+ {
+ my $iter = $self->{model}->get_iter(Gtk2::TreePath->new($path));
+ my $currentIdx = ($self->{model}->get($iter))[$self->{idxColumn}];
+ $self->{model}->set($iter, $self->{idxColumn}, ($currentIdx - $pathToChange{$path}));
+ if ($nextPath eq $path)
+ {
+ $newIdx = $currentIdx - $pathToChange{$path};
+ }
+ $offset++;
+ }
+
+ # Update caches
+ $self->{sorter}->clear_cache;
+ $self->{testCache} = [];
+
+ # Removing all the instances
+ foreach(@toBeRemoved)
+ {
+ $self->removeFromModel($self->{model}->get_iter($_->get_path));
+ }
+
+ $self->{deactivateSortCache} = 0;
+
+ return $newIdx;
+ }
+
+ sub changeItem
+ {
+ # Apply the changes from $previous to $new to the listView entry $idx
+ # Return the $idx of the new current item ($previous can now be hidden)
+ my ($self, $idx, $previous, $new) = @_;
+ $self->convertIdxToIter($idx);
+ return $self->changeCurrent($previous, $new, $idx, 0);
+ }
+
+ sub updateMenus
+ {
+ # Update menu items to reflect number of items selected
+ my ($self, $nbSelected) = @_;
+
+ my $menu = $self->{parent}->{menubar};
+ my @updateList;
+ if ($nbSelected > 1)
+ {
+ @updateList = (
+ [$menu, 'duplicateItem', 'MenuDuplicatePlural'],
+ [$menu, 'deleteCurrentItem', 'MenuEditDeleteCurrentPlural'],
+ [$self->{parent}, 'contextDuplicateItem', 'MenuDuplicatePlural'],
+ [$self->{parent}, 'contextItemDelete', 'MenuEditDeleteCurrentPlural'],
+ [$self->{parent}, 'contextNewWindow', 'MenuNewWindowPlural'],
+ );
+ }
+ else
+ {
+ @updateList = (
+ [$menu, 'duplicateItem', 'MenuDuplicate'],
+ [$menu, 'deleteCurrentItem', 'MenuEditDeleteCurrent'],
+ [$self->{parent}, 'contextDuplicateItem', 'MenuDuplicate'],
+ [$self->{parent}, 'contextItemDelete', 'MenuEditDeleteCurrent'],
+ [$self->{parent}, 'contextNewWindow', 'MenuNewWindow'],
+ );
+ }
+ foreach (@updateList)
+ {
+ $menu->updateItem($_->[0]->{$_->[1]}, $_->[2]);
+ }
+ }
+
+}
+
+{
+ package GCTextList;
+
+ use Gtk2::SimpleList;
+ use GCUtils;
+ use base 'GCBaseTextList';
+
+ sub new
+ {
+ my ($proto, $parent, $title) = @_;
+ my $class = ref($proto) || $proto;
+ my $self = $class->SUPER::new($parent);
+ bless ($self, $class);
+
+ $self->{titleField} = $parent->{model}->{commonFields}->{title};
+ $self->{idxColumn} = 1;
+ $self->{orderSet} = 0;
+
+ $self->set_policy ('automatic', 'automatic');
+ $self->set_shadow_type('none');
+
+ my $columnType = ($parent->{model}->{fieldsInfo}->{$self->{titleField}}->{type} eq 'number') ?
+ 'Glib::Int' :
+ 'Glib::String';
+ my $column = Gtk2::TreeViewColumn->new_with_attributes($title, Gtk2::CellRendererText->new,
+ 'text' => 0);
+ $column->set_resizable(1);
+ $column->set_reorderable(1);
+ $column->set_sort_column_id(0);
+ # Columns are: Title, Index, isVisible
+ $self->{model} = new Gtk2::TreeStore($columnType, 'Glib::Int', 'Glib::Boolean');
+ $self->{filter} = new Gtk2::TreeModelFilter($self->{model});
+ $self->{filter}->set_visible_column(2);
+ {
+ package GCSimpleTreeModelSort;
+ use Glib::Object::Subclass
+ Gtk2::TreeModelSort::,
+ interfaces => [ 'Gtk2::TreeDragDest' ],
+ ;
+
+ sub new
+ {
+ my ($proto, $childModel) = @_;
+ my $class = ref($proto) || $proto;
+ return Glib::Object::new ($class, model => $childModel);
+ }
+ }
+ $self->{sorter} = new GCSimpleTreeModelSort($self->{filter});
+ $self->{subModel} = $self->{filter};
+ $self->{completeModel} = $self->{sorter};
+ $self->{list} = Gtk2::TreeView->new_with_model($self->{sorter});
+ $self->{list}->append_column($column);
+ $self->{list}->set_headers_clickable(1);
+ $self->{list}->set_rules_hint(1);
+ $self->{list}->set_name('GCItemsTextList');
+ $self->{list}->get_selection->set_mode ('multiple');
+ if ($parent->{model}->{fieldsInfo}->{$self->{titleField}}->{type} ne 'number')
+ {
+ $self->{sorter}->set_sort_func(0,
+ \&sortCaseInsensitive,
+ $self);
+ }
+
+ $self->{list}->get_selection->signal_connect ('changed' => sub {
+ return if $self->{deactivateUpdate};
+ my @indexes;
+ my $nbSelected;
+ $self->{list}->get_selection->selected_foreach(sub {
+ my ($model, $path, $iter, $self) = @_;
+ push @indexes, $self->{completeModel}->get_value($iter, 1);
+ $nbSelected++;
+ }, $self);
+ return if scalar @indexes == 0;
+ $parent->display(@indexes);
+ my $iter = $self->getCurrentIter;
+ $self->{currentIterString} = $self->convertIterToString($iter);
+
+ # Update menus to reflect number of items selected
+ $self->updateMenus($nbSelected);
+ });
+
+ $self->{list}->signal_connect ('row-activated' => sub {
+ $parent->displayInWindow;
+ });
+ $self->{list}->signal_connect('button_press_event' => sub {
+ my ($widget, $event) = @_;
+ return 0 if $event->button ne 3;
+
+ # Check if row clicked on is in the current selection
+ my ($path, $column, $cell_x, $cell_y) = $widget->get_path_at_pos( $event->x, $event->y );
+ my $selection = $widget->get_selection;
+ my @rows = $selection->get_selected_rows;
+ my $clickedOnSelection = 0;
+ # Loop through selection to see if current row is selected
+ foreach my $row(@rows)
+ {
+ if ($row->to_string eq $path->to_string)
+ {
+ $clickedOnSelection = 1;
+ }
+ }
+
+ # Popup the menu
+ $self->{parent}->{context}->popup(undef, undef, undef, undef, $event->button, $event->time);
+
+ # If row clicked on was in the selection, return true, else return false to clear selection
+ # to clicked on item
+ if ($clickedOnSelection)
+ {
+ return 1;
+ }
+ else
+ {
+ return 0;
+ }
+ });
+
+ $self->{list}->signal_connect('key-press-event' => sub {
+ my ($widget, $event) = @_;
+ my $key = Gtk2::Gdk->keyval_name($event->keyval);
+ if ($key eq 'Delete')
+ {
+ $self->{parent}->deleteCurrentItem;
+ return 1;
+ }
+ return 0;
+ });
+
+ $self->add($self->{list});
+ $self->show_all;
+ $self->{currentIdx} = 0;
+
+ return $self;
+ }
+
+ sub savePreferences
+ {
+ my ($self, $preferences) = @_;
+ return if !$self->{orderSet};
+ my ($fieldId, $order) = $self->{sorter}->get_sort_column_id;
+ $preferences->sortField($self->{titleField});
+ $preferences->sortOrder(($order eq 'ascending') ? 1 : 0);
+ }
+
+ sub couldExpandAll
+ {
+ my $self = shift;
+
+ return 0;
+ }
+
+ sub convertIterToIdx
+ {
+ my ($self, $iter) = @_;
+ return 0 if ! $iter;
+ return $self->{completeModel}->get_value($iter, 1)
+ }
+
+ sub getCurrentIdx
+ {
+ my $self = shift;
+ my $currentIter = $self->getCurrentIter;
+ return $self->{completeModel}->get_value(
+ $self->getCurrentIter, 1
+ )
+ if $currentIter;
+ return 0;
+ }
+
+ sub isUsingDate
+ {
+ my ($self) = @_;
+ return 0;
+ }
+
+ sub setSortOrder
+ {
+ my ($self, $order, $splash, $willFilter) = @_;
+ $self->{orderSet} = 1;
+ my $progressNeeded = ($splash && !$willFilter);
+ my $step;
+ if ($progressNeeded)
+ {
+ $step = GCUtils::round($self->{count} / 7);
+ $splash->setProgressForItemsSort(2*$step);
+ }
+ $self->{sorter}->set_sort_column_id(0,
+ $self->{preferences}->sortOrder ? 'ascending' : 'descending');
+ $self->{sorter}->set_default_sort_func(undef, undef);
+ $splash->setProgressForItemsSort(4*$step) if $progressNeeded;
+ }
+
+ sub sortCaseInsensitive
+ {
+ my ($childModel, $iter1, $iter2, $self) = @_;
+
+ if ($self->{deactivateSortCache})
+ {
+ my $val1 = uc($childModel->get_value($iter1, 0));
+ my $val2 = uc($childModel->get_value($iter2, 0));
+ # Use a natural string sort method
+ return (GCUtils::gccmp($val1, $val2));
+ }
+ else
+ {
+ my $idx1 = $childModel->get_value($iter1, 1);
+ my $idx2 = $childModel->get_value($iter2, 1);
+ my $val1 = uc($childModel->get_value($iter1, 0));
+ my $val2 = uc($childModel->get_value($iter2, 0));
+ # Use a natural string sort method
+ return (GCUtils::gccmp($val1, $val2));
+ }
+ }
+
+ sub testIter
+ {
+ my ($self, $filter, $items, $iter) = @_;
+ my $idx = ($self->{model}->get($iter))[1];
+ my $displayed;
+ if (defined $self->{testCache}->[$idx])
+ {
+ $displayed = $self->{testCache}->[$idx];
+ }
+ else
+ {
+ $displayed = $self->{testCache}->[$idx] = $filter->test($items->[$idx]);
+ # We increment only here to count only unique items
+ $self->{count}++ if $displayed;
+ }
+ $self->{model}->set($iter,
+ 2,
+ $displayed
+ );
+ return $displayed;
+ }
+
+ sub setFilter
+ {
+ my ($self, $filter, $items, $refresh, $splash) = @_;
+ $self->{count} = 0;
+ $self->{testCache} = [];
+ $self->{tester} = $filter;
+ my $iter = $self->{model}->get_iter_first;
+ my $idx = 0;
+ $self->{model}->foreach(sub {
+ $splash->setProgressForItemsSort($idx++) if $splash;
+ my ($model, $path, $iter) = @_;
+ GCTextList::testIter($self, $filter, $items, $iter);
+ return 0;
+ });
+ $self->{filter}->refilter;
+
+ my $currentIter = $self->getCurrentIter;
+ return $self->{completeModel}->get_value($currentIter, 1)
+ if $currentIter;
+ if ($self->{completeModel}->get_iter_first)
+ {
+ my $idx = $self->{completeModel}->get_value($self->{completeModel}->get_iter_first, 1);
+ $self->select($idx);
+ return $idx;
+ }
+ else
+ {
+ return 0;
+ }
+
+ }
+
+ sub clearCache
+ {
+ my $self = shift;
+ }
+
+ sub reset
+ {
+ my $self = shift;
+ $self->{list}->set_model(undef);
+ $self->{model}->clear;
+ $self->{currentIdx} = 0;
+ $self->{nextItemIdx} = -1;
+ }
+
+ sub done
+ {
+ my $self = shift;
+ $self->{list}->set_model($self->{sorter});
+ }
+
+ sub getNextIter
+ {
+ my ($self, $viewedIter) = @_;
+ my $nextIter = $self->{completeModel}->iter_next($viewedIter);
+ # If we removed the last one, we are using the previous one.
+ $nextIter = $self->{completeModel}->iter_nth_child(undef, $self->{count} - 1)
+ if !$nextIter && ($self->{count} > 0);
+ my $newIdx = 0;
+ if ($nextIter)
+ {
+ $newIdx = $self->{completeModel}->get_value($nextIter, 1);
+ #$self->selectIter($nextIter);
+ }
+ return ($nextIter, $newIdx);
+ }
+
+ sub addItem
+ {
+ my ($self, $info, $immediate) = @_;
+ $self->{nextItemIdx}++;
+ $self->{count}++;
+ my @data = (
+ 0 => $self->{parent}->transformTitle($info->{$self->{titleField}}),
+ 1 => $self->{nextItemIdx},
+ 2 => 1
+ );
+ $self->{model}->set($self->{model}->append(undef), @data);
+ }
+
+ sub removeFromModel
+ {
+ my ($self, $iter) = @_;
+ $self->{model}->remove($iter);
+ }
+
+ sub removeItem
+ {
+ my ($self, $number) = @_;
+ splice @{$self->{testCache}}, $number, 1;
+ #Shift backward all following items.
+ $self->{model}->foreach(sub {
+ my ($model, $path, $iter) = @_;
+ my $currentIdx = $model->get_value($iter,1);
+ if ($currentIdx >= $number)
+ {
+ $model->set($iter, 1, $currentIdx - 1)
+ if $currentIdx != $number;
+ }
+ return 0;
+ });
+
+ $self->{count}--;
+ $self->{nextItemIdx}--;
+ my $viewedCurrentIter = $self->getCurrentIter;
+ my $currentIter = $self->convertIterToChildIter($viewedCurrentIter);
+ my $newIdx = $self->selectNextIter($viewedCurrentIter);
+ $self->{model}->remove($currentIter);
+ return $newIdx;
+ }
+
+ sub select
+ {
+ my ($self, $idx, $init) = @_;
+ if ($idx == -1)
+ {
+ $self->{currentIterString} = '0';
+ my $currentIter = $self->{completeModel}->get_iter_first;
+ if (!$currentIter)
+ {
+ $idx = 0;
+ return;
+ }
+ $idx = $self->{completeModel}->get_value($currentIter, 1);
+ $self->selectIter($currentIter);
+ }
+ else
+ {
+ $self->{currentIterString} = '0' if ! $self->{currentIterString};
+ $self->{completeModel}->foreach(sub {
+ my ($model, $path, $iter) = @_;
+ if ($model->get_value($iter, 1) == $idx)
+ {
+ $self->{currentIterString} = $model->get_path($iter)->to_string;
+ $self->selectIter($iter);
+ return 1;
+ }
+ return 0;
+ });
+ }
+ return $idx;
+ }
+
+ sub showCurrent
+ {
+ my $self = shift;
+
+ my $path = $self->{list}->get_selection->get_selected_rows;
+ $self->{list}->scroll_to_cell($path) if $path;
+ }
+
+ sub changeCurrent
+ {
+ my ($self, $previous, $new, $idx, $wantSelect) = @_;
+ my $selected = $self->getCurrentIterFromString;;
+ return if ! $selected;
+ my $currentIter = $self->convertIterToChildIter($selected);
+ my $newValue = $self->{parent}->transformTitle($new->{$self->{titleField}});
+ my $newIdx = $idx;
+ my $visible = $self->{tester} ? $self->{tester}->test($new) : 1;
+
+ if (!$visible)
+ {
+ my $nextIter;
+ ($nextIter, $newIdx) = $self->getNextIter($selected);
+ $self->selectIter($nextIter, 1) if $nextIter && $wantSelect;
+ $self->{count}--;
+ }
+ $self->{model}->set($currentIter, 0, $newValue);
+ # Update the isVisible field
+ $self->{model}->set($currentIter, 2, $visible);
+
+ my $iter = $self->getCurrentIter;
+ $self->{currentIterString} = $self->convertIterToString($iter);
+ return $newIdx;
+ }
+
+}
+
+{
+ package GCDetailedList;
+
+ use File::Basename;
+ use base 'GCBaseTextList';
+ use GCUtils;
+
+ sub new
+ {
+ my ($proto, $parent) = @_;
+ my $class = ref($proto) || $proto;
+ my $self = $class->SUPER::new($parent);
+ bless ($self, $class);
+
+ $self->{multi} = 1;
+ $self->{orderSet} = 0;
+ $self->{groupItems} = 0;
+ $self->{isUsingDate} = 0;
+ $self->{titleField} = $parent->{model}->{commonFields}->{title};
+
+ $self->{imgWidth} = 60;
+ $self->{imgHeight} = 80;
+
+ # Setting default options if they don't exist
+ $self->{preferences}->detailImgSize(2) if ! $self->{preferences}->exists('detailImgSize');
+ $self->{preferences}->details($self->{titleField})
+ if ! $self->{preferences}->details;
+ $self->{preferences}->groupedFirst(1)
+ if ! $self->{preferences}->exists('groupedFirst');
+ $self->{preferences}->addCount(0)
+ if ! $self->{preferences}->exists('addCount');
+
+ # Image size
+ my $size = $self->{preferences}->detailImgSize;
+ $self->{factor} = ($size == 0) ? 0.5
+ : ($size == 1) ? 0.8
+ : ($size == 3) ? 1.5
+ : ($size == 4) ? 2
+ : 1;
+ $self->{imgWidth} *= $self->{factor};
+ $self->{imgHeight} *= $self->{factor};
+
+ $self->clearCache;
+
+ $self->set_policy ('automatic', 'automatic');
+ $self->set_shadow_type('none');
+
+ my @tmpArray = split m/\|/, $self->{preferences}->details;
+ $self->{fieldsArray} = \@tmpArray;
+
+ $self->{imageIndex} = -1;
+ my @columnsType;
+ $self->{columnsArray} = [];
+ $self->{columns} = {};
+ my $col = 0;
+
+ $self->{borrowerField} = $parent->{model}->{commonFields}->{borrower}->{name};
+
+ $self->setGroupingInformation;
+ # We don't need count if not grouped
+ $self->{addCount} = $self->{groupItems} && $self->{preferences}->addCount;
+
+ $self->{secondaryField} = $self->{preferences}->secondarySort;
+ $self->{secondaryIndex} = -1;
+ $self->{addSecondary} = 0;
+
+ foreach my $field(@tmpArray)
+ {
+ my $title = $parent->{model}->{fieldsInfo}->{$field}->{displayed};
+
+ my $renderer;
+ my $attribute;
+ if ($parent->{model}->{fieldsInfo}->{$field}->{type} eq 'image')
+ {
+ push @columnsType, 'Gtk2::Gdk::Pixbuf';
+ $renderer = Gtk2::CellRendererPixbuf->new;
+ $attribute = 'pixbuf';
+ $self->{imageIndex} = $col;
+ }
+ elsif ($parent->{model}->{fieldsInfo}->{$field}->{type} eq 'yesno')
+ {
+ push @columnsType, 'Glib::Boolean';
+ $renderer = Gtk2::CellRendererToggle->new;
+ $attribute = 'active';
+ }
+ elsif ($parent->{model}->{fieldsInfo}->{$field}->{type} eq 'number')
+ {
+ push @columnsType, 'Glib::Double';
+ $renderer = Gtk2::CellRendererText->new;
+ $attribute = 'text';
+ }
+ else
+ {
+ $self->{isUsingDate} = 1
+ if $parent->{model}->{fieldsInfo}->{$field}->{type} eq 'date';
+ push @columnsType, 'Glib::String';
+ $renderer = Gtk2::CellRendererText->new;
+ $attribute = 'text';
+ }
+ $self->{secondaryIndex} = $col if $field eq $self->{secondaryField};
+ $self->{columns}->{$field} = Gtk2::TreeViewColumn->new_with_attributes($title, $renderer,
+ ($attribute) ? ($attribute => $col) : ());
+ if ($parent->{model}->{fieldsInfo}->{$field}->{type} eq 'number')
+ {
+ $self->{columns}->{$field}->set_cell_data_func($renderer, sub {
+ my ($column, $cell, $model, $iter, $colNum) = @_;
+ my $value = $model->get_value($iter, $colNum);
+ # TODO - not returning correct value when first column is a number
+ # and a grouping field is set.
+ # Remove trailing 0
+ $value =~ s/\.[0-9]*?0+$//;
+ $cell->set_property('text', $value);
+ }, $col);
+ }
+
+ # We store the field name in it to ease the save of the column order in preferences
+ $self->{columns}->{$field}->{field} = $field;
+ $self->{columns}->{$field}->set_resizable(1);
+ $self->{columns}->{$field}->set_reorderable(1);
+ if ($parent->{model}->{fieldsInfo}->{$field}->{type} eq 'image')
+ {
+ $self->{columns}->{$field}->set_clickable(0);
+ }
+ else
+ {
+ $self->{columns}->{$field}->set_sort_column_id($col);
+ }
+ push @{$self->{columnsArray}}, $self->{columns}->{$field};
+ $self->{fieldToId}->{$field} = $col;
+ $col++;
+ }
+ push @columnsType, 'Glib::Int';
+ $self->{idxColumn} = $col;
+ push @columnsType, 'Glib::Boolean';
+ $self->{visibleCol} = ++$col;
+
+ # There is a secondary field for sort, but we didn't add it yet
+ if ($self->{secondaryField} && ($self->{secondaryIndex} == -1))
+ {
+ push @columnsType, 'Glib::String';
+ $self->{addSecondary} = 1;
+ $self->{secondaryIndex} = ++$col;
+ }
+
+ $self->{model} = new Gtk2::TreeStore(@columnsType);
+ {
+ package GCTreeModelSort;
+ use Glib::Object::Subclass
+ Gtk2::TreeModelSort::,
+ interfaces => [ Gtk2::TreeDragDest:: ],
+ ;
+
+ sub new
+ {
+ my ($proto, $childModel) = @_;
+ my $class = ref($proto) || $proto;
+ return Glib::Object::new ($class, model => $childModel);
+ }
+ }
+
+ $self->{filter} = new Gtk2::TreeModelFilter($self->{model});
+ $self->{sorter} = new GCTreeModelSort($self->{filter});
+
+ $self->{filter}->set_visible_column($self->{visibleCol});
+
+ $self->{subModel} = $self->{filter};
+ $self->{completeModel} = $self->{sorter};
+
+ $self->{list} = Gtk2::TreeView->new_with_model($self->{completeModel});
+ $self->{list}->append_column($_) foreach (@{$self->{columnsArray}});
+
+ $self->{list}->set_name('GCItemsDetailsList');
+ $self->{list}->set_headers_clickable(1);
+ $self->{list}->set_rules_hint(1);
+ $self->{list}->set_reorderable(1);
+
+ # Restore size of columns
+ if ($self->{preferences}->exists('columnsWidths'))
+ {
+ my $i = 0;
+ my @widths = split /\|/, $self->{preferences}->columnsWidths;
+ #/ For syntax highlighting
+ foreach (@{$self->{columnsArray}})
+ {
+ $_->set_sizing('fixed');
+ $_->set_resizable(1);
+ $_->set_fixed_width($widths[$i] || 70);
+ $i++;
+ }
+ }
+
+ # Use grouping field for generated masters, or if that field isn't displayed,
+ # fallback on title, or first column
+ if (exists $self->{columns}->{$self->{collectionField}})
+ {
+ $self->{generatedField} = $self->{collectionField};
+ }
+ elsif (exists $self->{columns}->{$self->{titleField}})
+ {
+ $self->{generatedField} = $self->{titleField};
+ }
+ else
+ {
+ $self->{generatedField} = $tmpArray[0];
+ }
+ $self->{generatedIndex} = $self->{fieldToId}->{$self->{generatedField}};
+ $self->{list}->set_expander_column($self->{columns}->{$self->{generatedField}});
+
+ if (exists $self->{columns}->{$self->{titleField}})
+ {
+ $self->{searchField} = $self->{titleField};
+ }
+ else
+ {
+ $self->{searchField} = $tmpArray[0];
+ }
+ $self->{searchIndex} = $self->{fieldToId}->{$self->{searchField}};
+ $self->{list}->set_search_column($self->{searchIndex});
+
+ $self->{sorter}->signal_connect('rows-reordered' => sub {
+ my ($fieldId, $order) = $self->{sorter}->get_sort_column_id;
+ $self->{list}->set_search_column($fieldId);
+ });
+
+ # Initializing sort methods
+ my $colIdx = 0;
+ #my $secondarySort = 1;
+ my $sorttype = "";
+ foreach my $field(@tmpArray)
+ {
+ my ($secondaryIndex, $secondaryField) = ($self->{secondaryIndex}==-1)
+ ? ($colIdx, $field)
+ : ($self->{secondaryIndex}, $self->{secondaryField});
+ my $data = [$self, $colIdx, $secondaryIndex];
+ foreach my $sorter($field, $secondaryField)
+ {
+ next if !$sorter;
+ # Work out how to sort the column
+ if ((!exists $self->{columns}->{$self->{collectionField}}) &&
+ ($self->{groupItems}) &&
+ ($sorter eq $self->{generatedField}))
+ {
+ # The grouping field isn't visible, so we need to set
+ # the sort order on the column we're using for the group
+ # headers to be sorted using the grouping field type
+ if (($parent->{model}->{fieldsInfo}->{$self->{collectionField}}->{type} eq 'date') ||
+ ($parent->{model}->{fieldsInfo}->{$self->{collectionField}}->{sorttype} eq 'date'))
+ {
+ $sorttype = "date";
+ }
+ elsif (($parent->{model}->{fieldsInfo}->{$self->{collectionField}}->{type} eq 'number') ||
+ ($parent->{model}->{fieldsInfo}->{$self->{collectionField}}->{sorttype} eq 'number'))
+ {
+ $sorttype = "number";
+ }
+ else
+ {
+ $sorttype = "text";
+ }
+ }
+ elsif (($parent->{model}->{fieldsInfo}->{$sorter}->{type} eq 'number') ||
+ ($parent->{model}->{fieldsInfo}->{$sorter}->{sorttype} eq 'number'))
+ {
+ $sorttype = "number";
+ }
+ elsif (($parent->{model}->{fieldsInfo}->{$sorter}->{type} eq 'date') ||
+ ($parent->{model}->{fieldsInfo}->{$sorter}->{sorttype} eq 'date'))
+ {
+ $sorttype = "date";
+ }
+ else
+ {
+ $sorttype = "text";
+ }
+
+ # Set the column for the desired type of sorting
+ if ($sorttype eq "number")
+ {
+ # Small trick to convert number as follows with a letter
+ # in front of them so cmp will work as expected
+ # e.g.: 3 -> b3; 42 -> c42; 56 -> c56; 5897446 -> h5897446
+ # This could not work if your system uses a character encoding
+ # which is not contiguous as EBCDIC
+ push @$data, sub {return chr(length($_[0]*1000)+ord('a')).($_[0]*1000)};
+ }
+ elsif ($sorttype eq "date")
+ {
+ push @$data, \&GCPreProcess::reverseDate;
+ }
+ else
+ {
+ #push @$data, \&uc;
+ push @$data, sub {return uc $_[0]};
+ }
+ }
+ if ($self->{groupItems} && ($self->{preferences}->groupedFirst))
+ {
+ $self->{sorter}->set_sort_func($colIdx,
+ \&sortWithParentFirst,
+ $data);
+ }
+ else
+ {
+ $self->{sorter}->set_sort_func($colIdx,
+ \&sortAll,
+ $data);
+ }
+ $colIdx++;
+ }
+ $self->{list}->get_selection->set_mode ('multiple');
+
+ $self->{list}->get_selection->signal_connect('changed' => \&onSelectionChanged,
+ $self);
+
+# $self->{list}->get_selection->set_select_function(sub {
+# my ($selection, $model, $path) = @_;
+# return !$model->iter_has_child($model->get_iter($path));
+# });
+
+ $self->{list}->signal_connect ('row-activated' => sub {
+ $parent->displayInWindow;
+ });
+
+ $self->add($self->{list});
+
+ $self->{list}->signal_connect('button_press_event' => sub {
+ my ($widget, $event) = @_;
+ return 0 if $event->button ne 3;
+
+ # Check if row clicked on is in the current selection
+ my ($path, $column, $cell_x, $cell_y) = $widget->get_path_at_pos( $event->x, $event->y );
+ my $selection = $widget->get_selection;
+ my @rows = $selection->get_selected_rows;
+ my $clickedOnSelection = 0;
+ # Loop through selection to see if current row is selected
+ foreach my $row(@rows)
+ {
+ if ($row->to_string eq $path->to_string)
+ {
+ $clickedOnSelection = 1;
+ }
+ }
+
+ # Popup the menu
+ $self->{parent}->{context}->popup(undef, undef, undef, undef, $event->button, $event->time);
+
+ # If row clicked on was in the selection, return true, else return false to clear selection
+ # to clicked on item
+ if ($clickedOnSelection)
+ {
+ return 1;
+ }
+ else
+ {
+ return 0;
+ }
+ });
+
+ $self->{list}->signal_connect('key-press-event' => sub {
+ my ($treeview, $event) = @_;
+ my $key = Gtk2::Gdk->keyval_name($event->keyval);
+ if ($key eq 'Delete')
+ {
+ return 1 if !$self->{count};
+ return 1 if !$self->getCurrentIter;
+ return 1 if $self->{completeModel}->iter_has_child(
+ $self->getCurrentIter
+ );
+ $self->{parent}->deleteCurrentItem;
+ return 1;
+ }
+ return 0;
+ });
+
+ if ($self->{groupItems})
+ {
+ my $targetEntryMove = {
+ target => 'MY_ROW_TARGET',
+ flags => ['same-widget'],
+ info => 42,
+ };
+
+ $self->{list}->enable_model_drag_source('button1-mask','move', $targetEntryMove);
+ $self->{list}->enable_model_drag_dest('move', $targetEntryMove);
+
+ $self->{list}->signal_connect('drag_data_get' => sub {
+ return 1;
+ });
+
+ $self->{list}->signal_connect('drag_data_received' => \&dropHandler, $self);
+ }
+ else
+ {
+ $self->{list}->unset_rows_drag_dest;
+ $self->{list}->unset_rows_drag_source;
+ }
+
+ $self->reset;
+
+ $self->show_all;
+ return $self;
+ }
+
+ sub destroy
+ {
+ my $self = shift;
+ # Unlock panel if we locked it when displaying a category
+ $self->{parent}->{panel}->lock(0);
+ $self->SUPER::destroy;
+ }
+
+ sub onSelectionChanged
+ {
+ my ($selection, $self) = @_;
+ return if $self->{deactivateUpdate};
+ my @indexes;
+ my $nbSelected;
+ $self->{list}->get_selection->selected_foreach(sub {
+ my ($model, $path, $iter, $self) = @_;
+ push @indexes, $self->convertIterToIdx($iter);
+ $nbSelected++;
+ }, $self);
+ return if scalar @indexes == 0;
+ my $iter = $self->getCurrentIter;
+ $self->{selectedIterString} = $self->convertIterToString($iter);
+ $self->{currentRemoved} = 0;
+ $self->{parent}->display(@indexes);
+ $self->{currentIterString} = $self->{selectedIterString}
+ if !$self->{currentRemoved};
+ $self->checkLock;
+
+ # Update menu items to reflect number of items selected
+ $self->updateMenus($nbSelected);
+
+ }
+
+ sub savePreferences
+ {
+ my ($self, $preferences) = @_;
+
+ # Save the columns order and their sizes as pipe separated lists
+ my $details = '';
+ my $widths = '';
+ foreach my $col($self->{list}->get_columns)
+ {
+ $details .= $col->{field}.'|';
+ $widths .= $col->get_width.'|';
+ }
+ $preferences->details($details);
+ $preferences->columnsWidths($widths);
+
+ # We return here if the order has not been set previously
+ return if !$self->{orderSet};
+ my ($fieldId, $order) = $self->{sorter}->get_sort_column_id;
+ $preferences->sortField($self->{fieldsArray}->[$fieldId]);
+ $preferences->sortOrder(($order eq 'ascending') ? 1 : 0);
+
+ }
+
+ sub couldExpandAll
+ {
+ my $self = shift;
+
+ return 1;
+ }
+
+ sub expandAll
+ {
+ my $self = shift;
+
+ $self->{list}->expand_all;
+ }
+
+ sub collapseAll
+ {
+ my $self = shift;
+
+ $self->{list}->collapse_all;
+ }
+
+ sub setGroupingInformation
+ {
+ my $self = shift;
+ $self->{collectionField} = $self->{preferences}->groupBy;
+ $self->{groupItems} = ($self->{collectionField} ne '');
+ }
+
+ sub sortAll
+ {
+ my ($childModel, @iter, $data);
+ ($childModel, $iter[0], $iter[1], $data) = @_;
+ my ($self, $colId1, $colId2, $converter1, $converter2) = @$data;
+
+ my @val;
+ my $colId;
+ my $converter;
+ foreach my $i(0..1)
+ {
+ ($colId, $converter) = ($childModel->iter_parent($iter[$i]))
+ ? ($colId2, $converter2)
+ : ($colId1, $converter1);
+ push @val, $converter->($childModel->get_value($iter[$i], $colId));
+ }
+ return (GCUtils::gccmp($val[0], $val[1]));
+ }
+
+ sub sortWithParentFirst
+ {
+ my ($childModel, $iter1, $iter2, $data) = @_;
+ my ($self, $colId1, $colId2, $converter1, $converter2) = @$data;
+
+ my $hasChildren1 = $childModel->iter_has_child($iter1);
+ my $hasChildren2 = $childModel->iter_has_child($iter2);
+
+ my $colId;
+ my $converter;
+ if ($hasChildren1 == $hasChildren2)
+ {
+ ($colId, $converter) = ($childModel->iter_parent($iter1))
+ ? ($colId2, $converter2)
+ : ($colId1, $converter1);
+
+ # FIXME If we don't copy the value first, it will crash on win32 systems
+ # with an iterator not matching model
+ my $val1 = $converter->($childModel->get_value($iter1, $colId));
+ my $val2 = $converter->($childModel->get_value($iter2, $colId));
+ return (GCUtils::gccmp($val1, $val2));
+ }
+ else
+ {
+ return ($hasChildren1 ? -1 : 1);
+ }
+ }
+
+ sub dropHandler
+ {
+ my ($treeview, $context, $widget_x, $widget_y, $data, $info, $time, $self) = @_;
+ my $source = $context->get_source_widget;
+ return if ($source ne $treeview);
+ my $model = $treeview->get_model;
+ my ($targetPath, $targetPos) = $treeview->get_dest_row_at_pos($widget_x, $widget_y);
+ if ($targetPath)
+ {
+ my $targetIter = $model->get_iter($targetPath);
+ #my $origIter = $treeview->get_selection->get_selected;
+
+ # Deactivate DnD for multiple selection
+ my @rows = $self->{list}->get_selection->get_selected_rows;
+ if (scalar(@rows) > 1)
+ {
+ $context->finish(1,0,$time);
+ return;
+ }
+
+ my $origIter = $self->getCurrentIter;
+ if ($model->iter_has_child($origIter)) # We can't move a master
+ {
+ $context->finish(1,0,$time);
+ return;
+ }
+
+ my $origIdx = $self->convertIterToIdx($origIter);
+ my $origCollection = '';
+ my $origParentIter = $model->iter_parent($origIter);
+ my ($origParentPath, $origParentChildIter);
+ if ($origParentIter)
+ {
+ $origParentChildIter = $self->convertIterToChildIter($origParentIter);
+ $origParentPath = $self->{model}->get_path($origParentChildIter)
+ if ($self->{addCount});
+ }
+ $origCollection = $self->getIterCollection($origParentChildIter)
+ if $origParentChildIter;
+ #We cannot drop an item on itself
+ if ($targetIter == $origIter)
+ {
+ $context->finish(1,0,$time);
+ return;
+ }
+ my @origData;
+ my $i = 0;
+ foreach ($model->get_value($origIter))
+ {
+ push @origData, $i, $_;
+ $i++;
+ }
+ my $collectionIter = $model->iter_parent($targetIter);
+ #if ($collectionIter)
+ my $collection = $collectionIter
+ ? $self->getIterCollection($self->convertIterToChildIter($collectionIter))
+ : $self->getIterCollection($self->convertIterToChildIter($targetIter));
+
+ my $newIter;
+ my $refreshCountNeeded = 0;
+ if ($targetPos =~ /^into/)
+ {
+ if (
+ (!$model->iter_has_child($targetIter)) # We can't drop on a single item
+ || ($targetPath->get_depth > 1) # We can't add an item to an item in a collection.
+ || ($model->iter_has_child($origIter)) # We can't move a master
+ )
+ {
+ $context->finish(1,0,$time);
+ return;
+ }
+ else
+ {
+ #Creating a new collection item
+ $newIter = $self->{model}->append($self->convertIterToChildIter($targetIter));
+ $refreshCountNeeded = 1;
+ }
+ }
+ else
+ {
+ my $origPath = $model->get_path($origIter);
+ if ($targetPath->get_depth == 1)
+ {
+ if ($origPath->get_depth == 1)
+ {
+ #Just moving a master item is not allowed
+ $context->finish(1,0,$time);
+ return;
+ }
+ else
+ {
+ #We get an item out of a collection
+ $newIter = $self->{model}->append(undef);
+ $collection = '';
+ }
+ }
+ else
+ {
+ #We are placing a collection item
+ $newIter = $self->{model}->append(
+ $self->convertIterToChildIter($model->iter_parent($targetIter))
+ );
+ $refreshCountNeeded = 1;
+ }
+ }
+
+ $self->{model}->set($newIter, @origData);
+ if ($self->{addCount})
+ {
+ # Refreshing target
+ $self->refreshCount($self->{model}->iter_parent($newIter))
+ if ($refreshCountNeeded);
+
+ # Refreshing origin
+ # We remove 1 to the count because the original has not been removed yet
+ # It will be removed when returning from this method
+ $self->refreshCount($self->{model}->get_iter($origParentPath), 0, -1)
+ if $origParentPath;
+ }
+ #$origIter = $treeview->get_selection->get_selected;
+ $origIter = $self->getCurrentIter;
+ #$self->removeParentIfNeeded($origIter);
+
+ # Removing previous instances in other collections
+ $self->removeOtherInstances($origCollection, $origIdx);
+
+ $self->{parent}->{items}->setValue($origIdx, $self->{collectionField}, $collection);
+ $context->finish(1,1,$time);
+ $self->select($origIdx);
+ $self->{parent}->markAsUpdated;
+ }
+ }
+
+ sub removeOtherInstances
+ {
+ my ($self, $collection, $idx, $fullCollection) = @_;
+ if ($collection)
+ {
+ my $collectionArray =
+ $self->transformValue(
+ $fullCollection || $self->{parent}->{items}->getValue($idx, $self->{collectionField}),
+ $self->{collectionField},
+ 0,
+ $self->{groupItems}
+ );
+ if (ref($collectionArray) eq 'ARRAY')
+ {
+ foreach (@$collectionArray)
+ {
+ next if $_ eq $collection;
+ $self->removeInCollection($_, $idx);
+ }
+ }
+
+ }
+ }
+
+ sub removeParentIfNeeded
+ {
+ my ($self, $iter) = @_;
+ #Destroy the previous auto-generated item if there was only one child
+ my $parentIter = $self->{model}->iter_parent($self->convertIterToChildIter($iter));
+ if ($parentIter && ($self->{model}->iter_n_children($parentIter) <= 1))
+ {
+ $self->{model}->remove($parentIter);
+ }
+ }
+
+ sub removeFromModel
+ {
+ my ($self, $iter) = @_;
+ my $parentToRemove = undef;
+
+ my $parentIter = $self->{model}->iter_parent($iter);
+ my $refreshCountNeeded = 0;
+ if ($parentIter)
+ {
+ if ($self->{model}->iter_n_children($parentIter) <= 1)
+ {
+ $parentToRemove = $parentIter;
+ }
+ else
+ {
+ # If we removed the 1st one, we should change the index of the master
+ # to be the new 1st
+ my $removedIdx = $self->convertChildIterToIdx($iter);
+ my $firstIdx = $self->convertChildIterToIdx($self->{model}->iter_children($parentIter));
+ if ($firstIdx == $removedIdx)
+ {
+ my $newIdx = $self->convertChildIterToIdx($self->{model}->iter_nth_child($parentIter, 1));
+ $self->{model}->set($parentIter, $self->{idxColumn}, $newIdx);
+ }
+ }
+ # Update count if needed
+ if (($self->{addCount})
+ && ($self->{model}->get($iter, $self->{visibleCol}))
+ && ($self->{model}->get($parentIter, $self->{visibleCol})))
+ {
+ $refreshCountNeeded = 1;
+ }
+
+ }
+
+ $self->{model}->remove($iter);
+ $self->refreshCount($parentIter) if $refreshCountNeeded;
+ $self->{model}->remove($parentToRemove) if $parentToRemove;
+ }
+
+ sub isUsingDate
+ {
+ my ($self) = @_;
+ return $self->{isUsingDate};
+ }
+
+ sub setSortOrder
+ {
+ my ($self, $order, $splash, $willFilter) = @_;
+ $self->{orderSet} = 1;
+ my $progressNeeded = ($splash && !$willFilter);
+ my ($realOrder, $field) = ($self->{preferences}->sortOrder, $self->{preferences}->sortField);
+ my $step;
+ if ($progressNeeded)
+ {
+ $step = GCUtils::round($self->{count} / 7);
+ $splash->setProgressForItemsSort(2*$step);
+ }
+ $self->{sorter}->set_sort_column_id($self->{fieldToId}->{$field},
+ $realOrder ? 'ascending' : 'descending');
+ $self->{list}->set_search_column($self->{fieldToId}->{$field});
+ $self->{sorter}->set_default_sort_func(undef, undef);
+ $splash->setProgressForItemsSort(4*$step) if $progressNeeded;
+ }
+
+ sub testIter
+ {
+ my ($self, $filter, $items, $iter) = @_;
+ my $idx = $self->convertChildIterToIdx($iter);
+ my $displayed;
+ if (exists $self->{testCache}->[$idx])
+ {
+ $displayed = $self->{testCache}->[$idx];
+ }
+ else
+ {
+ $displayed = $self->{testCache}->[$idx] = $filter->test($items->[$idx]);
+ # We increment only here to count only unique items
+ $self->{count}++ if $displayed;
+ }
+ $self->{model}->set($iter,
+ $self->{visibleCol},
+ $displayed
+ );
+ return $displayed;
+ }
+
+ sub setFilter
+ {
+ my ($self, $filter, $items, $refresh, $splash) = @_;
+ $self->{count} = 0;
+ $self->{testCache} = [];
+ $self->{tester} = $filter;
+ my $idx = 0;
+ my $iter = $self->{model}->get_iter_first;
+ while ($iter)
+ {
+ $splash->setProgressForItemsSort($idx++) if $splash;
+ if ($self->{model}->iter_has_child($iter))
+ {
+ my $showParent = 0;
+ my $childIter = $self->{model}->iter_children($iter);
+ while ($childIter)
+ {
+ my $displayed = GCDetailedList::testIter($self, $filter, $items, $childIter);
+ $showParent ||= $displayed;
+ $childIter = $self->{model}->iter_next($childIter);
+ }
+ $self->{model}->set($iter,
+ $self->{visibleCol},
+ $showParent
+ );
+ }
+ else
+ {
+ GCDetailedList::testIter($self, $filter, $items, $iter);
+ }
+ $iter = $self->{model}->iter_next($iter);
+ }
+ $self->{filter}->refilter;
+ $self->refreshCounts if ($self->{addCount});
+ my $currentIter = $self->getCurrentIter;
+ return $self->convertIterToIdx($currentIter)
+ if $currentIter;
+ $idx = $self->convertIterToIdx($self->{completeModel}->get_iter_first);
+ return $idx;
+ }
+
+ sub getPixbufFromCache
+ {
+ my ($self, $path) = @_;
+
+ my $realPath = GCUtils::getDisplayedImage($path,
+ $self->{parent}->{defaultImage},
+ $self->{parent}->{options}->file);
+
+ if (! $self->{cache}->{$realPath})
+ {
+ my $pixbuf;
+ eval {
+ $pixbuf = Gtk2::Gdk::Pixbuf->new_from_file($realPath);
+ };
+ $pixbuf = Gtk2::Gdk::Pixbuf->new_from_file($self->{parent}->{defaultImage})
+ if $@;
+
+ $pixbuf = GCUtils::scaleMaxPixbuf($pixbuf,
+ $self->{imgWidth},
+ $self->{imgHeight},
+ $self->{withImage});
+ $self->{cache}->{$realPath} = $pixbuf;
+ }
+ return $self->{cache}->{$realPath};
+ }
+
+ sub clearCache
+ {
+ my $self = shift;
+ $self->{cache} = {};
+ }
+
+ sub reset
+ {
+ my $self = shift;
+ $self->{list}->set_model(undef);
+ $self->{model}->clear;
+ $self->{alreadyInserted} = {};
+ $self->{currentIdx} = 0;
+ $self->{nextItemIdx} = -1;
+
+ $self->setGroupingInformation;
+ }
+
+ sub done
+ {
+ my $self = shift;
+ $self->{list}->set_model($self->{completeModel});
+ $self->refreshCounts if ($self->{addCount});
+ }
+
+ sub refreshCount
+ {
+ my ($self, $iter, $added, $offset) = @_;
+
+ my $subIter = $self->{subModel}->convert_child_iter_to_iter($iter);
+ my $nbChildren = $subIter
+ ? $self->{subModel}->iter_n_children($subIter)
+ : ($added ? 1 : 0);
+ $nbChildren += $offset if defined $offset;
+ # Version below still works if the orders of filter and sorter are inverted
+ #my $nbChildren = 0;
+ #my $childIter = $self->{model}->iter_children($iter);
+ #while ($childIter)
+ #{
+ # $nbChildren++ if $self->{model}->get($childIter, $self->{visibleCol});
+ # $childIter = $self->{model}->iter_next($childIter);
+ #}
+ my $original = $self->getIterCollection($iter);
+ my $generated = "$original ($nbChildren)";
+ $self->{model}->set($iter,
+ $self->{fieldToId}->{$self->{generatedField}},
+ $generated);
+ $self->{originalValue}->{$generated} = $original;
+ $self->{model}->set($iter, $self->{visibleCol}, $nbChildren);
+ }
+
+ sub refreshCounts
+ {
+ my $self = shift;
+ $self->{model}->foreach(sub {
+ my ($model, $path, $iter) = @_;
+ if ($model->iter_has_child($iter))
+ {
+ $self->refreshCount($iter);
+ }
+ return 0;
+ });
+ }
+
+ sub transformValue
+ {
+ my ($self, $value, $field, $isGeneratedMaster, $multiAllowed, $isForSort, $forceArray) = @_;
+
+ my $type = '';
+ $type = $self->{parent}->{model}->{fieldsInfo}->{$field}->{type}
+ if defined $self->{parent}->{model}->{fieldsInfo}->{$field}->{type};
+
+ if ($type eq 'image')
+ {
+ $value = ($isGeneratedMaster ? undef : $self->getPixbufFromCache($value));
+ }
+ else
+ {
+ $value = $self->{parent}->transformValue($value, $field, ($multiAllowed && $self->{multi}));
+ }
+ if ($isForSort)
+ {
+ if ($type eq 'date')
+ {
+ $value = GCPreProcess::reverseDate($value);
+ }
+ else
+ {
+ $value = uc $value;
+ }
+ }
+ if ($forceArray)
+ {
+ if (ref($value) ne 'ARRAY')
+ {
+ my @array = ($value);
+ $value = \@array;
+ }
+ }
+ return $value;
+ }
+
+ sub convertChildPathToPath
+ {
+ my ($self, $path) = @_;
+
+ my $result = $path;
+ $result = $self->{subModel}->convert_child_path_to_path($result);
+ $result = $self->{completeModel}->convert_child_path_to_path($result)
+ if $result;
+ return $result;
+ }
+
+ sub getIterCollection
+ {
+ my ($self, $iter, $model) = @_;
+ $model ||= $self->{model};
+ my $val = ($model->get($iter))[$self->{generatedIndex}];
+ if ($self->{addCount} && ($val =~ / \(\d+\)$/))
+ {
+ $val = $self->{originalValue}->{$val}
+ if exists $self->{originalValue}->{$val};
+ }
+ return $val;
+ }
+
+ # This one should be called with a sorted/filtered iterator
+ sub convertIterToIdx
+ {
+ my ($self, $iter) = @_;
+ return 0 if ! $iter;
+ # If we have a master, we return idx from its 1st displayed child
+ if ($self->{completeModel}->iter_has_child($iter))
+ {
+ $iter = $self->{completeModel}->iter_children($iter);
+ }
+ return $self->{completeModel}->get_value($iter, $self->{idxColumn});
+ }
+
+ # This one should be called with an iterator from real model
+ sub convertChildIterToIdx
+ {
+ my ($self, $iter) = @_;
+ return 0 if ! $iter;
+ return ($self->{model}->get($iter))[$self->{idxColumn}];
+ }
+
+ sub getCurrentIdx
+ {
+ my $self = shift;
+ return $self->convertIterToIdx($self->getCurrentIter);
+ }
+
+ sub findMaster
+ {
+ my ($self, $collection) = @_;
+ my $realCollection = $self->{parent}->transformTitle($collection);
+ my $master = $self->{model}->get_iter_first;
+
+ while ($master)
+ {
+ return $master if ($self->{model}->iter_has_child($master))
+ && ($self->getIterCollection($master) eq $realCollection);
+ $master = $self->{model}->iter_next($master);
+ }
+ return undef;
+ }
+
+ sub removeInCollection
+ {
+ my ($self, $collec, $idx) = @_;
+ my $master = $self->findMaster($collec);
+ my $iter = $self->{model}->iter_nth_child($master, 0) || $master;
+ while ($iter)
+ {
+ last if $idx == $self->convertChildIterToIdx($iter);
+ $iter = $self->{model}->iter_next($iter);
+ }
+ if ($iter)
+ {
+ my $removedCurrent = ($self->{selectedIterString} eq $self->{currentIterString});
+ $self->removeFromModel($iter);
+ $self->{currentRemoved} = $removedCurrent;
+ }
+ }
+
+ sub createRowsData
+ {
+ my ($self, $info, $idx, $withTest) = @_;
+ my @data;
+ my $col = 0;
+ my $displayed = 1;
+ if ($withTest)
+ {
+ $displayed = $self->{tester}->test($info)
+ if $self->{tester};
+ $self->{testCache}->[$idx] = $displayed;
+ }
+ foreach my $field(@{$self->{fieldsArray}})
+ {
+ my $value = $self->transformValue($info->{$field}, $field, $info->{isGeneratedMaster});
+ push @data, $col, $value;
+ $col++;
+ }
+ push @data, $col++, $idx;
+ push @data, $col++, $displayed;
+ push @data, $col++, $self->transformValue($info->{$self->{secondaryField}})
+ if $self->{addSecondary};
+ return @data;
+ }
+
+ sub addItem
+ {
+ my ($self, $info, $immediate) = @_;
+
+ $self->{nextItemIdx}++;
+ $self->{count}++;
+
+ my $collection = $self->transformValue($info->{$self->{collectionField}}, $self->{collectionField},
+ 0, $self->{groupItems});
+
+ #Creating data;
+ my @data = $self->createRowsData($info, $self->{nextItemIdx});
+
+ if (
+ (! defined $collection)
+ || ($collection eq '')
+ || (!$self->{groupItems})
+ || (
+ (ref($collection) eq 'ARRAY')
+ && (
+ (! scalar @$collection)
+ || ((scalar @$collection == 1) && ((! defined $collection->[0]) || $collection->[0] eq ''))
+ )
+ )
+ )
+ {
+ #Simple entry
+ $self->{model}->set($self->{model}->append(undef), @data);
+ return;
+ }
+
+ if (ref($collection) ne 'ARRAY')
+ {
+ my @array = ($collection);
+ $collection = \@array;
+ }
+ foreach (@$collection)
+ {
+ next if $_ eq '';
+ if (exists $self->{alreadyInserted}->{$_})
+ {
+ #Master already exists
+ my $master;
+ $master = $self->findMaster($_);
+ my $childIter = $self->{model}->append($master);
+ $self->{model}->set($childIter, @data);
+ }
+ else
+ {
+ #No master and we are a child;
+ #Create the master
+ my $master = $self->{model}->append(undef);
+ $self->{alreadyInserted}->{$_} = 1;
+ my $masterName = $_;
+ my %info = (
+ $self->{generatedField} => $masterName,
+ #$self->{collectionField} => $_,
+ isGeneratedMaster => 1
+ );
+ my @masterData = $self->createRowsData(\%info, -1);
+ $self->{model}->set($master, @masterData);
+ #Insert the child
+ my $childIter = $self->{model}->append($master);
+ $self->{model}->set($childIter, @data);
+ }
+ }
+ }
+
+ sub getNextIter
+ {
+ my ($self, $iter, $indexes) = @_;
+ # Return index of iter following current one in view
+ my $nextIter = $self->{completeModel}->iter_next($iter);
+ if (!$nextIter)
+ {
+ my $parentIter = $self->{completeModel}->iter_parent($iter);
+ if ($parentIter)
+ {
+ $nextIter = $self->{completeModel}->iter_next($parentIter);
+ }
+ if (!$nextIter)
+ {
+ my $nbChildren = $self->{completeModel}->iter_n_children($parentIter);
+ if ($nbChildren > 1)
+ {
+ $nextIter = $self->{completeModel}->iter_nth_child(
+ $parentIter,
+ $self->{completeModel}->iter_n_children($parentIter) - 2
+ );
+ }
+ else
+ {
+ $nextIter = $self->{completeModel}->iter_nth_child(undef, 0);
+ }
+ }
+ }
+ my $idx = $self->convertIterToIdx($nextIter);
+
+ # If the one we got is in the list of the removed ones,
+ # We don't try to get another one, but we return the 1st one
+ if (GCUtils::inArrayTest($idx, @$indexes))
+ {
+ $nextIter = $self->{completeModel}->iter_nth_child(undef, 0);
+ $idx = $self->convertIterToIdx($nextIter);
+ }
+
+ return ($nextIter, $idx);
+ }
+
+ sub select
+ {
+ my ($self, $idx, $init) = @_;
+ my $currentIter;
+ if ($idx == -1)
+ {
+ $self->{currentIterString} = '0';
+ $currentIter = $self->{completeModel}->get_iter_first;
+ }
+ else
+ {
+ $self->convertIdxToIter($idx);
+ $self->{currentIterString} = '0' if ! $self->{currentIterString};
+ $currentIter = $self->getCurrentIterFromString;
+ }
+ $idx = $self->convertIterToIdx($currentIter);
+ return if !$currentIter;
+ if ($init)
+ {
+ my $parent = $self->{completeModel}->iter_parent($currentIter);
+ if ($parent)
+ {
+ my $treePath = $self->{completeModel}->get_path($parent);
+ if (!$self->{list}->row_expanded($treePath))
+ {
+ $self->{currentIterString} = $self->convertIterToString($parent);
+ $idx = $self->getCurrentIdx;
+ }
+ }
+ if ($self->{completeModel}->iter_has_child($currentIter))
+ {
+ $currentIter = $self->{completeModel}->iter_children($currentIter);
+ $idx = $self->convertIterToIdx($currentIter);
+ $self->{currentIterString} = $self->convertIterToString($currentIter);
+ }
+ }
+ $self->{list}->expand_to_path(Gtk2::TreePath->new($self->{currentIterString}))
+ if $self->{currentIterString} =~ /:/;
+ #Lock panel if we are on a master
+ $self->checkLock($currentIter);
+ if ($self->{list}->get_model)
+ {
+ $self->selectIter($currentIter);
+ }
+ return $idx;
+ }
+
+ sub checkLock
+ {
+ my ($self, $iter) = @_;
+ $iter = $self->getCurrentIter if !$iter;
+ if ($self->{completeModel}->iter_n_children($iter) > 0)
+ {
+ $self->{parent}->{panel}->lock(1);
+ }
+ else
+ {
+ $self->{parent}->{panel}->lock(0);
+ }
+ }
+
+ sub showCurrent
+ {
+ my $self = shift;
+
+ my $path = $self->{list}->get_selection->get_selected_rows;
+ $self->{list}->scroll_to_cell($path) if $path;
+ }
+
+ sub changeCurrent
+ {
+ my ($self, $previous, $new, $idx, $wantSelect) = @_;
+ my @data = ();
+ my $col = 0;
+ my $refilterNeeded = 0;
+ my $currentIter = $self->getCurrentIterFromString;
+ return $idx if !$currentIter;
+ my $currentDepth = $self->{completeModel}->get_path($currentIter)->get_depth;
+ my $newIdx = $idx;
+ if (($currentDepth == 1)
+ && $self->{completeModel}->iter_has_child($currentIter))
+ {
+ # We do nothing for generated masters
+ return $newIdx;
+ }
+ $idx = $self->convertIterToIdx($currentIter);
+
+ my $previousCollection = $self->transformValue($previous->{$self->{collectionField}}, $self->{collectionField});
+ my $newCollection = $self->transformValue($new->{$self->{collectionField}}, $self->{collectionField});
+ @data = $self->createRowsData($new, $idx, 1);
+ # If we hide it, we select next one
+ # We double the index because @data contains both values and indexes
+ my $visible = $data[2 * $self->{visibleCol} + 1];
+ if (! $visible)
+ {
+ my $nextIter;
+ ($nextIter, $newIdx) = $self->getNextIter($currentIter);
+ $self->selectIter($nextIter, 1) if $nextIter && $wantSelect;
+ $self->{count}--;
+ }
+
+ if ($self->{groupItems})
+ {
+ if ($previousCollection ne $newCollection)
+ {
+ my $previousCollectionArray;
+ my $newCollectionArray;
+ #An item is integrated or moved into a collection
+ #First we find its master
+ my @parents;
+
+ #Changing collection
+ $previousCollectionArray = $self->transformValue($previous->{$self->{collectionField}}, $self->{collectionField}, 0, $self->{groupItems}, 0, 1);
+ $newCollectionArray = $self->transformValue($new->{$self->{collectionField}}, $self->{collectionField}, 0, $self->{groupItems}, 0, 1);
+ foreach my $collec(@$newCollectionArray)
+ {
+ next if $collec eq '';
+ push @parents, $self->findMaster($collec);
+
+ if (!$parents[-1])
+ {
+ $refilterNeeded = 1;
+ #We have to create a new parent
+ #Create the master
+ $parents[-1] = $self->{model}->append(undef);
+ $self->{alreadyInserted}->{$newCollection} = 1;
+ my %info = (
+ $self->{generatedField} => $collec,
+ $self->{collectionField} => $newCollection,
+ isGeneratedMaster => 1
+ );
+ my @masterData = $self->createRowsData(\%info, -1);
+ $self->{model}->set($parents[-1], @masterData);
+ }
+ else
+ {
+ # The parent already exists
+ # Check if the child were already there
+
+ # If it was, we remove it from parents
+ pop @parents if GCUtils::inArrayTest($collec, @$previousCollectionArray);
+ }
+ }
+
+ my $childIter = 0;
+ if (!scalar @$newCollectionArray)
+ {
+ $childIter = $self->{model}->append(undef);
+ $self->{model}->set($childIter, @data);
+ }
+ else
+ {
+ foreach my $parent(@parents)
+ {
+ #First we insert it at correct position
+ my $cIter = $self->{model}->append($parent);
+ $self->{model}->set($cIter, @data);
+ # We point to the 1st one
+ $childIter = $cIter if !$childIter;
+ # Update count if needed
+ $self->refreshCount($parent, 1) if ($self->{addCount});
+ }
+ }
+
+ #First we store if we are removing the one that has been selected
+ $self->{currentRemoved} = ($self->{selectedIterString} eq $self->{currentIterString});
+
+ # For generated master, we could have copies of our item in many places.
+ # So we need to loop on all the previous collections
+ # If we have a real master, we have juste one occurrence, the current one
+ if (!scalar @$previousCollectionArray)
+ {
+ $self->removeInCollection(undef, $idx);
+ }
+ else
+ {
+ foreach my $collec(@$previousCollectionArray)
+ {
+ $self->removeInCollection($collec, $idx)
+ if !GCUtils::inArrayTest($collec, @$newCollectionArray);
+ }
+ }
+ if ($childIter)
+ {
+ my $childPath = $self->{model}->get_path($childIter);
+ $childPath = $self->convertChildPathToPath($childPath);
+ if ($childPath)
+ {
+ $self->{list}->expand_to_path($childPath);
+ $self->selectIter(
+ $self->{completeModel}->get_iter($childPath)
+ ) if $self->{currentRemoved} && $visible;
+ }
+ }
+ }
+ else
+ {
+ $self->{model}->foreach(sub {
+ my ($model, $path, $iter) = @_;
+ return 0 if $idx != $self->convertChildIterToIdx($iter);
+ $model->set($iter, @data);
+ return 0;
+ });
+ }
+ }
+ else
+ {
+ $self->{model}->set(
+ $self->convertIterToChildIter($currentIter),
+ @data
+ );
+ }
+
+ # Update count for each parents only if we are not changing collection
+ # Because we already did it otherwise
+ if ((! $visible)
+ && ($self->{groupItems})
+ && ($previousCollection eq $newCollection))
+ {
+ my $collectionArray = $self->transformValue($new->{$self->{collectionField}},
+ $self->{collectionField},
+ 0,
+ 1,
+ 0,
+ 1);
+ foreach my $collec(@$collectionArray)
+ {
+ next if $collec eq '';
+ $self->refreshCount($self->findMaster($collec))
+ if ($self->{addCount});
+ }
+ }
+
+ #my $iter = $self->{list}->get_selection->get_selected;
+ my $iter = $self->getCurrentIter;
+ $self->{selectedIterString} = $self->convertIterToString($iter);
+ $self->{currentIterString} = $self->{selectedIterString};
+ $self->showCurrent;
+ $self->{filter}->refilter if $refilterNeeded;
+ return $newIdx;
+ }
+
+}
+
+
+1;