diff options
author | Jörg Frings-Fürst <jff@merkur> | 2014-07-06 15:20:38 +0200 |
---|---|---|
committer | Jörg Frings-Fürst <jff@merkur> | 2014-07-06 15:20:38 +0200 |
commit | 126bb8cb6b93240bb4d3a2b816b74c286c3d422b (patch) | |
tree | e66e1dfe77d53a52539489765c88d23e4423ae27 /lib/gcstar/GCItemsLists |
Imported Upstream version 1.7.0upstream/1.7.0
Diffstat (limited to 'lib/gcstar/GCItemsLists')
-rw-r--r-- | lib/gcstar/GCItemsLists/GCImageListComponents.pm | 848 | ||||
-rw-r--r-- | lib/gcstar/GCItemsLists/GCImageLists.pm | 2028 | ||||
-rw-r--r-- | lib/gcstar/GCItemsLists/GCListOptions.pm | 496 | ||||
-rw-r--r-- | lib/gcstar/GCItemsLists/GCTextLists.pm | 2101 |
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/&/&/; + $fixedTitle =~ s/</</; + $fixedTitle =~ s/>/>/; + + 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; |