view bins-edit-gui @ 6:3021deef1599

Encoding related fixes * chomp encoding string * reformat else (seems someone hacked it in) * debug print encoding * do not use :utf8 binmode ("re-encode") on written file if the input is _already_ UTF-8
author Peter Gervai <grin@grin.hu>
date Thu, 16 Oct 2008 00:02:14 +0200
parents a84c32f131df
children
line wrap: on
line source

#!/usr/bin/perl
# bins-edit-gui -- graphical editor for BINS-format XML tag files
#
# Copyright 2002 Mark W. Eichin <eichin@thok.org>
# The Herd of Kittens
#
# -- GPL notice --
$gpl_text = <<EOF ;
# 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 2 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, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
EOF
# -- end GPL notice --

$version = "0.9";

@album_tags = qw(title longdesc shortdesc sampleimage);
@image_tags = qw(title event location people date description);
@known_tags = sort (@album_tags, @image_tags);
{
    # perlfaq4 uniquify
    my $prev = undef;
    @known_tags = grep($_ ne $prev && ($prev = $_, 1), @known_tags);
}
# allbery suggests
# keys %{{map(($_,1),@array)}}
# would be like the perlfaq unique-keys example...

use Getopt::Long;

use Gtk;
use Gtk::GladeXML;

use XML::DOM;
use XML::XQL;
use XML::XQL::DOM;
use XML::Writer;

use Gtk::Gdk::ImlibImage;
use Gnome;

use Text::Iconv;

# for localized text
# use POSIX qw(setlocale LC_MESSAGES); # apparently only works on Debian,
# mandrake, redhat, and suse are reported to need the following instead:
use POSIX 'locale_h';
use Locale::gettext;

sub _ {
   Locale::gettext::gettext(shift);
}

$debug=0;

$localePath = "/usr/local/share/locale"; # Base locale path  (for I18N)

setlocale(LC_MESSAGES, "");
bindtextdomain("bins-edit-gui", $localePath);
textdomain("bins-edit-gui");

sub usage {
    my $why = shift;
    my $usage =
"bins-edit-gui: [--debug] [--version] " . _("file [files...]") ."\n".
"   "._("Edit BINS-format tags (captions, titles, etc) for image files.") ."\n".
"\n".
"   --debug     " . _("enable developer-only debugging.") ."\n".
"   --version   " . _("display version information.") ."\n";

    if ($why eq 'error') {
	print STDERR $usage;
	exit 1;
    } elsif ($why eq 'user') {
	print STDOUT $usage;
	exit 0;
    }
}

$o_help=0;
$o_version=0;

$fullversion =
"bins-edit-gui $version " . _("for BINS") ."\n".
"Copyright 2002 Mark W. Eichin <eichin\@thok.org>" ."\n".
_("This is free software distributed under the GPL.  There is NO WARRANTY.") ."\n".


GetOptions('debug+' => \$debug,
	   'h|help' => \$o_help,
	   'v|version' => \$o_version,
	   ) or usage('error');
usage('user') if $o_help;
if ($o_version) {
    print $fullversion;
    exit 0;
}

# take this out when we have a file browser
usage('error') if scalar(@ARGV) < 1;

# find a way to do this in Perl; nl_langinfo seems to be coming in 5.8
my $localEncoding = "";
my $codeset;
eval {
    require I18N::Langinfo;
    I18N::Langinfo->import(qw(langinfo CODESET));
    $codeset = langinfo(CODESET());
};
# ANSI is unspeakably primitive, keep LATIN1 instead
# Solaris refers to ISO 646 as "646" but that is not a valid codeset
if (!$@ && $codeset && $codeset ne "ANSI_X3.4-1968" && $codeset ne "646") {
  $localEncoding = $codeset;
  print "Forcing encoding to $codeset";
}

chop($localEncoding);
if (! $localEncoding) {
  $localEncoding = "LATIN1";
}
$Latin2UTF = Text::Iconv->new($localEncoding, "UTF-8");
$UTF2Latin = Text::Iconv->new("UTF-8", $localEncoding);

my_init_rotations();

Gtk->set_locale;

Gnome->init("bins-edit-gui", $version);
Gtk->init;

