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 (