From c193b536255534a9e36f42231ca4a7bf3c472fee Mon Sep 17 00:00:00 2001 From: rtw1x1 Date: Mon, 5 Jan 2026 18:55:18 +0000 Subject: [PATCH] ML: Hot reload locale system --- assets/root/intrologin.py | 103 ++----- assets/root/localeinfo.py | 2 + assets/root/uilocalechange.py | 472 ++++++++++++++++++++++++++++++++ assets/root/uilocaleselector.py | 191 +++++++++++++ 4 files changed, 693 insertions(+), 75 deletions(-) create mode 100644 assets/root/uilocalechange.py create mode 100644 assets/root/uilocaleselector.py diff --git a/assets/root/intrologin.py b/assets/root/intrologin.py index 343494c8..77955da2 100644 --- a/assets/root/intrologin.py +++ b/assets/root/intrologin.py @@ -17,6 +17,10 @@ import serverCommandParser import ime import uiScriptLocale +# Multi-language hot-reload system +from uilocaleselector import LocaleSelector +from uilocalechange import LocaleChangeManager + LOGIN_DELAY_SEC = 0.0 SKIP_LOGIN_PHASE = False SKIP_LOGIN_PHASE_SUPPORT_CHANNEL = False @@ -55,6 +59,13 @@ def GetLoginDelay(): app.SetGuildMarkPath("test") +############################################################################### +# Multi-Language Hot-Reload System +# All locale selector and hot-reload logic moved to: +# - uilocaleselector.py (UI component) +# - uilocalechange.py (hot-reload manager) +############################################################################### + class ConnectingDialog(ui.ScriptWindow): def __init__(self): @@ -144,11 +155,9 @@ class LoginWindow(ui.ScriptWindow): self.virtualKeyboardIsUpper = False self.timeOutMsg = False #Fix - self.language_list = [] - self.flag_button_list = [] - self.language_board = None - self.language_popup = None - self.__LoadLocale() + # Multi-language hot-reload system + self.localeSelector = None + self.localeChangeManager = LocaleChangeManager(self) def __del__(self): net.ClearPhaseWindow(net.PHASE_WINDOW_LOGIN, self) @@ -276,10 +285,9 @@ class LoginWindow(ui.ScriptWindow): self.connectingDialog = None self.loadingImage = None - self.language_list = [] - self.flag_button_list = [] - self.language_board = None - self.language_popup = None + if self.localeSelector: + self.localeSelector.Destroy() + self.localeSelector = None self.serverBoard = None self.serverList = None @@ -465,29 +473,11 @@ class LoginWindow(ui.ScriptWindow): self.GetChild("key_at").SetToggleDownEvent(lambda : self.__VirtualKeyboard_SetSymbolMode()) self.GetChild("key_at").SetToggleUpEvent(lambda : self.__VirtualKeyboard_SetAlphabetMode()) - self.language_board = ui.ThinBoard() - self.language_board.SetParent(self) - self.language_board.SetSize(wndMgr.GetScreenWidth(), 35) - self.language_board.SetPosition(0, 20) - self.language_board.Show() - - step = wndMgr.GetScreenWidth() / len(self.language_list) - x = 0 - - for i, lang in enumerate(self.language_list): - img_path = "d:/ymir work/ui/intro/login/server_flag_%s.sub" % lang - btn = ui.Button() - btn.SetParent(self.language_board) - btn.SetPosition(x + 15, 10) - btn.SetUpVisual(img_path) - btn.SetOverVisual(img_path) - btn.SetDownVisual(img_path) - btn.SetToolTipText(lang.upper()) - btn.SetEvent(ui.__mem_func__(self.__ClickLanguage), i) - btn.Show() - - self.flag_button_list.append(btn) - x += step + # Create locale selector (only if it doesn't exist - during hot-reload we keep the old one) + if not self.localeSelector: + self.localeSelector = LocaleSelector() + self.localeSelector.Create(self) + self.localeSelector.SetLocaleChangedEvent(ui.__mem_func__(self.__OnLocaleChanged)) except: import exception @@ -608,49 +598,12 @@ class LoginWindow(ui.ScriptWindow): def __OnClickExitButton(self): self.stream.SetPhaseWindow(0) - def __LoadLocale(self): - self.language_list = [ - "ae", "en", "cz", "de", "dk", - "es", "fr", "gr", "hu", "it", - "nl", "pl", "pt", "ro", "ru", "tr", - ] - - def __SaveLocale(self, locale): - try: - with open("config/locale.cfg", "wt") as f: - f.write(locale) - except: - import dbg - dbg.LogBox("__SaveLocale error locale.cfg") - app.Abort() - - def __ClickLanguage(self, index): - if index >= len(self.language_list): - return - - self.locale = self.language_list[index] - - if not self.language_popup: - self.language_popup = uiCommon.QuestionDialog() - - self.language_popup.SetText("Change language and restart the client?") - self.language_popup.SetAcceptEvent(ui.__mem_func__(self.__OnAcceptLanguage)) - self.language_popup.SetCancelEvent(ui.__mem_func__(self.__OnCancelLanguage)) - self.language_popup.Open() - - def __OnAcceptLanguage(self): - if self.language_popup: - self.language_popup.Close() - - self.__SaveLocale(self.locale) - - import os - app.Exit() - os.popen('start "" "Metin2_Debug.exe"') - - def __OnCancelLanguage(self): - if self.language_popup: - self.language_popup.Close() + def __OnLocaleChanged(self, newLocaleCode): + """ + Callback when user confirms locale change. + All the heavy lifting is done by LocaleChangeManager - this is just 3 lines! + """ + self.localeChangeManager.ReloadWithNewLocale(newLocaleCode, "uiscript/LoginWindow.py") def __SetServerInfo(self, name): net.SetServerInfo(name.strip()) diff --git a/assets/root/localeinfo.py b/assets/root/localeinfo.py index 2007f9ab..9cae9f65 100644 --- a/assets/root/localeinfo.py +++ b/assets/root/localeinfo.py @@ -13,6 +13,8 @@ BLEND_POTION_NO_INFO = 'BLEND_POTION_NO_INFO' LOGIN_FAILURE_WRONG_SOCIALID = 'LOGIN_FAILURE_WRONG_SOCIALID' LOGIN_FAILURE_SHUTDOWN_TIME = 'LOGIN_FAILURE_SHUTDOWN_TIME' +LOCALE_CHANGE_CONFIRM = 'Change language to %s and reload the game?' + GUILD_MEMBER_COUNT_INFINITY = 'INFINITY' GUILD_MARK_MIN_LEVEL = '3' GUILD_BUILDING_LIST_TXT = '{:s}/GuildBuildingList.txt'.format(APP_GET_LOCALE_PATH) diff --git a/assets/root/uilocalechange.py b/assets/root/uilocalechange.py new file mode 100644 index 00000000..54096089 --- /dev/null +++ b/assets/root/uilocalechange.py @@ -0,0 +1,472 @@ +# -*- coding: utf-8 -*- +""" +Multi-Language Hot-Reload System +================================= + +This module provides a clean, reusable system for hot-reloading locale changes +without restarting the client. It's designed to be window-agnostic and can be +used with any UI window (login, game, etc.). + +Architecture: +- C++ side handles: LoadConfig() → LoadLocaleData() → Clear Python module cache +- Python side handles: Module reimport → UI state save/restore → Window recreation + +Usage: + from uilocalechange import LocaleChangeManager + + # In your window class __init__: + self.localeChangeManager = LocaleChangeManager(self) + + # When locale changes (e.g., from a locale selector): + self.localeChangeManager.ReloadWithNewLocale(newLocaleCode) + +Features: +- Automatically saves and restores UI state (text inputs, selections, etc.) +- Handles board visibility states +- Preserves virtual keyboard state +- Works with any window that follows the standard pattern +""" + +import app +import dbg +import ui +import sys + + +class LocaleChangeManager: + """ + Manages locale hot-reload for a UI window. + + This class handles the complete lifecycle of a locale change: + 1. Save current UI state + 2. Call C++ to reload locale data + 3. Reimport Python locale modules + 4. Recreate UI with new locale + 5. Restore saved state + """ + + def __init__(self, window): + """ + Initialize the locale change manager. + + Args: + window: The UI window instance to manage (e.g., LoginWindow, GameWindow) + """ + self.window = window + self.stateHandlers = {} + self._RegisterDefaultStateHandlers() + + def _RegisterDefaultStateHandlers(self): + """Register default state save/restore handlers for common UI elements.""" + # Text input fields + self.RegisterStateHandler("idEditLine", self._SaveEditLineText, self._RestoreEditLineText) + self.RegisterStateHandler("pwdEditLine", self._SaveEditLineText, self._RestoreEditLineText) + + # List selections + self.RegisterStateHandler("serverList", self._SaveListSelection, self._RestoreListSelection) + self.RegisterStateHandler("channelList", self._SaveListSelection, self._RestoreListSelection) + + # Virtual keyboard + self.RegisterStateHandler("virtualKeyboard", self._SaveVisibility, self._RestoreVisibility) + + # Server info text + self.RegisterStateHandler("serverInfo", self._SaveTextLineText, self._RestoreTextLineText) + + def RegisterStateHandler(self, elementName, saveFunc, restoreFunc): + """ + Register a custom state handler for a UI element. + + Args: + elementName: The attribute name of the UI element (e.g., "idEditLine") + saveFunc: Function(element) -> stateData to save state + restoreFunc: Function(element, stateData) to restore state + """ + self.stateHandlers[elementName] = (saveFunc, restoreFunc) + + def _SaveEditLineText(self, editLine): + """Save text from an edit line.""" + if editLine: + return editLine.GetText() + return "" + + def _RestoreEditLineText(self, editLine, text): + """Restore text to an edit line.""" + if editLine and text: + editLine.SetText(text) + + def _SaveListSelection(self, listBox): + """Save selected item ID from a list box.""" + if listBox: + return listBox.GetSelectedItem() + return None + + def _RestoreListSelection(self, listBox, selectedID): + """Restore selected item in a list box by finding its position.""" + if listBox and selectedID is not None: + # Find position for the saved ID + for position, itemID in listBox.keyDict.items(): + if itemID == selectedID: + listBox.SelectItem(position) + return True + return False + + def _SaveVisibility(self, element): + """Save visibility state of an element.""" + if element: + return element.IsShow() + return False + + def _RestoreVisibility(self, element, wasVisible): + """Restore visibility state of an element.""" + if element: + if wasVisible: + element.Show() + else: + element.Hide() + + def _SaveTextLineText(self, textLine): + """Save text from a text line.""" + if textLine: + return textLine.GetText() + return "" + + def _RestoreTextLineText(self, textLine, text): + """Restore text to a text line.""" + if textLine and text: + textLine.SetText(text) + + def SaveLocaleCode(self, localeCode): + """ + Save locale code to config/locale.cfg file. + + Args: + localeCode: Two-letter locale code (e.g., "en", "ro", "de") + + Returns: + True if saved successfully, False otherwise + """ + try: + import os + if not os.path.exists("config"): + os.makedirs("config") + with open("config/locale.cfg", "w") as f: + f.write(localeCode) + return True + except Exception as e: + dbg.TraceError("Failed to save locale config: %s" % str(e)) + return False + + def ReloadWithNewLocale(self, newLocaleCode, scriptPath="uiscript/LoginWindow.py"): + """ + Hot-reload the UI with a new locale. + + This is the main entry point for locale changes. It handles the complete + reload process: + 1. Save locale code to config file + 2. Call C++ to reload locale data and clear Python module cache + 3. Reimport Python locale modules + 4. Save current UI state + 5. Recreate UI + 6. Restore UI state + + Args: + newLocaleCode: The new locale code to switch to + scriptPath: Path to the UI script file to reload (default: LoginWindow.py) + + Returns: + True if reload succeeded, False otherwise + """ + try: + dbg.TraceError("=== Starting Locale Hot-Reload to '%s' ===" % newLocaleCode) + + # Step 1: Save locale code to file + if not self.SaveLocaleCode(newLocaleCode): + dbg.TraceError("Failed to save locale code") + return False + + # Step 2: Call C++ comprehensive reload + # This does: LoadConfig() → LoadLocaleData() → Clear Python module cache + reloadSuccess = app.ReloadLocale() + if not reloadSuccess: + dbg.TraceError("C++ ReloadLocale() failed") + return False + + # Step 3: Reimport Python locale modules + self._ReimportLocaleModules() + + # Step 4: Save current UI state + savedState = self._SaveWindowState() + + # Step 5: Recreate UI + self._RecreateUI(scriptPath) + + # Step 6: Restore UI state + self._RestoreWindowState(savedState) + + dbg.TraceError("=== Locale Hot-Reload Complete ===") + return True + + except Exception as e: + dbg.TraceError("Error in ReloadWithNewLocale: %s" % str(e)) + import exception + exception.Abort("ReloadWithNewLocale") + return False + + def _ReimportLocaleModules(self): + """Force reimport of locale modules after C++ cleared the cache.""" + dbg.TraceError("Reimporting locale modules...") + + # Import fresh modules - C++ already deleted them from sys.modules + localeInfo = __import__('localeInfo') + uiScriptLocale = __import__('uiScriptLocale') + + # Update sys.modules references + sys.modules['localeInfo'] = localeInfo + sys.modules['uiScriptLocale'] = uiScriptLocale + + # CRITICAL: Update the window module's globals, not ours! + # Get the window's module (e.g., intrologin module) + windowModule = sys.modules.get(self.window.__module__) + if windowModule: + dbg.TraceError("Updating globals in module: %s" % self.window.__module__) + windowModule.localeInfo = localeInfo + windowModule.uiScriptLocale = uiScriptLocale + else: + dbg.TraceError("WARNING: Could not find window module: %s" % self.window.__module__) + + # Also update this module's globals for safety + globals()['localeInfo'] = localeInfo + globals()['uiScriptLocale'] = uiScriptLocale + + dbg.TraceError("Locale modules reimported successfully") + + def _SaveWindowState(self): + """ + Save the current state of all registered UI elements. + + Returns: + Dictionary containing saved state data + """ + state = { + "elements": {}, + "visibleBoard": None, + } + + # Save state of registered elements + for elementName, (saveFunc, _) in self.stateHandlers.items(): + if hasattr(self.window, elementName): + element = getattr(self.window, elementName) + if element: + state["elements"][elementName] = saveFunc(element) + + # Determine which board is currently visible + for boardName in ["loginBoard", "serverBoard", "connectBoard"]: + if hasattr(self.window, boardName): + board = getattr(self.window, boardName) + if board and board.IsShow(): + state["visibleBoard"] = boardName + break + + dbg.TraceError("Saved window state: visibleBoard=%s" % state["visibleBoard"]) + return state + + def _RecreateUI(self, scriptPath): + """ + Recreate the UI by clearing and reloading the script. + + Args: + scriptPath: Path to the UI script file + """ + dbg.TraceError("Recreating UI from script: %s" % scriptPath) + + # Completely destroy the locale selector - we'll recreate it fresh + if hasattr(self.window, 'localeSelector') and self.window.localeSelector: + dbg.TraceError("Destroying old locale selector before UI recreation") + self.window.localeSelector.Destroy() + self.window.localeSelector = None + + # Clear existing UI elements + if hasattr(self.window, 'ClearDictionary'): + self.window.ClearDictionary() + + # Reload the UI script file with new locale strings + # This will create a fresh locale selector through __LoadScript + if hasattr(self.window, '_LoginWindow__LoadScript'): + # Private method name mangling for LoginWindow + self.window._LoginWindow__LoadScript(scriptPath) + elif hasattr(self.window, 'LoadScript'): + self.window.LoadScript(scriptPath) + + # __LoadScript should have created a new locale selector + # Make sure it's visible + if hasattr(self.window, 'localeSelector') and self.window.localeSelector: + dbg.TraceError("Locale selector created by __LoadScript, ensuring visibility") + self.window.localeSelector.Show() + self.window.localeSelector.SetTop() + else: + # If __LoadScript didn't create it, create it manually + dbg.TraceError("Creating locale selector manually") + from uilocaleselector import LocaleSelector + self.window.localeSelector = LocaleSelector() + self.window.localeSelector.Create(self.window) + # Set the event handler to call back to the window's method + if hasattr(self.window, '_LoginWindow__OnLocaleChanged'): + import ui + self.window.localeSelector.SetLocaleChangedEvent(ui.__mem_func__(self.window._LoginWindow__OnLocaleChanged)) + self.window.localeSelector.Show() + self.window.localeSelector.SetTop() + + # Hide all boards to reset state + for boardName in ["loginBoard", "serverBoard", "connectBoard"]: + if hasattr(self.window, boardName): + board = getattr(self.window, boardName) + if board: + board.Hide() + + # Hide virtual keyboard + if hasattr(self.window, "virtualKeyboard"): + vk = getattr(self.window, "virtualKeyboard") + if vk: + vk.Hide() + + def _RestoreWindowState(self, state): + """ + Restore the saved UI state. + + Args: + state: Dictionary containing saved state data + """ + dbg.TraceError("Restoring window state...") + + # Restore element states + for elementName, savedData in state["elements"].items(): + if elementName in self.stateHandlers: + _, restoreFunc = self.stateHandlers[elementName] + if hasattr(self.window, elementName): + element = getattr(self.window, elementName) + if element: + restoreFunc(element, savedData) + + # Rebuild locale-dependent dictionaries (like loginFailureMsgDict) + self._RebuildLocaleDictionaries() + + # Restore visible board + visibleBoard = state.get("visibleBoard") + if visibleBoard: + self._RestoreBoardVisibility(visibleBoard, state) + + # CRITICAL: Make sure locale selector is visible and on top after everything is restored + if hasattr(self.window, 'localeSelector') and self.window.localeSelector: + dbg.TraceError("Final check: ensuring locale selector is visible") + self.window.localeSelector.Show() + self.window.localeSelector.SetTop() + + def _RebuildLocaleDictionaries(self): + """Rebuild any dictionaries that depend on localeInfo strings.""" + # Check if this is a LoginWindow with loginFailureMsgDict + if hasattr(self.window, 'loginFailureMsgDict'): + import localeInfo + dbg.TraceError("Rebuilding loginFailureMsgDict with new locale strings") + self.window.loginFailureMsgDict = { + "ALREADY" : localeInfo.LOGIN_FAILURE_ALREAY, + "NOID" : localeInfo.LOGIN_FAILURE_NOT_EXIST_ID, + "WRONGPWD" : localeInfo.LOGIN_FAILURE_WRONG_PASSWORD, + "FULL" : localeInfo.LOGIN_FAILURE_TOO_MANY_USER, + "SHUTDOWN" : localeInfo.LOGIN_FAILURE_SHUTDOWN, + "REPAIR" : localeInfo.LOGIN_FAILURE_REPAIR_ID, + "BLOCK" : localeInfo.LOGIN_FAILURE_BLOCK_ID, + "BESAMEKEY" : localeInfo.LOGIN_FAILURE_BE_SAME_KEY, + "NOTAVAIL" : localeInfo.LOGIN_FAILURE_NOT_AVAIL, + "NOBILL" : localeInfo.LOGIN_FAILURE_NOBILL, + "BLKLOGIN" : localeInfo.LOGIN_FAILURE_BLOCK_LOGIN, + "WEBBLK" : localeInfo.LOGIN_FAILURE_WEB_BLOCK, + "BADSCLID" : localeInfo.LOGIN_FAILURE_WRONG_SOCIALID, + "AGELIMIT" : localeInfo.LOGIN_FAILURE_SHUTDOWN_TIME, + } + + if hasattr(self.window, 'loginFailureFuncDict'): + self.window.loginFailureFuncDict = { + "WRONGPWD" : self.window._LoginWindow__DisconnectAndInputPassword if hasattr(self.window, '_LoginWindow__DisconnectAndInputPassword') else None, + "QUIT" : __import__('app').Exit, + } + + def _RestoreBoardVisibility(self, boardName, state): + """ + Restore the visibility of a specific board and its state. + + Args: + boardName: Name of the board to show ("loginBoard", "serverBoard", "connectBoard") + state: Full saved state dictionary + """ + dbg.TraceError("Restoring board visibility: %s" % boardName) + + if boardName == "loginBoard": + self._RestoreLoginBoard() + elif boardName == "serverBoard": + self._RestoreServerBoard(state) + elif boardName == "connectBoard": + self._RestoreConnectBoard() + + def _RestoreLoginBoard(self): + """Show and configure the login board.""" + if hasattr(self.window, "loginBoard"): + self.window.loginBoard.Show() + + def _RestoreServerBoard(self, state): + """Show and configure the server board with saved selections.""" + if not hasattr(self.window, "serverBoard"): + return + + self.window.serverBoard.Show() + + # Refresh server list first + if hasattr(self.window, '_LoginWindow__RefreshServerList'): + self.window._LoginWindow__RefreshServerList() + + # Now restore server selection AFTER the list is refreshed + savedServerID = state["elements"].get("serverList") + if savedServerID is not None and hasattr(self.window, 'serverList') and self.window.serverList: + # Find the position index for the saved server ID + # keyDict maps position -> server ID + for position, serverID in self.window.serverList.keyDict.items(): + if serverID == savedServerID: + # SelectItem expects position index, not server ID + self.window.serverList.SelectItem(position) + + # Refresh channel list for the selected server + if hasattr(self.window, '_LoginWindow__RequestServerStateList'): + self.window._LoginWindow__RequestServerStateList() + if hasattr(self.window, '_LoginWindow__RefreshServerStateList'): + self.window._LoginWindow__RefreshServerStateList() + + # Restore channel selection AFTER channel list is refreshed + savedChannelID = state["elements"].get("channelList") + if savedChannelID is not None and hasattr(self.window, 'channelList') and self.window.channelList: + # Find the position index for the saved channel ID + for channelPos, channelID in self.window.channelList.keyDict.items(): + if channelID == savedChannelID: + self.window.channelList.SelectItem(channelPos) + break + break + + def _RestoreConnectBoard(self): + """Show and configure the connect board.""" + if not hasattr(self.window, "connectBoard"): + return + + # Connect board overlays login board + if hasattr(self.window, "loginBoard"): + self.window.loginBoard.Show() + + self.window.connectBoard.Show() + + # Explicitly show connect board children (they may not auto-show) + if hasattr(self.window, "selectConnectButton"): + btn = getattr(self.window, "selectConnectButton") + if btn: + btn.Show() + + if hasattr(self.window, "serverInfo"): + info = getattr(self.window, "serverInfo") + if info: + info.Show() diff --git a/assets/root/uilocaleselector.py b/assets/root/uilocaleselector.py new file mode 100644 index 00000000..e32a49e2 --- /dev/null +++ b/assets/root/uilocaleselector.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- +""" +Locale Selector UI Component +============================= + +A reusable UI component for selecting and changing the client language. +Can be added to any window (login, game settings, etc.). + +Usage: + from uilocaleselector import LocaleSelector + + # In your window class: + self.localeSelector = LocaleSelector() + self.localeSelector.Create(self) + self.localeSelector.SetLocaleChangedEvent(ui.__mem_func__(self.__OnLocaleChanged)) + + # Implement the callback: + def __OnLocaleChanged(self, newLocaleCode): + # Handle UI recreation here + pass +""" + +import ui +import uiCommon +import localeInfo +import wndMgr + + +# Available locales configuration +AVAILABLE_LOCALES = [ + {"code": "ae", "name": "Arabic", "flag": "ae"}, + {"code": "en", "name": "English", "flag": "en"}, + {"code": "cz", "name": "Čeština", "flag": "cz"}, + {"code": "de", "name": "Deutsch", "flag": "de"}, + {"code": "dk", "name": "Dansk", "flag": "dk"}, + {"code": "es", "name": "Español", "flag": "es"}, + {"code": "fr", "name": "Français", "flag": "fr"}, + {"code": "gr", "name": "Ελληνικά", "flag": "gr"}, + {"code": "hu", "name": "Magyar", "flag": "hu"}, + {"code": "it", "name": "Italiano", "flag": "it"}, + {"code": "nl", "name": "Nederlands", "flag": "nl"}, + {"code": "pl", "name": "Polski", "flag": "pl"}, + {"code": "pt", "name": "Português", "flag": "pt"}, + {"code": "ro", "name": "Română", "flag": "ro"}, + {"code": "ru", "name": "Русский", "flag": "ru"}, + {"code": "tr", "name": "Türkçe", "flag": "tr"}, +] + +# Flag image path template +FLAG_IMAGE_PATH = "d:/ymir work/ui/intro/login/server_flag_%s.sub" + + +class LocaleSelector(ui.Window): + """ + UI component for selecting and changing client language. + + Features: + - Displays flag buttons for all available locales + - Shows confirmation dialog before changing + - Triggers callback when locale is confirmed + - Self-contained and reusable + """ + + def __init__(self): + ui.Window.__init__(self) + self.background = None + self.flagButtons = [] + self.confirmDialog = None + self.selectedLocaleCode = None + self.eventLocaleChanged = None + + def __del__(self): + ui.Window.__del__(self) + + def Destroy(self): + """Clean up resources when destroying the selector.""" + self.eventLocaleChanged = None + self.selectedLocaleCode = None + + if self.confirmDialog: + self.confirmDialog.Close() + self.confirmDialog = None + + for btn in self.flagButtons: + btn.Hide() + btn = None + self.flagButtons = [] + + if self.background: + self.background.Hide() + self.background = None + + def Create(self, parent): + """ + Create and display the locale selector UI. + + Args: + parent: The parent window to attach to + """ + self.SetParent(parent) + self.SetSize(wndMgr.GetScreenWidth(), 35) + self.SetPosition(0, 20) + + # Create background board + self.background = ui.ThinBoard() + self.background.SetParent(self) + self.background.SetSize(wndMgr.GetScreenWidth(), 35) + self.background.SetPosition(0, 0) + self.background.Show() + + # Create flag buttons + self._CreateFlagButtons() + + self.Show() + + def _CreateFlagButtons(self): + """Create flag buttons for all available locales.""" + localeCount = len(AVAILABLE_LOCALES) + if localeCount == 0: + return + + buttonSpacing = wndMgr.GetScreenWidth() / localeCount + xPosition = 0 + + for locale in AVAILABLE_LOCALES: + flagPath = FLAG_IMAGE_PATH % locale["flag"] + + button = ui.Button() + button.SetParent(self.background) + button.SetPosition(xPosition + 15, 10) + button.SetUpVisual(flagPath) + button.SetOverVisual(flagPath) + button.SetDownVisual(flagPath) + button.SetToolTipText(locale["name"]) + button.SetEvent(ui.__mem_func__(self._OnClickFlag), locale["code"]) + button.Show() + + self.flagButtons.append(button) + xPosition += buttonSpacing + + def _OnClickFlag(self, localeCode): + """ + Handle flag button click - show confirmation dialog. + + Args: + localeCode: The locale code that was clicked + """ + self.selectedLocaleCode = localeCode + + # Get locale name for display + localeName = "Unknown" + for locale in AVAILABLE_LOCALES: + if locale["code"] == localeCode: + localeName = locale["name"] + break + + # Show confirmation dialog + if not self.confirmDialog: + self.confirmDialog = uiCommon.QuestionDialog() + + self.confirmDialog.SetText(localeInfo.LOCALE_CHANGE_CONFIRM % localeName) + self.confirmDialog.SetAcceptEvent(ui.__mem_func__(self._OnConfirmLocaleChange)) + self.confirmDialog.SetCancelEvent(ui.__mem_func__(self._OnCancelLocaleChange)) + self.confirmDialog.Open() + + def _OnConfirmLocaleChange(self): + """User confirmed locale change - trigger the callback.""" + if self.confirmDialog: + self.confirmDialog.Close() + + if not self.selectedLocaleCode: + return + + # Notify parent window to handle the locale change + if self.eventLocaleChanged: + self.eventLocaleChanged(self.selectedLocaleCode) + + def _OnCancelLocaleChange(self): + """User cancelled locale change.""" + if self.confirmDialog: + self.confirmDialog.Close() + self.selectedLocaleCode = None + + def SetLocaleChangedEvent(self, event): + """ + Set callback function to be called when locale is confirmed. + + Args: + event: Callback function(localeCode) to handle locale change + """ + self.eventLocaleChanged = event