$glade = "/usr/local/share/bins/bins-edit-gui.glade";
if (! -r $glade) {
    $glade = "bins-edit-gui.glade" ; # developer hack
    print "DEVELOPER HACK\n";
    if (! -r $glade) {
	die "No bins-edit-gui.glade available";
    }
}
#$g = new Gtk::GladeXML($glade, 'image_edit_top');
$g = new Gtk::GladeXML($glade);
die "Gtk::GladeXML($glade) initialization failed; check required packages" unless $g;

$g->signal_autoconnect_from_package('main'); # main:: so we can grab stuff directly

# get the "global" widgets

$w = $g->get_widget('image_edit_pixmap'); # GTK-Interface/widget/name
$w->signal_connect(expose_event => \&on_test_expose_event);
$name_entry = $g->get_widget("field_name_entry");
$value_entry = $g->get_widget("field_value_entry");
$ilist = $g->get_widget("image_prop_list");
$ilist->column_titles_passive;	# shouldn't this be in the .glade?
# work around libglade 0.17 bug (debbugs #147051)
$ilist->set_column_title(0, _("Property"));
$ilist->set_column_title(1, _("Value"));
# end workaround.
$statusbar = $g->get_widget('statusbar1');
$aboutbox = $g->get_widget('about_box');
$aboutbox->close_hides;
$licensebox = $g->get_widget('license_box');
$licensetext = $g->get_widget('license_text'); # GtkText
#print ref($licensetext),": ",join("\n\t|",%Gtk::Text::),"\n";
$gpl_text =~ s/^\# ?//gm;
$licensetext->insert(undef, undef, undef, $gpl_text);

## album-panel globals
$albumedit = $g->get_widget('album_edit_top');
$albumfile = $g->get_widget('album_edit_filename');
$albumprop = $g->get_widget('album_prop_list');
# work around libglade 0.17 bug (debbugs #147051)
$albumprop->set_column_title(0, _("Property"));
$albumprop->set_column_title(1, _("Value"));
# end workaround.
$albumname = $g->get_widget('album_name_entry');
$albumname->set_popdown_strings(@album_tags);
$albumvalue = $g->get_widget('album_edit_text');

sub on_dismiss_about_clicked {
    $licensebox->hide;
    status(_("Thank you for sharing."));
}

sub status {
    my $msg = shift;
    $statusbar->pop(1);
    $statusbar->push(1, $msg);
}

use Image::Info;

# original orientation for viewing
sub get_image_orientation($) {
    my $filename = shift;
    # bail directly if we have a tag-loaded value
    return $newimage_tagged_or if (defined $newimage_tagged_or);
    # try and find a way to get this from imlib?
    my $info = Image::Info::image_info($filename);
    if (exists $info->{'error'}) {
	my $msg = sprintf(_("Couldn't read EXIF info from %s: %s, ignoring."),
			  $filename, $info->{'error'});
			  
	status($msg);
	print $msg if $debug;
	return "top_left";
    }
    $info->{'Orientation'};
}

# see http://www.ba.wakwak.com/~tsuruzoh/Computer/Digicams/exif-e.html
# for the meaning of the orientations.
# see /usr/share/doc/imlib-base/html/index.html
# for the not-quite-apology for the function supplied.
# see the qiv sources for an example of how to use it anyway...

sub my_real_gdk_rotate($$) {
    my ($newimage,$orientation) = @_;
    my $degrees;

    if ($orientation eq "right_top"){
	$degrees = 90;
	$newimage->rotate_image('a 45 degree mirror is *not* a rotate');
	$newimage->flip_image_horizontal ();
    } elsif ($orientation eq "left_bot"){
	$degrees = 270;
	$newimage->rotate_image('a 45 degree mirror is *not* a rotate');
	$newimage->flip_image_vertical ();
    } elsif ($orientation eq "right_bot"){
	$degrees = 180;
	# test this, maybe simplify...
	$newimage->rotate_image('a 45 degree mirror is *not* a rotate');
	$newimage->flip_image_horizontal ();
	$newimage->rotate_image('a 45 degree mirror is *not* a rotate');
	$newimage->flip_image_horizontal ();
    } elsif ($orientation eq "top_left"){
	# do nothing
	return;
    } else {
	print STDERR "Warning, Orientation field has an unknown value '$orientation'.\n" if $debug;
	# still do nothing
	return 0;
    }

}


sub load_image_tags($);

