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
|
|
|
|
|
|
|
|