File Coverage

blib/lib/Tickit/Builder.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Tickit::Builder;
2             # ABSTRACT: Define Tickit widget structures
3 1     1   885 use strict;
  1         2  
  1         46  
4 1     1   6 use warnings FATAL => 'all';
  1         1  
  1         46  
5 1     1   500 use Tickit;
  0            
  0            
6             use Data::Dump qw();
7             use Module::Load qw();
8              
9             our $VERSION = '0.001';
10              
11             =head1 NAME
12              
13             Tickit::Builder - widget layout definition from Perl structure or file
14              
15             =head1 VERSION
16              
17             version 0.001
18              
19             =head1 SYNOPSIS
20              
21             use Tickit::Async;
22             use Tickit::Builder;
23             my $layout = Tickit::Builder->new;
24             $layout->run({
25             widget => {
26             type => 'VBox',
27             children => [
28             { widget => { type => "Menu", bg => 'blue', children => [
29             { widget => { type => "Menu::Item", text => "File" } },
30             { widget => { type => "Menu::Item", text => "Edit" } },
31             { widget => { type => "Menu::Spacer", text => " " }, expand => 1 },
32             { widget => { type => "Menu::Item", text => "Help" } },
33             ] }},
34             { widget => { type => "HBox", text => "Static entry", children => [
35             { widget => { type => "VBox", children => [
36             { widget => { type => "Static", text => "Left panel" } },
37             ] }, expand => 0.15 },
38             { widget => { type => "VBox", children => [
39             { widget => { type => "Frame", style => 'single', children => [
40             { widget => { type => "Static", text => "Centre bit", fg => 'yellow' }, expand => 1 },
41             ] }, expand => 1 },
42             { widget => { type => "VBox", children => [
43             { widget => { type => "Static", text => "lower panel" } },
44             ] } },
45             ] }, expand => 0.85 },
46             ] }, expand => 1 },
47             { widget => { type => "Static", text => "Status bar", bg => 0x04, fg => 'white', } },
48             ],
49             }
50             });
51              
52             =head1 DESCRIPTION
53              
54             Very basic helper class for reading a widget layout definition and instantiating the required
55             objects. Intended to be used with the web-based or Tickit-based layout editor.
56              
57             =head1 METHODS
58              
59             =cut
60              
61             =head2 new
62              
63             Instantiate a new L object. Takes no parameters.
64              
65             =cut
66              
67             sub new {
68             bless {}, shift;
69             }
70              
71             =head2 report
72              
73             Debug output.
74              
75             =cut
76              
77             sub report {
78             my $self = shift;
79             my $msg = shift;
80             my @args = @_;
81             foreach my $item (@args) {
82             while(my $ref = ref $item) {
83             if($ref eq 'CODE') {
84             $item = $_->();
85             } elsif(grep $ref eq $_, qw(ARRAY HASH)) {
86             $item = Data::Dump::dump($item);
87             } else {
88             $item = "$item";
89             }
90             }
91             }
92             if(@args) {
93             my $txt = join ' ', scalar(localtime), sprintf $msg, @args;
94             print "$txt\n";
95             } else {
96             printf("%s %s\n", scalar(localtime), $msg);
97             }
98             }
99              
100             =head2 parse_widget
101              
102             Parse a widget definition from a hashref.
103              
104             =cut
105              
106             sub parse_widget {
107             my $self = shift;
108             my $spec = shift;
109             $self->report("Parsing widget %s", $spec);
110              
111             my %args = %$spec;
112             my $class = 'Tickit::Widget::' . delete $args{type};
113             my $children = delete $args{children} || [];
114             my $kb = delete $args{keybindings} || {};
115             my $id = delete $args{id};
116             my $classname = delete $args{class};
117             Module::Load::load($class);
118             die "$class not found" unless $class->can('new');
119              
120             # Build up the widget in this object
121             my $w;
122              
123             # Manual overrides... expect to end up with a lot of these over time :(
124             if($class eq 'Tickit::Widget::Scroller::Item::Text') {
125             $w = $class->new($args{text});
126             } else {
127             $w = $class->new(%args);
128             }
129              
130             # Any nested children entries will be recursed into
131             foreach my $child_def (@$children) {
132             my %child_spec = %$child_def;
133             $self->report("Found child def %s", $child_def);
134             my $child = $self->parse_widget(delete $child_spec{widget});
135             if($class eq 'Tickit::Widget::Scroller') {
136             $w->push($child);
137             } else {
138             $w->add($child, %child_spec);
139             }
140             }
141              
142             # We'll also support some basic key binding
143             foreach my $k (keys %$kb) {
144             my $v = $kb->{$k};
145             if($w->can('bind_keys')) {
146             $self->report('%s is fine for binding', $class);
147             $w->bind_keys($k, $v);
148             } else {
149             $self->report('%s cannot bind', $class);
150             }
151             $self->report($k . " bind for " . $v);
152             my @ks = split ' ', $k;
153             # this looks incomplete, perhaps we should be doing something else here?
154             }
155              
156             if(defined $id) {
157             die "ID [$id] was defined already\n" if exists $self->{by_id}{$id};
158             Scalar::Util::weaken($self->{by_id}{$id} = $w);
159             }
160             if(defined $classname) {
161             push @{ $self->{by_class}{$classname} }, $w;
162             Scalar::Util::weaken($self->{by_class}{$classname}[-1]);
163             }
164             return $w;
165             }
166              
167             =head2 by_id
168              
169             Returns the widget with the given ID.
170              
171             =cut
172              
173             sub by_id { $_[0]->{by_id}{$_[1]} }
174              
175             =head2 by_class
176              
177             Returns a list of all widgets matching the given classname.
178              
179             =cut
180              
181             sub by_class { @{ $_[0]->{by_class}{$_[1]} } }
182              
183             =head2 parse
184              
185             Parse the top-level layout spec (hashref).
186              
187             =cut
188              
189             sub parse {
190             my $self = shift;
191             my $spec = shift;
192             $self->report("Parsing %s", $spec);
193             my $w;
194             if(my $widget_def = $spec->{widget}) {
195             $w = $self->parse_widget($widget_def);
196             }
197             die "no widget" unless $w;
198             $w;
199             }
200              
201             =head2 apply_layout
202              
203             Apply the given layout to the L instance.
204              
205             Takes two parameters:
206              
207             =over 4
208              
209             =item * $tickit - a L instance.
210              
211             =item * $layout - a hashref representing the requested layout.
212              
213             =back
214              
215             =cut
216              
217             sub apply_layout {
218             my $self = shift;
219             my $tickit = shift;
220             my $layout = shift;
221             my $root = $self->parse($layout);
222             $tickit->set_root_widget($root);
223             }
224              
225             =head2 run
226              
227             Helper method to parse and run the layout definition using L.
228              
229             =cut
230              
231             sub run {
232             my $self = shift;
233             my $spec = shift;
234             my $root = $self->parse($spec);
235              
236             require Tickit::Async;
237             require IO::Async::Loop;
238             my $tickit = Tickit::Async->new;
239             $tickit->set_root_widget($root);
240             my $loop = IO::Async::Loop->new;
241             $loop->add($tickit);
242             $tickit->run;
243             }
244              
245             =head2 parse_file
246              
247             Parse definition from a file.
248              
249             =cut
250              
251             sub parse_file {
252             my $self = shift;
253             my ($file, $type) = @_;
254             $type = 'json' unless $type;
255             open my $fh, '<:encoding(utf-8)', $file or die "opening $file - $!";
256             my $txt = do { local $/; <$fh> };
257             if($type eq 'json') {
258             require JSON;
259             return JSON->new->decode($txt);
260             } else {
261             die 'unsupported';
262             }
263             }
264              
265             1;
266              
267             __END__