File Coverage

blib/lib/ProgressMonitor/Stringify/AbstractMonitor.pm
Criterion Covered Total %
statement 137 163 84.0
branch 36 68 52.9
condition 11 37 29.7
subroutine 17 19 89.4
pod 0 3 0.0
total 201 290 69.3


line stmt bran cond sub pod time code
1             package ProgressMonitor::Stringify::AbstractMonitor;
2            
3 10     10   10108 use warnings;
  10         29  
  10         480  
4 10     10   60 use strict;
  10         21  
  10         358  
5            
6 10     10   980884 use ProgressMonitor::Exceptions;
  10         131  
  10         695  
7            
8             require ProgressMonitor::AbstractStatefulMonitor if 0;
9            
10             # Attributes:
11             # width
12             # The final width the field(s) this monitor manages will occupy
13             use classes
14 10         70 extends => 'ProgressMonitor::AbstractStatefulMonitor',
15             class_methods => ['_new'],
16             attrs_ro => ['width',],
17             attrs_pr => ['msgto'],
18 10     10   66 ;
  10         18  
19            
20 10     10   8330 use ProgressMonitor::SubTask;
  10         38  
  10         311  
21 10     10   65 use ProgressMonitor::SetMessageFlags;
  10         16  
  10         14451  
22            
23             sub _new
24             {
25 11     11   24 my $class = shift;
26 11         24 my $cfg = shift;
27 11         27 my $cfgPkg = shift;
28            
29             # get the instance from the super class
30             #
31 11         123 my $self = $class->SUPER::_new($cfg, $cfgPkg);
32            
33             # retrieve the configuration for easy reference
34             #
35 11         100 $cfg = $self->_get_cfg;
36            
37             # what max width has the user asked for?
38             #
39 11         39 my $maxWidth = $cfg->get_maxWidth;
40            
41 11         80 my $allFields = $cfg->get_fields;
42            
43             # what is the minimum combined width needed to begin with?
44             #
45 11         48 my $wsum = 0;
46 11         100 $wsum += $_->get_width for (@$allFields);
47 11 50       96 print STDERR ("WARNING: Insufficient width for monitor ($maxWidth < $wsum). Feedback output will not display properly.\n") if $wsum > $maxWidth;
48            
49             # now try to make the stringification fit 'best possible'
50             #
51 11         24 my $remainingWidth = $maxWidth - $wsum;
52 11 50       54 if ($remainingWidth < 0)
53             {
54             # in this case, the available line is too short
55             #
56             # just set the width we can use, regardless
57             #
58 0         0 $self->{$ATTR_width} = $maxWidth;
59             }
60             else
61             {
62             # in this case, the line may provide extra space for dynamic fields to get more
63             # than they minimally need, which may make them look nicer
64             #
65             # in a round robin fashion, try to fairly give dynfields
66             # extra width until all are full, or width is exhausted
67             #
68            
69             # first make a separate list of the dynamic fields
70             #
71 11         18 my @dynFields;
72 11         31 for (@$allFields)
73             {
74 11 50       84 push(@dynFields, $_) if $_->isDynamic;
75             }
76            
77             # begin with the width we have left to give out
78             # and loop while there is any width left and there are any dynamic fields
79             # that are 'still hungry'...
80             #
81 11   33     99 while ($remainingWidth && @dynFields)
82             {
83 0         0 my $dynFieldCount = @dynFields;
84            
85             # make a list with the current width we have fairly distributed
86             #
87 0         0 my @allotments;
88 0         0 $allotments[$_ % $dynFieldCount]++ for (0 .. ($remainingWidth - 1));
89            
90             # now iterate over the list and give the corresponding dynfield the
91             # width it has been allotted.
92             # it will report how much it 'used' (due to its own constraints, if any)
93             # and we can disseminate remains in the next loop
94             #
95 0         0 for (0 .. (@allotments - 1))
96             {
97 0         0 my $allottedExtraWidth = $allotments[$_];
98 0         0 my $unusedExtraWidth = $dynFields[$_]->grabExtraWidth($allottedExtraWidth);
99 0         0 $remainingWidth -= $allottedExtraWidth - $unusedExtraWidth;
100             }
101            
102             # now recalculate the list with dynfields (any fields that have
103             # reached their max width are no longer (dynamic')
104             #
105 0         0 @dynFields = ();
106 0         0 for (@$allFields)
107             {
108 0 0       0 push(@dynFields, $_) if $_->isDynamic;
109             }
110             }
111            
112             # finally set the width we've actually used
113             #
114 11         151 $self->{$ATTR_width} = $maxWidth - $remainingWidth;
115             }
116            
117 11         41 return $self;
118             }
119            
120             sub setMessage
121             {
122 200     200 0 808 my $self = shift;
123 200         245 my $msg = shift;
124 200   50     747 my $when = shift || SM_NOW;
125            
126 200 50       558 $self->{$ATTR_msgto} = undef if $when == SM_NOW;
127            
128 200         674 return $self->SUPER::setMessage($msg, $when);
129             }
130            
131             sub subMonitor
132             {
133 0     0 0 0 my $self = shift;
134 0   0     0 my $subCfg = shift || {};
135            
136 0         0 $subCfg->{parent} = $self;
137 0         0 return ProgressMonitor::SubTask->new($subCfg);
138             }
139            
140             sub setErrorMessage
141             {
142 0     0 0 0 my $self = shift;
143 0         0 my $msg = shift;
144            
145 0         0 return $msg;
146             }
147            
148             ### protected
149            
150             sub _get_message
151             {
152 600     600   726 my $self = shift;
153            
154 600         796 my $now = time;
155 600 50       1643 if (defined($self->{$ATTR_msgto}))
156             {
157 0 0       0 $self->_set_message(undef) if ($self->{$ATTR_msgto} <= $now);
158             }
159             else
160             {
161 600         1608 my $to = $self->_get_cfg->get_messageTimeout;
162 600 50       3104 $self->{$ATTR_msgto} = time + $to if $to >= 0;
163             }
164            
165 600         1966 return $self->SUPER::_get_message;
166             }
167            
168             sub _set_message
169             {
170 220     220   264 my $self = shift;
171 220         373 my $msg = shift;
172            
173 220         362 $self->{$ATTR_msgto} = undef;
174            
175 220         633 return $self->SUPER::_set_message($msg);
176             }
177            
178             # helper method to call each field and render a complete line
179             #
180             sub _toString
181             {
182 411     411   533 my $self = shift;
183 411         498 my $considerMessage = shift();
184            
185 411 50       907 $considerMessage = 1 unless defined($considerMessage);
186            
187 411         1072 my $state = $self->_get_state;
188 411         1056 my $ticks = $self->_get_ticks;
189 411         1162 my $totalTicks = $self->_get_totalTicks;
190            
191 411         1115 my $cfg = $self->_get_cfg;
192 411         999 my $ms = $cfg->get_messageStrategy;
193 411         2097 my $msg = $self->_get_message;
194 411         583 my $rendition = '';
195            
196 411         505 my $forceNewline = 0;
197 411 50       1140 if ($ms eq 'overlay_newline')
    50          
198             {
199 0         0 $forceNewline = 1;
200             }
201             elsif ($ms eq 'overlay_honor_newline')
202             {
203 0   0     0 $forceNewline = ($msg && $msg =~ /\n$/);
204             }
205            
206 411         1251 my $allFields = $cfg->get_fields;
207 411         1929 for (@$allFields)
208             {
209             # ask each field to render itself but ensure the result is exactly the width is
210             # what its supposed to be
211             #
212 411   33     2166 my $fr = $_->render($state, $ticks, $totalTicks, ($forceNewline && $considerMessage && $msg));
213 411         2419 my $fw = $_->get_width;
214 411         3155 $rendition .= sprintf("%*.*s", $fw, $fw, $fr);
215             }
216            
217 411 50       1349 if (!$cfg->get_allowOverflow)
218             {
219             # we must make sure the width of the rendition won't cause linewrapping
220             #
221 411         2360 my $w = $self->{$ATTR_width};
222 411 50       1066 $rendition = sprintf("%*.*s", $w, $w, $rendition) if (length($rendition) > $w);
223             }
224            
225 411 100       1406 if ($considerMessage)
226             {
227 222 100 66     1046 if ($msg && $ms ne 'none')
228             {
229 40         70 my $w = $self->{$ATTR_width};
230            
231 40 100       89 if ($ms eq 'newline')
232             {
233             # accept embedded newlines, but ensure the message filler is applied (if set)
234             # the split will also avoid stray empty lines at the end
235             #
236 20         27 my $fullMsg = '';
237 20         62 foreach my $msgLine (split(/\n/, $msg))
238             {
239 20 50       107 $msgLine .= $cfg->get_messageFiller x ($w - length($msgLine)) if ($w > length($msgLine));
240 20         135 $fullMsg .= "$msgLine\n";
241             }
242 20         61 $rendition = sprintf("%s%s", $fullMsg, $rendition);
243 20         45 $self->_set_message(undef);
244             }
245             else
246             {
247             # overlay or overlay_newline or overlay_honor_newline
248             #
249 20         48 my $nlConversion = $cfg->get_messageOverlayNewlineConversion;
250 20         95 my $start_ovrfld = $cfg->get_messageOverlayStartField;
251 20         86 my $end_ovrfld = $cfg->get_messageOverlayEndField;
252 20         62 my $start_ovrpos;
253             my $end_ovrpos;
254 20         22 my $offset = 0;
255 20         40 for (1 .. @$allFields)
256             {
257 20 50       39 $start_ovrpos = $offset if $start_ovrfld == $_;
258 20         49 $offset += $allFields->[$_ - 1]->get_width;
259 20 50       79 $end_ovrpos = $offset if $end_ovrfld == $_;
260 20 50 33     64 last if ($start_ovrpos && $end_ovrpos);
261             }
262            
263 20         51 $msg =~ s/\n/$nlConversion/g;
264 20         46 my $mf = $cfg->get_messageFiller;
265 20 50       88 my $len = $mf ? $end_ovrpos - $start_ovrpos : length($msg);
266 20 50       50 $msg .= $mf x ($len - length($msg)) if ($len > length($msg));
267            
268 20 50 0     119 if ($ms eq 'overlay' || ($ms eq 'overlay_honor_newline' && !$forceNewline))
      33        
269             {
270 20         45 substr($rendition, $start_ovrpos, $len) = sprintf("%*.*s", $len, $len, $msg);
271             }
272             else
273             {
274 0         0 substr($rendition, $start_ovrpos) = $msg;
275             }
276            
277 20 50       55 if ($forceNewline)
278             {
279 0         0 $rendition .= "\n";
280 0         0 $self->_set_message(undef);
281             }
282             }
283             }
284             }
285            
286 411         1586 return $rendition;
287             }
288            
289             ###
290            
291             package ProgressMonitor::Stringify::AbstractMonitorConfiguration;
292            
293 10     10   72 use strict;
  10         26  
  10         326  
