File Coverage

lib/UR/Object/View.pm
Criterion Covered Total %
statement 106 182 58.2
branch 36 78 46.1
condition 8 29 27.5
subroutine 10 21 47.6
pod 5 7 71.4
total 165 317 52.0


line stmt bran cond sub pod time code
1             package UR::Object::View;
2 266     266   2142 use warnings;
  266         345  
  266         7001  
3 266     266   893 use strict;
  266         330  
  266         457657  
4             require UR;
5             our $VERSION = "0.46"; # UR $VERSION;;
6              
7             class UR::Object::View {
8             has_abstract_constant => [
9             subject_class_name => { is_abstract => 1, is_constant => 1 },#is_classwide => 1, is_constant => 1, is_optional => 0 },
10             perspective => { is_abstract => 1, is_constant => 1 },#is_classwide => 1, is_constant => 1, is_optional => 0 },
11             toolkit => { is_abstract => 1, is_constant => 1 },#is_classwide => 1, is_constant => 1, is_optional => 0 },
12             ],
13             has_optional => [
14             parent_view => {
15             is => 'UR::Object::View',
16             id_by => 'parent_view_id',
17             doc => 'when nested inside another view, this references that view',
18             },
19             subject => {
20             is => 'UR::Object',
21             id_class_by => 'subject_class_name', id_by => 'subject_id',
22             doc => 'the object being observed'
23             },
24             aspects => {
25             is => 'UR::Object::View::Aspect',
26             reverse_as => 'parent_view',
27             is_many => 1,
28             specify_by => 'name',
29             order_by => 'number',
30             doc => 'the aspects of the subject this view renders'
31             },
32             default_aspects => {
33             is => 'ARRAY',
34             is_abstract => 1,
35             is_constant => 1,
36             is_many => 1, # technically this is one "ARRAY"
37             default_value => undef,
38             doc => 'a tree of default aspect descriptions' },
39             ],
40             has_optional_transient => [
41             _widget => {
42             doc => 'the object native to the specified toolkit which does the actual visualization'
43             },
44             _observer_data => {
45             is => 'HASH',
46             is_transient => 1,
47             value => undef, # hashref set at construction time
48             doc => ' hooks around the subject which monitor it for changes'
49             },
50             ],
51             has_many_optional => [
52             aspect_names => { via => 'aspects', to => 'name' },
53             ]
54             };
55              
56              
57             sub create {
58 102     102 1 158 my $class = shift;
59              
60 102         418 my ($params,@extra) = $class->define_boolexpr(@_);
61              
62             # set values not specified in the params which can be inferred from the class name
63 102         548 my ($expected_class,$expected_perspective,$expected_toolkit) = ($class =~ /^(.*)::View::(.*?)::([^\:]+)$/);
64 102 50       301 unless ($params->specifies_value_for('subject_class_name')) {
65 0         0 $params = $params->add_filter(subject_class_name => $expected_class);
66             }
67 102 50       294 unless ($params->specifies_value_for('perspective')) {
68 0         0 $expected_perspective = join('-', split( /(?=[A-Z])/, $expected_perspective) ); #convert CamelCase to hyphenated-words
69 0         0 $params = $params->add_filter(perspective => $expected_perspective);
70             }
71 102 50       274 unless ($params->specifies_value_for('toolkit')) {
72 0         0 $params = $params->add_filter(toolkit => $expected_toolkit);
73             }
74              
75             # now go the other way, and use both to infer a final class name
76 102         403 $expected_class = $class->_resolve_view_class_for_params($params);
77 102 50       234 unless ($expected_class) {
78 0         0 my $subject_class_name = $params->value_for('subject_class_name');
79 0         0 Carp::croak("Failed to resolve a subclass of " . __PACKAGE__
80             . " for $subject_class_name from parameters. "
81             . "Received $params.");
82             }
83              
84 102 100       454 unless ($class->isa($expected_class)) {
85 51         256 return $expected_class->create(@_);
86             }
87              
88 51         244 $params->add_filter(_observer_data => {});
89 51         287 my $self = $expected_class->SUPER::create($params);
90 51 50       160 return unless $self;
91              
92 51         97 $class = ref($self);
93 51         152 $expected_class = $class->_resolve_view_class_for_params(
94             subject_class_name => $self->subject_class_name,
95             perspective => $self->perspective,
96             toolkit => $self->toolkit
97             );
98 51 50 33     311 unless ($expected_class and $expected_class eq $class) {
99 0   0     0 $expected_class ||= '';
100 0         0 Carp::croak("constructed a $class object but properties indicate $expected_class should have been created.");
101             }
102              
103 51 50       156 unless ($params->specifies_value_for('aspects')) {
104 0         0 my @aspect_specs = $self->default_aspects();
105 0 0       0 if (! @aspect_specs) {
106 0         0 @aspect_specs = $self->_resolve_default_aspects();
107             }
108 0 0 0     0 if (@aspect_specs == 1 and ref($aspect_specs[0]) eq 'ARRAY') {
109             # Got an arrayref, expand back into an array
110 0         0 @aspect_specs = @{$aspect_specs[0]};
  0         0  
111             }
112              
113 0         0 for my $aspect_spec (@aspect_specs) {
114 0 0       0 my $aspect = $self->add_aspect(ref($aspect_spec) ? %$aspect_spec : $aspect_spec);
115 0 0       0 unless ($aspect) {
116 0         0 $self->error_message("Failed to add aspect @$aspect_spec to new view " . $self->id);
117 0         0 $self->delete;
118 0         0 return;
119             }
120             }
121             }
122              
123 51         175 return $self;
124             }
125              
126             our %view_class_cache = ();
127             sub _resolve_view_class_for_params {
128             # View modules use standardized naming: SubjectClassName::View::Perspective::Toolkit.
129             # The subject must be explicitly of class "SubjectClassName" or some subclass of it.
130 153     153   195 my $class = shift;
131 153         402 my $bx = $class->define_boolexpr(@_);
132              
133 153 100       373 if (exists $view_class_cache{$bx->id}) {
134 46 50       119 if (!defined $view_class_cache{$bx->id}) {
135 0         0 return;
136             }
137 46         118 return $view_class_cache{$bx->id};
138             }
139              
140 107         262 my %params = $bx->params_list;
141              
142 107         268 my $subject_class_name = delete $params{subject_class_name};
143 107         140 my $perspective = delete $params{perspective};
144 107         133 my $toolkit = delete $params{toolkit};
145 107         117 my $aspects = delete $params{aspects};
146              
147 107 50 33     608 unless($subject_class_name and $perspective and $toolkit) {
      33        
148 0         0 Carp::confess("Bad params @_. Expected subject_class_name, perspective, toolkit.");
149             }
150              
151 107         181 $perspective = lc($perspective);
152 107         155 $toolkit = lc($toolkit);
153              
154 107         415 my $namespace = $subject_class_name->__meta__->namespace;
155 107   33     553 my $vocabulary = ($namespace and $namespace->can("get_vocabulary") ? $namespace->get_vocabulary() : undef);
156 107         1092 $vocabulary = UR->get_vocabulary;
157              
158 107         254 my $subject_class_object = $subject_class_name->__meta__;
159 107         1772 my @possible_subject_class_names = ($subject_class_name,$subject_class_name->inheritance);
160              
161 107         125 my $subclass_name;
162 107         173 for my $possible_subject_class_name (@possible_subject_class_names) {
163              
164             $subclass_name = join("::",
165             $possible_subject_class_name,
166             "View",
167             join ("",
168             $vocabulary->convert_to_title_case (
169 251         1281 map { ucfirst(lc($_)) }
170             split(/-+|\s+/,$perspective)
171             )
172             ),
173             join ("",
174             $vocabulary->convert_to_title_case (
175 240         1171 map { ucfirst(lc($_)) }
  240         680  
176             split(/-+|\s+/,$toolkit)
177             )
178             )
179             );
180              
181 240         378 my $subclass_meta;
182 240         220 eval {
183 240         1652 $subclass_meta = $subclass_name->__meta__;
184             };
185 240 100 66     843 if ($@ or not $subclass_meta) {
186             #not a class... keep looking
187 133         252 next;
188             }
189              
190 107 50       444 unless($subclass_name->isa(__PACKAGE__)) {
191 0         0 Carp::carp("Subclass $subclass_name exists but is not a view?!");
192 0         0 next;
193             }
194              
195 107         342 $view_class_cache{$bx->id} = $subclass_name;
196 107         379 return $subclass_name;
197             }
198              
199 0         0 $view_class_cache{$bx->id} = undef;
200 0         0 return;
201             }
202              
203             sub _resolve_default_aspects {
204 36     36   67 my $self = shift;
205 36         159 my $parent_view = $self->parent_view;
206 36         114 my $subject_class_name = $self->subject_class_name;
207 36         139 my $meta = $subject_class_name->__meta__;
208 36         157 my @c = ($meta->class_name, $meta->ancestry_class_names);
209             my %aspects =
210 84         179 map { $_->property_name => 1 }
211 36         177 grep { not $_->implied_by }
  88         210  
212             UR::Object::Property->get(class_name => \@c);
213 36         132 my @aspects = sort keys %aspects;
214 36         179 return @aspects;
215             }
216              
217             sub __signal_change__ {
218             # ensure that changes to the view which occur
219             # after the widget is produced
220             # are reflected in the widget
221 69     69   166 my ($self,$method,@details) = @_;
222              
223 69 100       176 if ($self->_widget) {
224 20 50 33     177 if ($method eq 'subject' or $method =~ 'aspects') {
    50 33        
225 0         0 $self->_bind_subject();
226             }
227             elsif ($method eq 'delete' or $method eq 'unload') {
228 0         0 my $observer_data = $self->_observer_data;
229 0         0 for my $subscription (values %$observer_data) {
230 0         0 my ($class, $id, $method, $callback) = @$subscription;
231 0         0 $class->cancel_change_subscription($id, $method, $callback);
232             }
233 0         0 $self->_widget(undef);
234             }
235             }
236 69         143 return 1;
237             }
238              
239             # _encompassing_view() and _subject_is_used_in_an_encompassing_view() are used by the
240             # default views (UR::Object::View::Default::*) to detect an infinite recursion situation
241             # where it's asked to render an object A that references a B which refers back to A
242              
243             # If this view is embedded in another view, return the encompassing view
244             sub _encompassing_view {
245 45     45   52 my $self = shift;
246              
247 45         105 my @aspects = UR::Object::View::Aspect->get(delegate_view_id => $self->id);
248 45 100       103 if (@aspects) {
249             # FIXME - is it possible for there to be more than one thing in @aspects here?
250             # And if so, how do we differentiate them
251 30         99 return $aspects[0]->parent_view;
252             }
253              
254             # $self must be the top-level view
255 15         43 return;
256             }
257              
258             # If the subject of the view is also the subject of an encompassing view, return true
259             sub _subject_is_used_in_an_encompassing_view {
260 22     22   40 my($self,$subject) = @_;
261              
262 22 50       94 $subject = $self->subject unless (@_ == 2);
263              
264 22         78 my $encompassing = $self->_encompassing_view;
265 22         55 while($encompassing) {
266 30 100       74 if ($encompassing->subject eq $subject) {
267 7         20 return 1;
268             } else {
269 23         49 $encompassing = $encompassing->_encompassing_view();
270             }
271             }
272 15         43 return;
273             }
274              
275             sub all_subject_classes {
276 0     0 0 0 my $self = shift;
277 0         0 my @classes = ();
278              
279             # suppress error callbacks inside this method
280 0         0 my $old_cb = UR::ModuleBase->message_callback('error');
281 0 0   0   0 UR::ModuleBase->message_callback('error', sub {}) if ($old_cb);
282              
283 0         0 for my $aspect ($self->aspects) {
284 0 0       0 unless ($aspect->delegate_view) {
285 0         0 eval {
286 0         0 $aspect->generate_delegate_view;
287             };
288             }
289 0 0       0 if ($aspect->delegate_view) {
290 0         0 push @classes, $aspect->delegate_view->all_subject_classes
291             }
292             }
293 0 0       0 UR::ModuleBase->message_callback('error', $old_cb) if ($old_cb);
294              
295 0         0 push @classes, $self->subject_class_name;
296              
297 0         0 my %saw;
298 0         0 my @uclasses = grep(!$saw{$_}++,@classes);
299              
300 0         0 return @uclasses;
301             }
302              
303             sub all_subject_classes_ancestry {
304 0     0 0 0 my $self = shift;
305              
306 0         0 my @classes = $self->all_subject_classes;
307              
308 0         0 my @aclasses;
309 0         0 for my $class (@classes) {
310 0         0 my $m;
311 0         0 eval { $m = $class->__meta__ };
  0         0  
312 0 0 0     0 next if $@ or not $m;
313              
314 0         0 push @aclasses, reverse($class, $m->ancestry_class_names);
315             }
316              
317 0         0 my %saw;
318 0         0 my @uaclasses = grep(!$saw{$_}++,@aclasses);
319              
320 0         0 return @uaclasses;
321             }
322              
323             # rendering implementation
324              
325             sub widget {
326 236     236 1 234 my $self = shift;
327 236 50       417 if (@_) {
328 0         0 Carp::confess("Widget() is not settable! Its value is set from _create_widget() upon first use.");
329             }
330 236         475 my $widget = $self->_widget();
331 236 100       394 unless ($widget) {
332 47         164 $widget = $self->_create_widget();
333 47 50       95 return unless $widget;
334 47         104 $self->_widget($widget);
335 47         181 $self->_bind_subject(); # works even if subject is undef
336             }
337 236         340 return $widget;
338             }
339              
340             sub _create_widget {
341 0 0   0   0 Carp::confess("The _create_widget method must be implemented for all concrete "
342             . " view subclasses. No _create_widget for "
343             . (ref($_[0]) ? ref($_[0]) : $_[0]) . "!");
344             }
345              
346             sub _bind_subject {
347             # This is called whenever the subject changes, or when the widget is first created.
348             # It handles the case in which the subject is undef.
349 47     47   58 my $self = shift;
350 47         108 my $subject = $self->subject();
351 47 100       114 return unless defined $subject;
352              
353 44         125 my $observer_data = $self->_observer_data;
354 44 50       101 unless ($observer_data) {
355 44         96 $self->_observer_data({});
356 44         98 $observer_data = $self->_observer_data;
357             }
358 44 50       96 Carp::confess unless $self->_observer_data == $observer_data;
359              
360             # See if we've already done this.
361 44 50       136 return 1 if $observer_data->{$subject};
362              
363             # Wipe subscriptions from the last bound subscription(s).
364 44         134 for (keys %$observer_data) {
365 0         0 my $s = delete $observer_data->{$_};
366 0         0 my ($class, $id, $method,$callback) = @$s;
367 0         0 $class->cancel_change_subscription($id, $method,$callback);
368             }
369              
370 44 100       89 return unless $subject;
371              
372             # Make a new subscription for this subject
373             my $subscription = $subject->create_subscription(
374             callback => sub {
375 0     0   0 $self->_update_view_from_subject(@_);
376             }
377 41         392 );
378 41         124 $observer_data->{$subject} = $subscription;
379            
380             # Set the view to show initial data.
381 41         199 $self->_update_view_from_subject;
382            
383 41         57 return 1;
384             }
385              
386             sub _update_view_from_subject {
387             # This is called whenever the view changes, or the subject changes.
388             # It passes the change(s) along, so that the update can be targeted, if the developer chooses.
389 0 0   0     Carp::croak("The _update_view_from_subject method must be implemented for all concreate "
390             . " view subclasses. No _update_subject_from_view for "
391             . (ref($_[0]) ? ref($_[0]) : $_[0]) . "!");
392             }
393              
394             sub _update_subject_from_view {
395 0 0   0     Carp::croak("The _update_subject_from_view method must be implemented for all concreate "
396             . " view subclasses. No _update_subject_from_view for "
397             . (ref($_[0]) ? ref($_[0]) : $_[0]) . "!");
398             }
399              
400             # external controls
401              
402             sub show {
403 0     0 1   my $self = shift;
404 0           $self->_toolkit_package->show_view($self);
405             }
406              
407             sub show_modal {
408 0     0 1   my $self = shift;
409 0           $self->_toolkit_package->show_view_modally($self);
410             }
411              
412             sub hide {
413 0     0 1   my $self = shift;
414 0           $self->_toolkit_package->hide_view($self);
415             }
416              
417             sub _toolkit_package {
418 0     0     my $self = shift;
419 0           my $toolkit = $self->toolkit;
420 0           return "UR::Object::View::Toolkit::" . ucfirst(lc($toolkit));
421             }
422              
423             1;
424              
425             =pod
426              
427             =head1 NAME
428              
429             UR::Object::View - a base class for "views" of UR::Objects
430              
431             =head1 SYNOPSIS
432              
433             $object = Acme::Product->get(1234);
434              
435             ## Acme::Product::View::InventoryHistory::Gtk2
436              
437             $view = $object->create_view(
438             perspective => 'inventory history',
439             toolkit => 'gtk2',
440             );
441             $widget = $view->widget(); # returns the Gtk2::Widget itself directly
442             $view->show(); # puts the widget in a Gtk2::Window and shows everything
443            
444             ##
445              
446             $view = $object->create_view(
447             perspective => 'inventory history',
448             toolkit => 'xml',
449             );
450             $widget = $view->widget(); # returns an arrayref with the xml string reference, and the output filehandle (stdout)
451             $view->show(); # prints the current xml content to the handle
452            
453             $xml = $view->content(); # returns the XML directly
454            
455             ##
456            
457             $view = $object->create_view(
458             perspective => 'inventory history',
459             toolkit => 'html',
460             );
461             $widget = $view->widget(); # returns an arrayref with the html string reference, and the output filehandle (stdout)
462             $view->show(); # prints the html content to the handle
463            
464             $html = $view->content(); # returns the HTML text directly
465              
466              
467             =head1 USAGE API
468              
469             =over 4
470              
471             =item create
472              
473             The constructor requires that the subject_class_name, perspective,
474             and toolkit be set. Most concrete subclasses have perspective and toolkit
475             set as constant.
476              
477             Producing a view object does not "render" the view, just creates an
478             interface for controlling the view, including encapsualting its creation.
479              
480             The subject can be set later and changed. The aspects viewed may
481             be constant for a given perspective, or mutable, depending on how
482             flexible the of the perspective logic is.
483              
484             =item show
485              
486             For stand-alone views, this puts the view widget in its a window. For
487             views which are part of a larger view, this makes the view widget
488             visible in the parent.
489              
490             =item hide
491              
492             Makes the view invisible. This means hiding the window, or hiding the view
493             widget in the parent widget for subordinate views.
494              
495             =item show_modal
496              
497             This method shows the view in a window, and only returns after the window is closed.
498             It should only be used for views which are a full interface capable of closing itself
499             when done.
500              
501             =item widget
502              
503             Returns the "widget" which renders the view. This is built lazily
504             on demand. The actual object type depends on the toolkit named above.
505             This method might return HTML text, or a Gtk object. This can be used
506             directly, and is used internally by show/show_modal.
507              
508             (Note: see UR::Object::View::Toolkit::Text for details on the "text" widget,
509             used by HTML/XML views, etc. This is just the content and an I/O handle to
510             which it should stream.)
511              
512             =item delete
513              
514             Delete the view (along with the widget(s) and infrastructure underlying it).
515              
516             =back
517              
518             =head1 CONSTRUCTION PROPERTIES (CONSTANT)
519              
520             The following three properties are constant for a given view class. They
521             determine which class of view to construct, and must be provided to create().
522              
523             =over 4
524              
525             =item subject_class_name
526            
527             The class of subject this view will view. Constant for any given view,
528             but this may be any abstract class up-to UR::Object itself.
529            
530             =item perspective
531              
532             Used to describe the layout logic which gives logical content
533             to the view.
534              
535             =item toolkit
536              
537             The specific (typically graphical) toolkit used to construct the UI.
538             Examples are Gtk, Gkt2, Tk, HTML, XML.
539              
540             =back
541              
542             =head1 CONFIGURABLE PROPERTIES
543              
544             These methods control which object is being viewed, and what properties
545             of the object are viewed. They can be provided at construction time,
546             or afterward.
547              
548             =over 4
549              
550             =item subject
551              
552             The particular "model" object, in MVC parlance, which is viewed by this view.
553             This value may change
554              
555             =item aspects / add_aspect / remove_aspect
556              
557             Specifications for properties/methods of the subject which are rendered in
558             the view. Some views have mutable aspects, while others merely report
559             which aspects are revealed by the perspective in question.
560              
561             An "aspect" is some characteristic of the "subject" which is rendered in the
562             view. Any property of the subject is usable, as is any method.
563              
564             =back
565              
566             =head1 IMPLEMENTATION INTERFACE
567              
568             When writing new view logic, the class name is expected to
569             follow a formula:
570              
571             Acme::Rocket::View::FlightPath::Gtk2
572             \ / \ / \
573             subject class name perspective toolkit
574              
575             The toolkit is expected to be a single word. The perspective
576             is everything before the toolkit, and after the last 'View' word.
577             The subject_class_name is everything to the left of the final
578             '::View::'.
579              
580             There are three methods which require an implementation, unless
581             the developer inherits from a subclass of UR::Object::View which
582             provides these methods:
583              
584             =over 4
585              
586             =item _create_widget
587              
588             This creates the widget the first time ->widget() is called on a view.
589              
590             This should be implemented in a given perspective/toolkit module to actually
591             create the GUI using the appropriate toolkit.
592              
593             It will be called before the specific subject is known, so all widget creation
594             which is subject-specific should be done in _bind_subject(). As such it typically
595             only configures skeletal aspects of the view.
596              
597             =item _bind_subject
598              
599             This method is called when the subject is set, or when it is changed, or unset.
600             It updates the widget to reflect changes to the widget due to a change in subject.
601              
602             This method has a default implementation which does a general subscription
603             to changes on the subject. It probably does not need to be overridden
604             in custom views. Implementations which _do_ override this should take
605             an undef subject, and be sure to un-bind a previously existing subject if
606             there is one set.
607              
608             =item _update_view_from_subject
609              
610             If and when the property values of the subject change, this method will be called on
611             all views which render the changed aspect of the subject.
612              
613             =item _update_subject_from_view
614              
615             When the widget changes, it should call this method to save the UI changes
616             to the subject. This is not applicable to read-only views.
617              
618             =back
619              
620             =head1 OTHER METHODS
621              
622             =over 4
623              
624             =item _toolkit_package
625              
626             This method is useful to provide generic toolkit-based services to a view,
627             using a toolkit agnostic API. It can be used in abstract classes which,
628             for instance, want to share logic for a given perspective across toolkits.
629              
630             The toolkit class related to a view is responsible for handling show/hide logic,
631             etc. in the base UR::Object::View class.
632              
633             Returns the name of a class which is derived from UR::Object::View::Toolkit
634             which implements certain utility methods for views of a given toolkit.
635              
636             =back
637              
638             =head1 EXAMPLES
639              
640             $o = Acme::Product->get(1234);
641              
642             $v = Acme::Product::View::InventoryHistory::HTML->create();
643             $v->add_aspect('outstanding_orders');
644             $v->show;
645              
646             =cut
647