package XMLNavigator; use strict; use warnings; use Gtk2; use XML::DOM; use POSIX qw(ceil); use constant DEFAULT_COLLAPSED_BOX_WIDTH => 75; use constant DEFAULT_COLLAPSED_BOX_HEIGHT => 45; use constant DEFAULT_FULL_BOX_HEIGHT => 155; use constant DEFAULT_FULL_BOX_WIDTH => 160; use constant DEFAULT_INSETS => 40; use constant RETICENCIAS_WIDTH => 15; use constant RETICENCIAS_HEIGHT => 5; use Glib::Object::Subclass 'Gtk2::DrawingArea', signals => { model_changed => { method => 'do_model_changed', flags => [qw/run-first/], return_type => undef, param_types => [] } }, properties => [ Glib::ParamSpec->scalar ('domdocument', 'domdocument', 'Object containing the DOM Document', [qw/readable writable/]), Glib::ParamSpec->int ('collapsed_box_width', 'collapsed_box_width', 'Width of the collapsed box', 0, 500, DEFAULT_COLLAPSED_BOX_WIDTH, [qw/readable writable/]), Glib::ParamSpec->int ('collapsed_box_height', 'collapsed_box_height', 'Height of the collapsed box', 0, 500, DEFAULT_COLLAPSED_BOX_HEIGHT, [qw/readable writable/]), Glib::ParamSpec->int ('full_box_height', 'full_box_height', 'Height of the full box', 0, 500, DEFAULT_FULL_BOX_HEIGHT, [qw/readable writable/]), Glib::ParamSpec->int ('full_box_width', 'full_box_width', 'Width of the full box', 0, 500, DEFAULT_FULL_BOX_WIDTH, [qw/readable writable/]), Glib::ParamSpec->int ('insets', 'insets', 'Insets between boxes', 0, 500, DEFAULT_FULL_BOX_WIDTH, [qw/readable writable/]), Glib::ParamSpec->int ('max_hori_distance', 'max_hori_distance', 'Maximum Horizontal Distance', 0, 500, 3, [qw/readable writable/]), Glib::ParamSpec->int ('max_vert_distance', 'max_vert_distance', 'Maximum Vertical Distance', 0, 500, 3, [qw/readable writable/]) ]; sub INIT_INSTANCE { my $self = shift; $self->{domdocument} = undef; $self->{matrix} = {}; $self->{collapsed_box_height} = DEFAULT_COLLAPSED_BOX_HEIGHT; $self->{collapsed_box_width} = DEFAULT_COLLAPSED_BOX_WIDTH; $self->{full_box_width} = DEFAULT_FULL_BOX_WIDTH; $self->{full_box_height} = DEFAULT_FULL_BOX_HEIGHT; $self->{max_vert_distance} = 3; $self->{max_hori_distance} = 3; $self->{insets} = DEFAULT_INSETS; $self->signal_connect(button_press_event => \&button_press_event); $self->signal_connect(expose_event => \&expose_event); $self->signal_connect(configure_event => \&configure_event); $self->signal_connect(size_request => \&do_size_request); $self->signal_connect(scroll_event => \&scroll_event); $self->set_events ([qw(exposure-mask leave-notify-mask button-press-mask button-release-mask scroll-mask)]); } sub GET_PROPERTY { my ($self, $pspec) = @_; if (exists $self->{$pspec->get_name()}) { return $self->{$pspec->get_name()}; } } sub SET_PROPERTY { my ($self, $pspec, $newval) = @_; if (exists $self->{$pspec->get_name()}) { $self->{$pspec->get_name()} = $newval; } if ($pspec->get_name eq 'domdocument') { $self->{matrix} = {}; $self->plan_matrix(); $self->queue_draw(); } elsif ($pspec->get_name eq 'matrix') { $self->plan_matrix(); $self->queue_draw(); } } sub do_model_changed { } sub plan_matrix { my $self = shift; my $max_hori_distance = $self->{max_hori_distance}; my $max_vert_distance = $self->{max_vert_distance}; my $xmlthink_center = $self->{matrix}{0}{0}; my %center_on_row = (); foreach my $key (keys %{$self->{matrix}}) { $center_on_row{$key} = $self->{matrix}{$key}{0}; } $self->{matrix} = {}; if (not defined $self->{domdocument}) { return; } my $xmlthink_root = $self->{domdocument}->getDocumentElement(); if (not defined $xmlthink_root) { return; } if (not defined $xmlthink_center) { $xmlthink_center = $xmlthink_root; } #print "0, 0 = $xmlthink_center\n"; $self->{matrix}{0}{0} = $xmlthink_center; HORIZONTAL_BW: for my $hori_distance (1..$max_hori_distance+1) { $hori_distance *= -1; my $elem = $self->{matrix}{$hori_distance + 1}{0}; my $other = $elem->getParentNode(); if ($other && $other != $self->{domdocument}) { #print "$hori_distance, 0 = $other\n"; $self->{matrix}{$hori_distance}{0} = $other; } else { last HORIZONTAL_BW; } for my $sign_v (-1, 1) { VERTICAL_BW: for my $vert_distance (1..$max_vert_distance) { $vert_distance *= $sign_v; my $other = $self->{matrix}{$hori_distance + 1}{$vert_distance - $sign_v}; my $brot = $sign_v<0?$other->getPreviousSibling():$other->getNextSibling(); while (defined $brot && $brot->getNodeType() != XML::DOM::ELEMENT_NODE) { $brot = $sign_v<0?$brot->getPreviousSibling():$brot->getNextSibling(); } if ($brot) { #print "".($hori_distance+1).", $vert_distance = $brot\n"; $self->{matrix}{$hori_distance + 1}{$vert_distance} = $brot; my @list = grep { $_->getNodeType() == XML::DOM::ELEMENT_NODE } $brot->getChildNodes(); if ($#list >= 0) { my $la = $hori_distance+1.0001; $la =~ s/,/./g; #print "$la, $vert_distance = undef\n"; $self->{matrix}{$la}{$vert_distance} = undef } } else { last VERTICAL_BW; } if ($hori_distance == -1) { if (abs($vert_distance) == $max_vert_distance -1) { my $obrot = $sign_v<0?$other->getPreviousSibling():$other->getNextSibling(); if ($obrot) { #print "".($hori_distance+1).", ".($vert_distance + $sign_v)." = undef\n"; $self->{matrix}{$hori_distance + 1}{$vert_distance + $sign_v} = undef; } last VERTICAL_BW; } } else { if (abs($vert_distance) == $max_vert_distance) { my $obrot = $sign_v<0?$other->getPreviousSibling():$other->getNextSibling(); if ($obrot) { #print "".($hori_distance+1).", ".($vert_distance + $sign_v)." = undef\n"; $self->{matrix}{$hori_distance + 1}{$vert_distance + $sign_v} = undef; } } } } } if (abs($hori_distance) == $max_hori_distance + 1) { my $obrot = $other->getParentNode(); #print "$hori_distance, 0 = undef\n"; $self->{matrix}{$hori_distance}{0} = undef; } } HORIZONTAL_FW: for my $hori_distance (1..$max_hori_distance) { my $elem = $self->{matrix}{$hori_distance - 1}{0}; last HORIZONTAL_FW unless defined $elem; my @list = grep { $_->getNodeType() == XML::DOM::ELEMENT_NODE } $elem->getChildNodes(); last HORIZONTAL_FW unless $#list >= 0; if (not $center_on_row{$hori_distance}) { $center_on_row{$hori_distance} = $list[ceil($#list/2)]; } my $obj = $center_on_row{$hori_distance}; #print "$hori_distance, 0 = $obj\n"; $self->{matrix}{$hori_distance}{0} = $obj; for my $sign_v (-1, 1) { VERTICAL_FW: for my $vert_distance (1..$max_vert_distance) { $vert_distance *= $sign_v; my $other = $self->{matrix}{$hori_distance}{$vert_distance - $sign_v}; my $brot = $sign_v<0?$other->getPreviousSibling():$other->getNextSibling(); while (defined $brot && $brot->getNodeType() != XML::DOM::ELEMENT_NODE) { $brot = $sign_v<0?$brot->getPreviousSibling():$brot->getNextSibling(); } if ($brot) { #print "".($hori_distance).", $vert_distance = $brot\n"; $self->{matrix}{$hori_distance}{$vert_distance} = $brot; if (grep { $_->getNodeType() == XML::DOM::ELEMENT_NODE } $brot->getChildNodes()) { my $la = $hori_distance+0.0001; $la =~ s/,/./g; #print "".($la).", $vert_distance = undef\n"; $self->{matrix}{$la}{$vert_distance} = undef } } else { last VERTICAL_FW; } if (abs($vert_distance) == $max_vert_distance) { my $obrot = $sign_v<0?$other->getPreviousSibling():$other->getNextSibling(); if ($obrot) { #print "".($hori_distance).", ".($vert_distance+$sign_v)." = $brot\n"; $self->{matrix}{$hori_distance}{$vert_distance + $sign_v} = undef; } } } } if (abs($hori_distance) == $max_hori_distance) { my @list = grep { $_->getNodeType() == XML::DOM::ELEMENT_NODE } $elem->getChildNodes(); if ($#list >= 0) { #print "".($hori_distance+1).", 0 = undef\n"; $self->{matrix}{$hori_distance+1}{0} = undef; } } } $self->drawit_all(); } sub do_size_request { my ($self, $requisition) = @_; $requisition->width ($self->{full_box_width}); $requisition->height ($self->{full_box_height}); } sub configure_event { my ($self, $event) = @_; $self->{pixmap} = Gtk2::Gdk::Pixmap->new ($self->window, $self->allocation->width, $self->allocation->height, -1); # same depth as window $self->{max_vert_distance} = int((($self->allocation->height/2)-($self->{full_box_height}/2)-($self->{insets}/2))/($self->{collapsed_box_height}+$self->{insets})) || 1; $self->{max_hori_distance} = int((($self->allocation->width/2)-($self->{full_box_width}/2)-($self->{insets}/2))/($self->{collapsed_box_width}+$self->{insets})) || 1; $self->plan_matrix(); } sub expose_event { my ($self, $event) = @_; $self->window->draw_drawable ($self->style->fg_gc($self->state), $self->{pixmap}, $event->area->x, $event->area->y, $event->area->x, $event->area->y, $event->area->width, $event->area->height); } sub drawit_all { my $self = shift; return unless $self->{pixmap}; $self->drawit_in(0, 0, $self->allocation->width(), $self->allocation->height()); } sub drawit_in { my $self = shift; my ($x, $y, $w, $h) = @_; my $gc = $self->style->fg_gc($self->state); $gc->set_clip_rectangle(Gtk2::Gdk::Rectangle->new($x, $y, $w, $h)); $gc->set_rgb_fg_color(Gtk2::Gdk::Color->new(255*257, 255*257, 255*257)); $self->{pixmap}->draw_rectangle($gc, 1, 0, 0, $w, $h); $gc->set_rgb_fg_color(Gtk2::Gdk::Color->new(0, 0, 0)); $self->draw_data($self->{pixmap}, $gc); $gc->set_clip_rectangle(undef); $self->queue_draw(); } sub draw_data { my $self = shift; my ($area, $gc) = @_; my $max_hori_distance = $self->{max_hori_distance}; my $max_vert_distance = $self->{max_vert_distance}; my $xmlthink_doc = $self->{domdocument}; return unless $xmlthink_doc; my $xmlthink_root = $xmlthink_doc->getDocumentElement(); my $xmlthink_center = $self->{matrix}{0}{0}; if (not defined $xmlthink_center) { $self->plan_matrix(); $xmlthink_center = $self->{matrix}{0}{0}; return unless $xmlthink_center; } my $canvas_width = $self->allocation->width; my $canvas_height = $self->allocation->height; my $center_x = int($canvas_width/2); my $center_y = int($canvas_height/2); my $fullb_w = $self->{full_box_width}; my $fullb_h = $self->{full_box_height}; my $collb_w = $self->{collapsed_box_width}; my $collb_h = $self->{collapsed_box_height}; my $insets = $self->{insets}; $self->draw_center_element($area, $gc); if ($xmlthink_root != $xmlthink_center) { $self->draw_line_to_parent($area, $gc, $center_x - int($insets/2) - int($fullb_w/2) + 1, $center_y - int($insets/2)); } for my $hori (sort {$a <=> $b} keys %{$self->{matrix}}) { for my $vert (sort {$a <=> $b} keys %{$self->{matrix}{$hori}}) { next if ($hori == 0 && $vert == 0); my $obj = $self->{matrix}{$hori}{$vert}; my $abs_distance_x = (abs($hori) * ($collb_w + $insets)); my $abs_distance_y = (abs($vert) * ($collb_h + $insets)); $abs_distance_x += int(($fullb_w - $collb_w)/2); if (abs($hori)<0.1) { # Center Column !! $abs_distance_y += int(($fullb_h - $collb_h)/2); } my $distance_x = $abs_distance_x * ($hori>=1?1:-1); my $distance_y = $abs_distance_y * ($vert==0?0:$vert>0?1:-1); my $box_center_x = $center_x + $distance_x; my $box_center_y = $center_y + $distance_y; my $x = $box_center_x - (int($collb_w/2)+int($insets/2)); my $y = $box_center_y - (int($collb_h/2)+int($insets/2)); if (exists $self->{matrix}{$hori - 1} && int($hori)==$hori) { $self->draw_line_to_parent($area, $gc, $x, int($y+$collb_h/2)); } if ($obj) { $self->draw_collapsed_element($area, $gc, $obj, $x, $y); } else { if (int($hori)!=$hori) { # side reticencia $x = $box_center_x + (int($collb_w/2)-int($insets/2)); $y = $box_center_y - (int($collb_h/2)+int($insets/2)); $self->draw_reticencia($area, $gc, $x+2, $y); } else { # element reticencia $x = $box_center_x - (int($collb_w / 2)+int($insets/2)); $y = $box_center_y - (int(RETICENCIAS_HEIGHT / 2)+int($insets/2)); $self->draw_reticencia($area, $gc, $x, $y); } } } } $self->queue_draw(); } sub draw_center_element { my ($self, $area, $gc) = @_; my $canvas_width = $self->allocation->width; my $canvas_height = $self->allocation->height; my $pangoc = $self->get_pango_context(); my $fontdesc = Gtk2::Pango::FontDescription->from_string("Sans 10"); my $xmlthink_center = $self->{matrix}{0}{0}; my $center_x = int($canvas_width/2); my $center_y = int($canvas_height/2); my $fullb_w = $self->{full_box_width}; my $fullb_h = $self->{full_box_height}; my $insets = $self->{insets}; my $x = $center_x - int($fullb_w/2) - int($insets/2); my $y = $center_y - int($fullb_h/2) - int($insets/2); # BIG RECTANGLE $area->draw_rectangle($gc, 0, $x, $y, $fullb_w, $fullb_h); # TAG my $tag_rect = Gtk2::Gdk::Rectangle->new($x+2, $y+2, $fullb_w-4, 20); $area->draw_rectangle($gc, 0, $tag_rect->x, $tag_rect->y, $tag_rect->width, $tag_rect->height); $self->draw_text($gc, $xmlthink_center->getTagName(), $tag_rect->x+2, $tag_rect->y+2, $tag_rect->width-4, $tag_rect->height-4); # ATTRIBUTES my $attr_rect = Gtk2::Gdk::Rectangle->new($x+2, $y+24, $fullb_w-4, int(($fullb_h-32)/2)); $area->draw_rectangle($gc, 0, $attr_rect->x, $attr_rect->y, $attr_rect->width, $attr_rect->height); $self->draw_text($gc, $self->make_text_from_attributes($xmlthink_center, "\n"), $attr_rect->x+2, $attr_rect->y+2, $attr_rect->width-4, $attr_rect->height-4); # CDATA my $cdata_rect = Gtk2::Gdk::Rectangle->new($x+2, $y+24+int(($fullb_h-32)/2)+2, $fullb_w-4, int(($fullb_h-23)/2)); $area->draw_rectangle($gc, 0, $cdata_rect->x, $cdata_rect->y, $cdata_rect->width, $cdata_rect->height); $self->draw_text($gc, $self->make_text_from_cdata($xmlthink_center), $cdata_rect->x+2, $cdata_rect->y+2, $cdata_rect->width-4, $cdata_rect->height-4); } sub make_text_from_attributes { my ($draw, $element, $sep) = @_; my $str = ''; my $nmap = $element->getAttributes(); for my $i (0..($nmap->getLength() - 1)) { my $node = $nmap->item($i); $str .= $node->getNodeName()."=".$node->getNodeValue().$sep; } chop($str); return $str; } sub make_text_from_cdata { my ($draw, $element) = @_; my $str = ''; for my $child ($element->getChildNodes()) { if ($child->getNodeType() != XML::DOM::ELEMENT_NODE && $child->getNodeType() != XML::DOM::ATTRIBUTE_NODE) { my $o = $child->getNodeValue(); $o =~ s/^\s*//; $o =~ s/\s*$//; $str .= $o."\n" if $o; } } return $str; } sub draw_reticencia { my ($draw, $area, $gc, $x, $y) = @_; $area->draw_arc($gc, 0, $x, $y, (RETICENCIAS_WIDTH / 3), RETICENCIAS_HEIGHT, 90*64, 180*64); $area->draw_arc($gc, 0, $x +(2*RETICENCIAS_WIDTH / 3) - 1, $y, (RETICENCIAS_WIDTH / 3), RETICENCIAS_HEIGHT, 270*64, 180*64); $area->draw_line($gc, $x+(2*RETICENCIAS_WIDTH/6), $y+(RETICENCIAS_HEIGHT/2), $x+(4*RETICENCIAS_WIDTH/6), $y+(RETICENCIAS_HEIGHT/2)); $area->draw_line($gc, $x+(RETICENCIAS_WIDTH/2), $y, $x+(RETICENCIAS_WIDTH/2), $y+(RETICENCIAS_HEIGHT)); } sub draw_collapsed_element { my ($draw, $area, $gc, $element, $x, $y) = @_; my $collb_w = $draw->{collapsed_box_width}; my $collb_h = $draw->{collapsed_box_height}; my $tag = $element->getTagName(); my $attr = $draw->make_text_from_attributes($element, ", "); my $cdata = $draw->make_text_from_cdata($element); $draw->{pixmap}->draw_rectangle($gc, 0, $x, $y, $collb_w, $collb_h); $draw->draw_text($gc, $tag, $x+2, $y, $collb_w-4, int($collb_h/3)); $draw->draw_text($gc, $attr, $x+2, $y+int($collb_h/3), $collb_w-4, int($collb_h/3)); $draw->draw_text($gc, $cdata, $x+2, $y+2*int($collb_h/3), $collb_w-4, int($collb_h/3)); } sub draw_text { my ($draw, $gc, $text, $x, $y, $w, $h) = @_; my $pangoc = $draw->get_pango_context(); my $fontdesc = Gtk2::Pango::FontDescription->from_string("Sans 10"); my $rect = Gtk2::Gdk::Rectangle->new($x, $y, $w, $h); my $layout = Gtk2::Pango::Layout->new($pangoc); my $clipped = Gtk2::Gdk::GC->new($draw->{pixmap}); $clipped->set_clip_rectangle($rect); $layout->set_font_description($fontdesc); $layout->set_text($text); $draw->{pixmap}->draw_layout($clipped, $x, $y, $layout); } sub draw_line_to_parent { my ($self, $area, $gc, $x, $y) = @_; my $canvas_width = $self->allocation->width; my $canvas_height = $self->allocation->height; $area->draw_line($gc, $x, $y, $x-int($self->{insets}/2), $y); $area->draw_line($gc, $x-int($self->{insets}/2), $y, $x-int($self->{insets}/2), int($canvas_height/2)-int($self->{insets}/2)); $area->draw_line($gc, $x-int($self->{insets}/2), int($canvas_height/2)-int($self->{insets}/2), $x-$self->{insets}, int($canvas_height/2)-int($self->{insets}/2)); } sub scroll_event { my ($self, $event) = @_; my $x = $event->x; my $y = $event->y; my $dir = $event->direction; my ($col, $row) = $self->x_y_to_col_row($x, $y); if ($col > 0) { my $other = $dir eq 'up'?-1:1; if (exists $self->{matrix}{$col}{$other} && $self->{matrix}{$col}{$other}) { $self->{matrix}{$col}{0} = $self->{matrix}{$col}{$other}; while (exists $self->{matrix}{$col + 1}) { delete $self->{matrix}{$col + 1}; $col++; } $self->plan_matrix(); } } } sub element_on_col_row { my ($self, $col, $row) = @_; return $self->{matrix}{$col}{$row}; } sub x_y_to_col_row { my ($self, $x, $y) = @_; my ($col, $row); my $canvas_width = $self->allocation->width; my $canvas_height = $self->allocation->height; my $center_x = int($canvas_width/2); my $center_y = int($canvas_height/2); my $fullb_w = $self->{full_box_width}; my $fullb_h = $self->{full_box_height}; my $collb_w = $self->{collapsed_box_width}; my $collb_h = $self->{collapsed_box_height}; my $insets = $self->{insets}; if ($x > ($center_x + int($fullb_w/2))) { my $rel_x = $x - ($center_x + int($fullb_w/2)); $col = int($rel_x/($collb_w+$insets)) + 1; my $rel_y = $y - $center_y; $row = ceil($rel_y/($collb_h+$insets))+0; } elsif ($x < ($center_x - int($fullb_w/2))) { my $rel_x = $x - ($center_x - int($fullb_w/2)); $col = int($rel_x/($collb_w+$insets)) - 1; my $rel_y = $y - $center_y; $row = ceil($rel_y/($collb_h+$insets)); } else { $col = 0; if ($y > ($center_y + int($fullb_h/2))) { my $rel_y = $y - ($center_y + int($fullb_h/2)); $row = int($rel_y/($collb_h+$insets)) + 1; } elsif ($y < ($center_y - int($fullb_h/2))) { my $rel_y = $y - ($center_y - int($fullb_h/2) - int($insets/2)); $row = int($rel_y/($collb_h+$insets)) - 1; } } return ($col, $row); } sub button_press_event { my ($draw, $event) = @_; my $x = $event->x; my $y = $event->y; my $button = $event->button; my ($col, $row) = $draw->x_y_to_col_row($x, $y); $draw->click_col_row($button, $col, $row); } sub click_col_row { my ($self, $button, $col, $row) = @_; $col ||= 0; $row ||= 0; if (exists $self->{matrix}{$col}{$row} and $self->{matrix}{$col}{$row}) { my $obj = $self->{matrix}{$col}{$row}; if ($button == 1) { $self->{matrix} = {}; $self->{matrix}{0}{0} = $obj; $self->plan_matrix(); } } } 1;