sub load_image { # no proto, we're cheating and doing 1arg/2arg (or should that be @?)
    my $test_filename = shift;
    my $extra = shift;
    my $ilabel = $g->get_widget('image_filename_label');
    $newimage->destroy_image() if defined $newimage;
    $newimage = load_image Gtk::Gdk::ImlibImage($test_filename);
    if (defined $newimage) {
	if ($extra ne "keeptags") {
	    load_image_tags $test_filename;
	    $newimage_loaded_or = $newimage_or = get_image_orientation($test_filename);
	    print "$test_filename: got orientation <$newimage_or>\n" if $debug;
	}
	my_real_gdk_rotate($newimage,$newimage_or);
	$ilabel->set($test_filename);
	status(sprintf(_("Loaded %s."), $test_filename));
    } else {
	# no image - just skip.  we've already filtered xml files, though.
	$ilabel->set(_("Load failed: ") . $test_filename);
	status(sprintf(_("IMLib failed loading %s."), $test_filename));
    }
    # regardless, rerender
    if (defined $imagesized) {
	undef $imagesized;
	on_test_expose_event();	# cheat, it doesn't use it
    }

}
# from bins_edit

# my $grove_builder = XML::Grove::Builder->new;
# my $parser = XML::Parser::PerlSAX->new ( Handler => $grove_builder );
# $document = $parser->parse ( Source => { SystemId => "${test_filename}.xml" } );

# now only internal for load...
sub load_image_tags($) {
    %old_field = %field;	# save for later recall

    my $test_filename = shift;
    $parser = new XML::DOM::Parser;
    if (-r "${test_filename}.xml" ) {
	$doc = $parser->parsefile ("${test_filename}.xml");
    } else {
	# literal minimal tree
	$doc = $parser->parse('<?xml version="1.0" encoding="UTF-8"?><image><description></description><bins></bins><exif></exif></image>');
	status(sprintf(_("%s not found, constructing"), "${test_filename}.xml"));
    }


    undef %field;

    for my $i (@image_tags) {
	$field{$i} = "";
    }

    for my $field_node ($doc->xql("image/description/field")) {
	# <field name="location"></field>
	# <field name="description">Sjenka</field>
	my $fieldname = ($field_node->xql("\@name"))[0]->xql_toString;
	my $fieldval = $field_node->xql_toString;
	print "N: $fieldname  V: $fieldval\n" if $debug;
	my $newval;
	do {
	    $newval = $UTF2Latin->convert($fieldval);
	    charmap_failed("load_image_tags", $fieldval) if (not defined $newval);
	} until defined $newval;
	$field{$fieldname} = $newval;
    }

    undef $newimage_tagged_or;
    # <image><exif><tag name="Orientation" priority="1">right_top</tag> 
    for my $tag_node ($doc->xql("image/exif/tag[\@name = 'Orientation']")) {
	my $tagname = ($tag_node->xql("\@name"))[0]->xql_toString;
	my $tagprio_node = ($tag_node->xql("\@priority"))[0];
	my $tagprio;
	$tagprio = $tagprio_node->xql_toString if defined $tagprio_node;
	my $tagval = $tag_node->xql_toString;
	# but actually, we only care about orientation 
	print "N: $tagname  V: $tagval P: $tagprio\n" if $debug;
	if ($tagprio == 1) {
	    $newimage_tagged_or = $tagval;
	}
    }

    # a clist is output only.  someday, replace it with a list of
    # editboxes, even if we have to write one all in perl.

    # in the mean time, we vector out to a pair of combo boxes and let
    # the user edit there, while copying the changes back live.

    # save last index if any...
    my $oldtag;
    my $oldrow = $ilist->focus_row();
    $oldtag = $ilist->get_text($oldrow, 0) if $oldrow > 0;
    print "old $oldrow: $oldtag\n" if $debug;
    $ilist->clear;
    for my $i (sort keys %field) {
	$ilist->append("\u$i", $field{$i});
    }
    if ($oldrow > 0) {
	my $newrow = my_gtk_find_row($ilist, 0, $oldtag);
	print "new $newrow\n" if $debug;
	if ($newrow > 0) {
	    $ilist->set_focus_row($newrow);
	    $ilist->grab_focus();
	}
    }

    # help the user enter stuff
    $name_entry->set_popdown_strings(@known_tags);
    # tag as unchanged
    $dirty = 0;
}

