Cataclysm-DDA/src/popup.cpp

437 lines
12 KiB
C++

#include "popup.h"
#include <algorithm>
#include <array>
#include <memory>
#include "cached_options.h"
#include "catacharset.h"
#include "input.h"
#include "output.h"
#include "ui_manager.h"
query_popup::query_popup()
: cur( 0 ), default_text_color( c_white ), anykey( false ), cancel( false ),
ontop( false ), fullscr( false ), pref_kbd_mode( keyboard_mode::keycode )
{
}
query_popup &query_popup::context( const std::string &cat )
{
invalidate_ui();
category = cat;
return *this;
}
query_popup &query_popup::option( const std::string &opt )
{
invalidate_ui();
options.emplace_back( opt, []( const input_event & ) {
return true;
} );
return *this;
}
query_popup &query_popup::option( const std::string &opt,
const std::function<bool( const input_event & )> &filter )
{
invalidate_ui();
options.emplace_back( opt, filter );
return *this;
}
query_popup &query_popup::allow_anykey( bool allow )
{
// Change does not affect cache, do not invalidate the window
anykey = allow;
return *this;
}
query_popup &query_popup::allow_cancel( bool allow )
{
// Change does not affect cache, do not invalidate the window
cancel = allow;
return *this;
}
query_popup &query_popup::on_top( bool top )
{
invalidate_ui();
ontop = top;
return *this;
}
query_popup &query_popup::full_screen( bool full )
{
invalidate_ui();
fullscr = full;
return *this;
}
query_popup &query_popup::cursor( size_t pos )
{
// Change does not affect cache, do not invalidate window
cur = pos;
return *this;
}
query_popup &query_popup::default_color( const nc_color &d_color )
{
default_text_color = d_color;
return *this;
}
query_popup &query_popup::preferred_keyboard_mode( const keyboard_mode mode )
{
invalidate_ui();
pref_kbd_mode = mode;
return *this;
}
std::vector<std::vector<std::string>> query_popup::fold_query(
const std::string &category,
const keyboard_mode pref_kbd_mode,
const std::vector<query_option> &options,
const int max_width, const int horz_padding )
{
input_context ctxt( category, pref_kbd_mode );
std::vector<std::vector<std::string>> folded_query;
folded_query.emplace_back();
int query_cnt = 0;
int query_width = 0;
for( const query_popup::query_option &opt : options ) {
const std::string &name = ctxt.get_action_name( opt.action );
const std::string &desc = ctxt.get_desc( opt.action, name, opt.filter );
const int this_query_width = utf8_width( desc, true ) + horz_padding;
++query_cnt;
query_width += this_query_width;
if( query_width > max_width + horz_padding ) {
if( query_cnt == 1 ) {
// Each line has at least one query, so keep this query in the current line
folded_query.back().emplace_back( desc );
folded_query.emplace_back();
query_cnt = 0;
query_width = 0;
} else {
// Wrap this query to the next line
folded_query.emplace_back();
folded_query.back().emplace_back( desc );
query_cnt = 1;
query_width = this_query_width;
}
} else {
folded_query.back().emplace_back( desc );
}
}
if( folded_query.back().empty() ) {
folded_query.pop_back();
}
return folded_query;
}
void query_popup::invalidate_ui() const
{
if( win ) {
win = {};
folded_msg.clear();
buttons.clear();
}
std::shared_ptr<ui_adaptor> ui = adaptor.lock();
if( ui ) {
ui->mark_resize();
}
}
static constexpr int border_width = 1;
void query_popup::init() const
{
constexpr int horz_padding = 2;
constexpr int vert_padding = 1;
const int max_line_width = FULL_SCREEN_WIDTH - border_width * 2;
// Fold message text
folded_msg = foldstring( text, max_line_width );
// Fold query buttons
const auto &folded_query = fold_query( category, pref_kbd_mode, options, max_line_width,
horz_padding );
// Calculate size of message part
int msg_width = 0;
int msg_height = folded_msg.size();
for( const auto &line : folded_msg ) {
msg_width = std::max( msg_width, utf8_width( line, true ) );
}
// Calculate width with query buttons
for( const auto &line : folded_query ) {
if( !line.empty() ) {
int button_width = 0;
for( const auto &opt : line ) {
button_width += utf8_width( opt, true );
}
msg_width = std::max( msg_width, button_width +
horz_padding * static_cast<int>( line.size() - 1 ) );
}
}
msg_width = std::min( msg_width, max_line_width );
// Calculate height with query buttons & button positions
buttons.clear();
if( !folded_query.empty() ) {
msg_height += vert_padding;
for( const auto &line : folded_query ) {
if( !line.empty() ) {
int button_width = 0;
for( const auto &opt : line ) {
button_width += utf8_width( opt, true );
}
// Right align.
// TODO: multi-line buttons
int button_x = std::max( 0, msg_width - button_width -
horz_padding * static_cast<int>( line.size() - 1 ) );
for( const auto &opt : line ) {
buttons.emplace_back( opt, point( button_x, msg_height ) );
button_x += utf8_width( opt, true ) + horz_padding;
}
msg_height += 1 + vert_padding;
}
}
msg_height -= vert_padding;
}
// Calculate window size
const int win_width = std::min( TERMX,
fullscr ? FULL_SCREEN_WIDTH : msg_width + border_width * 2 );
const int win_height = std::min( TERMY,
fullscr ? FULL_SCREEN_HEIGHT : msg_height + border_width * 2 );
const point win_pos( ( TERMX - win_width ) / 2, ontop ? 0 : ( TERMY - win_height ) / 2 );
win = catacurses::newwin( win_height, win_width, win_pos );
std::shared_ptr<ui_adaptor> ui = adaptor.lock();
if( ui ) {
ui->position_from_window( win );
}
}
void query_popup::show() const
{
if( !win ) {
init();
}
werase( win );
draw_border( win );
for( size_t line = 0; line < folded_msg.size(); ++line ) {
nc_color col = default_text_color;
print_colored_text( win, point( border_width, border_width + line ), col, col,
folded_msg[line] );
}
for( size_t ind = 0; ind < buttons.size(); ++ind ) {
nc_color col = ind == cur ? hilite( c_white ) : c_white;
const query_popup::button &btn = buttons[ind];
print_colored_text( win, btn.pos + point( border_width, border_width ),
col, col, btn.text );
}
wnoutrefresh( win );
}
std::shared_ptr<ui_adaptor> query_popup::create_or_get_adaptor()
{
std::shared_ptr<ui_adaptor> ui = adaptor.lock();
if( !ui ) {
adaptor = ui = std::make_shared<ui_adaptor>();
ui->on_redraw( [this]( const ui_adaptor & ) {
show();
} );
ui->on_screen_resize( [this]( ui_adaptor & ) {
init();
} );
ui->mark_resize();
}
return ui;
}
query_popup::result query_popup::query_once()
{
if( !anykey && !cancel && options.empty() ) {
return { false, "ERROR", {} };
}
if( test_mode ) {
return { false, "ERROR", {} };
}
std::shared_ptr<ui_adaptor> ui = create_or_get_adaptor();
ui_manager::redraw();
input_context ctxt( category, pref_kbd_mode );
if( cancel || !options.empty() ) {
ctxt.register_action( "HELP_KEYBINDINGS" );
}
if( !options.empty() ) {
ctxt.register_action( "LEFT" );
ctxt.register_action( "RIGHT" );
ctxt.register_action( "CONFIRM" );
for( const query_popup::query_option &opt : options ) {
ctxt.register_action( opt.action );
}
// Mouse movement and button
ctxt.register_action( "SELECT" );
ctxt.register_action( "MOUSE_MOVE" );
}
if( anykey ) {
ctxt.register_action( "ANY_INPUT" );
// Mouse movement, button, and wheel
ctxt.register_action( "COORDINATE" );
}
if( cancel ) {
ctxt.register_action( "QUIT" );
}
result res;
// Assign outside construction of `res` to ensure execution order
res.wait_input = !anykey;
do {
res.action = ctxt.handle_input();
res.evt = ctxt.get_raw_input();
// If we're tracking mouse movement
if( !options.empty() && ( res.action == "MOUSE_MOVE" || res.action == "SELECT" ) ) {
cata::optional<point> coord = ctxt.get_coordinates_text( win );
for( size_t i = 0; i < buttons.size(); i++ ) {
if( coord.has_value() && buttons[i].contains( coord.value() ) ) {
if( i != cur ) {
// Mouse-over new button, switch selection
cur = i;
ui_manager::redraw();
}
if( res.action == "SELECT" ) {
// Left-click to confirm selection
res.action = "CONFIRM";
}
}
}
}
} while(
// Always ignore mouse movement
( res.evt.type == input_event_t::mouse &&
res.evt.get_first_input() == static_cast<int>( MouseInput::Move ) ) ||
// Ignore window losing focus in SDL
( res.evt.type == input_event_t::keyboard_char && res.evt.sequence.empty() )
);
if( cancel && res.action == "QUIT" ) {
res.wait_input = false;
} else if( res.action == "LEFT" ) {
if( cur > 0 ) {
--cur;
} else {
cur = options.size() - 1;
}
} else if( res.action == "RIGHT" ) {
if( cur + 1 < options.size() ) {
++cur;
} else {
cur = 0;
}
} else if( res.action == "CONFIRM" ) {
if( cur < options.size() ) {
res.wait_input = false;
res.action = options[cur].action;
}
} else if( res.action == "HELP_KEYBINDINGS" ) {
// Keybindings may have changed, regenerate the UI
init();
} else {
for( size_t ind = 0; ind < options.size(); ++ind ) {
if( res.action == options[ind].action ) {
cur = ind;
if( options[ind].filter( res.evt ) ) {
res.wait_input = false;
break;
}
}
}
}
return res;
}
query_popup::result query_popup::query()
{
std::shared_ptr<ui_adaptor> ui = create_or_get_adaptor();
result res;
do {
res = query_once();
} while( res.wait_input );
return res;
}
catacurses::window query_popup::get_window()
{
if( !win ) {
init();
}
return win;
}
std::string query_popup::wait_text( const std::string &text, const nc_color &bar_color )
{
static const std::array<std::string, 4> phase_icons = {{ "|", "/", "-", "\\" }};
static size_t phase = phase_icons.size() - 1;
phase = ( phase + 1 ) % phase_icons.size();
return string_format( " %s %s", colorize( phase_icons[phase], bar_color ), text );
}
std::string query_popup::wait_text( const std::string &text )
{
return wait_text( text, c_light_green );
}
query_popup::result::result()
: wait_input( false ), action( "ERROR" )
{
}
query_popup::result::result( bool wait_input, const std::string &action, const input_event &evt )
: wait_input( wait_input ), action( action ), evt( evt )
{
}
query_popup::query_option::query_option(
const std::string &action,
const std::function<bool( const input_event & )> &filter )
: action( action ), filter( filter )
{
}
query_popup::button::button( const std::string &text, const point &p )
: text( text ), pos( p )
{
width = utf8_width( text, true );
}
bool query_popup::button::contains( const point &p ) const
{
return p.x >= pos.x + border_width &&
p.x < pos.x + width + border_width &&
p.y == pos.y + border_width;
}
static_popup::static_popup()
{
ui = create_or_get_adaptor();
}