Term-Fabulous/lib/Term/Fabulous/Widget/Dialog.pm
package Term::Fabulous::Widget::Dialog;
use v5.32;
use warnings;
use feature 'signatures';
no warnings 'experimental::signatures';
use Object::Pad 0.825;
use Term::Fabulous::Widget::Box;
use Term::Fabulous::Widget::Dialog::Backdrop;
our $VERSION = '0.05';
class Term::Fabulous::Widget::Dialog
:isa(Term::Fabulous::Widget::Box)
:strict(params)
{
use Clay::XS qw(sizing_fit sizing_grow CLAY_TOP_TO_BOTTOM CLAY_ATTACH_TO_ROOT CLAY_ATTACH_POINT_RIGHT_CENTER);
use List::Util qw(first min);
use Scalar::Util qw(blessed refaddr weaken);
use Term::Fabulous::Check qw(boolean describe integer one_of string);
use Term::Fabulous::Event::Answer;
use Term::Fabulous::Event::Close;
use Term::Fabulous::Placement qw(TOPMOST_Z_INDEX);
use Term::Fabulous::Widget::Button;
use Term::Fabulous::Widget::Text;
use constant DISMISSED => 'cancel'; # the button a dismissed dialog answers with
use constant RIGHT_EDGE => -1; # a dialog on the right keeps a column free at the edge
use constant LOWEST_Z_INDEX => -32768; # Clay's z_index is a 16-bit integer, up to TOPMOST_Z_INDEX
my %DEFAULT_LAYOUT = (
layout_direction => CLAY_TOP_TO_BOTTOM,
padding => { left => 1, right => 1, top => 1, bottom => 1 },
child_gap => 1,
);
field $z_index :param = 1000;
field $close_on_escape :param = 1;
field $title :param = '';
field $notice_line :param = 'shown';
field $placement :param = 'center';
field @_buttons; # { id, label, primary, widget }, in order
field $_title_text;
field $_notice_line; # a box around the notice text, above the buttons
field $_notice_text;
field $_button_row;
# The Term::Fabulous::Widget::Dialog::Backdrop while open. The tree owns
# it (it holds the dialog as its child), so the reference is weak: a
# dialog left open in a dropped UI is freed with it.
field $_backdrop;
field $_is_open = 0;
# A dialog looks like a dialog unless told otherwise: a panel in the
# theme's dialog colors and border (dialog.border.enabled), laid out
# top to bottom with a cell of padding.
sub BUILDARGS ( $class, %params ) {
$params{layout} = { %DEFAULT_LAYOUT, %{ $params{layout} // {} } };
return $class->SUPER::BUILDARGS(%params);
}
ADJUST :params ( :$buttons = [] ) {
$z_index = $self->_checked_z_index($z_index);
$close_on_escape = boolean( $self, close_on_escape => $close_on_escape );
$title = string( $self, title => $title );
$notice_line = one_of( $self, notice_line => $notice_line, qw(shown kept row) );
$placement = one_of( $self, placement => $placement, qw(center right) );
@_buttons = $self->_parsed_buttons($buttons);
$_title_text = Term::Fabulous::Widget::Text->new( text => $title, bold => 1 );
$_notice_text = Term::Fabulous::Widget::Text->new( markup => '' );
$_notice_line = Term::Fabulous::Widget::Box->new( layout => { sizing => { width => sizing_grow(), height => sizing_fit( $notice_line eq 'row' ? 1 : 0 ) } } );
$_notice_line->add_child($_notice_text);
$_button_row = $self->_button_row if @_buttons;
$self->add_child($_title_text) if length $title;
$self->_float_on_the_right if $placement eq 'right';
}
method theme_family :common () {
return 'dialog';
}
method themed_params :common () {
return ( $class->SUPER::themed_params, backdrop_color => [ 'backdrop', 'normal', 'color' ] );
}
method layout_properties :common () {
return (
$class->SUPER::layout_properties,
z_index => 'scalar',
close_on_escape => 'boolean',
title => 'scalar',
notice_line => 'scalar',
placement => 'scalar',
);
}
# ---------------------------------------------------------------------
# Parameters
# ---------------------------------------------------------------------
method _parsed_buttons ($buttons) {
die ref($self) . ": buttons must be an array reference, got " . describe($buttons) unless ref $buttons eq 'ARRAY';
my ( @parsed, %seen );
foreach my $button (@$buttons) {
die ref($self) . ": a button must be a hash reference with an id and a label, got " . describe($button) unless ref $button eq 'HASH';
my %rest = %$button;
my $id = string( $self, 'the id of a button' => delete $rest{id} );
my $label = string( $self, "the label of the button '$id'" => delete $rest{label} );
my $primary = boolean( $self, "the primary flag of the button '$id'" => delete $rest{primary} );
die ref($self) . ": the button '$id' has unknown keys: " . join( ', ', sort keys %rest ) if %rest;
die ref($self) . ": two buttons have the id '$id'" if $seen{$id}++;
push @parsed, { id => $id, label => $label, primary => $primary };
}
return @parsed;
}
# A dialog on the right floats at the right edge of the screen, over
# its backdrop.
method _float_on_the_right () {
$self->floating(
{
attach_to => CLAY_ATTACH_TO_ROOT,
attach_points => { element => CLAY_ATTACH_POINT_RIGHT_CENTER, parent => CLAY_ATTACH_POINT_RIGHT_CENTER },
offset => { x => RIGHT_EDGE, y => 0 },
z_index => min( $z_index + 1, TOPMOST_Z_INDEX ),
}
);
return;
}
# ---------------------------------------------------------------------
# The parts: title, buttons and the notice line
# ---------------------------------------------------------------------
method _button_row () {
my $row = Term::Fabulous::Widget::Box->new( layout => { child_gap => 2 } );
foreach my $button (@_buttons) {
my $id = $button->{id};
my $widget = Term::Fabulous::Widget::Button->new( bordered => 1, classes => $button->{primary} ? ['primary'] : [], layout => { padding => { left => 1, right => 1 } } );
$widget->add_child( Term::Fabulous::Widget::Text->new( text => $button->{label} ) );
$widget->on( Activate => sub ( $self, $event ) { $self->press_button($id); return }, owner => $self );
$button->{widget} = $widget;
$row->add_child($widget);
}
return $row;
}
# The title first; the notice line, when it shows, and the buttons
# last, unless a subclass put the buttons elsewhere.
method place_parts () {
$self->_show_title;
$self->remove_child( grep { defined } $_notice_line, $_button_row ); # a row elsewhere stays there
$self->add_child($_notice_line) if $self->_shows_notice_line;
$self->add_child($_button_row) if defined $_button_row && !defined $_button_row->parent;
return $self;
}
method _holds ($part) {
my $parent = defined $part ? $part->parent : undef;
return defined $parent && refaddr($parent) == refaddr($self) ? 1 : 0;
}
method _show_title () {
return $self->remove_child($_title_text) unless length $title;
$self->insert_children( 0, $_title_text ) unless $self->_holds($_title_text);
return;
}
method _shows_notice_line () {
return $notice_line ne 'shown' || length $_notice_text->text;
}
# The notice line comes and goes in its place, above the buttons.
method _show_notice_line () {
return $self->remove_child($_notice_line) unless $self->_shows_notice_line;
return if $self->_holds($_notice_line);
my $children = $self->children;
my $row = first { refaddr( $children->[$_] ) == refaddr( $_button_row // $self ) } 0 .. $#$children;
$self->insert_children( $row // scalar @$children, $_notice_line );
return;
}
method title (@new) {
return $title unless @new;
$title = string( $self, title => $new[0] );
$_title_text->text($title);
$self->_show_title;
return $title;
}
# What the line above the buttons says, in rich markup: an error in
# the color of an invalid input, anything else in the text color.
method notice (@new) {
return length $_notice_text->text ? $_notice_text->markup : undef unless @new;
my ( $markup, $kind ) = @new;
$markup = string( $self, notice => $markup );
$kind = one_of( $self, 'the kind of a notice' => $kind // 'info', qw(info error) );
$_notice_text->markup($markup);
$kind eq 'error' ? $_notice_text->text_color( $self->family_look( input => 'text', 'invalid' ) ) : $_notice_text->reset_look('text_color');
$self->_show_notice_line;
return $self;
}
method button ($id) {
return $self->_button($id)->{widget};
}
method is_primary ($id) {
return $self->_button($id)->{primary};
}
method _button ($id) {
my $button = first { $_->{id} eq ( $id // '' ) } @_buttons;
die ref($self) . ": the dialog has no button with the id " . describe($id) unless defined $button;
return $button;
}
# The notice as it reads, without its markup.
method notice_text () {
my $text = $_notice_text->text;
return length $text ? $text : undef;
}
method button_ids () {
return map { $_->{id} } @_buttons;
}
method button_row () {
return $_button_row;
}
# The button Enter in a field presses where a dialog wants it to: the
# first primary one, or the first one.
method default_button () {
return undef unless @_buttons;
my $primary = first { $_->{primary} } @_buttons;
return ( $primary // $_buttons[0] )->{id};
}
# ---------------------------------------------------------------------
# Accessors
# ---------------------------------------------------------------------
# The backdrop reads the color when a frame is drawn.
method backdrop_color (@new) {
return $self->look_value('backdrop_color') unless @new;
return $self->set_look( backdrop_color => $new[0] );
}
method _checked_z_index ($value) {
$value = integer( $self, z_index => $value );
die ref($self) . ": z_index must be an integer from " . LOWEST_Z_INDEX . " to " . TOPMOST_Z_INDEX . ", got $value" if $value < LOWEST_Z_INDEX || $value > TOPMOST_Z_INDEX;
return $value;
}
method z_index (@new) {
return $z_index unless @new;
$z_index = $self->_checked_z_index( $new[0] );
$_backdrop->floating( { %{ $_backdrop->floating }, z_index => $z_index } ) if defined $_backdrop;
$self->_float_on_the_right if $placement eq 'right';
return $z_index;
}
method close_on_escape (@new) {
return $close_on_escape unless @new;
return $close_on_escape = boolean( $self, close_on_escape => $new[0] );
}
method notice_line (@new) {
return $notice_line unless @new;
$notice_line = one_of( $self, notice_line => $new[0], qw(shown kept row) );
$_notice_line->layout( { %{ $_notice_line->layout }, sizing => { width => sizing_grow(), height => sizing_fit( $notice_line eq 'row' ? 1 : 0 ) } } );
$self->_show_notice_line;
return $notice_line;
}
# A dialog in the center sits in its backdrop's layout; one on the
# right floats.
method placement (@new) {
return $placement unless @new;
$placement = one_of( $self, placement => $new[0], qw(center right) );
$placement eq 'right' ? $self->_float_on_the_right : $self->floating(undef);
return $placement;
}
method is_open () {
return $_is_open;
}
method backdrop () {
return $_backdrop;
}
# What an answer carries besides what answer is given: nothing for a
# plain dialog; a subclass with fields reports them.
method values () {
return {};
}
# ---------------------------------------------------------------------
# Opening, answering and closing
# ---------------------------------------------------------------------
method open ($ui) {
die "Term::Fabulous::Widget::Dialog: open needs the Term::Fabulous object, got " . ( ref $ui || $ui // 'undef' )
unless blessed $ui && $ui->isa('Clay::UI');
return $self if defined $_backdrop;
die "Term::Fabulous::Widget::Dialog: a dialog that is a child of another widget cannot open" if defined $self->parent;
$self->place_parts;
my $interaction = $ui->interaction;
my $backdrop = Term::Fabulous::Widget::Dialog::Backdrop->new(
dialog => $self,
z_index => $z_index,
focus_before => $interaction->get_focused_widget,
);
$backdrop->add_child($self);
$ui->root->add_child($backdrop);
weaken( $_backdrop = $backdrop );
$_is_open = 1;
$interaction->set_focused_widget( $backdrop->get_next_focus );
return $self;
}
# What pressing a button does: the dialog answers with its id. A
# subclass whose buttons do more overrides it.
method press_button ($id) {
return $self->answer($id);
}
method answer ( $id, %values ) {
$id = string( $self, 'the id of an answer' => $id );
$self->_button($id) if @_buttons;
die ref($self) . ": answer needs an open dialog" unless $_is_open;
return $self->_finish( $id, { %{ $self->values }, %values }, 0 );
}
# Escape, or close from the program: the dialog is dismissed and
# answers cancel with its values.
method close () {
return $self unless $_is_open;
return $self->_finish( DISMISSED, $self->values, 1 );
}
# Removing the backdrop gives the focus back: the backdrop names the
# widget that had it as its focus heir. The program may have taken the
# backdrop out of the tree already (with clear_children on the root,
# for example): then there is nothing to remove, but Close and Answer
# still fire.
method _finish ( $button, $values, $dismissed ) {
my $backdrop = $_backdrop;
( $_is_open, $_backdrop ) = ( 0, undef );
if ( defined $backdrop ) {
my $holder = $backdrop->parent;
$holder->remove_child($backdrop) if defined $holder;
$backdrop->clear_children;
}
$self->fire_event( Term::Fabulous::Event::Close->new );
$self->fire_event( Term::Fabulous::Event::Answer->new( button => $button, values => $values, dismissed => $dismissed ) );
return $self;
}
}
1;
__END__
=head1 NAME
Term::Fabulous::Widget::Dialog - A box that opens over the whole screen,
keeps the focus and answers
=head1 SYNOPSIS
use Term::Fabulous::Widget::Dialog;
use Term::Fabulous::Widget::Button;
use Term::Fabulous::Widget::Text;
use Clay::XS qw(sizing_fixed);
my $dialog = Term::Fabulous::Widget::Dialog->new(
id => 'confirm',
layout => { sizing => { width => sizing_fixed(40) } },
);
my $quit = Term::Fabulous::Widget::Button->new( background_color => [ 40, 60, 90, 255 ], layout => { padding => { left => 1, right => 1 } } );
$quit->add_child( Term::Fabulous::Widget::Text->new( text => 'Quit', text_color => [ 255, 255, 255, 255 ] ) );
$dialog->add_child(
Term::Fabulous::Widget::Text->new( text => 'Really quit? Escape cancels.', text_color => [ 220, 220, 220, 255 ] ),
$quit,
);
$quit->on( Activate => sub ($event) { $ui->loop->stop; return } );
$dialog->on( Close => sub ($event) { $status->text('cancelled'); return } );
# From a key binding or a button:
$dialog->open($ui);
# A dialog with its own title, buttons and notice line:
my $export = Term::Fabulous::Widget::Dialog->new(
title => 'Export the table',
buttons => [ { id => 'export', label => 'Export', primary => 1 }, { id => 'cancel', label => 'Cancel' } ],
);
$export->add_child( Term::Fabulous::Widget::Text->new( text => 'All 120 rows go to export.csv.' ) );
$export->on( Answer => sub ($event) {
export_csv() if $event->button eq 'export'; # Escape answers cancel, dismissed
return;
} );
$export->open($ui);
=begin html
<p><img src="https://raw.githubusercontent.com/davenonymous/perl-term-fabulous/v0.05/screenshots/widget-dialog.svg" alt="A Delete 3 files? dialog with Delete and Cancel buttons over a dimmed list of files; Delete has the focus and a blue border"></p>
=end html
The program is F<examples/widgets/dialog.pl>.
=head1 DESCRIPTION
A Dialog is a L<Term::Fabulous::Widget::Box> that is not part of the
layout until it is opened. L</open> puts it in the center of the
screen (or at its right edge, see C<placement>), on top of everything else, behind a translucent backdrop that
dims the rest of the screen, and gives the keyboard focus to the first
widget inside it. While it is open:
=over
=item *
Tab and Shift+Tab cycle through the widgets inside the dialog only;
=item *
mouse clicks outside the dialog reach nothing behind it, whatever the
C<backdrop_color>: they move the focus onto the backdrop, from where
Tab goes to the first widget of the dialog and Shift+Tab to the last,
and their C<Mouse> events are fired on the backdrop, from where they
bubble to the root widget (the backdrop's parent), not to the widgets
behind the dialog;
=item *
key presses go to the focused widget inside the dialog and bubble up
through the dialog to the backdrop, where C<Escape> closes the dialog
(unless C<close_on_escape> is off). They go no further: the widgets
and key bindings behind the dialog do not see them. Ctrl+C still ends
the program (unless the UI was made with C<< stop_on_ctrl_c =E<gt> 0 >>),
Tab and Shift+Tab still move the focus;
=item *
the focus stays inside the dialog: when the focused widget is disabled
or removed, its nearest ancestor that can take the focus does, which
is the backdrop unless a widget inside the dialog is closer, and when
the program focuses nothing the backdrop takes it (an C<OnBlur>
listener inside the dialog must let the event bubble for that, see
L<Term::Fabulous::Widget::Dialog::Backdrop>);
=item *
the widgets behind the dialog stay visible through the backdrop, but
are neither hovered nor pressed.
=back
L</close> takes the dialog off the screen, puts the focus back on the
widget that had it before (if that widget can still take the focus), and
fires C<Close> (L<Term::Fabulous::Event::Close>) and then C<Answer>
(L</Answers>) on the dialog. A closed dialog can be opened again, as
often as needed, and keeps its children and their state in between.
=head2 Title, buttons and the notice line
A dialog can bring the parts most dialogs have. A C<title> is a bold
line at the top. C<buttons> are one row of bordered buttons at the
bottom; pressing one makes the dialog L</answer> with the button's id.
The I<notice line> above the buttons says something with L</notice>,
for example what is wrong with an input. The children the program adds
go between the title and the notice line: the dialog puts its parts in
their places when it opens (see L</place_parts>).
=head2 Answers
Every time a dialog closes, it fires two events on itself: C<Close>,
then L<Answer|Term::Fabulous::Event::Answer>. An answer through
L</answer> (a button, or the program) names its button and carries the
dialog's L</values> and the values given. A dialog closed without an
answer (C<Escape>, or L</close> from the program) is I<dismissed>: it
answers with the button C<cancel> and the dialog's L</values>, and the
event's C<dismissed> is true. That is one rule for every dialog:
L<Term::Fabulous::Widget::Prompt> reports its fields as its values, and
L<Term::Fabulous::Widget::FileDialog> (its choice answers with the
path), L<Term::Fabulous::Widget::KeyReference> (its Close button answers
C<close>) and a plain dialog have none. When the listeners run, the
dialog is closed and the focus is back where it was.
A Dialog comes with a look from the theme's C<dialog> family (a dark
background and a round border in the accent blue in the built-in dark
theme), one cell of padding and a vertical layout with a gap of one
row between the children. Every one of these is an ordinary Box
parameter and can be overridden. Give the dialog a
width (C<sizing> in C<layout>); without one it is as wide as its
widest child.
=head1 CONSTRUCTOR
=head2 new
my $dialog = Term::Fabulous::Widget::Dialog->new(%parameters);
All parameters are optional; unknown parameters die. A Dialog takes
every parameter of L<Term::Fabulous::Widget::Box> (see
L<Term::Fabulous::Widget/new>), with the defaults described above,
plus:
=over
=item C<backdrop_color>
The color of the layer behind the dialog, in any format
L<Term::Fabulous::Color> accepts. Default: the theme's
C<dialog.backdrop>, C<[ 0, 0, 0, 128 ]> in the dark theme, black at
half opacity, which dims the screen behind the dialog. An opaque color
hides it; alpha 0 leaves it as it is.
=item C<z_index>
An integer from -32768 to 32767; other values die. Dialogs and other floating widgets with a higher value are
drawn over those with a lower one. Default: 1000. Popups (the open list
of a L<Term::Fabulous::Widget::Dropdown>, a popup
L<Term::Fabulous::Widget::Menu>, a table's column chooser) float at
L<Term::Fabulous::Placement/TOPMOST_Z_INDEX>, so they are drawn over the
dialog they are in.
=item C<close_on_escape>
A boolean. Default: 1: C<Escape>, pressed while the focus is inside the
dialog, closes it. With 0 the program closes the dialog itself.
=item C<title>
A character string, drawn in bold on the first row. Default: C<''>, no
title.
=item C<buttons>
An array reference of buttons, each a hash reference with an C<id> (a
string, required, unique), a C<label> (a string, required) and
C<primary> (a boolean, default 0). They are drawn in one row at the
bottom, in the order given, from the left, each bordered with a column
of padding on either side; primary buttons get the class C<primary>,
which a theme may give a look of its own (see
L<Term::Fabulous::Manual::Looks/Variants and classes>). Pressing one
calls L</press_button>, which answers with its id. Unknown keys die.
Default: C<[]>, no buttons.
=item C<notice_line>
When the notice line above the buttons takes room: C<shown> (the
default), only while it says something; C<kept>, always in its place,
without rows while it says nothing (the gap above it stays); C<row>,
always at least one row high, so that a notice does not change the
height of the dialog. Anything else dies.
=item C<placement>
C<center> (the default): the dialog sits in the middle of the screen.
C<right>: it floats at the right edge of the screen, a column from it,
centered from top to bottom, just above its backdrop's C<z_index>.
Anything else dies.
=back
=head1 METHODS
A Dialog has all methods of L<Term::Fabulous::Widget> plus these:
=head2 open
$dialog->open($ui);
Opens the dialog in the L<Term::Fabulous> object C<$ui>: adds it to
the screen as described above and focuses the first focusable widget
inside it (the backdrop when there is none). Opening an open dialog
does nothing. Dies when C<$ui> is not a L<Term::Fabulous> (or other
L<Clay::UI>) object, and when the dialog has been added to another
widget as a child. Returns the dialog.
=head2 close
$dialog->close;
Dismisses the dialog: removes it from the screen, gives the focus back
to the widget that had it when the dialog opened (if that widget still
exists and can take the focus; the backdrop names it as its focus heir,
see L<Term::Fabulous::Widget::Dialog::Backdrop/focus_heir>) and fires
C<Close> and C<Answer> (C<cancel>, dismissed) on the dialog. Closing a
closed dialog does nothing. A dialog
whose backdrop the program took out of the tree itself (with
C<clear_children> on the root, for example) is still open until
C<close> is called; the focus went back when the backdrop left, and
C<close> then only fires C<Close> and C<Answer>. Returns the dialog.
=head2 answer
$dialog->answer('export');
$dialog->answer( save => path => $path );
Closes the dialog and fires C<Close> and then C<Answer> with this
button id, the dialog's L</values> and the values given (which win
over them), not dismissed (see L</Answers>). A dialog with buttons dies
for an id that is none of them; one without buttons answers with any
string. Dies when the dialog is not open. Returns the dialog.
=head2 press_button
$dialog->press_button('export');
What pressing a button does. In a Dialog, it answers with the button's
id. A subclass whose buttons do more first (check the inputs, choose a
file) overrides it. Returns the dialog.
=head2 values
my $values = $dialog->values;
What every answer of the dialog carries, as a new hash reference: an
empty hash in a Dialog. A subclass with inputs reports them (see
L<Term::Fabulous::Widget::Prompt/values>).
=head2 notice
$dialog->notice( 'The name is taken.', 'error' );
$dialog->notice('Saved [bold]notes.txt[/].');
$dialog->notice(''); # says nothing
my $markup = $dialog->notice;
Says something in the notice line above the buttons, in the rich markup
of L<Term::Fabulous::Text::Markup> (escape text of the user with
L<escape_markup|Term::Fabulous::Text::Markup/escape_markup>). The kind
is C<info> (the default, the text color) or C<error> (the color of an
invalid input, the theme's C<input.text.invalid>); anything else dies.
An empty markup says nothing. The line wraps at the width of the
dialog; when it takes room depends on C<notice_line>. Returns the
dialog. Without arguments, returns the markup shown, or C<undef> while
the line says nothing.
=head2 notice_text
my $text = $dialog->notice_text;
What the notice line says, as plain text without the markup, or
C<undef> while it says nothing.
=head2 title
$dialog->title('Export 3 tables');
Accessor for the C<title>. Writing it changes the title line at once;
an empty string takes it away.
=head2 button, button_ids, is_primary, default_button
$dialog->button('export')->disabled(1);
my @ids = $dialog->button_ids;
my $checks = $dialog->is_primary('export');
my $default = $dialog->default_button;
The L<Term::Fabulous::Widget::Button> of a button (dying for an id the
dialog has no button of), the ids of the buttons in order, whether a
button is primary (1 or 0), and the button that a subclass presses for
C<Enter> in a field: the first primary button, the first button when
none is, or C<undef> without buttons.
=head2 notice_line, placement
$dialog->placement('right');
Accessors for the parameters of the same names.
=head2 is_open
if ( $dialog->is_open ) { ... }
1 while the dialog is open, 0 otherwise.
=head2 backdrop
my $backdrop = $dialog->backdrop;
The L<Term::Fabulous::Widget::Dialog::Backdrop> behind the open dialog,
or C<undef> while it is closed or after the program took it out of the
tree. Rarely needed.
=head2 backdrop_color, z_index, close_on_escape
$dialog->backdrop_color( [ 0, 0, 0, 200 ] );
$dialog->z_index(2000);
$dialog->close_on_escape(0);
Accessors for the constructor parameters of the same names. Without an
argument they return the current value; with one they set it and return
the stored form. Changes to C<backdrop_color> and C<z_index> show on an
open dialog with the next frame.
=head1 SUBCLASS INTERFACE
=head2 place_parts
$self->place_parts;
Puts the parts of the dialog in their places: the title first, the
notice line (when C<notice_line> says it takes room) and the button row
last. L</open> calls it, so children added before then end up between
them. A subclass that rebuilds its children while the dialog is open
calls it afterwards. A button row the subclass put inside one of its
own boxes stays there. Returns the dialog.
=head2 button_row
$search_row->add_child( $search, $self->button_row );
The box with the buttons, or C<undef> without buttons. A subclass may
put it somewhere else in the dialog (L<Term::Fabulous::Widget::KeyReference>
puts it next to its search field on a short screen).
=head1 EVENTS
Besides the events of every Box (L<Term::Fabulous::Widget::Box/EVENTS>),
a Dialog fires:
=over
=item C<Close> (L<Term::Fabulous::Event::Close>)
The dialog was closed: by an answer, by C<Escape> or by L</close>.
Fired on the dialog after it has left the screen, so it does not bubble
anywhere.
=item C<Answer> (L<Term::Fabulous::Event::Answer>)
Right after C<Close>, every time: the button that answered and the
values, or C<cancel> as dismissed (see L</Answers>).
=back
Events from the widgets inside the dialog (C<Activate> of a Button,
C<Submit> of a text field, C<Change>, ...) bubble through the dialog as
usual, so one listener on the dialog can handle them all. The
C<Activate> of the dialog's own C<buttons> stops at the button, which
calls L</press_button>.
=head1 KDL PROPERTIES
The properties of L<Term::Fabulous::Widget::Box/KDL PROPERTIES>, plus
C<backdrop_color> (a color string), C<z_index> (an integer from -32768
to 32767),
C<close_on_escape> (C<#true> or C<#false>), C<title>, C<notice_line>
and C<placement> (strings). C<buttons> is not a KDL property: a
dialog from a layout file has no buttons of its own; give it Button
children or build it in Perl.
A Dialog is added to the widget tree only when it opens, and a Dialog
that is a child of another widget cannot open. A layout file has only
one top-level widget, so a Dialog must be the root widget of a layout
file of its own. C<build> returns the dialog, which is not open yet.
With this in F<about.kdl>:
=for highlighter language=kdl
use Term::Fabulous::Widget::Dialog as Dialog
use Term::Fabulous::Widget::Text as Text
Dialog "about" {
sizing width="fixed(40)"
backdrop_color "rgba(0, 0, 0, 0.7)"
Text { text "Term::Fabulous"; text_color "#ffffff"; }
}
open the dialog from Perl:
=for highlighter language=perl
my $about = Term::Fabulous::Layout->new( file => 'about.kdl' )->build;
$about->open($ui);
=head1 SEE ALSO
L<Term::Fabulous::Widget::Box>, L<Term::Fabulous::Event::Close>,
L<Term::Fabulous::Event::Answer>,
L<Term::Fabulous::Widget::Dialog::Backdrop>,
L<Term::Fabulous::Widget::Prompt>, L<Term::Fabulous::Widget::FileDialog>,
L<Term::Fabulous::Widget::KeyReference>,
L<Term::Fabulous::Manual::Events/FOCUS>, L<Term::Fabulous::Widget/floating>,
L<Term::Fabulous::Manual::CustomWidgets/A dialog of your own>,
L<Term::Fabulous::Cookbook::Forms/Ask a question in a dialog (Dialog widget)>,
L<Term::Fabulous::Cookbook::Forms/A login form (centered dialog, masked password)>,
L<Term::Fabulous::Cookbook::Forms/Confirm before quitting (Prompt)>,
L<Term::Fabulous::Cookbook::Forms/Open and save a file (FileDialog)>,
L<Term::Fabulous::Cookbook::Menus/Show the keys in a help screen (KeyReference)>.
=cut