3 # Giddy - Git History Digger, a glorified pickaxe frontend
4 # (c) Petr Baudis <pasky@suse.cz> 2008
5 # GPLv2 / Perl Artistic License
16 our $ctrl = Giddy
::Controller
->new();
21 our ($Revspec, $Tree, $File, $Full_revlist);
24 if ($ARGV[0] eq '-h' or $ARGV[0] eq '--help') {
25 die "Usage: giddy [[<revspec>] <filename>]";
29 } elsif (@ARGV == 2) {
34 # Canonical form e.g. for autoselecting in commit browser
35 $Tree = $ctrl->repo()->command_oneline('rev-list', '-1', $Revspec);
38 ## Build window structure
40 our $fixed_font = Gtk2
::Pango
::FontDescription
->from_string("Monospace");
42 my $window = Gtk2
::Window
->new('toplevel');
43 $window->signal_connect(destroy
=> sub { Gtk2
->main_quit });
44 $window->set_default_size(1024, 650);
47 my $toolbar = Giddy
::Toolbar
->new($ctrl);
50 our $statusbar = Gtk2
::Statusbar
->new;
53 my $tree_browser = Giddy
::TreeBrowser
->new($ctrl);
54 $tree_browser->{widget
}->set_property('width-request', 100);
57 my $files_panel = Giddy
::FilesPanel
->new($ctrl);
60 my $commit_browser = Giddy
::CommitBrowser
->new($ctrl);
61 $commit_browser->{widget
}->set_property('width-request', 200);
63 # Compose middle section:
64 my $hpan1 = Gtk2
::HPaned
->new();
65 my $hpan2 = Gtk2
::HPaned
->new();
66 $hpan1->pack1($tree_browser->{widget
}, 0, 0);
67 $hpan1->pack2($hpan2, 1, 1);
68 $hpan2->pack1($files_panel->{widget
}, 1, 1);
69 $hpan2->pack2($commit_browser->{widget
}, 0, 0);
72 my $vbox = Gtk2
::VBox
->new();
73 $vbox->pack_start($toolbar->{widget
}, 0, 0, 0);
74 $vbox->pack_start($hpan1, 1, 1, 0);
75 $vbox->pack_end($statusbar, 0, 0, 0);
82 $tree_browser->load($Tree);
83 $files_panel->open_commit($Tree);
84 $files_panel->open($Tree, $File) if $File;
85 $commit_browser->load(revspec
=> $Revspec); # TODO
86 $Full_revlist = $commit_browser->get_commit_data();
100 my ($str, $len) = @_;
103 if (length($str) > $len) {
106 $str2 = substr($str, 0, $elen / 2);
108 $str2 .= substr($str, -$elen / 2);
117 # When hacking anything below this line, you are supposed to listen
118 # to The Dø - A Mouthful, the official soundtrack of this script.
119 # Otherwise, your code will be no good. Sorry.
120 # Passable alternatives: Hooverphonic, Massive Attack, Over the Rhine.
122 package Giddy
::Controller
;
124 # Tying the widgets together
131 $self->{repo
} = Git
->repository();
142 my ($entry, $popup) = @_;
143 $File = $entry->{name
};
145 $files_panel->open($Tree, $File);
147 $files_panel->load($Tree, $File);
154 my @files = $tree_browser->selected_files();
156 $self->file_selected($_, 1) for @files;
158 $self->file_selected($files[0]);
162 sub commit_selected
{
165 $Tree = $entry->{id
};
166 $tree_browser->load($Tree);
167 $files_panel->commit_changed();
170 sub previous_commit
{
172 $self->commit_selected({
173 id
=> $self->repo()->command_oneline('rev-parse', "$Tree^")
175 $commit_browser->commit_changed();
176 $files_panel->commit_changed();
181 my @files = map { $_->{name
} } $tree_browser->selected_files();
182 $commit_browser->load(
183 revspec
=> $commit_browser->{revspec
},
187 sub show_all_commits
{
189 $commit_browser->set_commit_data($Full_revlist);
190 $commit_browser->load(revspec
=> $commit_browser->{revspec
});
197 $commit_browser->load(revspec
=> $Revspec);
200 # if files is undef, keep current fileset; if files is [], empty fileset
203 my ($text, $files) = @_;
204 $commit_browser->load(
205 revspec
=> $commit_browser->{revspec
},
206 ($files ? @
$files ?
(fileset
=> $files) : () : (fileset
=> $commit_browser->{fileset
})),
213 exec('gitk', $commit_browser->log_args());
220 package Giddy
::Toolbar
;
225 my $self = { ctrl
=> $ctrl };
228 $self->{_tb_widget
} = Gtk2
::Toolbar
->new;
229 $self->{_tb_widget
}->set_icon_size('small-toolbar');
230 $self->{_tb_widget
}->insert_stock('gtk-open', 'Open in current tab', '...', \
&load
, $self, $i++);
231 $self->{_tb_widget
}->insert_stock('gtk-new', 'Open in new tab', '...', \
&open, $self, $i++);
232 $self->{_tb_widget
}->insert_stock('gtk-zoom-fit', 'Limit commits to selected files', '...', \
&limit
, $self, $i++);
233 $self->{_tb_widget
}->insert_stock('gtk-zoom-100', 'Show all commits', '...', \
&unlimit
, $self, $i++);
234 $self->{_tb_widget
}->insert_stock('gtk-convert', 'Open commits in gitk', '...', \
&gitk
, $self, $i++);
235 $self->{_tb_widget
}->insert_stock('gtk-go-back', 'Go to previous commit (first parent)', '...', \
&prevcommit
, $self, $i++);
237 $self->{_revspec_widget
} = Gtk2
::Entry
->new;
238 $self->{_revspec_widget
}->set_text($Revspec);
239 $self->{_revspec_widget
}->set_width_chars(20);
240 $self->{_revspec_widget
}->signal_connect(activate
=> \
&_set_revspec
, $self);
242 $self->{widget
} = Gtk2
::HBox
->new;
243 $self->{widget
}->pack_start($self->{_tb_widget
}, 1, 1, 0);
244 $self->{widget
}->pack_end($self->{_revspec_widget
}, 0, 0, 0);
251 $self->{ctrl
}->file_open(0);
256 $self->{ctrl
}->file_open(1);
261 $self->{ctrl
}->trim_commits();
266 $self->{ctrl
}->show_all_commits();
271 $self->{ctrl
}->open_gitk();
276 $self->{ctrl
}->previous_commit();
281 $self->{ctrl
}->set_revspec($e->get_text());
285 package Giddy
::TreeBrowser
;
292 # Create object and set up the widgets
296 my $self = { ctrl
=> $ctrl };
298 # Holds data about the tree; each item (keyed by name)
299 # is hashref with keys {mode}, {type}, {id}, {iter};
300 # iter points into the store.
303 $self->{_tree_store
} = Gtk2
::TreeStore
->new(qw
/Glib::Scalar Glib::String Glib::String/);
304 $self->{_tree_widget
} = Gtk2
::TreeView
->new($self->{_tree_store
});
305 $self->{_tree_widget
}->set_headers_visible(0);
306 $self->{_tree_widget
}->get_selection()->set_mode('GTK_SELECTION_MULTIPLE');
307 $self->{_tree_widget
}->signal_connect(row_activated
=> \
&_row_activated
, $self);
308 $self->{_tree_widget
}->signal_connect(button_press_event
=> \
&_button_press
, $self);
310 # I hate the overengineered crap called "GTK". --pasky
311 $self->{_tree_widget
}->append_column(
312 Gtk2
::TreeViewColumn
->new_with_attributes(
313 "Name", Gtk2
::CellRendererText
->new,
318 $self->{widget
} = Gtk2
::ScrolledWindow
->new;
319 $self->{widget
}->add($self->{_tree_widget
});
324 # Load given tree-ish to the browser
329 $self->{tree
} = $tree;
332 if (keys %{$self->{_tree
}}) {
333 %selection = map { $_->{name
} => 1 } $self->selected_files();
336 $selection{$File} = 1 if $File;
340 $self->{_tree_store
}->clear();
342 my @files = $self->{ctrl
}->repo()->command('ls-tree', '-r', $tree);
346 @entry{'mode', 'type', 'id', 'name'} = /^(\d+) (\w+) ([0-9a-f]+)\t(.+)/;
347 my $iter = $self->_add(\
%entry);
348 if ($selection{$entry{'name'}}) {
349 $self->{_tree_widget
}->expand_to_path($self->{_tree_store
}->get_path($iter));
350 $self->{_tree_widget
}->get_selection()->select_iter($iter);
357 # Get list of selected files
361 my @paths = $self->{_tree_widget
}->get_selection->get_selected_rows();
362 map { $self->{_tree_store
}->get($self->{_tree_store
}->get_iter($_), COL_REF
); } @paths;
365 # Get iter of directory where given entry belongs
370 my ($dir) = ($entry->{'name'} =~ m
#(.*)/#);
371 return undef unless $dir;
372 return $self->{_tree
}->{$dir}->{'iter'} if $self->{_tree
}->{$dir};
374 my $tree = { type
=> 'tree', name
=> $dir };
378 # Add entry to the tree
383 $entry->{'iter'} = $self->{_tree_store
}->append($self->_parent($entry));
384 my $s = $entry->{'name'}; $s =~ s
#.*/##;
385 $self->{_tree_store
}->set($entry->{'iter'},
388 COL_MODE
, $entry->{'mode'}
390 $self->{_tree
}->{$entry->{'name'}} = $entry;
396 my ($tv, $path, $column, $self, $popup) = @_;
398 my $model = $tv->get_model();
399 my ($entry) = $model->get($model->get_iter($path), COL_REF
);
400 if ($entry->{type
} eq 'tree') {
401 return $tv->row_expanded($path) ?
$tv->collapse_row($path) : $tv->expand_row($path, 0);
403 $self->{ctrl
}->file_selected($entry, $popup);
407 my ($tv, $ev, $self) = @_;
410 if ($ev->type() eq 'button-press' and $ev->button() == 2) {
411 my $path = $tv->get_path_at_pos($ev->x(), $ev->y());
412 return _row_activated
($tv, $path, undef, $self, 1);
420 package Giddy
::CommitBrowser
;
425 sub COL_AUTHOR
{ 2; }
426 sub COL_SUBJECT
{ 3; }
428 # Create object and set up the widgets
432 my $self = { ctrl
=> $ctrl };
434 # Holds data about the history tree; each item (keyed by id)
435 # is hashref with keys {id}, {author}, {subject}, {parents}, {iter};
436 # iter points into the store, parent is [].
437 $self->{_commits
} = {};
438 $self->{_commit_sequence
} = [];
440 $self->{_tree_store
} = Gtk2
::TreeStore
->new(qw
/Glib::Scalar Glib::String Glib::String Glib::String/);
441 $self->{_tree_widget
} = Gtk2
::TreeView
->new($self->{_tree_store
});
442 $self->{_tree_widget
}->set_rules_hint(1);
443 #$self->{_tree_widget}->set_headers_clickable(1);
444 $self->{_tree_widget
}->set_reorderable(1); # This does not work; why?
445 $self->{_tree_widget
}->set_grid_lines('GTK_TREE_VIEW_GRID_LINES_VERTICAL');
446 $self->{_tree_widget
}->get_selection()->set_mode('GTK_SELECTION_MULTIPLE');
447 $self->{_tree_widget
}->signal_connect(row_activated
=> \
&_row_activated
, $self);
449 # I hate the overengineered crap called "GTK". --pasky
451 my ($name, $id, $monospace) = @_;
452 my $cell = Gtk2
::CellRendererText
->new;
453 $cell->set_property('font-desc', $fixed_font) if $monospace;
454 my $col = Gtk2
::TreeViewColumn
->new_with_attributes($name, $cell, "text", $id);
455 $col->set_resizable(1);
456 $col->set_clickable(1);
459 $self->{_tree_widget
}->append_column(col
("Subject", COL_SUBJECT
));
460 $self->{_tree_widget
}->append_column(col
("Author", COL_AUTHOR
));
461 $self->{_tree_widget
}->append_column(col
("ID", COL_ID
, 1));
463 $self->{_sbc
} = $statusbar->get_context_id('c');
465 $self->{widget
} = Gtk2
::ScrolledWindow
->new;
466 $self->{widget
}->add($self->{_tree_widget
});
471 # Load given revision range (limited to given files) to the browser
476 my $gdkwin = $self->{widget
}->window();
479 $gdkwin->set_cursor(Gtk2
::Gdk
::Cursor
->new('watch'));
482 $self->{_load_sigh
} = $self->{widget
}->signal_connect(realize
=> sub {
483 $self->{widget
}->window()->set_cursor(Gtk2
::Gdk
::Cursor
->new('watch'));
487 $self->{revspec
} = $args{revspec
};
488 $self->{fileset
} = $args{fileset
};
489 $self->{pickaxe
} = $args{pickaxe
};
491 if (keys %{$self->{_commits
}}) {
492 $self->{_load_selection
} = { map { $_->{id
} => 1 } $self->selected_commits() };
495 $self->{_load_selection
} = { $Tree => 1 };
498 $self->{_commits
} = {};
499 $self->{_commit_sequence
} = [];
500 $self->{_tree_store
}->clear();
502 my @args = $self->log_args();
503 $statusbar->push($self->{_sbc
}, join(' ', 'git', 'log', (map { ::midtrim
($_, 40); } @args)));
505 # Asynchronous reading, this can take *long*
507 ($self->{_load_fh
}, $self->{_load_ctx
}) =
508 $self->{ctrl
}->repo()->command_output_pipe('log',
509 join("\t", '--pretty=format:%H', '%P', '%an', '%s'),
511 #binmode($self->{_load_fh}, ':utf8'); # XXX: doesn't work?
513 $self->{_glib_is_crap1
} = Glib
::IO
->add_watch(fileno($self->{_load_fh
}), 'in', \
&_log_line_read
, $self);
514 $self->{_glib_is_crap2
} = Glib
::IO
->add_watch(fileno($self->{_load_fh
}), 'hup', \
&_log_line_read
, $self);
519 # Get list of git log arguments corresponding to current list
524 #'-M', '-C', # makes git log output garbage
525 ($self->{pickaxe
} ?
('-S' . $self->{pickaxe
}) : ()),
526 ($self->{fileset
} ?
('--follow') : ()),
528 ($self->{fileset
} ?
('--', @
{$self->{fileset
}}) : ())
532 # Get list of selected commits
533 sub selected_commits
{
536 my @paths = $self->{_tree_widget
}->get_selection->get_selected_rows();
537 map { $self->{_tree_store
}->get($self->{_tree_store
}->get_iter($_), COL_REF
); } @paths;
540 # Select current commit
543 $self->{_tree_widget
}->get_selection()->unselect_all();
544 $self->{_tree_widget
}->get_selection()->select_iter($self->{_commits
}->{$Tree}->{'iter'});
548 # Get the state of commits list
549 sub get_commit_data
{
552 @
$data{'revspec', 'fileset', 'pickaxe', 'commits'} =
553 @
$self{'revspec', 'fileset', 'pickaxe', '_commit_sequence'};
557 # Load a state of commits list as returned by get_commit_data()
558 sub set_commit_data
{
562 @
$self{'revspec', 'fileset', 'pickaxe'} =
563 @
$data{'revspec', 'fileset', 'pickaxe'};
564 $self->{_commit_sequence
} = [];
565 $self->{_commits
} = {};
566 $self->{_tree_store
}->clear();
567 foreach my $entry (@
{$data->{commits
}}) {
574 my ($fd, $sel, $self) = @_;
576 my $fh = $self->{_load_fh
};
580 Glib
::Source
->remove($self->{_glib_is_crap1
});
581 Glib
::Source
->remove($self->{_glib_is_crap2
});
582 $self->{ctrl
}->repo()->command_close_pipe($self->{_load_fh
}, $self->{_load_ctx
});
584 my $gdkwin = $self->{widget
}->window();
586 $gdkwin->set_cursor(undef);
588 $self->{widget
}->signal_handler_disconnect($self->{_load_sigh
});
593 # XXX: git-log is broken and inserts spurious empty lines in case
594 # of some extensive pickaxes. TODO: Figure out why and fix.
597 @entry{'id', 'parents', 'author', 'subject'} = split(/\t/, $line);
598 $entry{'parents'} = [ split(/ /, $entry{'parents'}) ];
599 $self->_add(\
%entry);
609 $entry->{'iter'} = $self->{_tree_store
}->append(undef);
610 my $id = substr($entry->{'id'}, 0, 8);
611 my $author = ::midtrim
($entry->{'author'}, 20);
612 my $subj = ::midtrim
($entry->{'subject'}, 80);
613 $self->{_tree_store
}->set($entry->{'iter'},
619 push @
{$self->{_commit_sequence
}}, $entry;
620 $self->{_commits
}->{$entry->{'id'}} = $entry;
622 if ($self->{_load_selection
}->{$entry->{'id'}}) {
623 $self->{_tree_widget
}->get_selection()->select_iter($entry->{'iter'});
629 my ($tv, $path, $column, $self) = @_;
631 my $model = $tv->get_model();
632 my ($entry) = $model->get($model->get_iter($path), COL_REF
);
633 $self->{ctrl
}->commit_selected($entry);
639 package Giddy
::PickaxableViewer
;
641 use Glib
::Object
::Subclass
642 'Gtk2::ScrolledWindow';
644 sub _populate_popup
{
645 my ($tv, $m, $self) = @_;
647 # XXX: Cut off the stupid items below second separator
649 for ($m->get_children()) {
650 $sep++ if ref $_ eq 'Gtk2::SeparatorMenuItem';
651 $m->remove($_) if $sep > 1;
656 my ($self, $m, $i, $label, $handler) = @_;
657 my $mi = Gtk2
::MenuItem
->new_with_label($label);
658 $mi->signal_connect(activate
=> $handler, $self);
659 $m->insert($mi, $$i++);
663 if ($self->{buffer
}->get_has_selection()) {
664 menu_item
($self, $m, \
$i, 'Pickaxe', \
&_pickaxe_sel
);
665 menu_item
($self, $m, \
$i, 'Thorough Pickaxe', \
&_pickaxe_sel_thorough
);
668 my ($ll, $px, $py, $mm) = $tv->window()->get_pointer();
669 my ($bx, $by) = $tv->window_to_buffer_coords('widget', $px, $py);
670 $self->{_popup_at
} = $tv->get_iter_at_position($bx, $by);
672 my $idclass = qr/[a-zA-Z0-9_]/;
673 if ($self->{_popup_at
}->get_char() =~ $idclass) {
674 my $start = $self->{_popup_at
}->copy();
675 while ($start->backward_char()) {
676 if ($start->get_char() !~ $idclass) {
677 $start->forward_char();
681 my $end = $self->{_popup_at
}->copy();
682 while ($end->forward_char()) {
683 if ($end->get_char() !~ $idclass) {
687 $self->{_popup_id
} = $start->get_text($end);
688 menu_item
($self, $m, \
$i, 'Pickaxe "'.$self->{_popup_id
}.'" calls', \
&_pickaxe_id
);
691 menu_item
($self, $m, \
$i, 'Pickaxe this line', \
&_pickaxe_line
);
695 my $mi = Gtk2
::SeparatorMenuItem
->new;
696 $m->insert($mi, $i++);
701 sub _pickaxe_selection
{
702 my ($mi, $self, $thorough) = @_;
704 my $clipboard = Gtk2
::Clipboard
->get(Gtk2
::Gdk
->SELECTION_PRIMARY);
705 my $text = $clipboard->wait_for_text();
706 $text =~ s/^[ +-]//mg;
707 $self->{ctrl
}->pickaxe($text, $thorough ?
undef : $self->{name
} ?
[$self->{name
}] : undef);
711 _pickaxe_selection
(@_);
714 sub _pickaxe_sel_thorough
{
715 _pickaxe_selection
(@_, 1);
719 my ($mi, $self) = @_;
721 # XXX: Eew... Is there a better way to do this?
722 my $line1 = $self->{buffer
}->get_iter_at_line($self->{_popup_at
}->get_line());
723 my $line2 = $self->{buffer
}->get_iter_at_line($self->{_popup_at
}->get_line() + 1);
724 $self->{ctrl
}->pickaxe($line1->get_text($line2), $self->{name
} ?
[$self->{name
}] : undef);
728 my ($mi, $self) = @_;
730 $self->{ctrl
}->pickaxe($self->{_popup_id
});
736 package Giddy
::FileViewer
;
738 use base
'Giddy::PickaxableViewer';
740 # Create object and set up the widgets
744 my $self = Gtk2
::ScrolledWindow
->new;
746 $self->{ctrl
} = $ctrl;
748 $self->{widget
} = Gtk2
::TextView
->new;
749 $self->{widget
}->set_editable(0);
750 $self->{widget
}->modify_font($fixed_font);
751 $self->{widget
}->set_border_window_size('GTK_TEXT_WINDOW_LEFT', 40);
752 $self->{widget
}->signal_connect(expose_event
=> \
&_expose_lineno
, $self);
753 $self->{widget
}->signal_connect(populate_popup
=> \
&Giddy
::PickaxableViewer
::_populate_popup
, $self);
754 $self->{buffer
} = $self->{widget
}->get_buffer();
755 $self->{lineno
} = $self->{widget
}->get_window('GTK_TEXT_WINDOW_LEFT');
757 $self->{_tag_ins
} = $self->{buffer
}->create_tag('ins', background
=> 'green');
758 $self->{_tag_par_ins
} = $self->{buffer
}->create_tag('parins', paragraph_background
=> 'green');
759 $self->{_tag_chg
} = $self->{buffer
}->create_tag('chg', background
=> 'orange');
760 $self->{_tag_par_chg
} = $self->{buffer
}->create_tag('parchg', paragraph_background
=> 'orange');
764 $self->add($self->{widget
});
768 # Load given file to the viewer
771 my ($tree, $name) = @_;
773 $self->{tree
} = $tree;
774 $self->{name
} = $name;
776 my $text = $self->{ctrl
}->repo()->command('cat-file', 'blob', $tree.':'.$name);
777 utf8
::upgrade
($text);
779 my $buffer = Gtk2
::TextBuffer
->new($self->{buffer
}->get_tag_table());
780 $buffer->set_text($text);
781 # TODO: Recompute cursor position based on diff offsets
782 $buffer->place_cursor($buffer->get_iter_at_offset($self->{buffer
}->get_property('cursor-position')));
783 $self->{widget
}->set_buffer($buffer); $self->{buffer
} = $buffer;
785 $self->_analyse_diff($tree);
788 # Check diff introduced by $tree and tag buffer appropriately
793 my @diff = $self->{ctrl
}->repo()->command('diff', $tree.'^', $tree, '--', $self->{name
});
795 my ($line_old, $line_new);
798 sub minihunk_boundary
{
799 my ($self, $minihunk, $line) = @_;
800 return unless $minihunk->{start
};
801 my %tags = ( add
=> '_tag_par_ins', chg
=> '_tag_par_chg' );
803 # Gtk::TextBuffer counts lines from 0
804 my $line1 = $self->{buffer
}->get_iter_at_line($minihunk->{start
} - 1);
805 my $line2 = $self->{buffer
}->get_iter_at_line($line - 1);
807 if ($minihunk->{type
} eq 'del') {
808 # TODO: Insert expandable bar
812 $self->{buffer
}->apply_tag($self->{$tags{$minihunk->{type
}}}, $line1, $line2);
821 if (/^@@ -(\d+),(\d+) \+(\d+),(\d+) @@/) {
822 minihunk_boundary
($self, \
%minihunk, $line_new);
823 ($line_old, $line_new) = ($1, $3);
825 minihunk_boundary
($self, \
%minihunk, $line_new);
826 $line_old++, $line_new++;
828 if (!$minihunk{start
}) {
829 $minihunk{start
} = $line_new;
830 $minihunk{type
} = 'del';
831 } elsif ($minihunk{type
} eq 'add') {
832 $minihunk{type
} = 'chg';
835 } elsif (/^\+(.*)/) {
836 if (!$minihunk{start
}) {
837 $minihunk{start
} = $line_new;
838 $minihunk{type
} = 'add';
839 } elsif ($minihunk{type
} eq 'del') {
840 $minihunk{type
} = 'chg';
845 minihunk_boundary
($self, \
%minihunk, $line_new);
849 my ($tv, $ev, $self) = @_;
851 my $lineno = $self->{widget
}->get_window('GTK_TEXT_WINDOW_LEFT');
853 my $first_y = $ev->area->y;
854 my $last_y = $first_y + $ev->area->height;
856 $first_y = ($self->{widget
}->window_to_buffer_coords('GTK_TEXT_WINDOW_LEFT', 0, $first_y))[1];
857 $last_y = ($self->{widget
}->window_to_buffer_coords('GTK_TEXT_WINDOW_LEFT', 0, $last_y))[1];
859 my $layout = $self->{widget
}->create_pango_layout('');
861 my $iter = ($self->{widget
}->get_line_at_y($first_y))[0];
863 while (not $iter->is_end()) {
864 $layout->set_text($iter->get_line());
866 my ($buf_y, $height) = $self->{widget
}->get_line_yrange($iter);
867 my $y = ($self->{widget
}->buffer_to_window_coords('GTK_TEXT_WINDOW_LEFT', 0, $buf_y))[1];
868 $self->{widget
}->style->paint_layout($lineno,
869 $self->{widget
}->state, 0,
870 undef, $self->{widget
}, undef, 2,
874 $iter->forward_line();
875 last if ($buf_y + $height > $last_y);
884 package Giddy
::CommitViewer
;
886 use Glib
::Object
::Subclass
887 'Gtk2::ScrolledWindow';
889 # Create object and set up the widgets
893 my $self = Gtk2
::ScrolledWindow
->new;
895 $self->{ctrl
} = $ctrl;
897 $self->{widget
} = Gtk2
::TextView
->new;
898 $self->{widget
}->set_editable(0);
899 $self->{widget
}->modify_font($fixed_font);
900 $self->{buffer
} = $self->{widget
}->get_buffer();
904 $self->add($self->{widget
});
908 # Load given commit to the viewer
913 $self->{commit
} = $commit;
915 my $text = $self->{ctrl
}->repo()->command('cat-file', 'commit', $commit);
916 utf8
::upgrade
($text);
917 $self->{buffer
}->set_text($text);
923 package Giddy
::DiffViewer
;
925 use base
'Giddy::PickaxableViewer';
927 # Create object and set up the widgets
931 my $self = Gtk2
::ScrolledWindow
->new;
933 $self->{ctrl
} = $ctrl;
935 $self->{widget
} = Gtk2
::TextView
->new;
936 $self->{widget
}->set_editable(0);
937 $self->{widget
}->modify_font($fixed_font);
938 $self->{widget
}->signal_connect(populate_popup
=> \
&Giddy
::PickaxableViewer
::_populate_popup
, $self);
939 $self->{buffer
} = $self->{widget
}->get_buffer();
943 $self->add($self->{widget
});
947 # Load given file to the viewer
950 my ($commit, @files) = @_;
952 $self->{commit
} = $commit;
953 $self->{files
} = \
@files;
955 my $text = $self->{ctrl
}->repo()->command('show', '--pretty=format:', $commit, '--', @files);
956 utf8
::upgrade
($text);
957 $text =~ s/^\n//; # spurious leading newline inserted by show
958 $self->{buffer
}->set_text($text);
967 # Simple widget wrapping {File,Commit}Viewer | DiffViewer
969 package Giddy
::FileTab
;
971 use Glib
::Object
::Subclass
976 my ($viewer, $diff, $of_commit) = @_;
977 my $self = Gtk2
::VPaned
->new;
980 $self->{viewer
} = $viewer;
981 $self->{diff
} = $diff;
982 $self->pack1($viewer, 1, 1);
983 $self->pack2($diff, 0, 0);
984 $self->set_position($of_commit ?
200 : 400);
990 my ($tree, @files) = @_;
991 $self->{tree
} = $tree;
992 $self->{files
} = \
@files;
994 $self->{viewer
}->load($tree, @files);
995 $self->{diff
}->load($tree, @files);
1001 package Giddy
::FilesPanel
;
1003 # Create object and set up the widgets
1007 my $self = { ctrl
=> $ctrl };
1009 $self->{widget
} = Gtk2
::Notebook
->new;
1011 bless $self, $class;
1014 # Open new viewer tab
1017 my ($tree, $name) = @_;
1019 my $widget = Giddy
::FileTab
->new(Giddy
::FileViewer
->new($self->{ctrl
}),
1020 Giddy
::DiffViewer
->new($self->{ctrl
}));
1021 $widget->show_all();
1022 my $tab = $self->{widget
}->append_page($widget, Gtk2
::Label
->new);
1023 $self->load($tree, $name, $tab);
1027 # Open file in given/current existing tab
1030 my ($tree, $name, $tab) = @_;
1032 $tab = $self->{widget
}->get_current_page() unless defined $tab;
1035 # We won't load into commit tab; open new tab and shift focus
1042 $tab = $self->open($tree, $name);
1043 $chfocus and $self->{widget
}->set_current_page($tab);
1047 my $viewer_widget = $self->{widget
}->get_nth_page($tab);
1048 $viewer_widget->load($tree, $name);
1049 $self->{widget
}->set_tab_label($viewer_widget, Gtk2
::Label
->new($name));
1053 # Open new commit tab
1058 my $widget = Giddy
::FileTab
->new(Giddy
::CommitViewer
->new($self->{ctrl
}),
1059 Giddy
::DiffViewer
->new($self->{ctrl
}), 1);
1060 $widget->show_all();
1061 my $tab = $self->{widget
}->append_page($widget, Gtk2
::Label
->new);
1062 $self->load_commit($commit, $tab);
1065 # Load new commit in the commit tab
1070 my $viewer_widget = $self->{widget
}->get_nth_page(0);
1071 $viewer_widget->load($commit);
1073 my $label = "\x{0394} ".substr($Tree, 0, 8);
1074 $self->{widget
}->set_tab_label($viewer_widget, Gtk2
::Label
->new($label));
1077 # Reload all tabs for new commit
1078 sub commit_changed
{
1081 $self->load_commit($Tree);
1083 for my $tab (1..($self->{widget
}->get_n_pages()-1)) {
1084 my $viewer_widget = $self->{widget
}->get_nth_page($tab);
1085 $viewer_widget->load($Tree, @
{$viewer_widget->{files
}});