294 10     10   50 use warnings;
  10         17  
  10         351  
295            
296 10     10   51 use Scalar::Util qw(blessed);
  10         19  
  10         970  
297            
298             # Attributes:
299             # maxWidth
300             # The maximum width this monitor can occupy altogether.
301             # allowOverflow
302             # In case the width is too small, let it overflow and linewrap.
303             # Else, cut the finished rendition so no linewrap occurs, but loses info.
304             # fields
305             # An array of fields (or a single field if only one) that should be used
306             # A field instance can not be reused in the list!
307             # messageStrategy
308             # Determines the strategy to use when displaying messages.
309             # 'none' : doesn't display messages
310             # 'overlay': requires 'messageOverlaysFields' to be set
311             # 'newline': renders the message only with a newline at the end, in
312             # effect pushing the other fields 'down'. Handles and 'honors'
313             # embedded newlines, trailing newlines are dropped.
314             # 'overlay_newline' : combines the effects of 'overlay' and 'newline'
315             # 'overlay_honor_newline' : acts as 'overlay', but will ensure to make a
316             # newline if the message has a trailing one.
317             # messageOverlayStartfield
318             # The field on which message overlay should start. Defaults to 0.
319             # messageOverlayEndfield
320             # The field on which message overlay should end. Defaults to last field.
321             # messageFiller
322             # The character for filling out the length of the message if
323             # is not long enough to overlay the full length of the field(s)
324             # it is set to overlay.
325             # messageTimeout
326             # The time in seconds before the message is cleared automatically. This
327             # is only relevant for overlay (for newline, it only appears once).
328             # Defaults to 3 seconds. Set to -1 for 'no timeout'.
329             # messageOverlayNewlineConversion
330             # For 'overlay' and 'overlay_newline', any embedded/trailing newlines
331             # will be converted to another string, settable by this cfg variable.
332             # Defaults to ' ' (space).
333             #
334             use classes
335 10         66 extends => 'ProgressMonitor::AbstractStatefulMonitorConfiguration',
336             attrs => [
337             'maxWidth', 'allowOverflow', 'fields',
338             'messageStrategy', 'messageOverlayStartField',
339             'messageOverlayEndField', 'messageFiller',
340             'messageTimeout', 'messageOverlayNewlineConversion'
341             ],
342 10     10   50 ;
  10         21  
