From 126bb8cb6b93240bb4d3a2b816b74c286c3d422b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Frings-F=C3=BCrst?= Date: Sun, 6 Jul 2014 15:20:38 +0200 Subject: Imported Upstream version 1.7.0 --- lib/gcstar/GCItemsLists/GCImageLists.pm | 2028 +++++++++++++++++++++++++++++++ 1 file changed, 2028 insertions(+) create mode 100644 lib/gcstar/GCItemsLists/GCImageLists.pm (limited to 'lib/gcstar/GCItemsLists/GCImageLists.pm') 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 (