sub charmap_failed($$) {
    my $ipop = $g->get_widget('iconv_failed_dialog');

    my $lbutton = $g->get_widget('iconv_latin1_charmap_button');
    $lbutton->set_active(Gtk->true); # really we mean it
    my $ubutton = $g->get_widget('iconv_user_charmap_button');
    my $uentry = $g->get_widget('iconv_user_charmap_entry');
    # if there's a value there, it is from the previous attempt, and is wrong.
    $uentry->set_text("");

    $ipop->run_and_close;

    if ($lbutton->get_active) {
	set_encoding("LATIN1");
    } elsif ($ubutton->get_active) {
	set_encoding($uentry->get_text);
    }

}

sub save_image_tags {
    my $test_filename = shift;
    if ((not $dirty) && ($newimage_or eq $newimage_loaded_or)) {
	status(sprintf(_("%s not dirty, not saved"), ${test_filename}));
	return;
    }

    my $parent = ($doc->xql("image/description"))[0]; # first one
    my $exif = ($doc->xql("image/exif"))[0];
    my %f = %field;

    # write out the tree...
    for my $xmlfield ($doc->xql("image/description/field")) {
	my $namestr = $xmlfield->getAttribute("name");
	if (defined $f{$namestr}) {
	    # delete this node so we can append it later
	    $xmlfield->getParentNode->removeChild($xmlfield);
	}
    }
    # now append the remaining ones...
    for my $k (keys %f) {
	next if ($f{$k} eq "");
	my $newfield = new XML::DOM::Element($doc, "field");
	print "creating <$k> with <$f{$k}>\n" if $debug;
	$newfield->setAttribute("name", $k); # needs quoting!
	print "k: ", $k, " f: ", $f{$k}, " L2U: ", $Latin2UTF->convert($f{$k}), "\n" if $debug;
	my $newval;
	do {
	    $newval = $Latin2UTF->convert($f{$k});
	    charmap_failed("save_image_tags", $f{$k}) if (not defined $newval);
	} until defined $newval;
	
	$newfield->addText($newval);
	$parent->appendChild($newfield);
	print "created $k with $f{$k}\n" if $debug;
	}

    # and orientation, if set
    if ($newimage_or ne $newimage_loaded_or) {
	for my $tag_node ($doc->xql("image/exif/tag[\@name = 'Orientation']")) {
	    # delete the node, then construct a new one
	    $tag_node->getParentNode->removeChild($tag_node);
	}
	my $newtag = new XML::DOM::Element($doc, "tag");
	$newtag->setAttribute("name", "Orientation");
	$newtag->setAttribute("priority", "1");
	$newtag->addText($Latin2UTF->convert($newimage_or)); # unneeded conversion
	$exif->appendChild($newtag);
	print "Set orientation <$newimage_or> (loaded $newimage_loaded_or) in exif tag\n" if $debug;
    }

    $doc->printToFile("${test_filename}.xml");
    status(sprintf(_("Saved %s."), $test_filename));
    # undirty it
    $dirty = 0;
    $newimage_loaded_or = $newimage_or;
}

# if they enter or select a known one, snarf the value
$name_entry->entry->signal_connect('changed', sub {
    my $entry = shift;
    my $val = $field{$entry->get_text};
    if (defined $val) {
	$value_entry->entry->set_text($val);
	$value_entry->entry->set_editable(Gtk->true);
    }
    $dirty = 1;
});

sub my_album_replace_text($$) {
    my $aw = shift;
    my $text = shift;
    $aw->set_point(0);
    $aw->forward_delete($albumvalue->get_length());
    $aw->insert(undef, undef, undef, $text);
    $aw->set_editable(Gtk->true);
}

# album version
$albumname->entry->signal_connect('changed', sub {
    my $entry = shift;
    my $val = $album{$entry->get_text};
    if (defined $val) {
	my_album_replace_text($albumvalue, $val);
    }
    $album_dirty = 1;
});
    
sub my_gtk_find_row {		# returns row
    my ($clist, $col, $value) = @_;
    for my $i (0..$clist->rows) {
	my $cell = $clist->get_text($i, $col);
	return $i if ($cell ne "" && lc($cell) eq lc($value));
    }
    return -1;
}

