diff --git a/src/color.cpp b/src/color.cpp index 91ca093576cdd..7dc3dfb18b0a4 100644 --- a/src/color.cpp +++ b/src/color.cpp @@ -596,9 +596,11 @@ std::string hilite_string( const std::string &text ) pos += new_tag.length(); ++color_tag_count; } - if( color_tag_count < 1 ) { + // Assume c_white for incompletely-tagged strings and untagged strings + if( color_tag_count < 1 || highlighted.substr( 0, 1 ) != "<" ) { highlighted = colorize( highlighted, h_white ); } + return highlighted; } diff --git a/src/input_context.cpp b/src/input_context.cpp index 885a901f34a2a..126b050ca3a27 100644 --- a/src/input_context.cpp +++ b/src/input_context.cpp @@ -258,10 +258,17 @@ std::string input_context::get_desc( const std::string &action_descriptor, return pgettext( "keybinding", "None applicable" ); } + const std::string screen_reader_mode = get_option( "SCREEN_READER_MODE" ); const std::string separator = inputs_to_show.size() > 2 ? _( ", or " ) : _( " or " ); std::string rval; for( size_t i = 0; i < inputs_to_show.size(); ++i ) { - rval += inputs_to_show[i].long_description(); + std::string description = inputs_to_show[i].long_description(); + if( screen_reader_mode == "orca" ) { + if( description == "?" ) { + description = _( "Question mark" ); + } + } + rval += description; // We're generating a list separated by "," and "or" if( i + 2 == inputs_to_show.size() ) { @@ -270,6 +277,7 @@ std::string input_context::get_desc( const std::string &action_descriptor, rval += ", "; } } + return rval; } diff --git a/src/medical_ui.cpp b/src/medical_ui.cpp index ada81c0d69bcd..c40181cd82ad4 100644 --- a/src/medical_ui.cpp +++ b/src/medical_ui.cpp @@ -14,6 +14,7 @@ #include "flag.h" #include "game.h" #include "input_context.h" +#include "options.h" #include "output.h" #include "ui_manager.h" #include "weather.h" @@ -35,162 +36,21 @@ enum class medical_tab_mode { TAB_SUMMARY }; -class selection_line -{ - public: - selection_line() = default; - selection_line( const std::string &text, const std::string &desc_str, const int max_width ) - : desc_str( desc_str ) { - std::vector textformatted = foldstring( text, max_width, - ']' ); - row_count = textformatted.size(); - if( row_count > 1 ) { - //If there are too many tags, display them neatly on a new line. - std::string print_line = string_format( "%s\n", textformatted[0] ); - for( int i = 1; i < row_count; i++ ) { - if( i != row_count ) { - print_line += string_format( "->%s\n", textformatted[i] ); - } else { - print_line += string_format( "->%s", textformatted[i] ); - } - } - header_str = print_line; - } else { - header_str = text; - } - } - - std::string print() { - if( highlight_line ) { - header_str = colorize( ">", h_white ) + header_str; - } - return header_str; - } - - std::string description() { - return desc_str; - } - - void set_detail( const std::string &header, const std::string &detail ) { - detail_str = std::pair( header, detail ); - } - - std::pair get_detail() { - return detail_str; - } - - int get_row_count() const { - return row_count; - } - - void set_highlight() { - highlight_line = true; - } +struct medical_entry { + std::string entry_text; + std::string detail_text; - bool highlighted() const { - return highlight_line; - } - - private: - std::string header_str; - std::string desc_str; - std::pair detail_str; - int row_count; - bool highlight_line = false; -}; - -class medical_column -{ - public: - medical_column() = default; - medical_column( const int column_id, const point &COLUMN_START, - const std::pair &COLUMN_BOUNDS ) - : column_id( column_id ), COLUMN_BOUNDS( COLUMN_BOUNDS ), COLUMN_START( COLUMN_START ) {} - - void draw_column( const catacurses::window &window, const int BORDER_START, - const int BORDER_END ) const { - mvwvline( window, point( COLUMN_START.x, BORDER_START ), LINE_XOXO, - BORDER_END - 4 ); // | - mvwputch( window, point( COLUMN_START.x, BORDER_END - 1 ), BORDER_COLOR, - LINE_XXOX ); // _|_ - } - - void print_column( const catacurses::window &window, const int LINE_START, const int MAX_HEIGHT ) { - int linerow = 0; - int selectionrow = 0; - - for( selection_line &line : column_lines ) { - const point row_start( - line.highlighted() ? - COLUMN_START.x + left_padding - 1 : - COLUMN_START.x + left_padding, - COLUMN_START.y + linerow - ); - - if( row_start.y - LINE_START >= MAX_HEIGHT ) { - break; - } - - if( linerow >= LINE_START ) { - fold_and_print( window, row_start - point( 0, LINE_START ), max_width(), - c_light_gray, line.print() ); - linerow += line.get_row_count(); - } else { - linerow++; - } - ++selectionrow; - } - COLUMN_ROWS = { selectionrow, linerow }; - } - - void add_column_line( const selection_line &line ) { - column_lines.emplace_back( line ); - COLUMN_ROWS.second += line.get_row_count(); - } - - selection_line set_highlight( int y ) { - int offset = y % column_lines.size(); - column_lines[offset].set_highlight(); - return column_lines[offset]; - } - - int selection_count() const { - return COLUMN_ROWS.first; - } - - int row_count() const { - return COLUMN_ROWS.second; - } - - int max_width() const { - return COLUMN_BOUNDS.first - COLUMN_START.x - left_padding; - } - - bool empty() { - return column_lines.empty(); - } - - bool current_column( const int selected_id ) const { - return column_id == selected_id; - } - - std::pair detail_str( int y ) { - std::pair ret; - if( y < static_cast( column_lines.size() ) ) { - int offset = y % column_lines.size(); - ret = column_lines[offset].get_detail(); - } - return ret; - } + medical_entry( std::string _entry, std::string _detail ) { + entry_text = _entry; + detail_text = _detail; + } - private: - int column_id; - const int left_padding = 2; - std::pair COLUMN_ROWS = { 0, 0 }; // Selection Lines - Print Lines - std::pair COLUMN_BOUNDS; // Left Bound - Right Bound - point COLUMN_START; - std::vectorcolumn_lines; - std::string column_title; + multiline_list_entry get_entry() const { + multiline_list_entry entry; + entry.entry_text = entry_text; + entry.prefix = ""; + return entry; + } }; static std::string coloured_stat_display( int statCur, int statMax ) @@ -220,7 +80,9 @@ static void draw_medical_titlebar( const catacurses::window &window, avatar *pla const Character &you = *player->as_character(); werase( window ); - draw_border( window, BORDER_COLOR, _( " MEDICAL " ) ); + mvwhline( window, point( 0, 0 ), LINE_OXOX, getmaxx( window ) ); // - + mvwputch( window, point( 0, 0 ), BORDER_COLOR, LINE_OXXO ); // |^ + mvwputch( window, point( getmaxx( window ) - 1, 0 ), BORDER_COLOR, LINE_OOXX ); // ^| // Tabs const std::vector> tabs = { @@ -228,7 +90,7 @@ static void draw_medical_titlebar( const catacurses::window &window, avatar *pla }; draw_tabs( window, tabs, medical_tab_mode::TAB_SUMMARY ); - const int TAB_WIDTH = 12; + const int TAB_WIDTH = utf8_width( _( "SUMMARY" ) ) + 4; // Draw symbols to connect additional lines to border @@ -324,22 +186,12 @@ static void draw_medical_titlebar( const catacurses::window &window, avatar *pla right_print( window, 1, right_indent, c_white, desc ); } - const std::string TITLE_STR = "Medical"; - - // Window Title - if( WIDTH - details_width - utf8_width( TITLE_STR ) > WIDTH / 2 ) { - center_print( window, 0, c_blue, _( TITLE_STR ) ); - } + center_print( window, 0, c_red, _( " MEDICAL " ) ); } // Displays a summary of each bodypart's health, including a display for a few 'statuses' -static medical_column draw_health_summary( const int column_count, avatar *player, - const point &COLUMN_START, - const std::pair &COLUMN_BOUNDS ) +static void generate_health_summary( avatar *player, std::vector &bp_entries ) { - medical_column health_column = medical_column( column_count, COLUMN_START, COLUMN_BOUNDS ); - const int max_width = health_column.max_width(); - for( const bodypart_id &part : player->get_all_body_parts( get_body_part_flags::sorted ) ) { std::string header; // Bodypart Title std::string hp_str; // Bodypart HP @@ -421,11 +273,11 @@ static medical_column draw_health_summary( const int column_count, avatar *playe infected_effect.disp_short_desc() ); } - selection_line line; + std::string entry_text; if( !detail.empty() ) { - line = selection_line( string_format( "[%s] - %s", header, detail ), description, max_width ); + entry_text = string_format( "[%s] - %s", header, detail ); } else { - line = selection_line( string_format( "[%s]", header ), description, max_width ); + entry_text = string_format( "[%s]", header ); } const bodypart *bp = player->get_part( part ); @@ -490,26 +342,26 @@ static medical_column draw_health_summary( const int column_count, avatar *playe } } - line.set_detail( string_format( _( "%s STATS" ), to_upper_case( bp_name ) ), detail_str ); - health_column.add_column_line( line ); + std::string entry_details = colorize( string_format( _( "%s STATS" ), to_upper_case( bp_name ) ), + c_light_blue ); + entry_details.append( "\n" ); + entry_details.append( description ); + entry_details.append( "\n" ); + entry_details.append( detail_str ); + bp_entries.emplace_back( entry_text, entry_details ); } - return health_column; } // Displays a summary list of all visible effects. -static medical_column draw_effects_summary( const int column_count, avatar *player, - const point &COLUMN_START, - const std::pair &COLUMN_BOUNDS ) +static void generate_effects_summary( avatar *player, std::vector &effect_entries ) { - medical_column effects_column = medical_column( column_count, COLUMN_START, COLUMN_BOUNDS ); - const int max_width = effects_column.max_width(); - + effect_entries.clear(); for( const effect &eff : player->get_effects() ) { const std::string name = eff.disp_name(); if( name.empty() ) { continue; } - effects_column.add_column_line( selection_line( name, eff.disp_desc(), max_width ) ); + effect_entries.emplace_back( name, eff.disp_desc() ); } const float bmi = player->get_bmi_fat(); @@ -538,65 +390,41 @@ static medical_column draw_effects_summary( const int column_count, avatar *play str_penalty * 50.0f ); } - effects_column.add_column_line( selection_line( starvation_name, starvation_text, max_width ) ); + effect_entries.emplace_back( starvation_name, starvation_text ); } if( player->has_trait( trait_TROGLO3 ) && g->is_in_sunlight( player->pos() ) ) { - effects_column.add_column_line( selection_line( "In Sunlight", - "The sunlight irritates you terribly.\n", max_width ) ); + effect_entries.emplace_back( _( "In Sunlight" ), _( "The sunlight irritates you terribly." ) ); } else if( player->has_trait( trait_TROGLO2 ) && g->is_in_sunlight( player->pos() ) && incident_sun_irradiance( get_weather().weather_id, calendar::turn ) > irradiance::low ) { - effects_column.add_column_line( selection_line( "In Sunlight", - "The sunlight irritates you badly.\n", max_width ) ); + effect_entries.emplace_back( _( "In Sunlight" ), _( "The sunlight irritates you badly." ) ); } else if( ( player->has_trait( trait_TROGLO ) || player->has_trait( trait_TROGLO2 ) ) && g->is_in_sunlight( player->pos() ) && incident_sun_irradiance( get_weather().weather_id, calendar::turn ) > irradiance::moderate ) { - effects_column.add_column_line( selection_line( "In Sunlight", "The sunlight irritates you.\n", - max_width ) ); + effect_entries.emplace_back( _( "In Sunlight" ), _( "The sunlight irritates you." ) ); } for( addiction &elem : player->addictions ) { if( elem.sated < 0_turns && elem.intensity >= MIN_ADDICTION_LEVEL ) { - effects_column.add_column_line( selection_line( elem.type->get_name().translated(), - elem.type->get_description().translated(), max_width ) ); + effect_entries.emplace_back( elem.type->get_name().translated(), + elem.type->get_description().translated() ); } } - if( effects_column.empty() ) { - effects_column.add_column_line( selection_line( colorize( "None", c_dark_gray ), "", max_width ) ); + if( effect_entries.empty() ) { + effect_entries.emplace_back( colorize( _( "None" ), c_dark_gray ), _( "No effects active" ) ); } - - return effects_column; } -// Displays a summary list of the player's statistics. -static medical_column draw_stats_summary( const int column_count, avatar *player, - const point &COLUMN_START, - const std::pair &COLUMN_BOUNDS ) +// Generates a summary list of the player's statistics. +static void generate_stats_summary( avatar *player, std::vector &stat_entries ) { - medical_column stats_column = medical_column( column_count, COLUMN_START, COLUMN_BOUNDS ); - const int max_width = stats_column.max_width(); + stat_entries.clear(); std::string speed_detail_str; int runcost = player->run_cost( 100 ); int newmoves = player->get_speed(); - std::string coloured_str = colorize( string_format( _( "%d" ), runcost ), - ( runcost <= 100 ? c_green : c_red ) ); - selection_line runcost_line = selection_line( string_format( _( "Base Move Cost: %s" ), - coloured_str ), - colorize( _( "Base move cost is the final modified movement cost taken to traverse flat ground." ), - c_light_blue ), - max_width ); - - coloured_str = colorize( string_format( _( "%d" ), newmoves ), - ( newmoves >= 100 ? c_green : c_red ) ); - selection_line movecost_line = selection_line( string_format( _( "Current Speed: %s" ), - coloured_str ), - colorize( _( "Speed determines the amount of actions or movement points you can perform in a turn." ), - c_light_blue ), - max_width ); - const int speed_modifier = player->get_enchantment_speed_bonus(); std::string pge_str; @@ -673,41 +501,45 @@ static medical_column draw_stats_summary( const int column_count, avatar *player std::abs( speed_effect.second ) ), col ); } - runcost_line.set_detail( _( "SPEED" ), speed_detail_str ); - movecost_line.set_detail( _( "SPEED" ), speed_detail_str ); + std::string coloured_str = colorize( string_format( _( "%d" ), runcost ), + ( runcost <= 100 ? c_green : c_red ) ); + std::string assembled_details = colorize( + _( "Base move cost is the final modified movement cost taken to traverse flat ground." ), + c_light_blue ); + assembled_details.append( "\n" ); + assembled_details.append( speed_detail_str ); + stat_entries.emplace_back( string_format( _( "Base Move Cost: %s" ), coloured_str ), + assembled_details ); - stats_column.add_column_line( runcost_line ); - stats_column.add_column_line( movecost_line ); + coloured_str = colorize( string_format( _( "%d" ), newmoves ), + ( newmoves >= 100 ? c_green : c_red ) ); + assembled_details = colorize( + _( "Speed determines the amount of actions or movement points you can perform in a turn." ), + c_light_blue ); + assembled_details.append( "\n" ); + assembled_details.append( speed_detail_str ); + stat_entries.emplace_back( string_format( _( "Current Speed: %s" ), coloured_str ), + assembled_details ); std::string strength_str = coloured_stat_display( player->get_str(), player->get_str_base() ); - stats_column.add_column_line( - selection_line( string_format( _( "Strength: %s" ), strength_str ), - _( "Strength affects your melee damage, the amount of weight you can carry, your total HP, " - "your resistance to many diseases, and the effectiveness of actions which require brute force." ), - max_width ) ); + stat_entries.emplace_back( string_format( _( "Strength: %s" ), strength_str ), + _( "Strength affects your melee damage, the amount of weight you can carry, your total HP, " + "your resistance to many diseases, and the effectiveness of actions which require brute force." ) ); std::string dexterity_str = coloured_stat_display( player->get_dex(), player->get_dex_base() ); - stats_column.add_column_line( - selection_line( string_format( _( "Dexterity: %s" ), dexterity_str ), - _( "Dexterity affects your chance to hit in melee combat, helps you steady your " - "gun for ranged combat, and enhances many actions that require finesse." ), - max_width ) ); + stat_entries.emplace_back( string_format( _( "Dexterity: %s" ), dexterity_str ), + _( "Dexterity affects your chance to hit in melee combat, helps you steady your " + "gun for ranged combat, and enhances many actions that require finesse." ) ); std::string intelligence_str = coloured_stat_display( player->get_int(), player->get_int_base() ); - stats_column.add_column_line( - selection_line( string_format( _( "Intelligence: %s" ), intelligence_str ), - _( "Intelligence is less important in most situations, but it is vital for more complex tasks like " - "electronics crafting. It also affects how much skill you can pick up from reading a book." ), - max_width ) ); + stat_entries.emplace_back( string_format( _( "Intelligence: %s" ), intelligence_str ), + _( "Intelligence is less important in most situations, but it is vital for more complex tasks like " + "electronics crafting. It also affects how much skill you can pick up from reading a book." ) ); std::string perception_str = coloured_stat_display( player->get_per(), player->get_per_base() ); - stats_column.add_column_line( - selection_line( string_format( _( "Perception: %s" ), perception_str ), - _( "Perception is the most important stat for ranged combat. It's also used for " - "detecting traps and other things of interest." ), - max_width ) ); - - return stats_column; + stat_entries.emplace_back( string_format( _( "Perception: %s" ), perception_str ), + _( "Perception is the most important stat for ranged combat. It's also used for " + "detecting traps and other things of interest." ) ); } void avatar::disp_medical() @@ -716,13 +548,31 @@ void avatar::disp_medical() catacurses::window w_title; // Title Bar - Tabs, Pain Indicator & Blood Indicator catacurses::window wMedical; // Primary Window catacurses::window w_description; // Bottom Detail Bar + catacurses::window w_left; + catacurses::window w_mid; + catacurses::window w_right; + + scrolling_text_view details( w_description ); + bool details_recalc = true; + + multiline_list bp_list( w_left ); + bp_list.list_id = 0; + multiline_list effect_list( w_mid ); + effect_list.list_id = 1; + multiline_list stat_list( w_right ); + stat_list.list_id = 2; + std::vector bp_entries; + std::vector effect_entries; + std::vector stat_entries; + generate_health_summary( this, bp_entries ); + generate_stats_summary( this, stat_entries ); + generate_effects_summary( this, effect_entries ); // Window Definitions const int TITLE_W_HEIGHT = 3; const int DESC_W_HEIGHT = 6; // Consistent with Player Info (@) Menu const int HEADER_Y = TITLE_W_HEIGHT; - const int TEXT_START_Y = HEADER_Y + 2; - const int INFO_START_Y = HEADER_Y + 8; + const int TEXT_START_Y = HEADER_Y + 1; int DESC_W_BEGIN; int HEIGHT; int WIDTH; @@ -731,18 +581,11 @@ void avatar::disp_medical() int second_column_x = 0; int third_column_x = 0; - // Scrolling - int SCROLL_POINT; // The number of printed rows at which to enable scrolling - int scroll_position = 0; - int INFO_SCROLL_POINT; - int info_scroll_position = 0; - - int info_lines = 0; - // Cursor - std::array cursor_bounds; // Number of selectable rows in each column point cursor; + const std::string screen_reader_mode = get_option( "SCREEN_READER_MODE" ); + ui_adaptor ui; ui.on_screen_resize( [&]( ui_adaptor & ui ) { const int WIDTH_OFFSET = ( TERMX - FULL_SCREEN_WIDTH ) / 4; @@ -756,12 +599,23 @@ void avatar::disp_medical() w_title = catacurses::newwin( TITLE_W_HEIGHT, WIDTH, win ); DESC_W_BEGIN = HEIGHT - DESC_W_HEIGHT - 1; - w_description = catacurses::newwin( DESC_W_HEIGHT, WIDTH - 2, - win + point( 1, DESC_W_BEGIN ) ); + w_description = catacurses::newwin( DESC_W_HEIGHT, WIDTH - 1, + win + point( 0, DESC_W_BEGIN ) ); + details_recalc = true; //40% - 30% - 30% second_column_x = WIDTH / 2.5f; third_column_x = second_column_x + WIDTH / 3.3f; + w_left = catacurses::newwin( DESC_W_BEGIN - TEXT_START_Y - 1, WIDTH / 2.5f - 1, + win + point( 0, TEXT_START_Y ) ); + w_mid = catacurses::newwin( DESC_W_BEGIN - TEXT_START_Y - 1, WIDTH / 3.3f - 1, + win + point( second_column_x, TEXT_START_Y ) ); + w_right = catacurses::newwin( DESC_W_BEGIN - TEXT_START_Y - 1, WIDTH / 3.3f - 1, + win + point( third_column_x, TEXT_START_Y ) ); + + bp_list.fold_entries(); + effect_list.fold_entries(); + stat_list.fold_entries(); ui.position_from_window( wMedical ); } ); @@ -771,196 +625,113 @@ void avatar::disp_medical() ui.on_redraw( [&]( const ui_adaptor & ) { werase( wMedical ); - draw_border( wMedical, BORDER_COLOR, _( " MEDICAL " ) ); - mvwputch( wMedical, point( getmaxx( wMedical ) - 1, 2 ), BORDER_COLOR, LINE_XOXX ); // -| - - wnoutrefresh( wMedical ); - - draw_medical_titlebar( w_title, this, WIDTH ); - mvwputch( w_title, point( second_column_x, HEADER_Y - 1 ), BORDER_COLOR, LINE_OXXX ); // ^|^ - mvwputch( w_title, point( third_column_x, HEADER_Y - 1 ), BORDER_COLOR, LINE_OXXX ); // ^|^ - wnoutrefresh( w_title ); - - SCROLL_POINT = HEIGHT - TEXT_START_Y - DESC_W_HEIGHT - 3; - - // Columns - - int column_id = 0; - - // Health Summary - fold_and_print( wMedical, point( 2, HEADER_Y ), WIDTH - 2, c_light_blue, _( "HEALTH" ) ); - medical_column health_column = draw_health_summary( column_id++, this, point( 0, TEXT_START_Y ), - std::pair( second_column_x, HEIGHT ) ); - - // Effects Summary - mvwprintz( wMedical, point( second_column_x + 2, HEADER_Y ), c_light_blue, _( "EFFECTS" ) ); - medical_column effects_column = draw_effects_summary( column_id++, this, point( second_column_x, - TEXT_START_Y ), std::pair( third_column_x, HEIGHT ) ); - - // Stats Summary - mvwprintz( wMedical, point( third_column_x + 2, HEADER_Y ), c_light_blue, _( "STATS" ) ); - medical_column stats_column = draw_stats_summary( column_id++, this, point( third_column_x, - TEXT_START_Y ), std::pair( WIDTH - 2, 5 ) ); - // Description Text - std::string desc_str; - std::pair detail_str; + std::string detail_str = ""; switch( cursor.x ) { case 0: - desc_str = health_column.set_highlight( cursor.y ).description(); - detail_str = health_column.detail_str( cursor.y ); + if( screen_reader_mode == "orca" ) { + detail_str = bp_entries[cursor.y].entry_text + "\n"; + } + detail_str = detail_str.append( bp_entries[cursor.y].detail_text ); break; case 1: - desc_str = effects_column.set_highlight( cursor.y ).description(); - detail_str = effects_column.detail_str( cursor.y ); + if( screen_reader_mode == "orca" ) { + detail_str = effect_entries[cursor.y].entry_text + "\n"; + } + detail_str = detail_str.append( effect_entries[cursor.y].detail_text ); break; case 2: - desc_str = stats_column.set_highlight( cursor.y ).description(); - detail_str = stats_column.detail_str( cursor.y ); + if( screen_reader_mode == "orca" ) { + detail_str = stat_entries[cursor.y].entry_text + "\n"; + } + detail_str = detail_str.append( stat_entries[cursor.y].detail_text ); break; default: break; } - // Description Bar - werase( w_description ); - - int DESCRIPTION_WIN_OFFSET; // Y Position of the start of the description bar (wMedical) - - if( !desc_str.empty() ) { - // Number of display rows required by the highlighted line. - std::vector textformatted = foldstring( desc_str, WIDTH - 2, ' ' ); - - // Beginning row of description text [0-6] (w_description) - const int DESCRIPTION_TEXT_Y = DESC_W_HEIGHT - std::min( DESC_W_HEIGHT, - static_cast( textformatted.size() ) ); - - DESCRIPTION_WIN_OFFSET = DESC_W_BEGIN + DESCRIPTION_TEXT_Y; - mvwputch( wMedical, point( 0, DESCRIPTION_WIN_OFFSET - 1 ), BORDER_COLOR, LINE_XXXO ); - mvwhline( wMedical, point( 1, DESCRIPTION_WIN_OFFSET - 1 ), LINE_OXOX, getmaxx( wMedical ) - 2 ); - mvwputch( wMedical, point( getmaxx( wMedical ) - 1, DESCRIPTION_WIN_OFFSET - 1 ), BORDER_COLOR, - LINE_XOXX ); - fold_and_print( w_description, point( 1, DESCRIPTION_TEXT_Y ), WIDTH - 2, c_light_gray, - desc_str ); - } else { - DESCRIPTION_WIN_OFFSET = getmaxy( wMedical ); + if( details_recalc ) { + details.set_text( detail_str ); + details_recalc = false; } - wnoutrefresh( w_description ); - - // Info Menu - - INFO_SCROLL_POINT = HEIGHT - INFO_START_Y - DESC_W_HEIGHT - 3; - const int INFO_CUTOFF = DESCRIPTION_WIN_OFFSET - 1 - ( INFO_START_Y + 3 ); - - if( !detail_str.first.empty() ) { - mvwprintz( wMedical, point( third_column_x + 2, INFO_START_Y + 1 ), c_light_blue, - detail_str.first ); - - mvwputch( wMedical, point( third_column_x, INFO_START_Y ), BORDER_COLOR, LINE_XXXO ); // |- - mvwhline( wMedical, point( third_column_x + 1, INFO_START_Y ), LINE_OXOX, - getmaxx( wMedical ) - 2 ); // - - mvwputch( wMedical, point( getmaxx( wMedical ) - 1, INFO_START_Y ), BORDER_COLOR, // -| - LINE_XOXX ); + // Print column headers + fold_and_print( wMedical, point( 2, HEADER_Y ), WIDTH - 2, c_light_blue, _( "HEALTH" ) ); + mvwprintz( wMedical, point( second_column_x + 2, HEADER_Y ), c_light_blue, _( "EFFECTS" ) ); + mvwprintz( wMedical, point( third_column_x + 2, HEADER_Y ), c_light_blue, _( "STATS" ) ); - const int info_width = WIDTH - third_column_x - 3; - std::vector textformatted = foldstring( detail_str.second, info_width, - ' ' ); - info_lines = textformatted.size(); - int i = 0; - for( std::string &line : textformatted ) { - if( i - info_scroll_position >= INFO_CUTOFF ) { - break; - } - if( i++ >= info_scroll_position ) { - trim_and_print( wMedical, point( third_column_x + 2, INFO_START_Y + 2 + i - info_scroll_position ), - info_width, c_light_gray, line ); - } - } - } else { - info_lines = 0; + // Overall borders and header + draw_border( wMedical ); + if( !detail_str.empty() ) { + mvwputch( wMedical, point( 0, DESC_W_BEGIN - 1 ), BORDER_COLOR, LINE_XXXO ); // |- + mvwhline( wMedical, point( 1, DESC_W_BEGIN - 1 ), LINE_OXOX, getmaxx( wMedical ) - 2 ); // - + mvwputch( wMedical, point( getmaxx( wMedical ) - 1, DESC_W_BEGIN - 1 ), BORDER_COLOR, + LINE_XOXX ); // -| + mvwputch( wMedical, point( second_column_x, DESC_W_BEGIN - 1 ), BORDER_COLOR, LINE_XXOX ); // _|_ + mvwputch( wMedical, point( third_column_x, DESC_W_BEGIN - 1 ), BORDER_COLOR, LINE_XXOX ); // _|_ } - // Draw Selection Lines For Each Column - - const int MAX_COLUMN_HEIGHT = DESCRIPTION_WIN_OFFSET - 1; - - health_column.print_column( wMedical, - health_column.current_column( cursor.x ) ? scroll_position : 0, MAX_COLUMN_HEIGHT ); - effects_column.print_column( wMedical, - effects_column.current_column( cursor.x ) ? scroll_position : 0, MAX_COLUMN_HEIGHT ); - stats_column.print_column( wMedical, stats_column.current_column( cursor.x ) ? scroll_position : 0, - MAX_COLUMN_HEIGHT ); - - // Update Cursor Boundaries to number of selectable rows - - cursor_bounds[0] = health_column.selection_count(); - cursor_bounds[1] = effects_column.selection_count(); - cursor_bounds[2] = stats_column.selection_count(); - - // Draw Column Borders - - effects_column.draw_column( wMedical, HEADER_Y, DESCRIPTION_WIN_OFFSET ); - stats_column.draw_column( wMedical, HEADER_Y, DESCRIPTION_WIN_OFFSET ); - - // Draw Scrollbars - - const int content_size = ( cursor_bounds[cursor.x] - 1 ) * 2; - - scrollbar() - .offset_x( 0 ) - .offset_y( HEADER_Y ) - .content_size( content_size ) - .viewport_pos( cursor.y * 2 ) - .viewport_size( DESC_W_BEGIN - 3 ) - .scroll_to_last( true ) - .apply( wMedical ); + // UI Header + draw_medical_titlebar( w_title, this, WIDTH ); + mvwputch( w_title, point( second_column_x, HEADER_Y - 1 ), BORDER_COLOR, LINE_OXXX ); // ^|^ + mvwputch( w_title, point( third_column_x, HEADER_Y - 1 ), BORDER_COLOR, LINE_OXXX ); // ^|^ - scrollbar() - .offset_x( third_column_x ) - .offset_y( INFO_START_Y + 1 ) - .content_size( info_lines + INFO_SCROLL_POINT ) - .viewport_pos( info_scroll_position ) - .viewport_size( INFO_SCROLL_POINT + 1 ) - .scroll_to_last( true ) - .apply( wMedical ); + mvwputch( wMedical, point( 0, 2 ), BORDER_COLOR, LINE_XXXO ); // |- + mvwputch( wMedical, point( getmaxx( wMedical ) - 1, 2 ), BORDER_COLOR, LINE_XOXX ); // -| + mvwputch( wMedical, point( second_column_x, HEADER_Y ), BORDER_COLOR, LINE_XOXO ); // | + mvwputch( wMedical, point( third_column_x, HEADER_Y ), BORDER_COLOR, LINE_XOXO ); // |; wnoutrefresh( wMedical ); + wnoutrefresh( w_title ); + + // Print columns, then description. Screen reader cursor position is set when drawing description + bp_list.print_entries( cursor.x ); + effect_list.print_entries( cursor.x ); + stat_list.print_entries( cursor.x ); + details.draw( c_light_gray ); } ); input_context ctxt( "MEDICAL" ); - ctxt.register_action( "UP" ); - ctxt.register_action( "DOWN" ); ctxt.register_action( "LEFT" ); ctxt.register_action( "RIGHT" ); ctxt.register_action( "APPLY" ); ctxt.register_action( "HELP_KEYBINDINGS" ); - ctxt.register_action( "SCROLL_INFOBOX_UP", to_translation( "Scroll up" ) ); - ctxt.register_action( "SCROLL_INFOBOX_DOWN", to_translation( "Scroll down" ) ); ctxt.register_action( "QUIT" ); + details.set_up_navigation( ctxt, scrolling_key_scheme::angle_bracket_scroll ); + + /* Given the cramped layout, it can be difficult to move the mouse down to the description window + * without accidentally moving the mouse over another entry. So, require a click to select an entry + * rather than just hovering with the mouse. + */ + bp_list.set_up_navigation( ctxt, false ); + effect_list.set_up_navigation( ctxt, false ); + stat_list.set_up_navigation( ctxt, false ); + + bp_list.create_entries( bp_entries ); + bp_list.set_entry_pos( 0, false ); + effect_list.create_entries( effect_entries ); + effect_list.set_entry_pos( 0, false ); + stat_list.create_entries( stat_entries ); + stat_list.set_entry_pos( 0, false ); for( ;; ) { ui_manager::redraw(); - const std::string action = ctxt.handle_input(); - - if( action == "DOWN" || action == "UP" ) { - const int step = cursor.y + ( action == "DOWN" ? 1 : -1 ); - const int limit = cursor_bounds[cursor.x] - 1; - - if( step == -1 ) { - cursor.y = limit; - } else if( step > limit ) { - cursor.y = 0; - } else { - cursor.y = step; - } - - const int scroll_overflow = limit - SCROLL_POINT; - if( scroll_overflow > 0 ) { - const int half_list = SCROLL_POINT / 2; - scroll_position = std::max( 0, std::min( scroll_overflow, cursor.y - half_list ) ); - } - info_scroll_position = 0; + std::string action = ctxt.handle_input(); + + if( details.handle_navigation( action, ctxt ) ) { + // No further action required + } else if( bp_list.handle_navigation( action, ctxt, cursor.x ) ) { + cursor.x = bp_list.list_id;; + cursor.y = bp_list.get_entry_pos(); + details_recalc = true; + } else if( effect_list.handle_navigation( action, ctxt, cursor.x ) ) { + cursor.x = effect_list.list_id;; + cursor.y = effect_list.get_entry_pos(); + details_recalc = true; + } else if( stat_list.handle_navigation( action, ctxt, cursor.x ) ) { + cursor.x = stat_list.list_id;; + cursor.y = stat_list.get_entry_pos(); + details_recalc = true; } else if( action == "RIGHT" || action == "LEFT" ) { const int step = cursor.x + ( action == "RIGHT" ? 1 : -1 ); const int limit = 2; @@ -971,36 +742,20 @@ void avatar::disp_medical() } else { cursor.x = step; } - - // Match the cursor to the nearest row on the next column. - const int y_limit = cursor_bounds[cursor.x] - 1; - cursor.y = std::min( cursor.y - scroll_position, y_limit ); - - const int scroll_overflow = y_limit - SCROLL_POINT; - if( scroll_overflow > 0 ) { - const int half_list = SCROLL_POINT / 2; - scroll_position = std::max( 0, std::min( scroll_overflow, cursor.y - half_list ) ); + if( cursor.x == 0 ) { + cursor.y = bp_list.get_entry_pos(); + } else if( cursor.x == 1 ) { + cursor.y = effect_list.get_entry_pos(); } else { - scroll_position = 0; + cursor.y = stat_list.get_entry_pos(); } - info_scroll_position = 0; + details_recalc = true; } else if( action == "APPLY" ) { avatar_action::use_item( *this ); - } else if( action == "SCROLL_INFOBOX_UP" || action == "SCROLL_INFOBOX_DOWN" ) { - const int scroll_overflow = info_lines - INFO_SCROLL_POINT - 1; - if( scroll_overflow > 0 ) { - const int step = info_scroll_position + ( action == "SCROLL_INFOBOX_DOWN" ? 1 : -1 ); - - if( step == -1 ) { - info_scroll_position = scroll_overflow; - } else if( step > scroll_overflow ) { - info_scroll_position = 0; - } else { - info_scroll_position = step; - } - } else { - info_scroll_position = 0; - } + details_recalc = true; + generate_health_summary( this, bp_entries ); + generate_stats_summary( this, stat_entries ); + generate_effects_summary( this, effect_entries ); } else if( action == "QUIT" ) { break; } diff --git a/src/newcharacter.cpp b/src/newcharacter.cpp index 789910e2fd4ab..cf8fa470f3a7b 100644 --- a/src/newcharacter.cpp +++ b/src/newcharacter.cpp @@ -1688,37 +1688,22 @@ void set_traits( tab_manager &tabs, avatar &u, pool_type pool ) for( int i = start; i < end; i++ ) { const trait_and_var &cursor = *sorted_traits[iCurrentPage][i]; const trait_id &cur_trait = cursor.trait; - if( current == i && iCurrentPage == iCurWorkingPage ) { - int points = cur_trait->points; - bool negativeTrait = points < 0; - if( negativeTrait ) { - points *= -1; - } - if( pool != pool_type::FREEFORM ) { - mvwprintz( w, point( full_string_length + 3, 3 ), col_tr, - n_gettext( "%s %s %d point", "%s %s %d points", points ), - cursor.name(), - negativeTrait ? _( "earns" ) : _( "costs" ), - points ); - } - if( details_recalc ) { - details.set_text( colorize( cursor.desc(), col_tr ) ); - details_recalc = false; - } - } nc_color cLine = col_off_pas; + std::string color_descriptor = ""; if( iCurWorkingPage == iCurrentPage ) { cLine = col_off_act; if( current == i ) { cLine = hi_off; if( u.has_conflicting_trait( cur_trait ) ) { cLine = hilite( c_dark_gray ); + color_descriptor = _( " - unavailable" ); } else if( u.has_trait( cur_trait ) ) { if( !cur_trait->variants.empty() && !u.has_trait_variant( cursor ) ) { cLine = hilite( c_dark_gray ); } else { cLine = hi_on; + color_descriptor = _( " - active" ); } } } else { @@ -1735,7 +1720,6 @@ void set_traits( tab_manager &tabs, avatar &u, pool_type pool ) } } else if( u.has_trait( cur_trait ) ) { cLine = col_on_pas; - } else if( u.has_conflicting_trait( cur_trait ) || get_scenario()->is_forbidden_trait( cur_trait ) ) { cLine = c_light_gray; @@ -1744,11 +1728,39 @@ void set_traits( tab_manager &tabs, avatar &u, pool_type pool ) const int cur_line_y = iHeaderHeight + i - start; const int cur_line_x = 2 + iCurrentPage * page_width; const point opt_pos( cur_line_x, cur_line_y ); - if( iCurWorkingPage == iCurrentPage && current == i ) { + static_cast( ui ); + /*if( iCurWorkingPage == iCurrentPage && current == i ) { ui.set_cursor( w, opt_pos ); + }*/ + if( get_option( "SCREEN_READER_MODE" ) != "orca" ) { + mvwprintz( w, opt_pos, cLine, + utf8_truncate( cursor.name(), page_width - 2 ) ); + } + + if( current == i && iCurrentPage == iCurWorkingPage ) { + int points = cur_trait->points; + bool negativeTrait = points < 0; + if( negativeTrait ) { + points *= -1; + } + if( pool != pool_type::FREEFORM ) { + mvwprintz( w, point( full_string_length + 3, 3 ), col_tr, + n_gettext( "%s %s %d point", "%s %s %d points", points ), + cursor.name(), + negativeTrait ? _( "earns" ) : _( "costs" ), + points ); + } + if( details_recalc ) { + if( get_option( "SCREEN_READER_MODE" ) == "orca" ) { + std::string assembled = string_format( "%1s%2s:\n", cursor.name(), color_descriptor ); + assembled.append( cursor.desc() ); + details.set_text( colorize( assembled, col_tr ) ); + } else { + details.set_text( colorize( cursor.desc(), col_tr ) ); + } + details_recalc = false; + } } - mvwprintz( w, opt_pos, cLine, - utf8_truncate( cursor.name(), page_width - 2 ) ); } trait_sbs[iCurrentPage].offset_x( page_width * iCurrentPage ) @@ -1846,6 +1858,7 @@ void set_traits( tab_manager &tabs, avatar &u, pool_type pool ) traits_size[iCurWorkingPage], true ) ) { // No additional action required } else if( action == "CONFIRM" ) { + details_recalc = true; int inc_type = 0; const trait_id cur_trait = sorted_traits[iCurWorkingPage][iCurrentLine[iCurWorkingPage]]->trait; const std::string variant = sorted_traits[iCurWorkingPage][iCurrentLine[iCurWorkingPage]]->variant; @@ -2337,9 +2350,10 @@ void set_profession( tab_manager &tabs, avatar &u, pool_type pool ) sorted_profs[i] == sorted_profs[cur_id] ? hilite( c_light_green ) : COL_SKILL_USED ); } const point opt_pos( 2, iHeaderHeight + i - iStartPos ); - if( i == cur_id ) { + static_cast( ui ); + /*if( i == cur_id ) { ui.set_cursor( w, opt_pos ); - } + }*/ mvwprintz( w, opt_pos, col, sorted_profs[i]->gender_appropriate_name( u.male ) ); } @@ -3335,11 +3349,14 @@ void set_scenario( tab_manager &tabs, avatar &u, pool_type pool ) sorted_scens[i] == sorted_scens[cur_id] ? hilite( c_light_green ) : COL_SKILL_USED ); } const point opt_pos( 2, iHeaderHeight + i - iStartPos ); - if( i == cur_id ) { + static_cast( ui ); + /*if( i == cur_id ) { ui.set_cursor( w, opt_pos ); + }*/ + if( get_option( "SCREEN_READER_MODE" ) != "orca" ) { + mvwprintz( w, opt_pos, col, + sorted_scens[i]->gender_appropriate_name( u.male ) ); } - mvwprintz( w, opt_pos, col, - sorted_scens[i]->gender_appropriate_name( u.male ) ); } list_sb.offset_x( 0 ) diff --git a/src/options.cpp b/src/options.cpp index 479a5040e5871..109452b80ba04 100644 --- a/src/options.cpp +++ b/src/options.cpp @@ -1945,6 +1945,82 @@ void options_manager::add_options_interface() to_translation( "If true, highlight unread recipes to allow tracking of newly learned recipes." ), true ); + + add( "SCREEN_READER_MODE", page_id, to_translation( "Screen reader mode" ), + to_translation( "On supported UI screens, tweaks display of text to optimize for the selected screen reader software." ), + { + { "none", to_translation( "N/A" ) }, + { "orca", to_translation( "orca" ) } // Shouldn't actually be marked for translation. It's the name of a piece of software + /* In supported UI screens, tweaks the way text is displayed to optimize for screen readers. Testing based on orca 45.2-1 + * Some examples of how and why: + * + * TEXT COLOR + * It's fairly common in the UI to use text color to convey information. For instance, a currently-active item in a list + * might display as green, while one that cannot be selected is presented in dark gray. Orca does not give text color when + * reading the screen, so if that text colour provides information required by the player, the text should be modified to + * provide that information. e.g.: + * Evacuee turns into Evacuee - active + * The dash looks a bit funny, but it takes a lot less time for orca to read than "(active)" + * + * UI LAYOUT AND CURSOR POSITION + * Orca struggles with differentiating multiple columns of text. As an example, a UI screen with three columns listing + * things and a pane at the bottom with details: + * _______________________________ + * |List 1 | List 2 | List 3 | + * |>Item 1 | Item 2-1| Item 3-1 | + * |Item 2 | Item 2-2| Item 3-2 | + * |Item 3 | | + * ------------------------------- + * | Details relating to item 1 | + * | lorem ipsum | + * ------------------------------- + * + * In this layout, orca will read all the list panels of text at the same time, line by line, resulting in a confusing + * mess. So, on implementation, attempt to do this: + * _______________________________ + * |List 1 | List 2 | List 3 | + * |>Item 1 | Item 2-1| Item 3-1 | + * |Item 2 | Item 2-2| Item 3-2 | + * |Item 3 | | + * ------------------------------- + * |XList 1 - Item 1 | + * | Details relating to item 1 | + * ------------------------------- + * That is, set the cursor position to the X at the top left of the 'details' pane and pull the current + * heading/selected item into the text of that panel. This allows the user to scroll through each list + * line-by-line, hearing only what they want to hear. + * + * In this other common UI layout, with one pane for a list of items and a second pane for details on + * the currently-selected item, orca is unable to differentiate between the two panes, and will mix list + * items into the middle of the description of Item 1. + * _______________________________ + * |List | Details relating to| + * |>Item 1 | item 1. Lorem | + * |Item 2 | ipsum | + * |Item 3 | | + * |Item 4 | | + * |Item 5 | | + * ------------------------------- + * + * The solution to this is to ensure that "Item 1" appears in the details for item 1, set the cursor to + * top left of the details pane, and to _not_ draw the list of items: + * _______________________________ + * | |XItem 1 - Details | + * | | telating to item 1.| + * | | Lorem ipsum | + * | | | + * | | | + * | | | + * ------------------------------- + * In this implementation, orca will read the details cleanly, and the user is free to scroll to the desired line + * + * STANDALONE CHARACTERS + * Certain characters combinations are not read, or are not clearly annunciated by orca. For example, + * the question mark in [?] is ignored. This mode tells the input_context to replace ? and similar punctuation with + * translated text + */ + }, + "none" ); } ); add_empty_line(); diff --git a/src/output.cpp b/src/output.cpp index b6c7cc4cb1371..225128c0072f1 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -1972,8 +1972,14 @@ int multiline_list::get_offset_from_entry( const int entry ) return offset; } -bool multiline_list::handle_navigation( std::string &action, input_context &ctxt ) +bool multiline_list::handle_navigation( std::string &action, input_context &ctxt, + int active_list_id ) { + // Ignore input if the list is empty + if( entries.empty() || entry_sizes.empty() ) { + return false; + } + std::optional coord = ctxt.get_coordinates_text( catacurses::stdscr ); inclusive_rectangle mouseover_area( point( catacurses::getbegx( w ), catacurses::getbegy( w ) ), point( getmaxx( w ) + catacurses::getbegx( w ), @@ -1993,17 +1999,17 @@ bool multiline_list::handle_navigation( std::string &action, input_context &ctxt if( list_sb->handle_dragging( action, coord, offset_position ) ) { // No action required - } else if( action == "HOME" ) { + } else if( active_list_id == list_id && action == "HOME" ) { set_entry_pos( 0, false ); - } else if( action == "END" ) { + } else if( active_list_id == list_id && action == "END" ) { set_entry_pos( entries.size() - 1, false ); - } else if( action == "PAGE_DOWN" ) { + } else if( active_list_id == list_id && action == "PAGE_DOWN" ) { set_offset_pos( offset_position + getmaxy( w ), true ); - } else if( action == "PAGE_UP" ) { + } else if( active_list_id == list_id && action == "PAGE_UP" ) { set_offset_pos( offset_position - getmaxy( w ), true ); - } else if( action == "UP" ) { + } else if( active_list_id == list_id && action == "UP" ) { set_entry_pos( entry_position - 1, true ); - } else if( action == "DOWN" ) { + } else if( active_list_id == list_id && action == "DOWN" ) { set_entry_pos( entry_position + 1, true ); } else if( action == "SCROLL_UP" && mouse_in_window ) { // Scroll selection, but only adjust view as if we're scrolling offset @@ -2020,7 +2026,8 @@ bool multiline_list::handle_navigation( std::string &action, input_context &ctxt action = "CONFIRM"; mouseover_delay_end = std::chrono::steady_clock::now() + base_mouse_delay; } - } else if( action == "MOUSE_MOVE" && mouse_in_window && local_coord.has_value() ) { + } else if( mouseover_selection && action == "MOUSE_MOVE" && mouse_in_window && + local_coord.has_value() ) { if( std::chrono::steady_clock::now() > mouseover_delay_end ) { if( mouseover_position >= 0 ) { entry_position = mouseover_position; @@ -2043,7 +2050,7 @@ bool multiline_list::handle_navigation( std::string &action, input_context &ctxt return true; } -void multiline_list::print_entries() +void multiline_list::print_entries( const int active_list_id ) { werase( w ); entry_map.clear(); @@ -2051,10 +2058,14 @@ void multiline_list::print_entries() int ycurrent = 0; int current_offset = 0; int available_height = getmaxy( w ); + point cursor_pos = point( getcurx( w ), getcury( w ) ); for( size_t i = 0; i < entries.size(); ++i ) { + if( static_cast( i ) == entry_position && active_list_id == list_id ) { + cursor_pos = point( 2, ycurrent ); + } for( size_t j = 0; j < entries[i].folded_text.size(); ++j ) { if( current_offset >= offset_position && current_offset < offset_position + available_height ) { - print_line( i, point( 2, ycurrent ), entries[i].folded_text[j] ); + print_line( i, point( 2, ycurrent ), entries[i].folded_text[j], active_list_id ); ++ycurrent; } ++current_offset; @@ -2068,10 +2079,14 @@ void multiline_list::print_entries() .viewport_size( getmaxy( w ) ) .apply( w ); + /* If this list is active this will set the cursor for screen readers + * Otherwise, it will restore the previous cursor position */ + wmove( w, cursor_pos ); wnoutrefresh( w ); } -void multiline_list::print_line( int entry, const point &start, const std::string &text ) +void multiline_list::print_line( int entry, const point &start, const std::string &text, + const int active_list_id ) { nc_color cur_color = c_light_gray; std::string output_text = text; @@ -2079,7 +2094,7 @@ void multiline_list::print_line( int entry, const point &start, const std::strin cur_color = c_light_green; output_text = colorize( remove_color_tags( output_text ), cur_color ); } - if( entry == entry_position ) { + if( entry == entry_position && active_list_id == list_id ) { output_text = hilite_string( output_text ); } print_colored_text( w, start, cur_color, c_light_gray, output_text ); @@ -2089,20 +2104,24 @@ void multiline_list::print_line( int entry, const point &start, const std::strin void multiline_list::set_entry_pos( const int entry_pos, const bool looping = false ) { - if( looping && !entries.empty() ) { - int new_position = entry_pos; - const int list_size = static_cast( entries.size() ); - if( new_position < 0 ) { - // Ensure we have a positive position index by adding a multiple of the list_size to it - new_position += list_size * ( ( std::abs( new_position ) / list_size ) + 1 ); - } - entry_position = new_position % static_cast( entries.size() ); + if( entries.empty() || entry_sizes.empty() ) { + debugmsg( "List has no entries, unable to set current entry to %i.", entry_pos ); } else { - entry_position = clamp( entry_pos, 0, static_cast( entries.size() ) - 1 ); + if( looping ) { + int new_position = entry_pos; + const int list_size = static_cast( entries.size() ); + if( new_position < 0 ) { + // Ensure we have a positive position index by adding a multiple of the list_size to it + new_position += list_size * ( ( std::abs( new_position ) / list_size ) + 1 ); + } + entry_position = new_position % static_cast( entries.size() ); + } else { + entry_position = clamp( entry_pos, 0, static_cast( entries.size() ) - 1 ); + } + int available_space = getmaxy( w ) - entry_sizes[entry_position]; + set_offset_pos( get_offset_from_entry() - available_space / 2, false ); + mouseover_delay_end = std::chrono::steady_clock::now() + base_mouse_delay / mouseover_accel_counter; } - int available_space = getmaxy( w ) - entry_sizes[entry_position]; - set_offset_pos( get_offset_from_entry() - available_space / 2, false ); - mouseover_delay_end = std::chrono::steady_clock::now() + base_mouse_delay / mouseover_accel_counter; } void multiline_list::set_offset_pos( const int offset_pos, const bool update_selection ) @@ -2131,7 +2150,7 @@ void multiline_list::set_offset_pos( const int offset_pos, const bool update_sel mouseover_delay_end = std::chrono::steady_clock::now() + base_mouse_delay / mouseover_accel_counter; } -void multiline_list::set_up_navigation( input_context &ctxt ) +void multiline_list::set_up_navigation( input_context &ctxt, const bool _mouseover_selection ) { list_sb->set_draggable( ctxt ); ctxt.register_updown(); @@ -2143,6 +2162,7 @@ void multiline_list::set_up_navigation( input_context &ctxt ) ctxt.register_action( "SCROLL_DOWN" ); ctxt.register_action( "SCROLL_UP" ); ctxt.register_action( "SELECT" ); + mouseover_selection = _mouseover_selection; } void scrolling_text_view::set_text( const std::string &text, const bool scroll_to_top ) @@ -2206,6 +2226,7 @@ void scrolling_text_view::draw( const nc_color &base_color ) text_[line_num + offset_] ); } + wmove( w_, point_zero ); // Set cursor to beginning of text for screen readers wnoutrefresh( w_ ); } diff --git a/src/output.h b/src/output.h index ce4ffdfc59b69..52ad7d7bcd006 100644 --- a/src/output.h +++ b/src/output.h @@ -961,17 +961,21 @@ class multiline_list std::vector entry_sizes; int total_length; std::map> entry_map; + bool mouseover_selection; std::unique_ptr list_sb; catacurses::window &w; void add_entry( const multiline_list_entry &entry ); void create_entry_prep(); - void print_line( int entry, const point &start, const std::string &text ); + void print_line( int entry, const point &start, const std::string &text, int active_list_id ); public: + int list_id; //Used to differentiate input when multiple multiline_lists are visible at once + explicit multiline_list( catacurses::window &win ) : w( win ) { list_sb = std::make_unique(); + list_id = 0; } void activate_entry( size_t entry_pos, bool exclusive ); @@ -995,11 +999,11 @@ class multiline_list int get_entry_from_offset( int offset ); int get_offset_from_entry(); int get_offset_from_entry( int entry ); - bool handle_navigation( std::string &action, input_context &ctxt ); - void print_entries(); + bool handle_navigation( std::string &action, input_context &ctxt, int active_list_id = 0 ); + void print_entries( int active_list_id = 0 ); void set_entry_pos( int entry_pos, bool looping ); void set_offset_pos( int offset_pos, bool update_selection ); - void set_up_navigation( input_context &ctxt ); + void set_up_navigation( input_context &ctxt, bool _mouseover_selection = true ); }; /** A simple scrolling view onto some text. Given a window, it will use the