diff --git a/src/action.cpp b/src/action.cpp index 7cba3a9bb29f2..f20c194230679 100644 --- a/src/action.cpp +++ b/src/action.cpp @@ -21,9 +21,11 @@ #include "options.h" #include "output.h" #include "path_info.h" +#include "popup.h" #include "translations.h" #include "trap.h" #include "ui.h" +#include "ui_manager.h" #include "vehicle.h" #include "vpart_position.h" #include "creature.h" @@ -1030,35 +1032,38 @@ cata::optional choose_direction( const std::string &message, const boo ctxt.register_directions(); ctxt.register_action( "pause" ); ctxt.register_action( "QUIT" ); - // why not? ctxt.register_action( "HELP_KEYBINDINGS" ); if( allow_vertical ) { ctxt.register_action( "LEVEL_UP" ); ctxt.register_action( "LEVEL_DOWN" ); } - //~ appended to "Close where?" "Pry where?" etc. - const std::string query_text = message + _( " (Direction button)" ); - popup( query_text, PF_NO_WAIT_ON_TOP ); + static_popup popup; + //~ %s: "Close where?" "Pry where?" etc. + popup.message( _( "%s (Direction button)" ), message ).on_top( true ); - const std::string action = ctxt.handle_input(); - if( const cata::optional vec = ctxt.get_direction( action ) ) { - // Make player's sprite face left/right if interacting with something to the left or right - if( vec->x > 0 ) { - g->u.facing = FD_RIGHT; - } else if( vec->x < 0 ) { - g->u.facing = FD_LEFT; + std::string action; + do { + ui_manager::redraw(); + action = ctxt.handle_input(); + if( const cata::optional vec = ctxt.get_direction( action ) ) { + // Make player's sprite face left/right if interacting with something to the left or right + if( vec->x > 0 ) { + g->u.facing = FD_RIGHT; + } else if( vec->x < 0 ) { + g->u.facing = FD_LEFT; + } + return vec; + } else if( action == "pause" ) { + return tripoint_zero; + } else if( action == "LEVEL_UP" ) { + return tripoint_above; + } else if( action == "LEVEL_DOWN" ) { + return tripoint_below; } - return vec; - } else if( action == "pause" ) { - return tripoint_zero; - } else if( action == "LEVEL_UP" ) { - return tripoint_above; - } else if( action == "LEVEL_DOWN" ) { - return tripoint_below; - } + } while( action != "QUIT" ); - add_msg( _( "Invalid direction." ) ); + add_msg( _( "Never mind." ) ); return cata::nullopt; } diff --git a/src/game.cpp b/src/game.cpp index d0034f92968eb..d0bacd309aee3 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -615,9 +615,16 @@ void game::reenter_fullscreen() */ void game::setup() { - popup_status( _( "Please wait while the world data loads…" ), _( "Loading core data" ) ); loading_ui ui( true ); - load_core_data( ui ); + { + background_pane background; + static_popup popup; + popup.message( "%s", _( "Please wait while the world data loads…\nLoading core data" ) ); + ui_manager::redraw(); + refresh_display(); + + load_core_data( ui ); + } load_world_modfiles( ui ); @@ -696,9 +703,12 @@ bool game::start_game() init_autosave(); - catacurses::clear(); - catacurses::refresh(); - popup_nowait( _( "Please wait as we build your world" ) ); + background_pane background; + static_popup popup; + popup.message( "%s", _( "Please wait as we build your world" ) ); + ui_manager::redraw(); + refresh_display(); + load_master(); u.setID( assign_npc_id() ); // should be as soon as possible, but *after* load_master @@ -2691,7 +2701,11 @@ bool game::load( const std::string &world ) void game::load( const save_t &name ) { - popup_status( _( "Please wait…" ), _( "Loading the save…" ) ); + background_pane background; + static_popup popup; + popup.message( "%s", _( "Please wait…\nLoading the save…" ) ); + ui_manager::redraw(); + refresh_display(); using namespace std::placeholders; @@ -2830,9 +2844,13 @@ bool game::load_packs( const std::string &msg, const std::vector &packs, // if mod specifies legacy migrations load any that are required if( !mod.legacy.empty() ) { + static_popup popup; for( int i = get_option( "CORE_VERSION" ); i < core_version; ++i ) { - popup_status( msg.c_str(), _( "Applying legacy migration (%s %i/%i)" ), - e.c_str(), i, core_version - 1 ); + popup.message( _( "%s Applying legacy migration (%s %i/%i)" ), + msg, e.c_str(), i, core_version - 1 ); + ui_manager::redraw(); + refresh_display(); + load_data_from_dir( string_format( "%s/%i", mod.legacy.c_str(), i ), mod.ident.str(), ui ); } } @@ -11166,7 +11184,11 @@ void game::quicksave() return; } add_msg( m_info, _( "Saving game, this may take a while" ) ); - popup_nowait( _( "Saving game, this may take a while" ) ); + + static_popup popup; + popup.message( "%s", _( "Saving game, this may take a while" ) ); + ui_manager::redraw(); + refresh_display(); time_t now = time( nullptr ); //timestamp for start of saving procedure diff --git a/src/gamemode_defense.cpp b/src/gamemode_defense.cpp index b8d58ffecd2f9..b06f803765b06 100644 --- a/src/gamemode_defense.cpp +++ b/src/gamemode_defense.cpp @@ -21,9 +21,11 @@ #include "overmap.h" #include "overmapbuffer.h" #include "player.h" +#include "popup.h" #include "rng.h" #include "string_formatter.h" #include "translations.h" +#include "ui_manager.h" #include "cursesdef.h" #include "game_constants.h" #include "item.h" @@ -114,7 +116,6 @@ bool defense_game::init() init_to_style( DEFENSE_EASY ); setup(); g->u.cash = initial_cash; - popup_nowait( _( "Please wait as the map generates [ 0%% ]" ) ); // TODO: support multiple defense games? clean up old defense game defloc_pos = tripoint( 50, 50, 0 ); init_map(); @@ -214,6 +215,12 @@ void defense_game::init_constructions() void defense_game::init_map() { + background_pane background; + static_popup popup; + popup.message( _( "Please wait as the map generates [%2d%%]" ), 0 ); + ui_manager::redraw(); + refresh_display(); + auto &starting_om = overmap_buffer.get( point_zero ); for( int x = 0; x < OMAPX; x++ ) { for( int y = 0; y < OMAPY; y++ ) { @@ -263,7 +270,9 @@ void defense_game::init_map() int percent = 100 * ( ( j / 2 + MAPSIZE * ( i / 2 ) ) ) / ( ( MAPSIZE ) * ( MAPSIZE + 1 ) ); if( percent >= old_percent + 1 ) { - popup_nowait( _( "Please wait as the map generates [%2d%%]" ), percent ); + popup.message( _( "Please wait as the map generates [%2d%%]" ), percent ); + ui_manager::redraw(); + refresh_display(); old_percent = percent; } // Round down to the nearest even number diff --git a/src/loading_ui.cpp b/src/loading_ui.cpp index 4f7446b386284..e2f41bd93947b 100644 --- a/src/loading_ui.cpp +++ b/src/loading_ui.cpp @@ -49,15 +49,7 @@ void loading_ui::new_context( const std::string &desc ) void loading_ui::init() { if( menu != nullptr && ui == nullptr ) { - ui_background = std::make_unique(); - ui_background->on_screen_resize( []( ui_adaptor & ui_background ) { - ui_background.position_from_window( catacurses::stdscr ); - } ); - ui_background->position_from_window( catacurses::stdscr ); - ui_background->on_redraw( []( const ui_adaptor & ) { - catacurses::erase(); - catacurses::refresh(); - } ); + ui_background = std::make_unique(); ui = std::make_unique(); ui->on_screen_resize( [this]( ui_adaptor & ui ) { diff --git a/src/loading_ui.h b/src/loading_ui.h index f884b8711de05..b5af91cd8c00f 100644 --- a/src/loading_ui.h +++ b/src/loading_ui.h @@ -6,6 +6,7 @@ #include #include +class background_pane; class ui_adaptor; class uilist; @@ -14,7 +15,7 @@ class loading_ui private: std::unique_ptr menu; std::unique_ptr ui; - std::unique_ptr ui_background; + std::unique_ptr ui_background; void init(); public: diff --git a/src/main_menu.cpp b/src/main_menu.cpp index 647fe26bd39c4..f02d109ac6719 100644 --- a/src/main_menu.cpp +++ b/src/main_menu.cpp @@ -453,15 +453,7 @@ bool main_menu::opening_screen() sel1 = 2; } - ui_adaptor background; - background.on_redraw( []( const ui_adaptor & ) { - catacurses::erase(); - catacurses::refresh(); - } ); - background.on_screen_resize( []( ui_adaptor & background ) { - background.position_from_window( catacurses::stdscr ); - } ); - background.position_from_window( catacurses::stdscr ); + background_pane background; ui_adaptor ui; ui.on_redraw( [&]( const ui_adaptor & ) { @@ -996,6 +988,8 @@ bool main_menu::load_character_tab( bool transfer ) } wrefresh( w_open ); } else if( layer == 3 && sel1 == 2 ) { + savegames = world_generator->get_world( all_worldnames[sel2] )->world_saves; + const std::string &wn = all_worldnames[sel2]; mvwprintz( w_open, menu_offset + point( offset_x + extra_w / 2, -2 - sel2 + offset_y ), h_white, diff --git a/src/mapbuffer.cpp b/src/mapbuffer.cpp index 72d363dbc545c..649777055aa19 100644 --- a/src/mapbuffer.cpp +++ b/src/mapbuffer.cpp @@ -16,8 +16,10 @@ #include "json.h" #include "map.h" #include "output.h" +#include "popup.h" #include "submap.h" #include "translations.h" +#include "ui_manager.h" #include "game_constants.h" #define dbg(x) DebugLog((x),D_MAP) << __FILE__ << ":" << __LINE__ << ": " @@ -124,14 +126,18 @@ void mapbuffer::save( bool delete_after_save ) const tripoint map_origin = sm_to_omt_copy( g->m.get_abs_sub() ); const bool map_has_zlevels = g != nullptr && g->m.has_zlevels(); + static_popup popup; + // A set of already-saved submaps, in global overmap coordinates. std::set saved_submaps; std::list submaps_to_delete; int next_report = 0; for( auto &elem : submaps ) { if( num_total_submaps > 100 && num_saved_submaps >= next_report ) { - popup_nowait( _( "Please wait as the map saves [%d/%d]" ), - num_saved_submaps, num_total_submaps ); + popup.message( _( "Please wait as the map saves [%d/%d]" ), + num_saved_submaps, num_total_submaps ); + ui_manager::redraw(); + refresh_display(); next_report += std::max( 100, num_total_submaps / 20 ); } diff --git a/src/options.cpp b/src/options.cpp index dee4c0053616a..ec2202b9f6f61 100644 --- a/src/options.cpp +++ b/src/options.cpp @@ -17,6 +17,7 @@ #include "mapsharing.h" #include "output.h" #include "path_info.h" +#include "popup.h" #include "sdlsound.h" #include "sdltiles.h" #include "sounds.h" @@ -2848,7 +2849,11 @@ std::string options_manager::show( bool ingame, const bool world_options_only, if( options_changed ) { if( query_yn( _( "Save changes?" ) ) ) { - popup_status( _( "Please wait…" ), _( "Applying option changes…" ) ); + static_popup popup; + popup.message( "%s", _( "Please wait…\nApplying option changes…" ) ); + ui_manager::redraw(); + refresh_display(); + save(); if( ingame && world_options_changed ) { world_generator->active_world->WORLD_OPTIONS = ACTIVE_WORLD_OPTIONS; diff --git a/src/output.cpp b/src/output.cpp index 459daf60e54c3..eec14fd3df8d8 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -663,31 +663,13 @@ int popup( const std::string &text, PopupFlags flags ) pop.on_top( true ); } - if( flags & PF_NO_WAIT ) { - pop.show(); - catacurses::refresh(); - refresh_display(); - return UNKNOWN_UNICODE; + pop.context( "POPUP_WAIT" ); + const auto &res = pop.query(); + if( res.evt.type == CATA_INPUT_KEYBOARD ) { + return res.evt.get_first_input(); } else { - pop.context( "POPUP_WAIT" ); - const auto &res = pop.query(); - if( res.evt.type == CATA_INPUT_KEYBOARD ) { - return res.evt.get_first_input(); - } else { - return UNKNOWN_UNICODE; - } - } -} - -void popup_status( const char *const title, const std::string &mes ) -{ - std::string text; - if( !test_mode && title != nullptr ) { - text += title; - text += "\n"; + return UNKNOWN_UNICODE; } - - popup( text + mes, PF_NO_WAIT ); } //note that passing in iteminfo instances with sType == "DESCRIPTION" does special things diff --git a/src/output.h b/src/output.h index 1b10ee9c63e08..fa2b4ebef55d6 100644 --- a/src/output.h +++ b/src/output.h @@ -404,10 +404,6 @@ std::vector get_hotkeys( const std::string &s ); * - PF_GET_KEY (ignored when combined with PF_NO_WAIT) cancels the popup on *any* user input. * Without the flag the popup is only canceled when the user enters new-line, Space and Escape. * This flag is passed by @ref popup_getkey. - * - PF_NO_WAIT displays the popup, but does not wait for the user input. The popup window is - * immediately destroyed (but will be visible until another window is redrawn over it). - * The function always returns 0 upon this flag, no call to `getch` is done at all. - * This flag is passed by @ref popup_nowait. * - PF_ON_TOP makes the window appear on the top of the screen (at the upper most row). Without * this flag, the popup is centered on the screen. * The flag is passed by @ref popup_top. @@ -420,10 +416,8 @@ std::vector get_hotkeys( const std::string &s ); enum PopupFlags { PF_NONE = 0, PF_GET_KEY = 1 << 0, - PF_NO_WAIT = 1 << 1, PF_ON_TOP = 1 << 2, PF_FULLSCREEN = 1 << 3, - PF_NO_WAIT_ON_TOP = PF_NO_WAIT | PF_ON_TOP, }; template @@ -437,17 +431,6 @@ inline void popup_top( const char *const mes, Args &&... args ) popup( string_format( mes, std::forward( args )... ), PF_ON_TOP ); } template -inline void popup_nowait( const char *mes, Args &&... args ) -{ - popup( string_format( mes, std::forward( args )... ), PF_NO_WAIT ); -} -void popup_status( const char *title, const std::string &mes ); -template -inline void popup_status( const char *const title, const char *const fmt, Args &&... args ) -{ - return popup_status( title, string_format( fmt, std::forward( args )... ) ); -} -template inline void popup( const char *mes, Args &&... args ) { popup( string_format( mes, std::forward( args )... ), PF_NONE ); diff --git a/src/popup.cpp b/src/popup.cpp index b91a7474433e3..b28a94a8b9222 100644 --- a/src/popup.cpp +++ b/src/popup.cpp @@ -134,14 +134,16 @@ void query_popup::invalidate_ui() const folded_msg.clear(); buttons.clear(); } + std::shared_ptr ui = adaptor.lock(); + if( ui ) { + ui->mark_resize(); + } } constexpr int border_width = 1; void query_popup::init() const { - invalidate_ui(); - constexpr int horz_padding = 2; constexpr int vert_padding = 1; const int max_line_width = FULL_SCREEN_WIDTH - border_width * 2; @@ -250,6 +252,8 @@ std::shared_ptr query_popup::create_or_get_adaptor() } ); if( win ) { ui->position_from_window( win ); + } else { + ui->mark_resize(); } } return ui; @@ -392,3 +396,8 @@ query_popup::button::button( const std::string &text, const point &p ) : text( text ), pos( p ) { } + +static_popup::static_popup() +{ + ui = create_or_get_adaptor(); +} diff --git a/src/popup.h b/src/popup.h index 6affb0e7590ba..41a80679e6a32 100644 --- a/src/popup.h +++ b/src/popup.h @@ -189,6 +189,13 @@ class query_popup */ result query(); + protected: + /** + * Create or get a ui_adaptor on the UI stack to handle redrawing and + * resizing of the popup. + */ + std::shared_ptr create_or_get_adaptor(); + private: struct query_option { query_option( const std::string &action, @@ -216,7 +223,6 @@ class query_popup }; std::weak_ptr adaptor; - std::shared_ptr create_or_get_adaptor(); // UI caches mutable catacurses::window win; @@ -242,4 +248,34 @@ class query_popup static std::string wait_text( const std::string &text ); }; +/** + * Create a popup on the UI stack that gets displayed but receives no input itself. + * Call ui_manager::redraw() to redraw the popup along with other UIs on the stack, + * and refresh_display() to force refresh the display if not receiving input after + * redraw. The popup stays on the UI stack until its lifetime ends. + * + * Example: + * + * if( not_loaded ) { + * static_popup popup; + * popup.message( "Please wait…" ); + * while( loading ) { + * ui_manager::redraw(); + * refresh_display(); // force redraw since we're not receiving input here + * load_part(); + * } + * } + * // Popup removed from UI stack when going out of scope. + * // Note that the removal is not visible until the next time `ui_manager::redraw` + * // is called. + */ +class static_popup : public query_popup +{ + public: + static_popup(); + + private: + std::shared_ptr ui; +}; + #endif diff --git a/src/savegame.cpp b/src/savegame.cpp index 11dd355555441..49e3ab916dc6d 100644 --- a/src/savegame.cpp +++ b/src/savegame.cpp @@ -28,6 +28,7 @@ #include "options.h" #include "output.h" #include "overmap.h" +#include "popup.h" #include "scent_map.h" #include "translations.h" #include "hash_utils.h" @@ -38,6 +39,7 @@ #include "regional_settings.h" #include "stats_tracker.h" #include "string_id.h" +#include "ui_manager.h" #if defined(__ANDROID__) #include "input.h" @@ -1609,10 +1611,14 @@ void game::unserialize_master( std::istream &fin ) { savegame_loading_version = 0; chkversion( fin ); + std::unique_ptr popup; if( savegame_loading_version < 11 ) { - popup_nowait( + popup = std::make_unique(); + popup->message( _( "Cannot find loader for save data in old version %d, attempting to load as current version %d." ), savegame_loading_version, savegame_version ); + ui_manager::redraw(); + refresh_display(); } try { // single-pass parsing example diff --git a/src/start_location.cpp b/src/start_location.cpp index b5a156b4d3c6f..dea9b3967082e 100644 --- a/src/start_location.cpp +++ b/src/start_location.cpp @@ -195,7 +195,6 @@ void start_location::prepare_map( tinymap &m ) const tripoint start_location::find_player_initial_location() const { - popup_nowait( _( "Please wait as we build your world" ) ); // Spiral out from the world origin scanning for a compatible starting location, // creating overmaps as necessary. const int radius = 3; diff --git a/src/ui_manager.cpp b/src/ui_manager.cpp index 4bb8c2173dbaf..d10ee765f3b65 100644 --- a/src/ui_manager.cpp +++ b/src/ui_manager.cpp @@ -57,6 +57,11 @@ void ui_adaptor::on_screen_resize( const screen_resize_callback_t &fun ) screen_resized_cb = fun; } +void ui_adaptor::mark_resize() const +{ + deferred_resize = true; +} + static bool contains( const rectangle &lhs, const rectangle &rhs ) { return rhs.p_min.x >= lhs.p_min.x && rhs.p_max.x <= lhs.p_max.x && @@ -75,12 +80,29 @@ void ui_adaptor::invalidate( const rectangle &rect ) return; } // TODO avoid invalidating portions that do not need to be redrawn - for( auto it = ui_stack.crbegin(); it < ui_stack.crend(); ++it ) { - const ui_adaptor &ui = it->get(); - if( overlap( ui.dimensions, rect ) ) { - ui.invalidated = true; - if( contains( ui.dimensions, rect ) ) { - break; + for( auto it_upper = ui_stack.cbegin(); it_upper < ui_stack.cend(); ++it_upper ) { + const ui_adaptor &ui_upper = it_upper->get(); + if( !ui_upper.invalidated && overlap( ui_upper.dimensions, rect ) ) { + // invalidated by `rect` + ui_upper.invalidated = true; + } + for( auto it_lower = ui_stack.cbegin(); it_lower < it_upper; ++it_lower ) { + const ui_adaptor &ui_lower = it_lower->get(); + if( !ui_upper.invalidated && ui_lower.invalidated && + overlap( ui_upper.dimensions, ui_lower.dimensions ) ) { + // invalidated by lower invalidated UIs + ui_upper.invalidated = true; + } + if( ui_upper.invalidated && ui_lower.invalidated && + contains( ui_upper.dimensions, ui_lower.dimensions ) ) { + // fully obscured lower UIs do not need to be redrawn. + ui_lower.invalidated = false; + // Note: we don't need to re-test ui_lower from earlier iterations + // during which ui_upper.invalidated hadn't yet been determined to + // be true, because if the ui_lower would be obscured by ui_upper, + // it implies that ui_lower would overlap with ui_upper, by which + // we would have already determined ui_upper.invalidated to be true + // then. } } } @@ -137,6 +159,18 @@ void ui_adaptor::screen_resized() redraw(); } +background_pane::background_pane() +{ + ui.on_screen_resize( []( ui_adaptor & ui ) { + ui.position_from_window( catacurses::stdscr ); + } ); + ui.position_from_window( catacurses::stdscr ); + ui.on_redraw( []( const ui_adaptor & ) { + catacurses::erase(); + catacurses::refresh(); + } ); +} + namespace ui_manager { diff --git a/src/ui_manager.h b/src/ui_manager.h index e1052b3b45564..1f6f9e2b41345 100644 --- a/src/ui_manager.h +++ b/src/ui_manager.h @@ -35,9 +35,17 @@ class ui_adaptor // Set redraw and resizing callbacks. These callbacks should NOT call // `debugmsg`, construct new `ui_adaptor` instances, deconstruct old // `ui_adaptor` instances, call `redraw`, or call `screen_resized`. + // + // The redraw callback should also not call `position_from_window`, + // otherwise it may cause UI glitch if the window position changes. void on_redraw( const redraw_callback_t &fun ); void on_screen_resize( const screen_resize_callback_t &fun ); + // Mark this ui_adaptor for resizing the next time `redraw()` is called. + // This is useful for deferring initialization of the UI when explicit + // initialization is not possible or wanted. + void mark_resize() const; + static void invalidate( const rectangle &rect ); static void redraw(); static void screen_resized(); @@ -53,6 +61,16 @@ class ui_adaptor mutable bool deferred_resize; }; +// Helper class that fills the background and obscures all UIs below. It stays +// on the UI stack until its lifetime ends. +class background_pane +{ + public: + background_pane(); + private: + ui_adaptor ui; +}; + // export static funcs of ui_adaptor with a more coherent scope name namespace ui_manager {