#!/usr/bin/env perl
#
# Don't try to follow the logic in here, XML::Twig allows for some real nonsense
use strict;
use warnings;
use Getopt::Long qw(GetOptions);
use Pod::Usage qw(pod2usage);
use XML::Twig;
my $inSettings = 0;
my @list = ();
my @clipboard = ();
my $help = 0;
my $after;
my $move;
my $to;
GetOptions(
'help|?' => \$help,
'add-after=s' => \$after,
'move=s' => \$move,
'to=s' => \$to,
) or pod2usage(2);
pod2usage(1) if $help || ($move && !$to) || (!$move && $to);
my $xml = shift || pod2usage(2);
my $twig = XML::Twig->new(
twig_handlers => {
'#COMMENT' => sub {
my ($t, $comment) = @_;
if ($comment->{comment} eq 'Settings View Controller') {
$inSettings = 1;
}
else {
$inSettings = 0;
}
},
label => sub {
my ($t, $label) = @_;
if ($inSettings) {
push @list, [ label => $label ];
}
},
segmentedControl => sub {
my ($t, $control) = @_;
if ($inSettings) {
push @list, [ control => $control ];
}
},
slider => sub {
my ($t, $slider) = @_;
if ($inSettings) {
push @list, [ slider => $slider ];
}
},
view => sub {
my ($t, $view) = @_;
if ($inSettings) {
push @list, [ view => $view ];
}
},
},
comments => 'process',
keep_atts_order => 1,
pretty_print => 'indented',
empty_tags => 'normal',
);
$twig->parsefile($xml);
if ($after) {
do_after($twig, $after);
}
elsif ($move && $to) {
do_move($twig, $move, $to);
}
do_output($twig);
sub do_after {
my ($twig, $after) = @_;
my $LABEL_TEMPLATE = qq{
};
my $CONTROL_TEMPLATE = qq{
};
my @to_add = (
[ label => XML::Twig::Elt->parse( sprintf($LABEL_TEMPLATE, generate_id()) ) ],
[ control => XML::Twig::Elt->parse( sprintf($CONTROL_TEMPLATE, generate_id()) ) ],
);
# Find where to insert them
for (my $i = 0; $i < scalar @list; $i++) {
my ($type, $e) = @{$list[$i]};
if ($type eq 'label' && ($e->att('text') // q{}) eq $after) {
($type, $e) = @{$list[$i + 1]};
# paste the block to a new location
for my $tm (@to_add) {
$tm->[1]->paste(after => $e);
$e = $tm->[1];
}
# also move the block in the ordering list, which is where the y values are adjusted
$i += 2;
splice(@list, $i, 0, @to_add);
}
}
}
sub do_move {
my ($twig, $move, $to) = @_;
my @to_move = ();
# Pull out the bits to move
for (my $i = 0; $i < scalar @list; $i++) {
my ($type, $e) = @{$list[$i]};
if ($type eq 'label' && ($e->att('text') // q{}) eq $move) {
# move this item and the next one (the control)
push @to_move, [ $type => $e->cut ];
($type, $e) = @{$list[$i + 1]};
push @to_move, [ $type => $e->cut ];
splice(@list, $i, 2);
last;
}
}
# Find where to insert them
for (my $i = 0; $i < scalar @list; $i++) {
my ($type, $e) = @{$list[$i]};
if ($type eq 'label' && ($e->att('text') // q{}) eq $to) {
($type, $e) = @{$list[$i + 1]};
# paste the block to a new location
for my $tm (@to_move) {
$tm->[1]->paste(after => $e);
$e = $tm->[1];
}
# also move the block in the ordering list, which is where the y values are adjusted
$i += 2;
splice(@list, $i, 0, @to_move);
}
}
}
sub do_output {
my $twig = shift;
# first label
my $x = 16;
my $y = 20;
# spacing
my $label_to_control = 29;
my $control_to_label = 35;
my $label_to_label = 43;
my $slider_to_next = 29;
my $view_to_next = 43;
my $seen_rdv = 0;
my $last = q{};
printf STDERR "%30s %s %s\n\n", q{}, "auto", "current";
for my $row (@list) {
my ($type, $e) = @{$row};
my $rect = $e->has_child('rect');
my $orig_y = $rect->att('y');
$rect->set_att(x => $x);
$rect->set_att(y => $y);
if ($type eq "label") {
my $text = $e->att('userLabel') // $e->att('text') // "(unknown label)";
printf STDERR "%30s %d %4d\n", $text, $y, $orig_y;
if ($text =~ /Caption/) { # space after captions is a bit larger
$y += $label_to_label;
}
else {
$y += $label_to_control;
}
}
elsif ($type eq "control") {
my ($userLabel) = $e->att('userLabel') // "(unknown control)";
printf STDERR "%30s %d %4d\n", $userLabel, $y, $orig_y;
$y += $control_to_label;
}
elsif ($type eq "slider") {
printf STDERR "%30s %d %4d\n", "slider", $y, $orig_y;
$y += $slider_to_next;
}
elsif ($type eq "view") {
my $customClass = $e->att('customClass') // q{};
if ($customClass && $customClass eq "UIScrollView") {
# This is the entire view, so we're done
last;
}
printf STDERR "%30s %d %4d\n", "view", $y, $orig_y;
$y += $view_to_next;
}
$last = $type;
}
# Output the entire XML with our changes
$twig->print;
}
sub generate_id {
# generate an id of the form K4A-oH-Bs0
my $chars = [ split //, 'abcdefghiklmnopqrstuvwxyzABCDEFGHIKLMNOPQRSTUVWXYZ0123456789' ];
my $random_fragment = sub {
my $length = shift;
return '' if $length < 1;
my $fragment = '';
$fragment .= $chars->[int(rand(scalar @{$chars}))] for (1..$length);
return $fragment;
};
return join '-', (
$random_fragment->(3),
$random_fragment->(2),
$random_fragment->(3),
);
}
=pod
=head1 NAME
SettingsHelper.pl - Less painful storyboard settings elements
=head1 SYNOPSIS
SettingsHelper.pl [options] [iPad.storyboard] > New.storyboard
Run with no options to display the list of items in a storyboard.
Options:
--add-after LABEL Insert a new empty label + control pair after the given label (and its control)
--move FROM --to TO Move the first label and control after the "to" label
--help This help message.
=cut