From 12226bf34cb112f1025ae691118789daa27eed2a Mon Sep 17 00:00:00 2001 From: ZeroInternalReflection <89038572+ZeroInternalReflection@users.noreply.github.com> Date: Mon, 11 Mar 2024 15:13:41 -0400 Subject: [PATCH] TEMPORARY - Screen reader mode for testing --- doc/USER_INTERFACE_AND_ACCESSIBILITY.md | 165 ++++++++++++++- src/newcharacter.cpp | 270 ++++++++++++++++++------ src/options.cpp | 6 + 3 files changed, 364 insertions(+), 77 deletions(-) diff --git a/doc/USER_INTERFACE_AND_ACCESSIBILITY.md b/doc/USER_INTERFACE_AND_ACCESSIBILITY.md index 687ae9941cff2..067f1ef70db04 100644 --- a/doc/USER_INTERFACE_AND_ACCESSIBILITY.md +++ b/doc/USER_INTERFACE_AND_ACCESSIBILITY.md @@ -40,15 +40,42 @@ text to show correctly. ## Compatibility with screen readers -There are people who use screen readers to play Cataclysm DDA. In order for screen -readers to announce the most important information in a UI, the terminal cursor has -to be placed at the correct location. This information may be text such as selected -item names in a list, etc, and the cursor has to be placed exactly at the beginning -of the text for screen readers to announce it. (Note: from my test with an Ubuntu -VM, if the cursor is placed after the end of the text, the cursor might be wrapped -to the next line and cause the screen reader to announce the text incorrectly. It -also seems to be easier for people to control the screen reader to read from the -beginning of the text where the cursor is placed.) +There are people who use screen readers to play Cataclysm DDA. There are several +things to keep in mind when checking the user interface for compatibility with +screen readers. These include: + +1. Screen reader mode: +An option ("SCREEN_READER_MODE") has been added where the user can indicate that +they are using a screen reader. If a change to the UI to optimize for screen +readers would be undesirable for other users, consider checking against +"SCREEN_READER_MODE", and including the change when true. + +2. Text color: +Text color is not announced by screen readers. If key information is only +communicated to the player through text color, that information will not be +available to players using screen readers. For example, a simple +uilist where some options have been disabled: + +---------------------------------------------------------------------------- +|Execute which action? | +|--------------------------------------------------------------------------| +|1 Cut up an item | +|f Start a fire quickly | +|h Use holster | +---------------------------------------------------------------------------- + +A screen reader user will only know that "Use holster" is unavailable when they +attempt to use it. When presenting information, consider hiding inaccessible +options and adding text to confirm information conveyed by text color. As an +example, the display of food spoilage status: + +Acorns (fresh) +Acorns (old) + +3. Cursor placement +In general, screen readers will start reading where the program has set the terminal +cursor. Thus, the cursor should be set to the beginning of the most important +section of the UI. The recommended way to place the cursor is to use `ui_adaptor`. This ensures the desired cursor position is preserved when subsequent output code changes the @@ -62,4 +89,122 @@ For debugging purposes, you can set the `DEBUG_CURSES_CURSOR` compile flag to always show the cursor in the terminal (currently only works with the curses build). -For more information and examples, please see the documentation in `ui_manager.h`. +4. Window layout +While setting the terminal cursor position is sufficient to set things up for +screen readers in many cases, there are caveats to this. Screen readers will also +attempt to detect text changes. If that text change is after the terminal cursor, +the screen reader may skip over text. As an example (terminal position noted +by X): + +XEye color: amber + +changing into + +XEye color: blue + +The screen reader will only read the word "blue", even though the cursor is +set to start reading at "Eye". + +Another complication is if the screen reader detects text changes above the +terminal cursor. As an example: + +Lifestyle: underpowered +XStrength: 8 + +changing into + +Lifestyle: weak +XStrength: 9 + +The screen reader will start reading at "weak". + +Behaviour like this means that screen readers struggle with certain common UI +layouts. Examples of how to implement SCREEN_READER_MODE in various UI layouts +can be found in src/newcharacter.cpp. + +Note: In all following examples, the terminal cursor is marked "X". + +4a. List with additional information at the top or bottom: + +----------------------------------------------------- +|Choose type of butchery: | +|---------------------------------------------------| +XB Quick butchery 6 minutes | +|b Full butchery 39 minutes | +|---------------------------------------------------| +|This technique is used when you are in a hurry, but| +|still want to harvest something from the corpse. | +----------------------------------------------------- +Figure 4ai - List with details at the bottom, current configuration + +If the cursor is set at "Quick butchery", then the screen reader will read all of the +other options before getting to the description of what "Quick butchery" entails. The +preferred layout in SCREEN_READER_MODE is the following: + +----------------------------------------------------- +|Choose type of butchery: | +|---------------------------------------------------| +| | +| | +|---------------------------------------------------| +XB Quick butchery 6 minutes | +|This technique is used when you are in a hurry, but| +----------------------------------------------------- +Figure 4aii - List with details at the bottom, recommended "SCREEN_READER_MODE" + +That is: +1. Add the currently-selected list entry to the detail pane at the bottom, and +2. Disable display of the list of options. +The screen reader user can then scroll through the list of available options, only +hearing about the details of the currently-selected option. + +Hiding the list of options is necessary even when the cursor is set to point X. +In addition to following the terminal cursor, screen readers will try to detect when +text changes, and can be misled by scrolling text. As an example: + +----------------------------------------------------- +|Choose type of butchery: | +|---------------------------------------------------| +Yb Full butchery 39 minutes | +|f Field dress corpse 5 minutes | +|---------------------------------------------------| +XThis technique is used to properly butcher a corpse| +|and requires a rope & a tree or a butchering rack, | +----------------------------------------------------- +Figure 4aiii - List with details at the bottom with scrolling issues + +When the list scrolls, the text at the top changes, and the screen reader will +'helpfully' start reading at point Y rather than the current cursor position X. + +4b. List with additional information at the side: + +_______________________________ +|List | Details relating to| +XItem 1 | item 1. Lorem | +|Item 2 | ipsum | +|Item 3 | | +|Item 4 | | +|Item 5 | | +------------------------------- +Figure 4bi - List with details at the side, current configuration + +In this common layout, the screen reader is unable to differentiate between the two +panes, and ends up reading the list interwoven with the details of the currently-selected +item. This is true whether the terminal cursor is set to the current item or to the top +left of the details pane. + +The recommended layout in SCREEN_READER_MODE is the following: + +_______________________________ +| | XItem 1 | +| | Details relating to| +| | item 1. Lorem | +| | ipsum | +| | | +| | | +------------------------------- +Figure 4bii - List with details at the side, recommended "SCREEN_READER_MODE" + +That is: +1. Add the currently-selected list entry to the detail pane at the side, and +2. Disable display of the list of options. diff --git a/src/newcharacter.cpp b/src/newcharacter.cpp index 321c1ff39ac2b..b02e10ac35329 100644 --- a/src/newcharacter.cpp +++ b/src/newcharacter.cpp @@ -1054,6 +1054,7 @@ void set_points( tab_manager &tabs, avatar &u, pool_type &pool ) const int iHeaderHeight = 6; // guessing most likely, but it doesn't matter, it will be recalculated if wrong int iHelpHeight = 3; + const bool screen_reader_mode = get_option( "SCREEN_READER_MODE" ); ui_adaptor ui; catacurses::window w; @@ -1126,7 +1127,16 @@ void set_points( tab_manager &tabs, avatar &u, pool_type &pool ) werase( w ); tabs.draw( w ); - const auto &cur_opt = opts[highlighted]; + std::string title = std::get<1>( opts[highlighted] ); + std::string description = std::get<2>( opts[highlighted] ); + + if( screen_reader_mode ) { + // Include option title in option description, and say whether it's active + if( std::get<0>( opts[highlighted] ) == pool ) { + title.append( _( " - active" ) ); + } + description = title + "\n" + description; + } draw_points( w, pool, u ); @@ -1145,11 +1155,15 @@ void set_points( tab_manager &tabs, avatar &u, pool_type &pool ) if( highlighted == i ) { ui.set_cursor( w, opt_pos ); } - mvwprintz( w, opt_pos, color, std::get<1>( opts[i] ) ); + if( screen_reader_mode ) { + // The list of options only clutters up the screen in screen reader mode + } else { + mvwprintz( w, opt_pos, color, std::get<1>( opts[i] ) ); + } } fold_and_print( w_description, point_zero, getmaxx( w_description ), - COL_SKILL_USED, std::get<2>( cur_opt ) ); + COL_SKILL_USED, description ); // Helptext points tab fold_and_print( w, point( 2, TERMY - foldstring( help_text, getmaxx( w ) - 4 ).size() - 1 ), @@ -1334,6 +1348,8 @@ void set_stats( tab_manager &tabs, avatar &u, pool_type pool ) unsigned char sel = 0; + const bool screen_reader_mode = get_option( "SCREEN_READER_MODE" ); + int iSecondColumn; const int iHeaderHeight = 6; // guessing most likely, but it doesn't matter, it will be recalculated if wrong @@ -1381,11 +1397,14 @@ void set_stats( tab_manager &tabs, avatar &u, pool_type pool ) help_text ); const point opt_pos( 2, sel + iHeaderHeight ); - ui.set_cursor( w, opt_pos ); - for( int i = 0; i < 4; i++ ) { - mvwprintz( w, point( 2, i + iHeaderHeight ), i == sel ? COL_SELECT : c_light_gray, "%s:", - stat_labels[i].translated() ); - mvwprintz( w, point( 16, i + iHeaderHeight ), c_light_gray, "%2d", *stats[i] ); + if( screen_reader_mode ) { + // This list only clutters up the screen in screen reader mode + } else { + for( int i = 0; i < 4; i++ ) { + mvwprintz( w, point( 2, i + iHeaderHeight ), i == sel ? COL_SELECT : c_light_gray, "%s:", + stat_labels[i].translated() ); + mvwprintz( w, point( 16, i + iHeaderHeight ), c_light_gray, "%2d", *stats[i] ); + } } draw_points( w, pool, u ); @@ -1411,11 +1430,19 @@ void set_stats( tab_manager &tabs, avatar &u, pool_type pool ) u.set_stored_kcal( u.get_healthy_kcal() ); u.reset_bonuses(); // Removes pollution of stats by modifications appearing inside reset_stats(). Is reset_stats() even necessary in this context? if( details_recalc ) { - details.set_text( assemble_stat_details( u, sel ) ); + std::string stat_details; + if( screen_reader_mode ) { + stat_details = string_format( "%s: %i", stat_labels[sel].translated(), + *stats[sel] ) + "\n" + assemble_stat_details( u, sel ); + } else { + stat_details = assemble_stat_details( u, sel ); + } + details.set_text( stat_details ); details_recalc = false; } wnoutrefresh( w ); + ui.set_cursor( w_details_pane, point_zero ); details.draw( COL_STAT_NEUTRAL ); } ); @@ -1471,7 +1498,7 @@ static void add_trait( std::vector &to, const trait_id &trait ) void set_traits( tab_manager &tabs, avatar &u, pool_type pool ) { const int max_trait_points = get_option( "MAX_TRAIT_POINTS" ); - + const bool screen_reader_mode = get_option( "SCREEN_READER_MODE" ); // Track how many good / bad POINTS we have; cap both at MAX_TRAIT_POINTS int num_good = 0; int num_bad = 0; @@ -1704,10 +1731,6 @@ void set_traits( tab_manager &tabs, avatar &u, pool_type pool ) negativeTrait ? _( "earns" ) : _( "costs" ), points ); } - if( details_recalc ) { - details.set_text( colorize( cursor.desc(), col_tr ) ); - details_recalc = false; - } } nc_color cLine = col_off_pas; @@ -1747,11 +1770,41 @@ 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 ) { - ui.set_cursor( w, opt_pos ); + if( screen_reader_mode ) { + // This list only clutters up the screen in screen reader mode + } else { + mvwprintz( w, opt_pos, cLine, + utf8_truncate( cursor.name(), page_width - 2 ) ); } - mvwprintz( w, opt_pos, cLine, - utf8_truncate( cursor.name(), page_width - 2 ) ); + } + + if( details_recalc ) { + std::string description; + const trait_and_var ¤t = *sorted_traits[iCurWorkingPage][iCurrentLine[iCurWorkingPage]]; + if( screen_reader_mode ) { + std::string cur_trait_notes; + description = current.name(); + + if( u.has_conflicting_trait( current.trait ) ) { + cur_trait_notes = _( "a conflicting trait is active" ); + } else if( u.has_trait( current.trait ) ) { + if( !current.trait->variants.empty() && !u.has_trait_variant( current ) ) { + cur_trait_notes = _( "a different variant of this trait is active" ); + } else { + cur_trait_notes = _( "active" ); + } + } + + if( !cur_trait_notes.empty() ) { + description.append( string_format( " - %s", cur_trait_notes ) ); + } + + description.append( "\n" + current.desc() ); + } else { + description = current.desc(); + } + details.set_text( colorize( description, col_tr ) ); + details_recalc = false; } trait_sbs[iCurrentPage].offset_x( page_width * iCurrentPage ) @@ -1763,6 +1816,7 @@ void set_traits( tab_manager &tabs, avatar &u, pool_type pool ) } wnoutrefresh( w ); + ui.set_cursor( w_details_pane, point_zero ); // color is never visible (COL_TR_NEUT), text is already colorized details.draw( COL_TR_NEUT ); } ); @@ -1924,6 +1978,7 @@ void set_traits( tab_manager &tabs, avatar &u, pool_type pool ) //inc_type is either -1 or 1, so we can just multiply by it to invert if( inc_type != 0 ) { + details_recalc = true; u.toggle_trait_deps( cur_trait, variant ); if( iCurWorkingPage == 0 ) { num_good += mdata.points * inc_type; @@ -1984,7 +2039,7 @@ static struct { } profession_sorter; static std::string assemble_profession_details( const avatar &u, const input_context &ctxt, - const std::vector> &sorted_profs, const int cur_id ) + const std::vector> &sorted_profs, const int cur_id, const std::string ¬es ) { std::string assembled; @@ -1995,8 +2050,12 @@ static std::string assemble_profession_details( const avatar &u, const input_con }, enumeration_conjunction::arrow ); assembled += string_format( _( "Origin: %s" ), mod_src ) + "\n"; + std::string profession_name = sorted_profs[cur_id]->gender_appropriate_name( u.male ); + if( get_option( "SCREEN_READER_MODE" ) && !notes.empty() ) { + profession_name = profession_name.append( string_format( " - %s", notes ) ); + } assembled += string_format( g_switch_msg( u ), ctxt.get_desc( "CHANGE_GENDER" ), - sorted_profs[cur_id]->gender_appropriate_name( u.male ) ) + "\n"; + profession_name ) + "\n"; assembled += string_format( dress_switch_msg(), ctxt.get_desc( "CHANGE_OUTFIT" ) ) + "\n"; if( sorted_profs[cur_id]->get_requirement().has_value() ) { @@ -2237,6 +2296,8 @@ void set_profession( tab_manager &tabs, avatar &u, pool_type pool ) size_t iContentHeight = 0; int iStartPos = 0; + const bool screen_reader_mode = get_option( "SCREEN_READER_MODE" ); + ui_adaptor ui; catacurses::window w; catacurses::window w_details_pane; @@ -2283,11 +2344,6 @@ void set_profession( tab_manager &tabs, avatar &u, pool_type pool ) const bool cur_id_is_valid = cur_id >= 0 && static_cast( cur_id ) < sorted_profs.size(); if( cur_id_is_valid ) { - if( details_recalc ) { - details.set_text( assemble_profession_details( u, ctxt, sorted_profs, cur_id ) ); - details_recalc = false; - } - int netPointCost = sorted_profs[cur_id]->point_cost() - u.prof->point_cost(); ret_val can_afford = sorted_profs[cur_id]->can_afford( u, skill_points_left( u, pool ) ); ret_val can_pick = sorted_profs[cur_id]->can_pick(); @@ -2322,6 +2378,7 @@ void set_profession( tab_manager &tabs, avatar &u, pool_type pool ) //Draw options calcStartPos( iStartPos, cur_id, iContentHeight, profs_length ); const int end_pos = iStartPos + std::min( iContentHeight, profs_length ); + std::string cur_prof_notes = ""; for( int i = iStartPos; i < end_pos; i++ ) { nc_color col; if( u.prof != &sorted_profs[i].obj() ) { @@ -2329,22 +2386,37 @@ void set_profession( tab_manager &tabs, avatar &u, pool_type pool ) if( cur_id_is_valid && sorted_profs[i] == sorted_profs[cur_id] && !sorted_profs[i]->can_pick().success() ) { col = h_dark_gray; + if( i == cur_id ) { + cur_prof_notes = _( "unavailable" ); + } } else if( cur_id_is_valid && sorted_profs[i] != sorted_profs[cur_id] && !sorted_profs[i]->can_pick().success() ) { col = c_dark_gray; + if( i == cur_id ) { + cur_prof_notes = _( "unavailable" ); + } } else { col = ( cur_id_is_valid && sorted_profs[i] == sorted_profs[cur_id] ? COL_SELECT : c_light_gray ); } } else { col = ( cur_id_is_valid && sorted_profs[i] == sorted_profs[cur_id] ? hilite( c_light_green ) : COL_SKILL_USED ); + if( i == cur_id ) { + cur_prof_notes = _( "active" ); + } } - const point opt_pos( 2, iHeaderHeight + i - iStartPos ); - if( i == cur_id ) { - ui.set_cursor( w, opt_pos ); + if( screen_reader_mode ) { + // This list only clutters up the screen in screen reader mode + } else { + const point opt_pos( 2, iHeaderHeight + i - iStartPos ); + mvwprintz( w, opt_pos, col, + sorted_profs[i]->gender_appropriate_name( u.male ) ); } - mvwprintz( w, opt_pos, col, - sorted_profs[i]->gender_appropriate_name( u.male ) ); + } + + if( details_recalc && cur_id_is_valid ) { + details.set_text( assemble_profession_details( u, ctxt, sorted_profs, cur_id, cur_prof_notes ) ); + details_recalc = false; } list_sb.offset_x( 0 ) @@ -2355,6 +2427,7 @@ void set_profession( tab_manager &tabs, avatar &u, pool_type pool ) .apply( w ); wnoutrefresh( w ); + ui.set_cursor( w_details_pane, point_zero ); details.draw( c_light_gray ); } ); @@ -2397,6 +2470,9 @@ void set_profession( tab_manager &tabs, avatar &u, pool_type pool ) continue; } + // Selecting a profession will, under certain circumstances, change the detail text + details_recalc = true; + // Remove traits from the previous profession for( const trait_and_var &old : u.prof->get_locked_traits() ) { u.toggle_trait_deps( old.trait ); @@ -2456,12 +2532,18 @@ void set_profession( tab_manager &tabs, avatar &u, pool_type pool ) } static std::string assemble_hobby_details( const avatar &u, const input_context &ctxt, - const std::vector> &sorted_hobbies, const int cur_id ) + const std::vector> &sorted_hobbies, const int cur_id, + const std::string ¬es ) { std::string assembled; + std::string hobby_name = sorted_hobbies[cur_id]->gender_appropriate_name( u.male ); + if( get_option( "SCREEN_READER_MODE" ) && !notes.empty() ) { + hobby_name = hobby_name.append( string_format( " - %s", notes ) ); + } + assembled += string_format( g_switch_msg( u ), ctxt.get_desc( "CHANGE_GENDER" ), - sorted_hobbies[cur_id]->gender_appropriate_name( !u.male ) ) + "\n"; + hobby_name ) + "\n"; assembled += string_format( dress_switch_msg(), ctxt.get_desc( "CHANGE_OUTFIT" ) ) + "\n"; assembled += "\n" + colorize( _( "Background story:" ), COL_HEADER ) + "\n"; @@ -2550,6 +2632,8 @@ void set_hobbies( tab_manager &tabs, avatar &u, pool_type pool ) size_t iContentHeight = 0; int iStartPos = 0; + const bool screen_reader_mode = get_option( "SCREEN_READER_MODE" ); + ui_adaptor ui; catacurses::window w; catacurses::window w_details_pane; @@ -2597,10 +2681,6 @@ void set_hobbies( tab_manager &tabs, avatar &u, pool_type pool ) const bool cur_id_is_valid = cur_id >= 0 && static_cast( cur_id ) < sorted_hobbies.size(); if( cur_id_is_valid ) { - if( details_recalc ) { - details.set_text( assemble_hobby_details( u, ctxt, sorted_hobbies, cur_id ) ); - details_recalc = false; - } int netPointCost = sorted_hobbies[cur_id]->point_cost() - u.prof->point_cost(); ret_val can_pick = sorted_hobbies[cur_id]->can_afford( u, skill_points_left( u, pool ) ); int pointsForProf = sorted_hobbies[cur_id]->point_cost(); @@ -2634,11 +2714,15 @@ void set_hobbies( tab_manager &tabs, avatar &u, pool_type pool ) //Draw options calcStartPos( iStartPos, cur_id, iContentHeight, hobbies_length ); const int end_pos = iStartPos + std::min( iContentHeight, hobbies_length ); + std::string cur_hob_notes = ""; for( int i = iStartPos; i < end_pos; i++ ) { nc_color col; if( u.hobbies.count( &sorted_hobbies[i].obj() ) != 0 ) { col = ( cur_id_is_valid && sorted_hobbies[i] == sorted_hobbies[cur_id] ? hilite( c_light_green ) : COL_SKILL_USED ); + if( i == cur_id ) { + cur_hob_notes = _( "active" ); + } } else { col = ( cur_id_is_valid && sorted_hobbies[i] == sorted_hobbies[cur_id] ? COL_SELECT : c_light_gray ); @@ -2646,12 +2730,22 @@ void set_hobbies( tab_manager &tabs, avatar &u, pool_type pool ) const point opt_pos( 2, iHeaderHeight + i - iStartPos ); if( i == cur_id ) { - ui.set_cursor( w, opt_pos ); + + } + if( screen_reader_mode ) { + // This list only clutters up the screen in screen reader mode + } else { + mvwprintz( w, opt_pos, col, + sorted_hobbies[i]->gender_appropriate_name( u.male ) ); } - mvwprintz( w, opt_pos, col, - sorted_hobbies[i]->gender_appropriate_name( u.male ) ); } + if( details_recalc && cur_id_is_valid ) { + details.set_text( assemble_hobby_details( u, ctxt, sorted_hobbies, cur_id, cur_hob_notes ) ); + details_recalc = false; + } + + list_sb.offset_x( 0 ) .offset_y( 6 ) .content_size( hobbies_length ) @@ -2660,6 +2754,7 @@ void set_hobbies( tab_manager &tabs, avatar &u, pool_type pool ) .apply( w ); wnoutrefresh( w ); + ui.set_cursor( w_details_pane, point_zero ); details.draw( c_light_gray ); } ); @@ -2734,6 +2829,9 @@ void set_hobbies( tab_manager &tabs, avatar &u, pool_type pool ) u.hobbies.erase( hobb ); } + // Selecting a hobby will, under certain circumstances, change the detail text + details_recalc = true; + // Add or remove traits from hobby for( const trait_and_var &cur : hobb->get_locked_traits() ) { const trait_id &trait = cur.trait; @@ -2875,6 +2973,7 @@ static std::string assemble_skill_help( const input_context &ctxt ) void set_skills( tab_manager &tabs, avatar &u, pool_type pool ) { + const bool screen_reader_mode = get_option( "SCREEN_READER_MODE" ); ui_adaptor ui; catacurses::window w; catacurses::window w_list; @@ -2946,6 +3045,7 @@ void set_skills( tab_manager &tabs, avatar &u, pool_type pool ) const int remaining_points_length = utf8_width( pools_to_string( u, pool ), true ); ui.on_redraw( [&]( ui_adaptor & ui ) { + std::string cur_skill_text; const std::string help_text = assemble_skill_help( ctxt ); const int new_iHelpHeight = foldstring( help_text, getmaxx( w ) - 4 ).size(); if( new_iHelpHeight != iHelpHeight ) { @@ -2961,11 +3061,6 @@ void set_skills( tab_manager &tabs, avatar &u, pool_type pool ) fold_and_print( w, point( 2, TERMY - iHelpHeight - 1 ), getmaxx( w ) - 4, COL_NOTE_MINOR, help_text ); - if( details_recalc ) { - details.set_text( assemble_skill_details( u, prof_skills, currentSkill ) ); - details_recalc = false; - } - // Write the hint as to upgrade costs const int cost = skill_increment_cost( u, currentSkill->ident() ); const int level = u.get_skill_level( currentSkill->ident() ); @@ -2998,26 +3093,46 @@ void set_skills( tab_manager &tabs, avatar &u, pool_type pool ) } } const point opt_pos( 1, y ); - if( i == cur_pos ) { - ui.set_cursor( w_list, opt_pos ); - } + std::string skill_text; if( skill_list[i].is_header ) { - mvwprintz( w_list, opt_pos, c_yellow, thisSkill->display_category()->display_string() ); + skill_text = colorize( thisSkill->display_category()->display_string(), c_yellow ); } else if( static_cast( u.get_skill_level( thisSkill->ident() ) ) + prof_skill_level == 0 ) { - mvwprintz( w_list, opt_pos, - ( i == cur_pos ? COL_SELECT : c_light_gray ), thisSkill->name() ); + skill_text = colorize( thisSkill->name(), ( i == cur_pos ? COL_SELECT : c_light_gray ) ); } else { - mvwprintz( w_list, opt_pos, - ( i == cur_pos ? hilite( COL_SKILL_USED ) : COL_SKILL_USED ), - thisSkill->name() ); + skill_text = colorize( thisSkill->name(), + ( i == cur_pos ? hilite( COL_SKILL_USED ) : COL_SKILL_USED ) ); if( prof_skill_level > 0 ) { - wprintz( w_list, ( i == cur_pos ? hilite( COL_SKILL_USED ) : COL_SKILL_USED ), - " ( %d + %d )", prof_skill_level, static_cast( u.get_skill_level( thisSkill->ident() ) ) ); + skill_text.append( colorize( string_format( " ( %d + %d )", prof_skill_level, + static_cast( u.get_skill_level( thisSkill->ident() ) ) ), + ( i == cur_pos ? hilite( COL_SKILL_USED ) : COL_SKILL_USED ) ) ); } else { - wprintz( w_list, ( i == cur_pos ? hilite( COL_SKILL_USED ) : COL_SKILL_USED ), - " ( %d )", static_cast( u.get_skill_level( thisSkill->ident() ) ) ); + skill_text.append( colorize( string_format( " ( %d )", + static_cast( u.get_skill_level( thisSkill->ident() ) ) ), + ( i == cur_pos ? hilite( COL_SKILL_USED ) : COL_SKILL_USED ) ) ); } } + if( i == cur_pos ) { + cur_skill_text = skill_text; + } + if( screen_reader_mode ) { + // This list only clutters up the screen in screen reader mode + } else { + nc_color dummy = c_light_gray; + print_colored_text( w_list, opt_pos, dummy, c_light_gray, skill_text ); + } + } + + if( details_recalc ) { + std::string description; + if( screen_reader_mode ) { + description = currentSkill->display_category()->display_string() + " - "; + description.append( cur_skill_text + "\n" ); + description.append( assemble_skill_details( u, prof_skills, currentSkill ) ); + } else { + description = assemble_skill_details( u, prof_skills, currentSkill ); + } + details.set_text( description ); + details_recalc = false; } list_sb.offset_x( 0 ) @@ -3029,6 +3144,7 @@ void set_skills( tab_manager &tabs, avatar &u, pool_type pool ) wnoutrefresh( w ); wnoutrefresh( w_list ); + ui.set_cursor( w_details_pane, point_zero ); details.draw( c_light_gray ); } ); @@ -3120,7 +3236,7 @@ static struct { } scenario_sorter; static std::string assemble_scenario_details( const avatar &u, const input_context &ctxt, - const scenario *current_scenario ) + const scenario *current_scenario, const std::string ¬es ) { std::string assembled; // Display Origin @@ -3130,8 +3246,12 @@ static std::string assemble_scenario_details( const avatar &u, const input_conte }, enumeration_conjunction::arrow ); assembled += string_format( _( "Origin: %s" ), mod_src ) + "\n"; + std::string scenario_name = current_scenario->gender_appropriate_name( !u.male ); + if( get_option( "SCREEN_READER_MODE" ) && !notes.empty() ) { + scenario_name = scenario_name.append( string_format( " - %s", notes ) ); + } assembled += string_format( g_switch_msg( u ), ctxt.get_desc( "CHANGE_GENDER" ), - current_scenario->gender_appropriate_name( !u.male ) ) + "\n"; + scenario_name ) + "\n"; assembled += string_format( dress_switch_msg(), ctxt.get_desc( "CHANGE_OUTFIT" ) ) + "\n"; assembled += string_format( @@ -3239,6 +3359,8 @@ void set_scenario( tab_manager &tabs, avatar &u, pool_type pool ) const int iHeaderHeight = 6; scrollbar list_sb; + const bool screen_reader_mode = get_option( "SCREEN_READER_MODE" ); + const auto init_windows = [&]( ui_adaptor & ui ) { iContentHeight = TERMY - iHeaderHeight - 1; w = catacurses::newwin( TERMY, TERMX, point_zero ); @@ -3281,10 +3403,6 @@ void set_scenario( tab_manager &tabs, avatar &u, pool_type pool ) const bool cur_id_is_valid = cur_id >= 0 && static_cast( cur_id ) < sorted_scens.size(); if( cur_id_is_valid ) { - if( details_recalc ) { - details.set_text( assemble_scenario_details( u, ctxt, sorted_scens[cur_id] ) ); - details_recalc = false; - } int netPointCost = sorted_scens[cur_id]->point_cost() - get_scenario()->point_cost(); ret_val can_afford = sorted_scens[cur_id]->can_afford( *get_scenario(), @@ -3319,6 +3437,7 @@ void set_scenario( tab_manager &tabs, avatar &u, pool_type pool ) //Draw options calcStartPos( iStartPos, cur_id, iContentHeight, scens_length ); const int end_pos = iStartPos + std::min( iContentHeight, scens_length ); + std::string current_scenario_notes = ""; for( int i = iStartPos; i < end_pos; i++ ) { nc_color col; if( get_scenario() != sorted_scens[i] ) { @@ -3326,23 +3445,39 @@ void set_scenario( tab_manager &tabs, avatar &u, pool_type pool ) ( ( sorted_scens[i]->has_flag( "CITY_START" ) && !scenario_sorter.cities_enabled ) || !sorted_scens[i]->can_pick().success() ) ) { col = h_dark_gray; + if( i == cur_id ) { + current_scenario_notes = _( "unavailable" ); + } } else if( cur_id_is_valid && sorted_scens[i] != sorted_scens[cur_id] && ( ( sorted_scens[i]->has_flag( "CITY_START" ) && !scenario_sorter.cities_enabled ) || !sorted_scens[i]->can_pick().success() ) ) { col = c_dark_gray; + if( i == cur_id ) { + current_scenario_notes = _( "unavailable" ); + } } else { col = ( cur_id_is_valid && sorted_scens[i] == sorted_scens[cur_id] ? COL_SELECT : c_light_gray ); } } else { col = ( cur_id_is_valid && sorted_scens[i] == sorted_scens[cur_id] ? hilite( c_light_green ) : COL_SKILL_USED ); + if( i == cur_id ) { + current_scenario_notes = _( "active" ); + } } const point opt_pos( 2, iHeaderHeight + i - iStartPos ); - if( i == cur_id ) { - ui.set_cursor( w, opt_pos ); + if( screen_reader_mode ) { + // The list of options only clutters up the screen in screen reader mode + } else { + 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 ) ); + } + + if( details_recalc && cur_id_is_valid ) { + details.set_text( assemble_scenario_details( u, ctxt, sorted_scens[cur_id], + current_scenario_notes ) ); + details_recalc = false; } list_sb.offset_x( 0 ) @@ -3353,6 +3488,7 @@ void set_scenario( tab_manager &tabs, avatar &u, pool_type pool ) .apply( w ); wnoutrefresh( w ); + ui.set_cursor( w_details_pane, point_zero ); details.draw( c_light_gray ); } ); diff --git a/src/options.cpp b/src/options.cpp index 8c87681611575..ffe1c0c269bc7 100644 --- a/src/options.cpp +++ b/src/options.cpp @@ -1951,6 +1951,12 @@ 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 screen readers. Targeted towards using the open-source screen reader 'orca' using curses for display." ), + // See doc/USER_INTERFACE_AND_ACCESSIBILITY.md for testing and implementation notes + false + ); } ); add_empty_line();