File Coverage

blib/lib/Gtk2/Ex/ErrorTextDialog.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             # Copyright 2007, 2008, 2009, 2010, 2011, 2013 Kevin Ryde
2              
3             # This file is part of Gtk2-Ex-ErrorTextDialog.
4             #
5             # Gtk2-Ex-ErrorTextDialog is free software; you can redistribute it and/or
6             # modify it under the terms of the GNU General Public License as published
7             # by the Free Software Foundation; either version 3, or (at your option) any
8             # later version.
9             #
10             # Gtk2-Ex-ErrorTextDialog is distributed in the hope that it will be useful,
11             # but WITHOUT ANY WARRANTY; without even the implied warranty of
12             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
13             # Public License for more details.
14             #
15             # You should have received a copy of the GNU General Public License along
16             # with Gtk2-Ex-ErrorTextDialog. If not, see .
17              
18              
19             package Gtk2::Ex::ErrorTextDialog;
20 2     2   35084 use 5.008001; # for utf8::is_utf8()
  2         7  
  2         77  
21 2     2   11 use strict;
  2         3  
  2         58  
22 2     2   11 use warnings;
  2         7  
  2         55  
23 2     2   3338 use Gtk2;
  0            
  0            
24             use List::Util 'max';
25             use Locale::TextDomain 1.16; # version 1.16 for bind_textdomain_filter()
26             use Locale::TextDomain ('Gtk2-Ex-ErrorTextDialog');
27             use Locale::Messages;
28             use POSIX ();
29             use Glib::Ex::ObjectBits;
30             use Gtk2::Ex::Units 14; # version 14 for char_width
31              
32             our $VERSION = 11;
33              
34             # set this to 1 for some diagnostic prints
35             use constant DEBUG => 0;
36              
37             Locale::Messages::bind_textdomain_codeset ('Gtk2-Ex-ErrorTextDialog','UTF-8');
38             Locale::Messages::bind_textdomain_filter ('Gtk2-Ex-ErrorTextDialog',
39             \&Locale::Messages::turn_utf_8_on);
40              
41             use Glib::Object::Subclass
42             'Gtk2::MessageDialog',
43             signals => { destroy => \&_do_destroy,
44              
45             clear =>
46             { param_types => [],
47             return_type => undef,
48             class_closure => \&_do_clear,
49             flags => ['run-last','action'] },
50              
51             popup_save_dialog =>
52             { param_types => [],
53             return_type => undef,
54             class_closure => \&_do_popup_save_dialog,
55             flags => ['run-last','action'] },
56             },
57              
58             properties => [ Glib::ParamSpec->int
59             ('max-chars',
60             __('Maximum characters'),
61             'Maximum number of characters to retain, or -1 for unlimited.',
62             -1, # minimum
63             POSIX::INT_MAX(), # maximum
64             200_000, # default
65             Glib::G_PARAM_READWRITE) ];
66              
67             use constant RESPONSE_CLEAR => 0;
68             use constant RESPONSE_SAVE => 1;
69              
70             # not yet documented ...
71             use constant _MESSAGE_SEPARATOR => "--------\n";
72              
73             my $instance;
74             our @_instance_pending;
75              
76             sub instance {
77             my ($class) = @_;
78             if (! $instance) {
79             $instance = $class->new;
80             $instance->signal_connect (delete_event=>\&Gtk2::Widget::hide_on_delete);
81             }
82             return $instance;
83             }
84              
85             # return true if $class_or_self is the shared $instance
86             sub _is_instance {
87             my ($class_or_self) = @_;
88             return (! ref $class_or_self # class name means the instance
89             || ($instance && $class_or_self == $instance));
90             }
91              
92             sub INIT_INSTANCE {
93             my ($self) = @_;
94              
95             {
96             my $title = __('Errors');
97             if (defined (my $appname = Glib::get_application_name())) {
98             $title = "$appname: $title";
99             }
100             $self->set_title ($title);
101             }
102             $self->set (message_type => 'error',
103             resizable => 1);
104              
105             {
106             my $check = $self->{'popup_checkbutton'}
107             = Gtk2::CheckButton->new_with_mnemonic (__('_Popup on Error'));
108             $check->set_active (1);
109             Glib::Ex::ObjectBits::set_property_maybe
110             ($check,
111             # tooltip-text new in Gtk 2.12
112             tooltip_text => __('Whether to popup this dialog when an error occurs.
113             If errors are occurring repeatedly you might not want a popup every time.'));
114              
115             $self->add_action_widget ($check, 'none');
116             }
117             {
118             my $button = $self->add_button ('gtk-save-as', $self->RESPONSE_SAVE);
119             Glib::Ex::ObjectBits::set_property_maybe
120             ($button,
121             # tooltip-text new in Gtk 2.12
122             tooltip_text => __('Save the error messages to a file, perhaps to include in a bug report.
123             (Cut and paste works too, but saving may be better for very long messages.)'));
124             }
125             $self->add_buttons ('gtk-clear' => $self->RESPONSE_CLEAR,
126             'gtk-close' => 'close');
127              
128             # connect to self instead of a class handler because as of Gtk2-Perl 1.220
129             # a Gtk2::Dialog class handler for 'response' is called with response IDs
130             # as numbers, not enum strings like 'close'
131             $self->signal_connect (response => \&_do_response);
132              
133             my $vbox = $self->vbox;
134              
135             my $scrolled = Gtk2::ScrolledWindow->new;
136             $scrolled->set_policy ('never', 'always');
137             $vbox->pack_start ($scrolled, 1,1,0);
138              
139             my $textbuf = $self->{'textbuf'} = Gtk2::TextBuffer->new;
140             $textbuf->signal_connect ('changed', \&_do_textbuf_changed, $self);
141             _do_textbuf_changed ($textbuf, $self); # initial settings
142              
143             require Gtk2::Ex::TextView::FollowAppend;
144             my $textview = $self->{'textview'}
145             = Gtk2::Ex::TextView::FollowAppend->new_with_buffer ($textbuf);
146             $textview->set (wrap_mode => 'char',
147             editable => 0);
148             $scrolled->add ($textview);
149              
150             $vbox->show_all;
151             $self->set_default_size_chars (70, 20);
152             }
153              
154             # 'destroy' class closure
155             # this can be called more than once!
156             sub _do_destroy {
157             my ($self) = @_;
158             if (DEBUG) { print "ErrorTextDialog destroy $self\n"; }
159              
160             # Break circular reference from $textbuf 'changed' signal userdata $self.
161             # Nothing for $self->{'save_dialog'} as it's destroy-with-parent already.
162             delete $self->{'textbuf'};
163              
164             if ($self->_is_instance) {
165             # ready for subsequence instance() call to make a new one
166             undef $instance;
167             }
168             $self->signal_chain_from_overridden;
169             }
170              
171             # 'changed' signal on the textbuf
172             sub _do_textbuf_changed {
173             my ($textbuf, $self) = @_;
174             if (DEBUG) { print "ErrorTextDialog textbuf changed\n"; }
175             my $any_errors = ($textbuf->get_char_count != 0);
176             _message_dialog_set_text ($self, $any_errors
177             ? __('An error has occurred')
178             : __('No errors'));
179             $self->set_response_sensitive ($self->RESPONSE_CLEAR, $any_errors);
180             }
181              
182             # set_default_size() based on desired size_request() with a sensible rows
183             # and columns size for the TextView. This is just a default, the user can
184             # resize to smaller. Must have 'resizable' turned on in INIT_INSTANCE above
185             # to make this work (the default from GtkMessageDialog is resizable false).
186             #
187             # not documented yet ...
188             sub set_default_size_chars {
189             my ($self, $width_chars, $height_lines) = @_;
190             my $textview = $self->{'textview'};
191             my $scrolled = $textview->get_parent;
192              
193             # Width set on textview so the vertical scrollbar is added on top. But
194             # height set on the scrolled since its vertical scrollbar means any
195             # desired height from the textview is ignored.
196             #
197             Gtk2::Ex::Units::set_default_size_with_subsizes
198             ($self,
199             [ $scrolled, -1, $height_lines*Gtk2::Ex::Units::line_height($textview)],
200             [ $textview, "$width_chars chars", -1 ]);
201             }
202              
203             #-----------------------------------------------------------------------------
204             # button/response actions
205              
206             sub _do_response {
207             my ($self, $response) = @_;
208             if ($response eq $self->RESPONSE_CLEAR) {
209             $self->clear;
210              
211             } elsif ($response eq $self->RESPONSE_SAVE) {
212             $self->popup_save_dialog;
213              
214             } elsif ($response eq 'close') {
215             # as per a keyboard close, defaults to raising 'delete-event', which in
216             # turn defaults to a destroy
217             $self->signal_emit ('close');
218             }
219             }
220              
221             sub clear {
222             my ($self) = @_;
223             $self = $self->instance unless ref $self;
224             $self->signal_emit ('clear');
225             }
226             sub _do_clear {
227             my ($self) = @_;
228             my $textbuf = $self->{'textbuf'};
229             $textbuf->delete ($textbuf->get_start_iter, $textbuf->get_end_iter);
230             }
231              
232             sub popup_save_dialog {
233             my ($self) = @_;
234             $self = $self->instance unless ref $self;
235             $self->signal_emit ('popup-save-dialog');
236             }
237             sub _do_popup_save_dialog {
238             my ($self) = @_;
239             $self->_save_dialog->present;
240             }
241              
242             # create and return the save dialog -- might make this public one day
243             sub _save_dialog {
244             my ($self) = @_;
245             return ($self->{'save_dialog'} ||= do {
246             require Gtk2::Ex::ErrorTextDialog::SaveDialog;
247             my $save_dialog = Gtk2::Ex::ErrorTextDialog::SaveDialog->new;
248             # set_transient_for() is always available, whereas 'transient-for' as
249             # property only since gtk 2.10
250             $save_dialog->set_transient_for ($self);
251             $save_dialog
252             });
253             }
254              
255             #-----------------------------------------------------------------------------
256             # messages
257              
258             sub get_text {
259             my ($self) = @_;
260             return $self->{'textbuf'}->get('text');
261             }
262              
263             sub add_message {
264             my ($self, $msg) = @_;
265             if (DEBUG) { print "add_message()\n"; }
266             $self = $self->instance unless ref $self;
267              
268             require Gtk2::Ex::ErrorTextDialog::Handler;
269             my $textbuf = $self->{'textbuf'};
270             my @msgs;
271              
272             if ($self->_is_instance && @_instance_pending) {
273             if (DEBUG) { print " ", scalar(@_instance_pending), " pending\n"; }
274              
275             # copy the global in case some warning from the code here extending it,
276             # making an infinite loop
277             @msgs = @_instance_pending;
278             @_instance_pending = ();
279              
280             foreach my $pending (@msgs) {
281             $pending = Gtk2::Ex::ErrorTextDialog::Handler::_maybe_locale_bytes_to_wide ($pending);
282             if ($pending !~ /\n$/) { $pending .= "\n"; }
283             }
284              
285             # Various internal Perl_warn() and Perl_warner() calls have the warning
286             # followed immediately by a second warn call with an extra remark about
287             # what might be wrong. The extras begin with a tab, join them up to the
288             # initial warning instead of a separate message. Do this after
289             # bytes->wide crunch, just in case one is wide and the other bytes.
290             #
291             # The initial message and any continuations are always in
292             # @_instance_pending together, because the idle handler deferring lets
293             # the continuation go through $SIG{__WARN__} before the code here runs.
294             #
295             # die() gives similar tab continuations when "propagating" an error (see
296             # L), but in that case it's within a single string so
297             # needs nothing special.
298             #
299             for (my $i = 0; $i < $#msgs; ) {
300             if ($msgs[$i+1] =~ /^\t/) {
301             $msgs[$i] .= splice @msgs, $i+1, 1;
302             } else {
303             $i++;
304             }
305             }
306             }
307              
308             if (defined $msg) {
309             $msg = Gtk2::Ex::ErrorTextDialog::Handler::_maybe_locale_bytes_to_wide ($msg);
310             if ($msg !~ /\n$/) { $msg .= "\n"; }
311             push @msgs, $msg;
312             }
313              
314             # can have no messages here if the idle handler for @_instance_pending
315             # runs after that array has been crunched by an explicit add_message()
316             # call
317             if (@msgs) {
318             if ($textbuf->get_char_count) {
319             unshift @msgs, ''; # want separator after existing textbuf text
320             }
321             my $text = join (_MESSAGE_SEPARATOR, @msgs);
322             $textbuf->insert ($textbuf->get_end_iter, $text);
323             _truncate ($self);
324             }
325             }
326              
327             sub _truncate {
328             my ($self) = @_;
329             my $max_chars = $self->get('max-chars');
330             return if ($max_chars == -1);
331              
332             my $textbuf = $self->{'textbuf'};
333             my $len = $textbuf->get_char_count;
334             # extra 82 for $discard_message, possibly translated, and "\n\n"
335             return if ($len <= $max_chars + 82);
336              
337             # TRANSLATORS: The code currently assumes this string is 80 chars or less.
338             my $discard_message = __('[Older messages discarded]');
339              
340             $textbuf->delete ($textbuf->get_start_iter,
341             $textbuf->get_iter_at_offset ($len - $max_chars));
342             $textbuf->insert ($textbuf->get_start_iter,
343             "$discard_message\n\n");
344             }
345              
346             # not sure about this yet, an in particular which popup follows the
347             # popup-on-error checkbox and which is a programmatic always popup ...
348             #
349             # =item C<< Gtk2::Ex::ErrorTextDialog->popup_add_message ($str) >>
350             #
351             # =item C<< Gtk2::Ex::ErrorTextDialog->popup_add_message ($str, $parent) >>
352             #
353             # =item C<< $errordialog->popup_add_message ($str) >>
354             #
355             # =item C<< $errordialog->popup_add_message ($str, $parent) >>
356             #
357             # Add C<$str> to the error dialog with C below, and popup the
358             # dialog so it's visible.
359             #
360             # Optional C<$parent> is a widget which the error relates to, or C for
361             # none. C<$parent> may help the window manager position the error dialog when
362             # first displayed, but is not used after that.
363             #
364             # not documented yet ...
365             sub popup_add_message {
366             my ($self, $msg, $parent) = @_;
367             $self = $self->instance unless ref $self;
368              
369             if ($self->{'popup_checkbutton'}->get_active) {
370             $self->popup ($parent);
371             }
372             $self->add_message ($msg);
373             }
374             # not documented yet ...
375             sub popup {
376             my ($self, $parent) = @_;
377             $self = $self->instance unless ref $self;
378              
379             if ($self->mapped) {
380             # too intrusive to raise every time
381             # $self->window->raise;
382             } else {
383             # allow for $parent a non-toplevel
384             if ($parent) { $parent = $parent->get_toplevel; }
385             $self->set_transient_for ($parent);
386             $self->present;
387             $self->set_transient_for (undef);
388             }
389             }
390              
391             # ENHANCE-ME: would prefer to show the same string as
392             # g_log_default_handler(), or even what gperl_log_handler() gives
393             sub _log_to_string {
394             my ($log_domain, $log_level, $message) = @_;
395              
396             $log_level -= ['recursion','fatal'];
397             $log_level = join('-', @$log_level) || 'LOG';
398              
399             return (($log_domain ? "$log_domain-" : "** ")
400             . "\U$log_level\E: "
401             . (defined $message ? $message : "(no message)"));
402             }
403              
404             # probably not wanted ...
405             # sub popup_add_log {
406             # my ($class_or_self, $log_domain, $log_level, $message, $parent) = @_;
407             # $self->popup ($parent);
408             # $self->add_log ($log_domain, $log_level, $message);
409             # }
410             # sub add_log {
411             # my ($class_or_self, $log_domain, $log_level, $message) = @_;
412             # $class_or_self->add_message
413             # (_log_to_string ($log_domain, $log_level, $message));
414             # }
415              
416             #-----------------------------------------------------------------------------
417             # generic helpers
418              
419             # _message_dialog_set_text($messagedialog,$text) sets the text part of a
420             # Gtk2::MessageDialog. Gtk 2.10 up has this as a 'text' property, or in
421             # past versions it's necessary to dig out the label child widget.
422             #
423             # It doesn't work to choose between the dialog or sub-widget and to make a
424             # set() or set_text() call on. Gtk2::MessageDialog doesn't have a
425             # set_text() method, and Gtk2::Label doesn't have a 'text' property, so must
426             # have separate code for the old or new gtk.
427             #
428             # This is in a BEGIN block so the unused sub is garbage collected.
429             #
430             BEGIN {
431             *_message_dialog_set_text = Gtk2::MessageDialog->find_property('text')
432             ? sub {
433             my ($dialog, $text) = @_;
434             $dialog->set (text => $text);
435             }
436             : sub {
437             my ($dialog, $text) = @_;
438             my $label = ($dialog->{__PACKAGE__.'--text-widget'} ||= do {
439             require List::Util;
440             my $l;
441             my @w = grep {$_->isa('Gtk2::HBox')} $dialog->vbox->get_children;
442             for (;;) {
443             if (! @w) {
444             require Carp;
445             Carp::croak ('_message_dialog_text_widget(): oops, label not found');
446             }
447             $l = List::Util::first (sub {ref $_ eq 'Gtk2::Label'}, @w)
448             and last;
449             @w = map {$_->isa('Gtk2::Box') ? $_->get_children : ()} @w;
450             }
451             $l
452             });
453             $label->set_text ($text);
454             };
455             }
456              
457             1;
458             __END__