#!/usr/local/bin/perl -w # Copyright 1998-2009, Paul Johnson (paul@pjcj.net) # This software is free. It is licensed under the same terms as Perl itself. # The latest version of this software should be available from my homepage: # http://www.pjcj.net # Version 1.16 - 24th April 2009 use strict; require 5.005; use diagnostics; use Tk::FileSelect; use Tk::Font; use Tk::Listbox; use Tk::WaitBox; use Tk; use Carp; use Data::Dumper; use Gedcom 1.16; use vars qw( $VERSION ); $VERSION = "1.16"; eval "use Date::Manip"; Date_Init("DateFormat=UK") if $INC{"Date/Manip.pm"}; my %Options = ( font_point => 240, font_width => "*", ); my %Colour_scheme = ( background => "grey75", foreground => "blue2", ); my $Grammar_file = "gedcom-5.5.grammar"; my $Grammar; my $Ged; my $Top; my $Rec; my $Fontname; # TODO - put in Gedcom.pm my %Tags = ( BAPL => "Baptism", BIRT => "Birth", BURI => "Burrial", CHR => "Christening", DEAT => "Death", ENDL => "Endownment", MARR => "Marriage", NAME => "Name", REFN => "Reference", SEX => "Sex", SLGC => "Sealing to Parents", TITL => "Title", FAMC => "Child of Family Id", FAMS => "Family Id", DATE => "Date", PLAC => "Place", COMM => "Comment", NOTE => "Note", ); main(); sub main() { $| = 1; $Top = MainWindow->new; $Top->geometry("900x400"); my %font_spec = ( # foundry => "adobe", family => "times", weight => "bold", slant => "r", point => $Options{font_point}, # space => "m", # registry => "iso8859", ); my $font = $Top->Font(%font_spec); confess "Cannot allocate font - try changing some parameters" unless $font; $Fontname = $font->Name; $Top->optionAdd('*font' => $Fontname); $Top->setPalette(%Colour_scheme); while (my($col, $val) = each %{$Top->Palette}) { # print "setting $col to $val\n"; $Top->optionAdd('*' . $col => $val); } $Top->bind("all", "", "Backspace"); create_windows(); load(shift @ARGV); # if @ARGV; MainLoop; } sub load($) { my ($gedcom_file) = @_; $Top->Busy; my $cont = 1; my $progress; $progress = $Top->WaitBox(-title => "Reading...", -txt1 => "", -canceltext => "Cancel", -cancelroutine => sub { $cont = 0 }); my $u = $progress->{SubWidget}{uframe}; my $utxt; my @pk = (-expand => 1, -fill => "both"); $u->pack(@pk); $u->Label(-textvariable => \$utxt)->pack(@pk); my $width = 700; my $height = 25; my $canv = $u->Canvas(-width => $width, -height => $height, -background => "red") ->pack(-expand => 0); $progress->Show; $Top->update; $Ged = Gedcom->new( # grammar_file => $Grammar_file, gedcom_file => $gedcom_file, callback => sub { my ($title, $txt1, $txt2, $current, $total) = @_; if ($total) { my $ratio = $current / $total; $utxt = sprintf("%5.2f%% complete", $ratio * 100); $canv->delete("all"); $canv->createLine(0, $height / 2, $ratio * $width, $height / 2, -width => $height, -fill => "green"); } $progress->configure(-title => $title, -txt1 => $txt1, -txt2 => $txt2); $Top->update; $progress->unShow unless $cont ||= (box("No", "Do you really want to cancel?", -title => "Cancel", -buttons => ["Yes", "No"]) eq "No"); $cont; }); $progress->unShow if $cont; my @individuals = $Ged->{record}->get_children("INDI"); if (@individuals) { show_record("", $individuals[0], "full"); } $Top->Unbusy; } sub save($) { my ($gedcom_file) = @_; $Top->Busy; $Top->update; $Ged->write($gedcom_file); $Top->Unbusy; } sub updown($$) { my ($list, $pos) = @_; $list->activate($pos =~ /^[+-]\d+$/ ? $list->index("active") + $pos : $pos); $list->see("active"); $list->selectionClear(0, "end"); $list->selectionSet("active") } sub select_record($) { my ($type) = @_; return undef unless $Ged->{record}; $Top->Busy; my @records = $Ged->{record}->get_children($type); # print "records are ", Dumper \@records; unless (exists $Top->{_box}) { my $box = $Top->{_box} = $Top->DialogBox(-default_button => "Ok", -title => "Select", -buttons => [ "Ok", "Cancel" ]); my $frame = $box->add("Frame")->pack(-fill => "both", -expand => 1); my $list = $frame->Scrolled("Listbox", -scrollbars => "w") ->pack(-fill => "both", -expand => 1); my $listbox = $Top->{_list} = $list->Subwidget("listbox"); my %font_spec = ( family => "courier", weight => "bold", slant => "r", point => $Options{font_point} * .75, ); my $font = $Top->Font(%font_spec); confess "Cannot allocate font - try changing some parameters" unless $font; $listbox->configure(-font => $font, -width => 65, -height => 20); $box->bind("" => sub { $box->{selected_button} = $listbox->curselection }); } $Top->{_list}->delete(0, "end"); for my $i (@records) { $Top->{_list}->insert("end", $i->summary) } updown($Top->{_list}, "+0"); $Top->Unbusy; my $i = $Top->{_box}->Show; return undef if $i eq "Cancel"; $i = $Top->{_list}->curselection if $i eq "Ok"; $records[$i]; } # TODO - put in Gedcom.pm sub get_tag($) { my ($tag) = @_; return $tag unless my ($t, $n) = $tag =~ /^([A-z]+)(\d*)$/; # print "Checking tag for <$t> <$n> => <$Tags{$t}>\n"; my $r = (exists($Tags{$t}) ? $Tags{$t} : $t) . $n; # print "got <$r>\n"; $r; } # TODO - put in Gedcom.pm sub get_name($) { my ($tag) = @_; # print "tag for $tag\n"; join(" ", map { get_tag($_) } split(/_/, $tag)); } sub create_items(%) { my (%a) = @_; my $height = $a{height} || 40; # print "lines is $a{canv}{_lines}\n"; my $y = $a{canv}{_lines}++ * ($height * 1.1); my $width = 850; $a{canv}->configure(-scrollregion => [0, 0, $width, $y + $height]); # print "size is ($width, $y + $height)\n"; my $x = 0; for my $item (@{$a{items}}) { my $tag = $item->{tag}; warn "no record for $tag" unless exists $a{canv}->{_ged}{$tag}; my $rec = $a{canv}->{_ged}{$tag}; if (exists $item->{widget}) { my $widget= $rec->{"_Frame"} = $a{canv}->Frame(-width => $width * $item->{relwidth}, -height => $height); my $w = $item->{widget}; my $bind = $rec->{"_$w"} = $widget->$w(%{$item->{options}}) ->pack(-expand => 1, -fill => "both"); $a{canv}->createWindow($width * $x, $y, -width => $width * $item->{relwidth}, -height => $height, -tags => [ $tag ], -anchor => "nw", -window => $widget); while (exists $item->{"bind"} && @{$item->{"bind"}}) { $bind->bind(splice @{$item->{"bind"}}, 0, 2); } } elsif (exists $item->{item}) { my $i = "create" . ucfirst $item->{item}; my $inc = $item->{item} eq "text" ? 8 : 0; my $t = "${tag}_$item->{item}"; my @tags = ($tag, $t); push (@tags, @{$item->{tags}}) if exists $item->{tags}; if ($item->{change}) { my $change = $tag . "_change"; push @tags, $change; ($rec->{_canvas_text} = $tag) =~ s/(NAME|XREF)1$/value/; # print "canvas text is $rec->{_canvas_text}\n"; } $a{canv}->$i($width * $x + $inc, $y + $inc, -tags => \@tags, -anchor => "nw", -font => $Fontname, -fill => $item->{colour} || "black", %{$item->{options}}); if (exists $item->{"bind"} && @{$item->{"bind"}}) { while (@{$item->{"bind"}}) { $a{canv}->bind(splice @{$item->{"bind"}}, 0, 3); } } } else { die "No widget or item specified for ", Dumper $item; } $x += $item->{relwidth}; $rec->{$tag =~ /_XREF1$/ ? "xref" : "value"} |= ""; # print "$w is ", $rec->{"_$w"}, # " and ", $Top->{_canv}{_ged}{$tag}{"_$w"}, "\n"; } confess "Width of $x should be 1" unless abs($x - 1) < 0.01; } sub create_record($$) { my ($canv, $tag) = @_; # print "creating record for $tag\n"; create_items ( canv => $canv, items => [ { tag => $tag, item => "text", options => { -text => get_name($tag) }, "bind" => [ $tag, "<1>" => sub { print "qaz\n" } ], relwidth => 0.25, }, { widget => "Entry", tag => $tag, options => {}, relwidth => 0.75, }, ] ); set_entry($tag); } sub create_person($$$) { my ($canv, $label, $tag) = @_; my $ref = "${tag}_XREF1"; # print "getting $ref\n"; my $xref = $canv->{_ged}{$ref}{xref}; my $me = $xref ? $Ged->{xrefs}{$canv->{_ged}{$ref}{xref}} : undef; # print "I am ", Dumper $me; create_items ( canv => $canv, items => [ { tag => "${tag}_NAME1", item => "text", tags => [ $tag, "${tag}_title" ], options => { -text => $label }, relwidth => 0.15, }, $tag ? { tag => "${tag}_NAME1", item => "text", tags => [ $tag, "${tag}_value" ], change => 1, options => { -text => $me ? $me->child_value("NAME1") : ""}, colour => "blue", "bind" => [ $tag, "<1>" => sub { my $me = $Ged->{xrefs}{$canv->{_ged}{"${tag}_XREF1"}{xref}}; # print "me is $me\n"; show_record("", $me, "full"); }, $tag, "<3>" => sub { my $ind = select_record("INDI"); # print "ind is ", Dumper $ind; return unless $ind; if (my ($fam, $person) = $tag =~ /^(_FAM[CS]\d*)_([^_]+)/) { $canv->itemconfigure("${tag}_XREF1_change", -text => $ind->{xref}); $canv->itemconfigure("${tag}_NAME1_change", -text => $ind->child_value("NAME1")); } record_changed(); }, $tag, "" => sub { my $c = shift; $canv->itemconfigure("${tag}_title", -fill => "red"); }, $tag, "" => sub { my $c = shift; $canv->itemconfigure("${tag}_title", -fill => "black"); }, ], relwidth => 0.65, } : { tag => "${tag}_NAME1", widget => "Entry", options => {}, relwidth => 0.65, }, { tag => "${tag}_XREF1", item => "text", tags => [ $tag, "${tag}_title" ], options => { -text => "Id" }, relwidth => 0.05, }, $tag ? { tag => "${tag}_XREF1", item => "text", tags => [ $tag, "${tag}_value" ], change => 1, options => { -text => $me ? $me->{xref} : "" }, colour => "blue", relwidth => 0.15, } : { tag => "${tag}_XREF1", widget => "Entry", options => {}, relwidth => 0.15, }, ] ); } sub create_event($$$) { my ($canv, $label, $tag) = @_; create_items ( canv => $canv, items => [ { tag => "${tag}_DATE1", item => "text", options => { -text => $label }, relwidth => 0.15, }, { widget => "Entry", tag => "${tag}_DATE1", options => {}, relwidth => 0.2, }, { tag => "${tag}_PLAC1", item => "text", options => { -text => "At" }, relwidth => 0.05, }, { widget => "Entry", tag => "${tag}_PLAC1", options => {}, relwidth => 0.6, }, ] ); } sub create_windows() { $Top->Busy; my $top_fr = $Top->Frame->pack(-fill => "both", -expand => 1); my $menu_fr = $Top->{_menu_fr} = $top_fr->Frame(-relief => "raised", -borderwidth => 5) ->pack(-fill => "x", -expand => 0); my $main_fr = $top_fr->Frame->pack(-fill => "both", -expand => 1); my $load = sub { my $gedcom_file = $Top->FileSelect(-filter => "*.ged")->Show or return; load($gedcom_file); }; my $save = sub { save_changes(); record_changed(); my $gedcom_file = $Top->FileSelect(-filter => "*.ged")->Show or return; save($gedcom_file); }; my $quit = sub { exit; }; my $iselect = sub { save_changes(); show_record("", select_record("INDI"), "full"); }; my $inew = sub { save_changes(); my $max = 0; for ($Ged->{record}->get_children("INDI")) { if (my ($val) = $_->{xref} =~ /I(\d+)/) { $max = $val if $val > $max; } } $max++; my $indi_id = "I$max"; my $rec = Gedcom::Record->new ( tag => "INDI", xref => $indi_id, grammar => $Ged->{record}{grammar}->child("INDI"), ); add_record($rec, "_NAME1"); add_record($rec, "_BIRT1_DATE1"); add_record($rec, "_BIRT1_PLAC1"); # print "new record is ", Dumper $rec; splice @{$Ged->{record}{children}}, -1, 0, $rec; $Ged->{xrefs}{$rec->{xref}} = $rec; show_record("", $rec, "full"); }; my $idelete = sub { if (box("No", "Are you sure you want to delete this record?", -title => "Delete record", -buttons => ["Yes", "No"]) eq "Yes") { my $i = 0; for (; $i < @{$Ged->{record}{children}}; $i++) { last if exists $Ged->{record}{children}[$i]{xref} && $Ged->{record}{children}[$i]{xref} eq $Rec->{xref}; } unless ($i < @{$Ged->{record}{children}}) { box("Whoops", "I can't find record $Rec->{xref}", -title => "Unknown record id"); return; } delete $Ged->{xrefs}{$Rec->{xref}}; splice @{$Ged->{record}{children}}, $i, 1; for my $fam ($Ged->{record}->get_children("FAM")) { my $i = 0; for (; $i < @{$fam->{children}}; $i++) { # print "$fam->{tag} $fam->{children}[$i]{tag} ", # "checking $fam->{children}[$i]{value} eq $Rec->{xref}\n"; last if exists $fam->{children}[$i]{value} && $fam->{children}[$i]{value} eq $Rec->{xref}; } if ($i < @{$fam->{children}}) { splice @{$fam->{children}}, $i, 1; } } my @individuals = $Ged->{record}->get_children("INDI"); if (@individuals) { show_record("", $individuals[0], "full", "no_save"); } } }; my $rsave = sub { save_changes("no_ask"); record_changed(); }; my $fselect = sub { save_changes(); show_record("", select_record("FAM"), "full"); }; my $file_menu = $Top->{_file_menu} = $menu_fr->Menubutton(-text => "File", -underline => 0) ->pack(-side => "left"); $file_menu->command(-label => "Load", -underline => 0, -command => $load); $Top->bind("", $load); $file_menu->command(-label => "Save", -underline => 2, -command => $save); $Top->bind("", $save); $file_menu->command(-label => "Quit", -underline => 0, -command => $quit); $Top->bind("", $quit); my $ind_menu = $Top->{_ind_menu} = $menu_fr->Menubutton(-text => "Individual", -underline => 0) ->pack(-side => "left"); $ind_menu->command(-label => "Select", -underline => 0, -command => $iselect); $Top->bind("", $iselect); $ind_menu->command(-label => "New", -underline => 0, -command => $inew); $Top->bind("", $inew); $ind_menu->command(-label => "Save", -underline => 2, -command => $rsave); $Top->bind("", $rsave); $ind_menu->command(-label => "Delete", -underline => 0, -command => $idelete); $Top->bind("", $rsave); my $fam_menu = $Top->{_fam_menu} = $menu_fr->Menubutton(-text => "Family", -underline => 5) ->pack(-side => "left"); $fam_menu->command(-label => "Select", -underline => 0, -command => $fselect); $Top->bind("", $fselect); $fam_menu->command(-label => "Save", -underline => 2, -command => $rsave); # $Top->bind("