Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 739 lines (704 sloc) 26.8 KB
#!/usr/bin/perl
#
# ds-findbytags, v1.1.8, CLI / GTK+ 2
# Copyright © 2016-2017 Dmitry Sokolov
#
# This program 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 3 of the License, or
# (at your option) any later version.
#
# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
#
# Dmitry Sokolov <dmitry@sokolov.website>
use strict;
use warnings;
use utf8;
use open qw/:encoding(UTF-8) :std/;
use POSIX;
use Cwd qw/abs_path/;
use File::Find;
use File::Spec::Functions;
use Getopt::Std;
use constant VERSION => "v1.1.8";
use constant COPYRIGHT => "Copyright © 2016-2017";
use constant AUTHOR => "Dmitry Sokolov <dmitry\@sokolov.website>";
use constant WEBSITE =>
"http://sokolov.website/programs/ds-utils/ds-findbytags";
use constant LICENSE =>
"This program is free software: you can redistribute it and/or modify it" .
"\nunder the terms of the GNU General Public License as published\n" .
"by the Free Software Foundation, either version 3 of the License,\n" .
"or (at your option) any later version.\n\n" .
"This program is distributed in the hope that it will be useful,\n" .
"but WITHOUT ANY WARRANTY; without even the implied warranty\n" .
"of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n" .
"See the GNU General Public License for more details.\n\n" .
"You should have received a copy of the GNU General Public License\n" .
"along with this program. If not, see <http://www.gnu.org/licenses/>";
use constant COMMENTS =>
"Perl / GTK+ 2 script, that searches in the specified directories " .
"the images with keywords (tags) specified in XMP or IPTC metadata " .
"and satisfy the specified AND, OR, NOT conditions. Optionally the found " .
"images will open in the specified program. Along the way, you can " .
"massively add, remove or replace tags in the found images and to save " .
"symlinks to these images in the specified directory with original " .
"or random names.\n\n" .
"Depends: perl, exiv2. Recommends: gtk2.\n\n" .
"For more information run the script without any keys or arguments, " .
"or with -h key.";
use constant HELP =>
"ds-findbytags, " . VERSION . ", CLI / GTK+ 2\n" .
"Copyright © 2016-2017 Dmitry Sokolov\n" .
"\n" .
"This program is free software: you can redistribute it and/or modify\n" .
"it under the terms of the GNU General Public License as published by\n" .
"the Free Software Foundation, either version 3 of the License, or\n" .
"(at your option) any later version.\n" .
"\n" .
"This program is distributed in the hope that it will be useful,\n" .
"but WITHOUT ANY WARRANTY; without even the implied warranty of\n" .
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" .
"GNU General Public License for more details.\n" .
"\n" .
"You should have received a copy of the GNU General Public License\n" .
"along with this program. If not, see <http://www.gnu.org/licenses/>.\n" .
"\n" .
"Perl / GTK+ 2 script, that searches in the specified directories the images\n" .
"with keywords (tags) specified in XMP or IPTC metadata and satisfy\n" .
"the specified AND, OR, NOT conditions. Optionally the found images will open\n" .
"in the specified program. Along the way, you can massively add, remove\n" .
"or replace tags in the found images and to save symlinks to these images\n" .
"in the specified directory with original or random names.\n" .
"Read more: http://sokolov.website/programs/ds-utils/ds-findbytags\n" .
"\n" .
"Depends: perl, exiv2\n" .
"Recommends: gtk2\n" .
"\n" .
"ds-findbytags [-g] [-k] [-t path_to_tags_file] [-e encoding]\n" .
" [-a \"tag, tag, ...\"] [-o \"tag, tag, ...\"] [-n \"tag, tag, ...\"]\n" .
" [-i \"tag, tag, ...\"] [-d \"tag, tag, ...\"]\n" .
" [-c \"tag, tag, [tag, tag], ...\"] [-l viewer]\n" .
" [-s path_the_symlinks_are_saved] path(s)_where_to_search\n" .
"\n" .
"-g — (GUI) — If the key is present, the program runs\n" .
" in the GTK interface instead of the CLI interface.\n" .
"-k — (keep) — If the key is present, the symlinks are named by\n" .
" the names of the files found. In the absence of the key,\n" .
" the symlinks are named by a random character set.\n" .
"-t — (tags) — Path to the XML file with the tags tree in the Geeqie\n" .
" format (by default — \$HOME/.config/geeqie/geeqierc.xml).\n" .
" Everything in this file, that is not belong to tags,\n" .
" is ignored. If the path is incorrect, the tags tree is\n" .
" not displayed. Tags from the tree are added by draggging\n" .
" to the required field, or — to the field “AND” —\n" .
" by double-click of the right mouse button.\n" .
"-e — (encoding) — Encoding of the Geeqie-formatted XML file with tags\n" .
" (by default — * — autodetect with enca).\n" .
"-a — (and) — File must contain all the tags specified in this key.\n" .
"-o — (or) — File must contain at least one of the specified tags.\n" .
"-n — (not) — File must not contain any of the specified tags.\n" .
"-i — (insert) — Add the tags specified in this key to the found files.\n" .
"-d — (delete) — Delete the specified tags from the found files.\n" .
"-c — (change) — Replace the specified tags in the found files, in pairs.\n" .
"-l — (look) — The program in which the directory with the found files\n" .
" is opened; the default is Geeqie. If you do not need\n" .
" to open it at all, the key value must be “no”.\n" .
"-s — (save) — The directory (absolute path), in which the symlinks to\n" .
" the found files are saved after the end of the program.\n" .
" If the key is not specified, a temporary directory\n" .
" is created at the location of the program start,\n" .
" which after the end of the program is deleted.\n" .
"\n" .
"Example: ds-findbytags -g -k -t \"~/.config/geeqie/geeqierc.xml\" \\\n" .
" -a \"sea, gloomy clouds, stones\" -o \"birds, dolphins\" \\\n" .
" -n \"sharks, bees\" -i \"wow\" -d \"sea\" -c \"birds, cats\" \\\n" .
" -l \"no\" -s \"~/chosen\" ~/photos1 ~/photos2\n" .
"\n" .
"Dmitry Sokolov <dmitry\@sokolov.website>\n";
my (@dirs, @files, %opts, %opts_v);
my $exts = 'gif|jpe?g|pdf|png|tiff?';
my @worden = ('0' .. '9', 'A' .. 'Z', 'a' .. 'z');
sub wanted {
my $fi = Encode::decode("utf8", $_);
my $fd = Encode::decode("utf8", $File::Find::dir);
my $fn = Encode::decode("utf8", $File::Find::name);
if(-f && -s && /\.$exts$/i) {
my @tags = `exiv2 -PXnt '$fn'`
=~ /^subject\s+(?=\S)(.+)$/gm;
if(@tags != 0) {
@tags = split /\s*,\s*/, $tags[0];
} else {
@tags = `exiv2 -PInt '$fn'`
=~ /^Keywords\s+(?=\S)(.+)$/gm;
}
if(
(exists $opts_v{a}
? @{(&jis(\@tags, $opts_v{a}))[1]} == scalar(@{$opts_v{a}})
: 1) &&
(exists $opts_v{o} ? @{(&jis(\@tags, $opts_v{o}))[1]} > 0 : 1) &&
(exists $opts_v{n} ? @{(&jis(\@tags, $opts_v{n}))[1]} == 0 : 1)
) {
if(exists $opts_v{d} || exists $opts_v{c} || exists $opts_v{i}) {
my %ntags = map {$fi => 1} @tags;
if(exists $opts_v{d}) {
foreach(@{$opts_v{d}}) {
if($ntags{$fi}) {
delete $ntags{$fi};
}
}
}
if(exists $opts_v{c}) {
foreach(@{$opts_v{c}}) {
if($ntags{@{$fi}[0]}) {
delete $ntags{@{$fi}[0]};
$ntags{@{$fi}[1]} = 1;
}
}
}
if(exists $opts_v{i}) {
foreach(@{$opts_v{i}}) {
$ntags{$fi} = 1;
}
}
my @ntags = keys %ntags;
my $do =
"exiv2 -M\"add Xmp.dc.subject\" " .
"-M\"del Iptc.Application2.Keywords\" " .
"\'$fn\'";
`$do`;
foreach(@ntags) {
my $do =
"exiv2 -M\"set Xmp.dc.subject XmpBag $fi\" " .
"-M\"add Iptc.Application2.Keywords String $fi\" " .
"\'$fn\'";
`$do`;
}
}
push @files, {path => $fd, file => $fi};
}
}
}
sub jis {
my ($a, $b) = @_;
my (@un, %un, @is, %is);
foreach(@$a, @$b) {$un{$_}++ && $is{$_}++}
@un = keys %un; @is = keys %is;
return (\@un, \@is);
}
sub begin {
$opts_v{a} = [split /\s*,\s*/, $opts_v{a}] if exists $opts_v{a};
$opts_v{o} = [split /\s*,\s*/, $opts_v{o}] if exists $opts_v{o};
$opts_v{n} = [split /\s*,\s*/, $opts_v{n}] if exists $opts_v{n};
$opts_v{i} = [split /\s*,\s*/, $opts_v{i}] if exists $opts_v{i};
$opts_v{d} = [split /\s*,\s*/, $opts_v{d}] if exists $opts_v{d};
if(exists $opts_v{c}) {
$opts_v{c} = [split /\s*,\s*/, $opts_v{c}];
if(@{$opts_v{c}} % 2) {
if($opts_v{g}) {
&show_message("error", "Нечётное количество тэгов для замены.");
return;
} else {
die "Нечётное количество элементов в значении ключа -c.\n";
}
}
for(my $i = 0; $i < @{$opts_v{c}} / 2; $i++) {
push @{$opts_v{c}}, [shift @{$opts_v{c}}, shift @{$opts_v{c}}];
}
}
my $tosave = 1;
unless(exists $opts_v{s}) {
$tosave = 0;
$opts_v{s} = join("", map {$worden[int(rand scalar @worden)]} 1 .. 16);
}
find(\&wanted, @dirs);
system("mkdir", $opts_v{s}) unless(-d $opts_v{s});
foreach(@files) {
system(
"ln", "-s",
catfile($_->{path}, $_->{file}),
catfile(
$opts_v{s},
exists $opts_v{k}
? $_->{file}
: join("", map {$worden[int(rand scalar @worden)]} 1 .. 16)
. join("", $_->{file} =~ /(\..*?)$/)
)
);
}
system($opts_v{l}, $opts_v{s}) if exists $opts_v{l} and $opts_v{l} ne "no";
system("rm", "-r", $opts_v{s}) if $tosave == 0;
@dirs = (); @files = (); %opts_v = %opts;
}
sub gtk {
use Gtk2 "-init";
use XML::Parser;
use constant TRUE => 1;
use constant FALSE => 0;
our $last_path = ".";
our (
$window, $tooltips, $hpaned, $hbox2, $vbox, $findin_frame,
$findin_table, @findin_b, @findin_m, $findin_p, $search_frame,
$search_table, $search_a_l, $search_a, $search_o_l, $search_o,
$search_n_l, $search_n, $action_frame, $action_table, $action_i_l,
$action_i, $action_d_l, $action_d, $action_c_l, $action_c,
$begin_savecb, $begin_savefc, $begin_keepcb, $begin_button,
$scrolledwindow
);
sub gtk_begin {
$opts_v{k} = 1 if $begin_keepcb->get_active;
$opts_v{s} = $begin_savefc->get_filename if $begin_savecb->get_active;
if($search_a->get_text =~ m/\S/) {$opts_v{a} = $search_a->get_text}
else {delete $opts_v{a}}
if($search_o->get_text =~ m/\S/) {$opts_v{o} = $search_o->get_text}
else {delete $opts_v{o}}
if($search_n->get_text =~ m/\S/) {$opts_v{n} = $search_n->get_text}
else {delete $opts_v{n}}
if($action_i->get_text =~ m/\S/) {$opts_v{i} = $action_i->get_text}
else {delete $opts_v{i}}
if($action_d->get_text =~ m/\S/) {$opts_v{d} = $action_d->get_text}
else {delete $opts_v{d}}
if($action_c->get_text =~ m/\S/) {$opts_v{c} = $action_c->get_text}
else {delete $opts_v{c}}
@dirs = (); foreach(@findin_b) {push @dirs, $_->get_label;}
&begin() if @dirs > 0;
}
sub was_path {
my $p = shift;
foreach(@findin_b) {return 1 if $_->get_label eq $p;}
return 0;
}
sub choose_path {
my $p = shift;
my $c = Gtk2::FileChooserDialog->new(
"Искать в…", undef, "select-folder",
"gtk-cancel" => "cancel", "gtk-ok" => "ok"
);
$c->set_current_folder($p);
$p = ("ok" eq $c->run) ? $c->get_current_folder : 0;
$c->destroy;
return $p;
}
sub add_path {
my $p = shift;
if(&was_path($p)) {
&show_message("error", "Этот путь уже добавлен. Выберите другой.");
return 0;
}
$findin_b[scalar @findin_b] = Gtk2::Button->new($p);
$findin_m[scalar @findin_m] = Gtk2::Button->new("-");
$findin_b[-1]->signal_connect(clicked => sub {
my $p_chosen = &choose_path($p);
if($p_chosen && !&was_path($p_chosen)) {
$_[0]->set_label($p_chosen);
$last_path = $p_chosen;
}
});
$findin_m[-1]->signal_connect(clicked => \&remove_path);
$findin_b[-1]->set_alignment(0, 0);
$findin_table->attach_defaults(
$findin_b[-1], 1, 2, $#findin_b, $#findin_b + 1
);
$findin_table->attach(
$findin_m[-1], 2, 3, $#findin_m, $#findin_m + 1,
["shrink", "fill"], "fill", 0, 0
);
$tooltips->set_tip($findin_b[-1], "Изменить путь для поиска");
$tooltips->set_tip($findin_m[-1], "Удалить путь для поиска");
$findin_b[-1]->show;
$findin_m[-1]->show;
}
sub remove_path {
my $p = shift;
my $f = 0;
for(my $i = 0; $i < @findin_m; $i++) {
if($findin_m[$i] == $p) {
$findin_b[$i]->destroy; splice @findin_b, $i, 1;
$findin_m[$i]->destroy; splice @findin_m, $i, 1;
$f = 1;
}
if($f && $i < @findin_m) {
$findin_table->remove($findin_b[$i]);
$findin_table->attach_defaults(
$findin_b[$i], 1, 2, $i, $i + 1
);
$findin_table->remove($findin_m[$i]);
$findin_table->attach(
$findin_m[$i], 2, 3, $i, $i + 1,
["shrink", "fill"], "fill", 0, 0
);
}
}
$findin_table->set_property(
"n-rows", $findin_table->get_property("n-rows") - 1
) if $findin_table->get_property("n-rows") > 1;
}
sub tree {
my ($tree, $source, $file, $root) = (undef, undef, @_);
if(-f $file && -s $file) {
if($opts_v{e} eq '*') {
$opts_v{e} = `enca -r '$file'` || "UTF-8"; chomp $opts_v{e};
}
open(IN, "<:encoding($opts_v{e})", $file)
or die "Can’t open $file: $!\n";
while($_ = <IN>) {$source .= $_;}
close(IN);
$source =~ s/.*(<$root[^<>]*>.*?<\/$root[^<>]*>).*/$1/ms if $root;
my $parser = new XML::Parser(Style => "Tree");
$tree = $parser->parse($source);
}
return $tree;
}
sub build_tree {
my $tree = shift;
my $treeview = undef;
if($tree) {
my $columns = [{ColumnName => "name"}, {ColumnName => "kw"}];
my @treestore_types = map {"Glib::String"} @$columns;
my $treestore = Gtk2::TreeStore->new(@treestore_types);
$treeview = Gtk2::TreeView->new($treestore);
$treeview->set_headers_visible(FALSE);
my $column_count = 0;
foreach my $column (@$columns) {
my $col = Gtk2::TreeViewColumn->new_with_attributes(
$column->{ColumnName},
Gtk2::CellRendererText->new,
text => $column_count
);
$treeview->append_column($col);
$col->set_visible(FALSE);
$column_count++;
}
$treeview->get_column(0)->set_visible(TRUE);
foreach my $child (@{$tree->[1]}) {
if(ref($child) eq "ARRAY") {
&append_children(
$treeview->get_model(), undef, $child, $columns
);
}
}
}
return $treeview;
}
sub append_children {
my ($treestore, $i, $tree, $columns) = @_;
if($tree) {
my $count = 0;
my $child_i = $treestore->append($i);
foreach my $column (@$columns) {
my $column_name = $column->{ColumnName};
if($tree->[0]{$column_name}) {
$treestore->set($child_i, $count, $tree->[0]{$column_name});
}
$count++;
}
foreach my $child (@$tree) {
if(ref($child) eq "ARRAY") {
&append_children($treestore, $child_i, $child, $columns);
}
}
}
}
sub treeview_clicked {
my ($widget, $event, $data) = @_;
if(&button_pressed == 2 && $event->button == 1) {
&entry_drag_data_received(
$widget, undef, undef, undef,
$widget, undef, undef, $widget, "expand"
);
}
if(&button_pressed == 2 && $event->button == 3) {
&entry_drag_data_received(
$search_a, undef, undef, undef,
$search_a, undef, undef, $widget, undef, TRUE
);
}
}
sub entry_drag_data_received {
my (
$widget, $context, $x, $y, $data, $info, $time, $tview, $action,
$append
) = @_;
my @paths = $tview->get_selection->get_selected_rows;
foreach my $path (@paths) {
my $iter = $tview->get_model->get_iter($path);
if($action && $action eq "expand") {
$widget->row_expanded($path)
? $widget->collapse_row($path)
: $widget->expand_row($path, FALSE);
} elsif($tview->get_model->get_value($iter, 1) eq "true") {
$data->set_text(
($append ? $data->get_text : "") .
($widget->get_text ? ", " : "") .
$tview->get_model->get_value($iter, 0)
);
}
}
}
sub button_pressed {
my ($widget, $event, $data) = @_;
return 3 if $event->type eq "3button-press";
return 2 if $event->type eq "2button-press";
return 1 if $event->type eq "button-press";
return 0;
}
sub show_message {
my $dialog = Gtk2::MessageDialog->new(
$window,
"destroy-with-parent",
shift,
"close",
shift
);
$dialog->run;
$dialog->destroy;
}
sub show_about {
my $dialog = Gtk2::AboutDialog->new;
$dialog->set_version(VERSION);
$dialog->set_copyright(COPYRIGHT);
$dialog->set_authors(AUTHOR);
$dialog->set_website(WEBSITE);
$dialog->set_license(LICENSE);
$dialog->set_comments(COMMENTS);
$dialog->run;
$dialog->destroy;
}
$window = Gtk2::Window->new;
$window->signal_connect(destroy => sub {Gtk2->main_quit;});
$window->signal_connect(key_release_event => sub {
my ($widget, $event) = @_;
if($event->hardware_keycode == 67) {
&show_about();
}
});
$window->set_title(
"ds-findbytags " . VERSION . " — " .
"Поиск картинок по тэгам и операции с тэгами найденных картинок"
);
$window->set_default_size(700, 100);
$window->set_border_width(5);
$tooltips = Gtk2::Tooltips->new;
$hpaned = Gtk2::HPaned->new;
$window->add($hpaned);
$vbox = Gtk2::VBox->new(FALSE, 5);
$vbox->set_size_request(385, -1);
$hpaned->pack1($vbox, TRUE, FALSE);
my $treeview = &build_tree(&tree($opts_v{t}, "keyword_tree"));
if($treeview) {
$treeview->enable_model_drag_source(
"button1-mask", ["copy"],
{"target" => "text/plain", "flags" => [], "info" => 0}
);
$treeview->signal_connect(button_press_event => \&treeview_clicked);
$scrolledwindow = Gtk2::ScrolledWindow->new;
$scrolledwindow->set_size_request(130, -1);
$scrolledwindow->set_policy("automatic", "automatic");
$scrolledwindow->add($treeview);
$hpaned->pack2($scrolledwindow, TRUE, FALSE);
}
$findin_frame = Gtk2::Frame->new(" Искать в ");
$vbox->pack_start($findin_frame, FALSE, TRUE, 0);
$findin_table = Gtk2::Table->new(1, 3, FALSE);
$findin_table->set_border_width(5);
$findin_table->set_col_spacings(5);
$findin_frame->add($findin_table);
$findin_p = Gtk2::Button->new("+");
$findin_p->signal_connect(clicked => sub {
my $p_chosen = &choose_path($last_path);
if($p_chosen) {&add_path($p_chosen); $last_path = $p_chosen;}
});
$findin_table->attach(
$findin_p, 0, 1, 0, 1, ["shrink", "fill"], "fill", 0, 0
);
$tooltips->set_tip($findin_p, "Добавить новый путь для поиска");
$search_frame = Gtk2::Frame->new(" Поиск по тэгам ");
$vbox->pack_start($search_frame, FALSE, TRUE, 0);
$search_table = Gtk2::Table->new(3, 2, FALSE);
$search_table->set_border_width(5);
$search_table->set_col_spacings(5);
$search_frame->add($search_table);
$search_a_l = Gtk2::Label->new("Присутствуют все тэги из:");
$search_a_l->set_alignment(1, 0.5);
$search_table->attach(
$search_a_l, 0, 1, 0, 1, ["shrink", "fill"], "fill", 0, 0
);
$search_a = Gtk2::Entry->new;
$search_table->attach_defaults($search_a, 1, 2, 0, 1);
$tooltips->set_tip(
$search_a,
"Поиск ведётся по файлам, в которых\nПРИСУТСТВУЮТ ВСЕ\n" .
"из перечисленных здесь тэгов (разделённых запятыми)"
);
$search_a->set_text($opts_v{a}) if exists $opts_v{a};
$search_a->signal_connect(
drag_data_received => \&entry_drag_data_received, $treeview
);
$search_o_l = Gtk2::Label->new("Присутствует хотя бы один тэг из:");
$search_o_l->set_alignment(1, 0.5);
$search_table->attach(
$search_o_l, 0, 1, 1, 2, ["shrink", "fill"], "fill", 0, 0
);
$search_o = Gtk2::Entry->new;
$search_table->attach_defaults($search_o, 1, 2, 1, 2);
$tooltips->set_tip(
$search_o,
"Поиск ведётся по файлам, в которых\nПРИСУТСТВУЕТ ХОТЯ БЫ ОДИН\n" .
"из перечисленных здесь тэгов (разделённых запятыми)"
);
$search_o->set_text($opts_v{o}) if exists $opts_v{o};
$search_o->signal_connect(
drag_data_received => \&entry_drag_data_received, $treeview
);
$search_n_l = Gtk2::Label->new("Нет ни одного тэга из:");
$search_n_l->set_alignment(1, 0.5);
$search_table->attach(
$search_n_l, 0, 1, 2, 3, ["shrink", "fill"], "fill", 0, 0
);
$search_n = Gtk2::Entry->new;
$search_table->attach_defaults($search_n, 1, 2, 2, 3);
$tooltips->set_tip(
$search_n,
"Поиск ведётся по файлам, в которых\nНЕТ НИ ОДНОГО\n" .
"из перечисленных здесь тэгов (разделённых запятыми)"
);
$search_n->set_text($opts_v{n}) if exists $opts_v{n};
$search_n->signal_connect(
drag_data_received => \&entry_drag_data_received, $treeview
);
$action_frame = Gtk2::Frame->new(" Действия с найденным ");
$vbox->pack_start($action_frame, FALSE, TRUE, 0);
$action_table = Gtk2::Table->new(3, 2, FALSE);
$action_table->set_border_width(5);
$action_table->set_col_spacings(5);
$action_frame->add($action_table);
$action_i_l = Gtk2::Label->new("Добавить тэги в найденные файлы:");
$action_i_l->set_alignment(1, 0.5);
$action_table->attach(
$action_i_l, 0, 1, 0, 1, ["shrink", "fill"], "fill", 0, 0
);
$action_i = Gtk2::Entry->new;
$action_table->attach_defaults($action_i, 1, 2, 0, 1);
$tooltips->set_tip(
$action_i,
"ДОБАВИТЬ в найденные файлы перечисленные здесь тэги " .
"(разделённые запятыми)"
);
$action_i->set_text($opts_v{i}) if exists $opts_v{i};
$action_i->signal_connect
(drag_data_received => \&entry_drag_data_received, $treeview
);
$action_d_l = Gtk2::Label->new("Удалить тэги из найденных файлов:");
$action_d_l->set_alignment(1, 0.5);
$action_table->attach(
$action_d_l, 0, 1, 1, 2, ["shrink", "fill"], "fill", 0, 0
);
$action_d = Gtk2::Entry->new;
$action_table->attach_defaults($action_d, 1, 2, 1, 2);
$tooltips->set_tip(
$action_d,
"УДАЛИТЬ из найденных файлов перечисленные здесь тэги " .
"(разделённые запятыми)"
);
$action_d->set_text($opts_v{d}) if exists $opts_v{d};
$action_d->signal_connect(
drag_data_received => \&entry_drag_data_received, $treeview
);
$action_c_l = Gtk2::Label->new("Заменить тэги в найденных файлах:");
$action_c_l->set_alignment(1, 0.5);
$action_table->attach(
$action_c_l, 0, 1, 2, 3, ["shrink", "fill"], "fill", 0, 0
);
$action_c = Gtk2::Entry->new;
$action_table->attach_defaults($action_c, 1, 2, 2, 3);
$tooltips->set_tip(
$action_c,
"ЗАМЕНИТЬ в найденных файлах одни тэги на другие. Тэги перечисляются " .
"в этом поле попарно через запятые. Например, если ввести в это поле " .
"[овсянка, борщ, лада калина, мотоцикл], то в найденных файлах " .
"тэг «овсянка» заменится на «борщ», а тэг «лада калина» заменится " .
"на «мотоцикл»."
);
$action_c->set_text($opts_v{c}) if exists $opts_v{c};
$action_c->signal_connect(
drag_data_received => \&entry_drag_data_received, $treeview
);
$hbox2 = Gtk2::HBox->new(FALSE, 5);
$vbox->pack_end($hbox2, FALSE, TRUE, 0);
$begin_savecb = Gtk2::CheckButton->new("Сохранять ссылки в:");
$begin_savecb->signal_connect(toggled => sub {
$begin_savefc->set_sensitive($begin_savecb->get_active);
$begin_keepcb->set_sensitive($begin_savecb->get_active);
});
$hbox2->pack_start($begin_savecb, FALSE, FALSE, 0);
$tooltips->set_tip(
$begin_savecb,
"Если отмечено, в выбранном правее каталоге сохраняются " .
"символические ссылки на найденные файлы. Это может быть удобно " .
"для составления альбомов."
);
$begin_savefc = Gtk2::FileChooserButton->new(
"Сохранять ссылки в…", "select-folder"
);
$begin_savefc->set_sensitive(FALSE);
$hbox2->pack_start($begin_savefc, FALSE, FALSE, 0);
$tooltips->set_tip(
$begin_savefc,
"Каталог, в котором сохраняются " .
"символические ссылки на найденные файлы."
);
$begin_keepcb = Gtk2::CheckButton->new("Сохранять имена");
$begin_keepcb->set_sensitive(FALSE);
$hbox2->pack_start($begin_keepcb, FALSE, FALSE, 0);
$tooltips->set_tip(
$begin_keepcb,
"Если отмечено, символические ссылки на найденные файлы сохраняются " .
"с именами самих файлов. В противном случае ссылки получают " .
"случайные имена. Ставьте эту галочку только если уверены, " .
"что все найденные файлы имеют уникальные имена."
);
$begin_button = Gtk2::Button->new("Начать");
$begin_button->signal_connect(clicked => \&gtk_begin);
$hbox2->pack_end($begin_button, FALSE, FALSE, 0);
$tooltips->set_tip(
$begin_button,
"Начать поиск (и операции с найденными файлами, если они заданы). " .
"Поиск может продолжаться довольно долго, если происходит " .
"по большому количеству файлов."
);
for(my $i = 0; $i < @dirs; $i++) {
&add_path($dirs[$i]);
}
$begin_keepcb->set_active(TRUE) if exists $opts_v{k};
if(exists $opts_v{s}) {
$begin_savecb->set_active(TRUE);
$begin_savefc->set_filename($opts_v{s});
}
$window->show_all;
Gtk2->main;
}
getopts("hgka:o:n:i:d:c:l:s:t:e:", \%opts);
@dirs = @ARGV;
foreach(@dirs) {$_ = abs_path $_;}
$opts{t} = $ENV{HOME} . "/.config/geeqie/geeqierc.xml" unless exists $opts{t};
$opts{e} = "*" unless exists $opts{e};
$opts{l} = "geeqie" unless exists $opts{l} and $opts{l} ne "no";
%opts_v = %opts;
die "\n" . HELP . "\n" if exists $opts_v{h};
if(exists $opts_v{g}) {
&gtk();
} else {
die
"\nAt least one path or -g key must be specified (see below).\n\n" .
HELP .
"\n"
if @dirs == 0;
&begin();
}
exit 0;