# if the value changes, update the text
$value_entry->entry->signal_connect('changed', sub {
    my $entry = shift;
    my $newval = $entry->get_text;
    my $tag = lc($name_entry->entry->get_text);
    $field{$tag} = $newval;
    $dirty = 1;
    my $row = my_gtk_find_row($ilist, 0, $tag);
    print "row: $row tag: $tag newval: $newval\n" if $debug;
    # oh, no tag yet, add one
    if ($row != -1) {
	$ilist->set_text($row, 1, $newval);
    } else {
	# triggers select-row?
	$ilist->append("\u$tag", $newval);
	# update the dropdown too
	@known_tags = sort (@known_tags, $tag);
	$name_entry->set_popdown_strings(@known_tags);
	# force it all back
	$ilist->select_row($ilist->rows()-1, 0);
    }
});
# album version
$albumvalue->signal_connect('changed', sub {
    my $entry = shift;
    my $newval = $entry->get_chars(0,-1);
    my $tag = lc($albumname->entry->get_text);
    $album{$tag} = $newval;
    $dirty = 1;
    my $row = my_gtk_find_row($albumprop, 0, $tag);
    print "row: $row tag: $tag newval: $newval\n" if $debug;
    # oh, no tag yet, add one
    if ($row != -1) {
	$albumprop->set_text($row, 1, $newval);
    } else {
	# triggers select-row?
	$albumprob->append("\u$tag", $newval);
	# update the dropdown too
	@known_tags = sort (@known_tags, $tag);
	$albumname->set_popdown_strings(@known_tags);
	# force it all back
	$albumprob->select_row($ilist->rows()-1, 0);
    }
});

$ilist->signal_connect('select-row', sub {
    my ($clist, $row, $col, $event, $udata) = @_;
    print "list: $clist row: $row col: $col event: $event udata: $udata\n" 
	if $debug;
    $name_entry->entry->set_text($clist->get_text($row,0));
    $value_entry->entry->set_text($clist->get_text($row,1));
    print "focus on $value_entry\n" if $debug;
    $value_entry->entry->grab_focus();
});

# album version
$albumprop->signal_connect('select-row', sub {
    my ($clist, $row, $col, $event, $udata) = @_;
    print "list: $clist row: $row col: $col event: $event udata: $udata\n" 
	if $debug;
    $albumname->entry->set_text($clist->get_text($row,0));
    my_album_replace_text($albumvalue, $clist->get_text($row,1));
    print "focus on $value_entry\n" if $debug;
    $albumvalue->grab_focus();
});


# filter out .xml files, as we always derive them from the images
# (also lets us eventually use the images as database keys instead)
@filenames = grep (!/\.xml$/, @ARGV);

$current_n = 0;
$current_filename = $filenames[$current_n];

load_image $current_filename;

Gtk->main;

## callbacks..
# new callbacks
# sgrep -o '%r\n' 'stag("HANDLER") __ etag("HANDLER") ' bins-edit-gui.glade | while read sub; do grep "$sub" bins-edit-gui.pl >/dev/null || echo "$sub"; done

sub on_save1_activate {
    save_image_tags $current_filename;
}

sub on_about2_activate {
    $aboutbox->show;
}

sub on_license1_activate {
    $licensebox->show;
    my $button = $g->get_widget('dismiss_about');
    $button->grab_focus();
}

sub on_open2_activate {
    status(_("File browser not yet implemented."));
}

sub on_revert1_activate {
    load_image $current_filename;
    status(sprintf(_("Reverted from %s."), $current_filename));
}

sub set_filename_index($) {
    $current_n = shift;
    $current_filename = $filenames[$current_n];
    load_image $current_filename;
}

sub move_filename_index($) {
    save_image_tags $current_filename;
    my $delta = shift;
    my $new_n = $current_n + $delta;

    # clamp it
    $new_n = 0 if $new_n < 0;
    $new_n = $#filenames if $new_n > $#filenames;
    if ($new_n == $current_n) {
        # we didn't move
        status(_("Out of filenames."));
    } else {
        set_filename_index($new_n);
    }
}

sub on_next_file1_activate {
    move_filename_index(+1);
}

sub on_prev_file1_activate {
    move_filename_index(-1);
}

