Add a language option to the VR menu, and fix a crash switching to it
build / build (push) Successful in 2m22s

- New "Language" row on the Main Menu tab (English/French/German),
  backed by a new "general" group in questshock.config.json
- Fixes a device-reproduced crash: switching language used to call
  language_dealfunc(), whose render_run()/opanel_redraw() repaint
  assumes the game's own in-game options panel is on screen - crashes
  (SIGSEGV) when triggered from our independent VR overlay instead.
  Now calls language_change() directly, which is all that's actually
  needed to reload resources and refresh the inventory/MFD/side-icon UI
- Fixes a second crash: this data set ships frnstrng.res/gerstrng.res
  but not mfdfrn.res/mfdger.res, and language_change() closes the
  current resource files before discovering the new ones don't exist -
  leaving no valid resource file open and crashing on the next lookup.
  Now checks both files exist before ever switching, refusing the
  change (with an in-headset popup) instead of corrupting resource state
- extract_assets.sh can now also pull mfdfrn.res/mfdger.res out of a
  System Shock: Classic installer (which includes them, unlike Enhanced
  Edition) if one is present in res/assets/ - entirely optional
- Documented all of the above in README.md/CONFIG.md
This commit is contained in:
ml
2026-09-05 08:09:12 +02:00
parent 09280dd410
commit c0619a55ab
9 changed files with 422 additions and 48 deletions
+190 -24
View File
@@ -284,9 +284,15 @@ typedef struct {
char resolution[16]; // "" = not present in the file
} QsParsedVideoOptions;
typedef struct {
char language[16]; // "" = not present in the file
} QsParsedGeneral;
typedef struct {
QsParsedVideoOptions video_options;
bool has_video_options;
QsParsedGeneral general;
bool has_general;
} QsParsedConfig;
static void qs_parse_video_options_field(const char *key, QsJsonParser *p, void *ctx) {
@@ -299,10 +305,20 @@ static void qs_parse_video_options_field(const char *key, QsJsonParser *p, void
qs_json_skip_value(p);
}
static void qs_parse_general_field(const char *key, QsJsonParser *p, void *ctx) {
QsParsedGeneral *out = (QsParsedGeneral *)ctx;
if (strcmp(key, "language") == 0)
qs_json_take_string_field(p, out->language, sizeof(out->language));
else
qs_json_skip_value(p);
}
static void qs_parse_top_level_field(const char *key, QsJsonParser *p, void *ctx) {
QsParsedConfig *out = (QsParsedConfig *)ctx;
if (strcmp(key, "video_options") == 0 && !p->error && p->p < p->end && *p->p == '{')
out->has_video_options = qs_json_parse_object(p, qs_parse_video_options_field, &out->video_options);
else if (strcmp(key, "general") == 0 && !p->error && p->p < p->end && *p->p == '{')
out->has_general = qs_json_parse_object(p, qs_parse_general_field, &out->general);
else
qs_json_skip_value(p);
}
@@ -442,6 +458,117 @@ static const char *qs_current_resolution(void) {
return "640x480"; // shouldn't happen - safe fallback for Save
}
// ===========================================================================
// General: language
// ===========================================================================
// Live-appliable: language_change() (below) is the resource-reload half of
// what the game's own options menu calls when the player changes language
// there (see wrapper.c's language_dealfunc(), wired up via
// multi_init(..., REF_STR_Language, ...)) - closes/reopens the
// language-specific resource files, updates which_lang/QUESTVAR, and
// refreshes the inventory/MFD/side-icon panels' own language-dependent
// text/art. Deliberately NOT calling language_dealfunc() itself: its extra
// render_run()/opanel_redraw() repaint step assumes the game's own
// in-game options-panel widget is the thing currently on screen, which
// isn't true when this is triggered from our independent VR overlay menu -
// confirmed by a device crash (SIGSEGV, null pointer) inside render_run()'s
// 3D-view frustum-clipping code when called this way. The game's normal
// per-frame render loop already redraws everything anyway.
#define QS_LANGUAGE_REQUIRES_RESTART false
// wrapper.h declares this one, but including the whole header pulls in
// other prototypes referencing types this file has no other reason to need
// - same reasoning as screen_draw() above.
extern void language_change(uchar lang);
// gamestrn.c/init.c - declared locally rather than pulling in their
// headers, same reasoning as language_change() above. language_change()
// itself indexes these two by lang (0/1/2) to reopen the string/MFD-art
// resource files for the new language - but it does so destructively,
// closing the *current* (working) files first, with no fallback if the
// reopen then fails. qs_language_resources_available() below checks both
// files exist *before* ever calling language_change(), so a language whose
// resource pack wasn't fully installed can't leave the game with no valid
// resource file open at all.
extern char *language_files[];
extern char *mfdart_files[];
static const struct {
const char *name;
short language;
} kLanguages[] = {
{"english", 0},
{"french", 1},
{"german", 2},
};
#define QS_NUM_LANGUAGES (sizeof(kLanguages) / sizeof(kLanguages[0]))
// Confirmed necessary on a real device: this project's own Android data set
// ships frnstrng.res/gerstrng.res (language_files[]) but not
// mfdfrn.res/mfdger.res (mfdart_files[]) - selecting French or German used
// to call language_change(), which closes the working English resource
// files before discovering the new ones don't exist, leaving no valid
// resource file open at all and crashing (SIGBUS, null resource pointer)
// the next time any resource lookup happened.
static bool qs_language_resources_available(short lang) {
FILE *f = fopen(language_files[lang], "rb");
if (!f)
return false;
fclose(f);
f = fopen(mfdart_files[lang], "rb");
if (!f)
return false;
fclose(f);
return true;
}
// Defined further down, alongside qs_show_restart_dialog() (same
// find-class-via-classloader JNI plumbing) - forward-declared here since
// qs_apply_language() above is defined earlier in the file.
static void qs_show_message_dialog(const char *message);
static bool qs_apply_language(const char *value, bool live) {
for (size_t i = 0; i < QS_NUM_LANGUAGES; i++) {
if (strcmp(value, kLanguages[i].name) != 0)
continue;
if (!qs_language_resources_available(kLanguages[i].language)) {
WARN("%s: general.language=\"%s\" - resource files for this language aren't installed "
"- not switching",
QS_CONFIG_FILENAME, value);
// Only when there's a menu to show it in - live=false is the
// startup path (qs_config_load_startup()), before the menu
// overlay exists; qs_show_message_dialog() would just silently
// no-op there anyway (MenuOverlay.instance is still null), but
// there's no point trying.
if (live) {
char message[256];
snprintf(message, sizeof(message),
"Can't switch to %s: its resource files (%s, %s) aren't installed in "
"res/data/ - see CONFIG.md.",
value, language_files[kLanguages[i].language],
mfdart_files[kLanguages[i].language]);
qs_show_message_dialog(message);
}
return false;
}
bool changed = gShockPrefs.goLanguage != kLanguages[i].language;
gShockPrefs.goLanguage = kLanguages[i].language;
if (changed && live)
language_change((uchar)kLanguages[i].language);
return changed;
}
WARN("%s: general.language=\"%s\" is invalid - ignoring", QS_CONFIG_FILENAME, value);
return false;
}
static const char *qs_current_language_name(void) {
for (size_t i = 0; i < QS_NUM_LANGUAGES; i++)
if (kLanguages[i].language == gShockPrefs.goLanguage)
return kLanguages[i].name;
return "english"; // shouldn't happen - safe fallback for Save
}
// ===========================================================================
// Read-only queries for the Video Options tab (see qs_config.h)
// ===========================================================================
@@ -483,17 +610,24 @@ static void qs_config_apply_parsed(const QsParsedConfig *parsed, bool live, char
size_t restart_needed_size) {
if (restart_needed && restart_needed_size > 0)
restart_needed[0] = '\0';
if (!parsed->has_video_options)
return;
if (parsed->video_options.renderer[0] != '\0') {
bool changed = qs_apply_renderer(parsed->video_options.renderer, live);
if (changed && QS_RENDERER_REQUIRES_RESTART)
qs_append_restart_reason(restart_needed, restart_needed_size, "renderer");
if (parsed->has_video_options) {
if (parsed->video_options.renderer[0] != '\0') {
bool changed = qs_apply_renderer(parsed->video_options.renderer, live);
if (changed && QS_RENDERER_REQUIRES_RESTART)
qs_append_restart_reason(restart_needed, restart_needed_size, "renderer");
}
if (parsed->video_options.resolution[0] != '\0') {
bool changed = qs_apply_resolution(parsed->video_options.resolution, live);
if (changed && QS_RESOLUTION_REQUIRES_RESTART)
qs_append_restart_reason(restart_needed, restart_needed_size, "resolution");
}
}
if (parsed->video_options.resolution[0] != '\0') {
bool changed = qs_apply_resolution(parsed->video_options.resolution, live);
if (changed && QS_RESOLUTION_REQUIRES_RESTART)
qs_append_restart_reason(restart_needed, restart_needed_size, "resolution");
if (parsed->has_general) {
if (parsed->general.language[0] != '\0') {
bool changed = qs_apply_language(parsed->general.language, live);
if (changed && QS_LANGUAGE_REQUIRES_RESTART)
qs_append_restart_reason(restart_needed, restart_needed_size, "language");
}
}
}
@@ -507,6 +641,9 @@ static bool qs_config_write_file(void) {
fprintf(f, " \"video_options\": {\n");
fprintf(f, " \"renderer\": \"%s\",\n", qs_current_renderer());
fprintf(f, " \"resolution\": \"%s\"\n", qs_current_resolution());
fprintf(f, " },\n");
fprintf(f, " \"general\": {\n");
fprintf(f, " \"language\": \"%s\"\n", qs_current_language_name());
fprintf(f, " }\n");
fprintf(f, "}\n");
fclose(f);
@@ -538,7 +675,10 @@ static jclass qs_find_class(JNIEnv *env, jobject activity, const char *name) {
return result;
}
static void qs_show_restart_dialog(const char *restart_needed) {
// Shared by qs_show_restart_dialog() and qs_show_message_dialog() below -
// both call a MenuOverlay static method taking a single String and
// returning void, differing only in which one.
static void qs_call_dialog_method(const char *java_method_name, const char *message) {
JNIEnv *env = (JNIEnv *)SDL_AndroidGetJNIEnv();
jobject activity = (jobject)SDL_AndroidGetActivity();
if (!env || !activity)
@@ -548,24 +688,34 @@ static void qs_show_restart_dialog(const char *restart_needed) {
if (!menuOverlayClass)
return;
jmethodID showRestartDialog =
(*env)->GetStaticMethodID(env, menuOverlayClass, "showRestartDialog", "(Ljava/lang/String;)V");
if (!showRestartDialog) {
jmethodID method =
(*env)->GetStaticMethodID(env, menuOverlayClass, java_method_name, "(Ljava/lang/String;)V");
if (!method) {
(*env)->ExceptionClear(env);
return;
}
jstring messageStr = (*env)->NewStringUTF(env, message);
(*env)->CallStaticVoidMethod(env, menuOverlayClass, method, messageStr);
if ((*env)->ExceptionCheck(env))
(*env)->ExceptionClear(env);
}
static void qs_show_restart_dialog(const char *restart_needed) {
char message[256];
snprintf(message, sizeof(message),
"The following changes require a restart to take effect: %s. Everything else has "
"already been applied.",
restart_needed);
jstring messageStr = (*env)->NewStringUTF(env, message);
(*env)->CallStaticVoidMethod(env, menuOverlayClass, showRestartDialog, messageStr);
if ((*env)->ExceptionCheck(env))
(*env)->ExceptionClear(env);
qs_call_dialog_method("showRestartDialog", message);
}
// Same restart-confirmation dialog view (MenuOverlay.java's
// showMessageDialog()), minus its Restart Now button - for a plain
// informational/warning message with nothing to restart into, currently
// only qs_apply_language()'s missing-resource-files warning above.
static void qs_show_message_dialog(const char *message) { qs_call_dialog_method("showMessageDialog", message); }
// ===========================================================================
// Public API (see qs_config.h)
// ===========================================================================
@@ -574,19 +724,21 @@ static Uint32 s_save_event_type;
static Uint32 s_load_event_type;
static Uint32 s_set_renderer_event_type;
static Uint32 s_set_resolution_event_type;
static Uint32 s_set_language_event_type;
void qs_config_load_startup(void) {
// Registers the custom SDL event types qs_config_request_save()/_load()/
// _set_renderer()/_set_resolution() enqueue - done here, once, on the
// engine's own thread and before the menu/its buttons could possibly
// exist yet, rather than lazily from those functions themselves (which
// run on Android's UI thread and could otherwise race each other doing
// this lazily).
Uint32 base = SDL_RegisterEvents(4);
// _set_renderer()/_set_resolution()/_set_language() enqueue - done here,
// once, on the engine's own thread and before the menu/its buttons could
// possibly exist yet, rather than lazily from those functions themselves
// (which run on Android's UI thread and could otherwise race each other
// doing this lazily).
Uint32 base = SDL_RegisterEvents(5);
s_save_event_type = base;
s_load_event_type = base + 1;
s_set_renderer_event_type = base + 2;
s_set_resolution_event_type = base + 3;
s_set_language_event_type = base + 4;
QsParsedConfig parsed;
if (qs_config_read_file(&parsed))
@@ -623,6 +775,14 @@ void qs_config_request_set_resolution(int resolution_index) {
SDL_PushEvent(&event);
}
void qs_config_request_set_language(int language_index) {
SDL_Event event;
SDL_zero(event);
event.type = s_set_language_event_type;
event.user.code = language_index;
SDL_PushEvent(&event);
}
bool qs_config_handle_event(const SDL_Event *event) {
if (event->type == s_save_event_type) {
if (qs_config_write_file())
@@ -655,5 +815,11 @@ bool qs_config_handle_event(const SDL_Event *event) {
qs_apply_resolution(kResolutions[index].name, /*live=*/true);
return true;
}
if (event->type == s_set_language_event_type) {
int index = event->user.code;
if (index >= 0 && (size_t)index < QS_NUM_LANGUAGES)
qs_apply_language(kLanguages[index].name, /*live=*/true);
return true;
}
return false;
}
+17 -7
View File
@@ -49,13 +49,20 @@ void qs_config_request_load(void);
void qs_config_request_set_renderer(bool opengl);
void qs_config_request_set_resolution(int resolution_index);
// Returns true and handles the event if it's one of the four custom events
// qs_config_request_save()/_load()/_set_renderer()/_set_resolution()
// enqueue - called from pump_events() (sdl_events.c, see
// Same cross-thread-dispatch reasoning as the two above - called from the
// Main Menu tab's Language row (MenuOverlay.java). language_index is 0
// English/1 French/2 German (gShockPrefs.goLanguage's own numbering,
// Prefs.h). Applies live immediately, the same way the game's own options
// menu applies a language change (see qs_config.c's qs_apply_language()).
void qs_config_request_set_language(int language_index);
// Returns true and handles the event if it's one of the five custom events
// qs_config_request_save()/_load()/_set_renderer()/_set_resolution()/
// _set_language() enqueue - called from pump_events() (sdl_events.c, see
// android/engine-patches/21-android-config.patch) on the engine's own
// thread, once per polled event, before its normal switch on well-known
// SDL_* event types. Returns false (no-op) for any other event type,
// including before the four custom types have even been registered.
// including before the five custom types have even been registered.
bool qs_config_handle_event(const SDL_Event *event);
// Read-only queries for the Video Options tab's own selection highlight
@@ -73,9 +80,12 @@ int qs_config_current_resolution_index(void);
// gShockPrefs.goLanguage (0 English/1 French/2 German, Prefs.h) - read by
// MenuOverlay.java's/KeyboardOverlay.java's periodic header-refresh timer
// to pick which language's chrome text (Strings.java) to display, so a
// language change made mid-session via the game's own options menu is
// picked up without needing to reopen either panel. Same
// off-thread-scalar-read reasoning as the two getters above.
// language change made mid-session via the game's own options menu (or our
// own Language row, or Load Configuration) is picked up without needing to
// reopen either panel; also doubles as the Main Menu tab's Language row's
// own selection highlight, same as qs_config_current_renderer_is_opengl()/
// qs_config_current_resolution_index() above. Same off-thread-scalar-read
// reasoning as those two getters.
int qs_config_current_language(void);
#ifdef __cplusplus
@@ -118,6 +118,13 @@ Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetResolution(JNIEnv *env, jc
qs_config_request_set_resolution(resolutionIndex);
}
// MenuOverlay's Main Menu tab's Language row - same enqueue-only reasoning as above.
JNIEXPORT void JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetLanguage(JNIEnv *env, jclass clazz,
jint languageIndex) {
qs_config_request_set_language(languageIndex);
}
// Read-only queries - safe to call directly (no enqueue needed, see
// qs_config.h's own comment on each of these).
JNIEXPORT jboolean JNICALL
@@ -29,8 +29,10 @@ import java.util.Locale;
* version, FPS/frame-time stats, refreshed on a periodic timer), a content
* area holding one tab's content at a time, and a bottom tab bar. Tab 1
* ("Main Menu") is the original "Keyboard"/"Save Configuration"/"Load
* Configuration" row plus the restart-confirmation view (shown instead of
* that row - see CONFIG.md and qs_config.c's *_REQUIRES_RESTART comments).
* Configuration" row plus a Language row, with a dialog view (shown instead
* of that content) doubling as both the restart-confirmation prompt (see
* CONFIG.md and qs_config.c's *_REQUIRES_RESTART comments) and a plain
* informational/warning popup (showMessageDialog() below).
* Tab 2 ("Video Options") has live-appliable controls for the config file's
* video_options group, mirroring the same buttons' selection to whatever's
* actually active (queried fresh whenever the tab is shown, and on the same
@@ -55,6 +57,10 @@ public class MenuOverlay extends OverlayPanel {
private static final long STATS_REFRESH_MS = 500;
private static final String[] RESOLUTION_LABELS = {"320x200", "320x400", "640x400", "640x480", "1024x768"};
// Order must match qs_config.c's kLanguages[]/gShockPrefs.goLanguage's
// own numbering (Prefs.h: 0 English/1 French/2 German) - also the keys
// this same language's own Strings.java translation is filed under.
private static final String[] LANGUAGE_KEYS = {"english", "french", "german"};
private static MenuOverlay instance;
@@ -79,7 +85,10 @@ public class MenuOverlay extends OverlayPanel {
private Button videoOptionsTabButton;
// Main Menu tab's own two states (only one VISIBLE at a time, within
// mainMenuTab)
// mainMenuTab) - mainMenuContent is the normal state (button row +
// Language row); restartDialog replaces it when Load Configuration
// finds a restart-required change.
private LinearLayout mainMenuContent;
private LinearLayout buttonRow;
private LinearLayout restartDialog;
private TextView restartMessageView;
@@ -89,6 +98,10 @@ public class MenuOverlay extends OverlayPanel {
private Button dismissButton;
private Button restartNowButton;
// Main Menu tab's Language row
private TextView languageLabel;
private Button[] languageButtons;
// Video Options tab controls
private TextView rendererLabel;
private TextView resolutionLabel;
@@ -187,6 +200,10 @@ public class MenuOverlay extends OverlayPanel {
private View buildMainMenuTab() {
FrameLayout tab = new FrameLayout(activity);
mainMenuContent = new LinearLayout(activity);
mainMenuContent.setOrientation(LinearLayout.VERTICAL);
mainMenuContent.setGravity(Gravity.CENTER);
buttonRow = new LinearLayout(activity);
buttonRow.setOrientation(LinearLayout.HORIZONTAL);
buttonRow.setGravity(Gravity.CENTER);
@@ -196,10 +213,32 @@ public class MenuOverlay extends OverlayPanel {
v -> nativeRequestSaveConfig());
loadButton = addRowButton(buttonRow, Strings.get("load_configuration", currentLanguage),
v -> nativeRequestLoadConfig());
FrameLayout.LayoutParams buttonRowParams = new FrameLayout.LayoutParams(
mainMenuContent.addView(buttonRow, new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
languageLabel = new TextView(activity);
languageLabel.setTextColor(0xFFFFFFFF);
languageLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18);
languageLabel.setGravity(Gravity.CENTER);
languageLabel.setPadding(0, 24, 0, 0);
languageLabel.setText(Strings.get("language", currentLanguage));
mainMenuContent.addView(languageLabel);
LinearLayout languageRow = new LinearLayout(activity);
languageRow.setOrientation(LinearLayout.HORIZONTAL);
languageButtons = new Button[LANGUAGE_KEYS.length];
for (int i = 0; i < LANGUAGE_KEYS.length; i++) {
final int index = i;
languageButtons[i] = addSelectableButton(languageRow, Strings.get(LANGUAGE_KEYS[i], currentLanguage),
v -> nativeRequestSetLanguage(index));
}
mainMenuContent.addView(languageRow, new LinearLayout.LayoutParams(
600, LinearLayout.LayoutParams.WRAP_CONTENT));
FrameLayout.LayoutParams mainMenuContentParams = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT);
buttonRowParams.gravity = Gravity.CENTER;
tab.addView(buttonRow, buttonRowParams);
mainMenuContentParams.gravity = Gravity.CENTER;
tab.addView(mainMenuContent, mainMenuContentParams);
restartMessageView = new TextView(activity);
restartMessageView.setTextColor(0xFFFFFFFF);
@@ -272,12 +311,17 @@ public class MenuOverlay extends OverlayPanel {
tabBar.setOrientation(LinearLayout.HORIZONTAL);
tabBar.setBackgroundColor(0xFF000080);
// Selectable background (like the Video Options tab's own Renderer/
// Resolution buttons) so the currently-active tab stays visibly
// highlighted, not just hovered/pressed - see the setSelected()
// calls in switchTab() below.
mainMenuTabButton = new Button(activity);
mainMenuTabButton.setText(Strings.get("main_menu", currentLanguage));
mainMenuTabButton.setBackground(buttonBackground());
mainMenuTabButton.setBackground(selectableButtonBackground());
mainMenuTabButton.setTextColor(buttonTextColors());
mainMenuTabButton.setOnClickListener(v -> switchTab(mainMenuTab));
mainMenuTabButton.setOnTouchListener(CLICK_ON_TOUCH_UP);
mainMenuTabButton.setSelected(true);
registerHoverable(mainMenuTabButton);
tabBar.addView(mainMenuTabButton,
new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
@@ -288,7 +332,7 @@ public class MenuOverlay extends OverlayPanel {
videoOptionsTabButton = new Button(activity);
videoOptionsTabButton.setText(Strings.get("video_options", currentLanguage));
videoOptionsTabButton.setBackground(buttonBackground());
videoOptionsTabButton.setBackground(selectableButtonBackground());
videoOptionsTabButton.setTextColor(buttonTextColors());
videoOptionsTabButton.setOnClickListener(v -> switchTab(videoOptionsTab));
videoOptionsTabButton.setOnTouchListener(CLICK_ON_TOUCH_UP);
@@ -306,9 +350,13 @@ public class MenuOverlay extends OverlayPanel {
currentTab.setVisibility(View.GONE);
tab.setVisibility(View.VISIBLE);
currentTab = tab;
mainMenuTabButton.setSelected(tab == mainMenuTab);
videoOptionsTabButton.setSelected(tab == videoOptionsTab);
resetHover();
if (tab == videoOptionsTab) {
refreshVideoOptionsSelection();
} else if (tab == mainMenuTab) {
refreshLanguageSelection();
}
relayoutAndRedraw();
}
@@ -389,6 +437,18 @@ public class MenuOverlay extends OverlayPanel {
}
}
// Same reasoning as refreshVideoOptionsSelection() above, for the Main
// Menu tab's Language row - nativeGetLanguage() is the same getter
// refreshStats() already polls to pick the menu/keyboard chrome text's
// own language, so this reflects a language change made any way (our
// own Language row, the game's own options menu, or Load Configuration).
private void refreshLanguageSelection() {
int language = nativeGetLanguage();
for (int i = 0; i < languageButtons.length; i++) {
languageButtons[i].setSelected(i == language);
}
}
// Polled every STATS_REFRESH_MS while this instance exists (started
// from the constructor) rather than only while the menu is actually
// visible - simpler than plumbing a new native->Java "menu became
@@ -423,6 +483,8 @@ public class MenuOverlay extends OverlayPanel {
if (currentTab == videoOptionsTab) {
refreshVideoOptionsSelection();
} else if (currentTab == mainMenuTab) {
refreshLanguageSelection();
}
// The stats text's width changes every tick (digit count, "ms"
@@ -461,6 +523,10 @@ public class MenuOverlay extends OverlayPanel {
softwareButton.setText(Strings.get("software", currentLanguage));
// openglButton's "OpenGL" label is never translated - see
// buildVideoOptionsTab()'s own comment on it.
languageLabel.setText(Strings.get("language", currentLanguage));
for (int i = 0; i < languageButtons.length; i++) {
languageButtons[i].setText(Strings.get(LANGUAGE_KEYS[i], currentLanguage));
}
}
// "Dismiss" on the restart dialog - the live-appliable subset was
@@ -468,7 +534,7 @@ public class MenuOverlay extends OverlayPanel {
// shown - see qs_config.c's qs_config_handle_event()), so this is
// purely a local UI action, no native call needed.
private void showButtonRow() {
buttonRow.setVisibility(View.VISIBLE);
mainMenuContent.setVisibility(View.VISIBLE);
restartDialog.setVisibility(View.GONE);
resetHover();
relayoutAndRedraw();
@@ -506,6 +572,8 @@ public class MenuOverlay extends OverlayPanel {
// Video Options tab's controls - same enqueue-only reasoning as above.
private static native void nativeRequestSetRenderer(boolean opengl);
private static native void nativeRequestSetResolution(int resolutionIndex);
// Main Menu tab's Language row - same enqueue-only reasoning as above.
private static native void nativeRequestSetLanguage(int languageIndex);
// Read-only queries - safe to call directly, see qs_config.h's own
// comment on each of these.
@@ -545,7 +613,27 @@ public class MenuOverlay extends OverlayPanel {
}
instance.activity.runOnUiThread(() -> {
instance.restartMessageView.setText(message);
instance.buttonRow.setVisibility(View.GONE);
instance.restartNowButton.setVisibility(View.VISIBLE);
instance.mainMenuContent.setVisibility(View.GONE);
instance.restartDialog.setVisibility(View.VISIBLE);
instance.resetHover();
instance.relayoutAndRedraw();
});
}
// Called from qs_config.c's qs_show_message_dialog(), same thread/
// dispatch reasoning as showRestartDialog() above - a plain
// informational/warning popup with nothing to restart into (currently
// only qs_apply_language()'s missing-resource-files warning), so it
// reuses the same dialog view minus the Restart Now button.
public static void showMessageDialog(final String message) {
if (instance == null) {
return;
}
instance.activity.runOnUiThread(() -> {
instance.restartMessageView.setText(message);
instance.restartNowButton.setVisibility(View.GONE);
instance.mainMenuContent.setVisibility(View.GONE);
instance.restartDialog.setVisibility(View.VISIBLE);
instance.resetHover();
instance.relayoutAndRedraw();
@@ -44,6 +44,10 @@ final class Strings {
put("renderer", "Renderer", "Moteur de rendu", "Renderer");
put("resolution", "Resolution", "Résolution", "Auflösung");
put("software", "Software", "Logiciel", "Software");
put("language", "Language", "Langue", "Sprache");
put("english", "English", "Anglais", "Englisch");
put("french", "French", "Français", "Französisch");
put("german", "German", "Allemand", "Deutsch");
// "OpenGL" is a proper/brand name - not translated, same in every
// language, so it has no entry here; call sites just use it as a
// literal.