343            
344             sub defaultAttributeValues
345             {
346 11     11   25 my $self = shift;
347            
348             return {
349 11         24 %{$self->SUPER::defaultAttributeValues()},
  11         107  
350             maxWidth => 0,
351             allowOverflow => 0,
352             fields => [],
353             messageStrategy => 'newline',
354             messageOverlayStartField => 1,
355             messageOverlayEndField => undef,
356             messageFiller => ' ',
357             messageTimeout => -1,
358             messageOverlayNewlineConversion => ' ',
359             };
360             }
361            
362             sub checkAttributeValues
363             {
364 11     11   45 my $self = shift;
365            
366 11         76 $self->SUPER::checkAttributeValues;
367            
368 11         113 my $maxWidth = $self->get_maxWidth;
369 11 50       76 X::Usage->throw("invalid maxWidth: $maxWidth") unless $maxWidth >= 0;
370            
371 11         119 my $fields = $self->get_fields;
372 11 50       122 if (ref($fields) ne 'ARRAY')
373             {
374 0         0 $fields = [$fields];
375 0         0 $self->set_fields($fields);
376             }
377            
378 11         23 my %seenFields;
379 11         31 for (@$fields)
380             {
381 11 50 33     201 X::Usage->throw("not a field: $_") unless (blessed($_) && $_->isa("ProgressMonitor::Stringify::Fields::AbstractField"));
382 11 50       54 X::Usage->throw("same instance of field used more than once: $_") if $seenFields{$_};
383 11         68 $seenFields{$_} = 1;
384             }
385            
386 11         85 my $ms = $self->get_messageStrategy;
387 11 50       130 X::Usage->throw("invalid value for messageStrategy: $ms")
388             unless $ms =~ /^(?:none|overlay|newline|overlay_newline|overlay_honor_newline)$/;
389            
390 11 100       60 if ($ms =~ /^overlay/)
391             {
392 2         4 my $maxFieldNum = @$fields;
393 2 50       13 $self->set_messageOverlayEndField($maxFieldNum) unless defined($self->get_messageOverlayEndField);
394            
395 2         29 my $start = $self->get_messageOverlayStartField;
396 2         11 my $end = $self->get_messageOverlayEndField;
397 2 50 33     18 X::Usage->throw("illegal overlay start field: $start") if ($start < 1 || $start > $maxFieldNum);
398 2 50 33     30 X::Usage->throw("illegal overlay end field: $end")
      33        
399             if ($end < 1 || $end > $maxFieldNum || $end < $start);
400             }
401            
402 11         74 my $mf = $self->get_messageFiller;
403 11 50       80 X::Usage->throw("messageFiller not a character: $mf") if length($mf) > 1;
404            
405 11         49 return;
406             }
407            
408             ############################
409            
410             =head1 NAME
411            
412             ProgressMonitor::Stringify::AbstractMonitor - A reusable/abstract monitor implementation
413             that deals in stringified feedback.
414            
415             =head1 DESCRIPTION
416            
417             This is an abstract base class for monitors that will render their result as a string
418             through the use of 'fields' (see the L packages).
419            
420             =head1 PROTECTED METHODS
421            
422             =over 2
423            
424             =item _new( $hashRef, $package )
425            
426             Configuration data:
427            
428             =over 2
429            
430             =item maxWidth (default => 79)
431            
432             The monitor should have this maxWidth. The actual width used may be less. This
433             depends on the fields it uses; specifically, if dynamic fields are used, they
434             will be given width until all is used or until the dynamic fields themselves
435             have reached their maxWidth if any.
436            
437             If the maxWidth is too small to handle the minimum requirements for all fields
438             the C setting controls whether the rendition causes linewrapping
439             or if it's just cut.
440            
441             =item allowOverflow (default => 0)
442            
443             If set to true and maxWidth is exceeded, linewrapping will occur for a possibly ugly display.
444             If set to false, the rendition will be cut to avoid linewrapping, for a possible loss of important
445             information.
446            
447             =item fields (default => [])
448            
449             An array ref with field instances.
450            
451             =item messageStrategy (default => newline)
452            
453             An identifiers that describes how messages should be inserted into the
454             rendition:
455            
456             =over 2
457            
458             =item none
459            
460             Not surprisingly, this suppresses message presentation.
461            
462             =item overlay
463            
464             This will cause the message to overlay one or more of the other
465             fields, so as to keep things on one line. This setting will work
466             in conjunction with messageTimeout, messageOverlayStartField and
467             messageOverlayEndField.
468            
469             =item newline
470            
471             This will cause the message and a newline to be inserted in front
472             of the regular rendition, causing the running rendition to be
473             'pushed' forward.
474            
475             =item overlay_newline
476            
477             This will combine the effects of 'overlay' and 'newline'.
478            
479             =back
480            
481             =item messageFiller (default => ' ')
482            
483             If the message is too short for the allotted space, it will be filled with
484             this character. Can be set to the empty string or undef to skip filling,
485             causing a 'partial overlay', i.e. just as much as the string is, which
486             obviously can give a confusing mixed message with the underlying field.
487            
488             =item messageTimeout (default => 3 seconds)
489            
490             This is only relevant for the 'overlay' strategy. If the code doesn't
491             explicitly set the message to undef/blank, the timeout will automatically
492             remove it. Set to -1 for infinite.
493            
494             =item messageOverlayStartField, messageOverlayEndField (defaults => all fields)
495            
496             Together these define the starting and ending field number that the message
497             should overlay. This defaults to 'all fields'.
498            
499             =item messageOverlayNewlineConversion (default => ' ')
500            
501             Embedded/trailing newlines will be converted to this string for the 'overlay'
502             and 'overlay_newline' strategies.
503            
504             =back
505            
506             =item _toString
507            
508             Contains the logic to assemble the fields into a current string.
509            
510             =back
511            
512             =head1 AUTHOR
513            
514             Kenneth Olwing, C<< >>
515            
516             =head1 BUGS
517            
518             I wouldn't be surprised! If you can come up with a minimal test that shows the
519             problem I might be able to take a look. Even better, send me a patch.
520            
521             Please report any bugs or feature requests to
522             C, or through the web interface at
523             L.
524             I will be notified, and then you'll automatically be notified of progress on
525             your bug as I make changes.
526            
527             =head1 SUPPORT
528            
529             You can find general documentation for this module with the perldoc command:
530            
531             perldoc ProgressMonitor
532            
533             =head1 ACKNOWLEDGEMENTS
534            
535             Thanks to my family. I'm deeply grateful for you!
536            
537             =head1 COPYRIGHT & LICENSE
538            
539             Copyright 2006,2007 Kenneth Olwing, all rights reserved.
540            
541             This program is free software; you can redistribute it and/or modify it
542             under the same terms as Perl itself.
543            
544             =cut
545            
546             1; # End of ProgressMonitor::Stringify::AbstractMonitor