sub on_forward_10_activate {
    move_filename_index(+10);
}
sub on_back_10_activate {
    move_filename_index(-10);
}
sub on_start_of_list1_activate {
    set_filename_index(0);
}
sub on_end_of_list1_activate {
    set_filename_index($#filenames);
}


# auto fill from old_field
sub on_auto_fill1_activate {
    for my $k (keys %old_field) {
	if ($field{$k} eq "" && $old_field{$k} ne "") {
	    $field{$k} = $old_field{$k};
	    $dirty = 1;
	    # and change it on-screen
	    my $row = my_gtk_find_row($ilist, 0, $k);
	    if ($row != -1) {
		print "updating row $row with $k ($field{$k})\n" if $debug;
		$ilist->set_text($row, 1, $field{$k});
	    } # maybe warn, or add field, if not found?
	}
    }
    status("Auto-filled fields.");
}

sub on_exit1_activate {
    save_image_tags $current_filename;

    Gtk->main_quit;
}

sub on_test_expose_event {
    my($widget) = @_;

    # print "otee, ",defined $imagesized,"\n";
    # if (1 || not defined $imagesized) {
	my $w = $g->get_widget('image_edit_pixmap'); # GTK-Interface/widget/name
	# x,y,width,height
	my ($alloc_x, $alloc_y, $alloc_w, $alloc_h) = @{$w->allocation};
	my $widget_w = $alloc_w;
	my $widget_h = $alloc_h;

	if ($debug) {
	    print join("| ", @{$w->allocation});
	    print "| x: $widget_w  y: $widget_h ";
	    print
		"nw: ", $newimage->rgb_width, 
		" nh: ", $newimage->rgb_height, "\n" if defined $newimage;
	    print
		" x/y: ",$widget_w/$widget_h,
		" nw/x: ", $newimage->rgb_width/$widget_w, 
		" nh/y: ", $newimage->rgb_height/$widget_h, 
		"\n"  if defined $newimage;
	}
	# print "ox: $old_x oy: $old_y ww: $widget_w wh: $widget_h I:$imagesized\n";
        # return if same size
	if ($old_x == $widget_w && $old_y == $widget_h) {
	    # but not if we never dealt before
	    return if (defined $imagesized);
	}
	if (defined $newimage) {
	    my ($use_w, $use_h) = ($newimage->rgb_width, $newimage->rgb_height);
	    my $rat_w = $use_w/$widget_w;
	    my $rat_h = $use_h/$widget_h;
	    $rat = ($rat_w > $rat_h) ? $rat_w : $rat_h;

	    $use_w = $use_w / $rat;
	    $use_h = $use_h / $rat;
	    $newimage->render($use_w, $use_h);
	    my $my_image = $newimage->copy_image(); # returns Gtk::Gdk::Pixmap
	    my $my_mask  = $newimage->copy_mask();  # returns Gtk::Gdk::Bitmap
	    $w->set($my_image, $my_mask);
	    $my_image->imlib_free();
	} else {
	    # come up with more clever "test pattern" later?
	    $w->set(undef, undef);
	}
	$old_x = $widget_w;
	$old_y = $widget_h;
	#undef $imagesized;
	$imagesized = 1;
    # }
}

# rotations that override, or rather compound, the EXIF values
# build the rotation-ring first
sub my_init_rotations {
    my @rotation_order = ("right_top", "right_bot", "left_bot", "top_left");
    my $left = $rotation_order[$#rotation_order];
    for my $curr (@rotation_order) {
	$rotate_right{$left} = $curr;
	$rotate_left{$curr} = $left;
	$left = $curr;
    }
    # special case none->top_left
    $rotate_right{""} = $rotate_right{"top_left"};
    $rotate_left{""} = $rotate_left{"top_left"};
}

sub my_image_rotate {
    my $delta_or = shift;
    print "$test_filename: new user-requested rotation $delta_or to $newimage_or\n" if $debug;

    my_real_gdk_rotate($newimage,$delta_or);
    # consider noticing that $newimage_or == $newimage_loaded_or and reverting.
    # regardless, rerender
    if (defined $imagesized) {
	undef $imagesized;
	on_test_expose_event();
    }
    status(_("Rotated."));
}

sub on_rotate_right_cw1_activate {
    $newimage_or = $rotate_right{$newimage_or};
    my_image_rotate("right_top");
}

sub on_rotate_left1_activate {
    $newimage_or = $rotate_left{$newimage_or};
    my_image_rotate("left_bot");
}

# don't actually undo the rotations, just reload - but don't lose tags
sub on_cancel_rotation1_activate {
    load_image $current_filename, "keeptags";
    status(_("Image restored."));
}

### album stuff ###
sub load_album_tags($);

sub on_album1_activate {
    $current_album = $current_filename;
    # basename
    $current_album =~ s{[^/]*$}{}; # } perl-mode-sucks
    # make a complete name out of it
    $current_album .= "album.xml"; #if (-d "${current_album}");
    $albumfile->set($current_album);
    load_album_tags($current_album);
    $albumedit->show;
}

sub on_open2_activate {
    status(_("File browser not yet implemented."));
}

sub on_close1_activate {
    # save if dirty
    save_album_tags($current_album) if $album_dirty;
    $albumedit->hide;
}

sub on_revert2_activate {
    load_album_tags $current_album;
    status(sprintf(_("Reverted from %s."), $current_album));
}

sub save_album_tags;

sub on_save2_activate {
    save_album_tags $current_album;
}

# sub on_exit2_activate {
#     &on_exit1_activate(@_);
# }

sub load_album_tags($) {
    %old_album = %album;	# save for later recall

    my $test_filename = shift;
    $parser = new XML::DOM::Parser;
    if (-r "${test_filename}" ) {
	$album_doc = $parser->parsefile ("${test_filename}");
    } else {
	# literal minimal tree
	$album_doc = $parser->parse('<?xml version="1.0" encoding="UTF-8"?><album><description></description><bins></bins></album>');
	status(sprintf(_("%s not found, constructing"), "${test_filename}"));
    }


    undef %album;

    for my $i (@album_tags) {
	$album{$i} = "";
    }

    for my $field_node ($album_doc->xql("album/description/field")) {
	my $fieldname = ($field_node->xql("\@name"))[0]->xql_toString;
	my $fieldval = $field_node->xql_toString;
	print "N: $fieldname  V: $fieldval\n" if $debug;
	$album{$fieldname} = $UTF2Latin->convert($fieldval);
    }

    # a clist is output only.  someday, replace it with a list of
    # editboxes, even if we have to write one all in perl.

    # in the mean time, we vector out to a pair of combo boxes and let
    # the user edit there, while copying the changes back live.

    # save last index if any...
    my $oldtag;
    my $oldrow = $albumprop->focus_row();
    $oldtag = $albumprop->get_text($oldrow, 0) if $oldrow > 0;
    print "old $oldrow: $oldtag\n" if $debug;
    $albumprop->clear;
    for my $i (sort keys %album) {
	$albumprop->append("\u$i", $album{$i});
    }
    if ($oldrow > 0) {
	my $newrow = my_gtk_find_row($albumprop, 0, $oldtag);
	print "new $newrow\n" if $debug;
	if ($newrow > 0) {
	    $albumprop->set_focus_row($newrow);
	    $albumprop->grab_focus();
	}
    }

    # help the user enter stuff
    $albumname->set_popdown_strings(@known_tags);
    # tag as unchanged
    $dirty = 0;
}

sub save_album_tags {
    my $test_filename = shift;
    if (not $album_dirty) {
	status(sprintf(_("%s not dirty, not saved"), ${test_filename}));
	return;
    }

    my $parent = ($album_doc->xql("album/description"))[0]; # first one
    my %f = %album;

    # write out the tree...
    for my $xmlfield ($album_doc->xql("album/description/field")) {
	my $namestr = $xmlfield->getAttribute("name");
	if (defined $f{$namestr}) {
	    # delete this node so we can append it later
	    $xmlfield->getParentNode->removeChild($xmlfield);
	}
    }
    # now append the remaining ones...
    for my $k (keys %f) {
	next if ($f{$k} eq "");
	my $newfield = new XML::DOM::Element($album_doc, "field");
	print "creating <$k> with <$f{$k}>\n" if $debug;
	$newfield->setAttribute("name", $k); # needs quoting!
	$newfield->addText($Latin2UTF->convert($f{$k}));
	$parent->appendChild($newfield);
	print "created $k with $f{$k}\n" if $debug;
	}

    $album_doc->printToFile("${test_filename}");
    status(sprintf(_("Saved %s."), $test_filename));
    # undirty it
    $album_dirty = 0;
}