package UR::Object::View::Default::Text; use strict; use warnings; require UR; our $VERSION = "0.42_01"; # UR $VERSION; class UR::Object::View::Default::Text { is => 'UR::Object::View', has_constant => [ perspective => { value => 'default' }, toolkit => { value => 'text' }, ], has => [ indent_text => { is => 'Text', default_value => ' ', doc => 'indent child views with this text' }, ], }; # general view API sub _create_widget { # The "widget" for a text view is a pair of items: # - a scalar reference to hold the content # - an I/O handle to which it will display (the "window" it lives in) # Note that the former could be something tied to an object, # a file, or other external storage, though it is # simple by default. The later might also be tied. # The later is STDOUT unless overridden/changed. my $self = shift; my $scalar_ref = ''; my $fh = 'STDOUT'; return [\$scalar_ref,$fh]; } sub show { # Showing a text view typically prints to STDOUT my $self = shift; my $widget = $self->widget(); my ($content_ref,$output_stream) = @$widget; $output_stream->print($$content_ref,"\n"); } sub _update_subject_from_view { Carp::confess('currently text views are read-only!'); } sub _update_view_from_subject { my $self = shift; my $content = $self->_generate_content(@_); my $widget = $self->widget(); my ($content_ref,$fh) = @$widget; $$content_ref = $content; return 1; } # text view API sub content { # retuns the current value of the scalar ref containing the text content. my $self = shift; my $widget = $self->widget(); if (@_) { die "the widget reference for a view isn't changeable. change its content.."; } my ($content_ref,$output_stream) = @$widget; return $$content_ref; } sub output_stream { # retuns the current value of the handle to which we render. my $self = shift; my $widget = $self->widget(); if (@_) { return $widget->[1] = shift; } my ($content_ref,$output_stream) = @$widget; return $output_stream; } sub _generate_content { my $self = shift; # the header line is the class followed by the id my $text = $self->subject_class_name; $text =~ s/::/ /g; my $subject = $self->subject(); if ($subject) { my $subject_id_txt = $subject->id; $subject_id_txt = "'$subject_id_txt'" if $subject_id_txt =~ /\s/; $text .= " $subject_id_txt"; } # Don't recurse back into something we're already in the process of showing if ($self->_subject_is_used_in_an_encompassing_view()) { $text .= " (REUSED ADDR)\n"; } else { $text .= "\n"; # the content for any given aspect is handled separately my @aspects = $self->aspects; my @sorted_aspects = map { $_->[1] } sort { $a->[0] <=> $b->[0] } map { [ $_->number, $_ ] } @aspects; for my $aspect (@sorted_aspects) { next if $aspect->name eq 'id'; my $aspect_text = $self->_generate_content_for_aspect($aspect); $text .= $aspect_text; } } return $text; } sub _generate_content_for_aspect { # This does two odd things: # 1. It gets the value(s) for an aspect, then expects to just print them # unless there is a delegate view. In which case, it replaces them # with the delegate's content. # 2. In cases where more than one value is returned, it recycles the same # view and keeps the content. # # These shortcuts make it hard to abstract out logic from toolkit-specifics my $self = shift; my $aspect = shift; my $subject = $self->subject; my $indent_text = $self->indent_text; my $aspect_text = $indent_text . $aspect->label . ": "; if (!$subject) { $aspect_text .= "-\n"; return $aspect_text; } my $aspect_name = $aspect->name; my @value; eval { @value = $subject->$aspect_name; }; if (@value == 0) { $aspect_text .= "-\n"; return $aspect_text; } if (@value == 1 and ref($value[0]) eq 'ARRAY') { @value = @{$value[0]}; } unless ($aspect->delegate_view) { $aspect->generate_delegate_view; } # Delegate to a subordinate view if needed. # This means we replace the value(s) with their # subordinate widget content. if (my $delegate_view = $aspect->delegate_view) { # TODO: it is bad to recycle a view here?? # Switch to a set view, which is the standard lister. foreach my $value ( @value ) { if (Scalar::Util::blessed($value)) { $delegate_view->subject($value); } else { $delegate_view->subject_id($value); } $delegate_view->_update_view_from_subject(); $value = $delegate_view->content(); } } if (@value == 1 and defined($value[0]) and index($value[0],"\n") == -1) { # one item, one row in the value or sub-view of the item: $aspect_text .= $value[0] . "\n"; } else { my $aspect_indent; if (@value == 1) { # one level of indent for this sub-view's sub-aspects # zero added indent for the identity line b/c it's next-to the field label # aspect1: class with id ID # sub-aspect1: value1 # sub-aspect2: value2 $aspect_indent = $indent_text; } else { # two levels of indent for this sub-view's sub-aspects # just one level for each identity # aspect1: ... # class with id ID # sub-aspect1: value1 # sub-aspect2: value2 # class with id ID # sub-aspect1: value1 # sub-aspect2: value2 $aspect_text .= "\n"; $aspect_indent = $indent_text . $indent_text; } for my $value (@value) { my $value_indented = ''; if (defined $value) { my @rows = split(/\n/,$value); $value_indented = join("\n", map { $aspect_indent . $_ } @rows); chomp $value_indented; } $aspect_text .= $value_indented . "\n"; } } return $aspect_text; } 1; =pod =head1 NAME UR::Object::View::Default::Text - object views in text format =head1 SYNOPSIS $o = Acme::Product->get(1234); # generates a UR::Object::View::Default::Text object: $v = $o->create_view( toolkit => 'text', aspects => [ 'id', 'name', 'qty_on_hand', 'outstanding_orders' => [ 'id', 'status', 'customer' => [ 'id', 'name', ] ], ], ); $txt1 = $v->content; $o->qty_on_hand(200); $txt2 = $v->content; =head1 DESCRIPTION This class implements basic text views of objects. It is used for command-line tools, and is the base class for other specific text formats like XML, HTML, JSON, etc. =head1 WRITING A SUBCLASS # In Acme/Product/View/OutstandingOrders/Text.pm package Acme::Product::View::OutstandingOrders::Text; use UR; class Acme::Product::View::OutstandingOrders::Text { is => 'UR::Object::View::Default::Text' }; sub _initial_aspects { return ( 'id', 'name', 'qty_on_hand', 'outstanding_orders' => [ 'id', 'status', 'customer' => [ 'id', 'name', ] ], ); } $v = $o->create_view(perspective => 'outstanding orders', toolkit => 'text'); print $v->content; =head1 SEE ALSO UR::Object::View, UR::Object::View::Toolkit::Text, UR::Object =cut