Repository: MyreMylar/pygame_gui (python)

Issue:
Only English words are supported in UITextEntryBox and UITextEntryLine, instead of multiple languages.
**Describe the bug**
There will be no some particular displays of typing something lke mandarin, Japanese, etc.

**To Reproduce**
Steps to reproduce the behaviour:
1. typing mandarin or any other language except English, but not just copy it to the UITextEntryBox or UITextEntryLine :)

**Expected behaviour**
Nothing will show on the textbox.

**Platform and software (please complete the following information):**
 - OS: Windows
 - Pygame GUI version 0.6.8
 - Pygame version 2.2.0

**Additional context**
i do not have any ideas to solve this problem now, waiting for some big cheeses :)


Pull request description:
Switch to processing text input with TEXTINPUT events
Will hopefully allow better handling of text input in other languages.

maybe fixes #413 

Patch:
diff --git a/pygame_gui/elements/ui_text_entry_box.py b/pygame_gui/elements/ui_text_entry_box.py
index 133c403..9c60804 100644
--- a/pygame_gui/elements/ui_text_entry_box.py
+++ b/pygame_gui/elements/ui_text_entry_box.py
@@ -3,7 +3,7 @@ from typing import Union, Tuple, Dict, Optional
 
 from pygame_gui.core.utility import clipboard_paste, clipboard_copy
 
-from pygame import Rect, MOUSEBUTTONDOWN, MOUSEBUTTONUP, BUTTON_LEFT, KEYDOWN
+from pygame import Rect, MOUSEBUTTONDOWN, MOUSEBUTTONUP, BUTTON_LEFT, KEYDOWN, TEXTINPUT
 from pygame import KMOD_META, KMOD_CTRL, K_a, K_x, K_c, K_v
 from pygame import K_LEFT, K_RIGHT, K_UP, K_DOWN, K_HOME, K_END, K_BACKSPACE, K_DELETE, K_RETURN
 from pygame import key
@@ -239,8 +239,13 @@ class UITextEntryBox(UITextBox):
                 consumed_event = True
             elif self._process_action_key_event(event):
                 consumed_event = True
-            elif self._process_text_entry_key(event):
-                consumed_event = True
+
+        if self.is_enabled and self.is_focused and event.type == TEXTINPUT:
+            processed_any_char = False
+            for char in event.text:
+                if self._process_entered_character(char):
+                    processed_any_char = True
+            consumed_event = processed_any_char
 
         if self.html_text != initial_text_state:
             # new event
@@ -262,8 +267,16 @@ class UITextEntryBox(UITextBox):
 
         """
         consumed_event = False
-
-        if event.key == K_BACKSPACE:
+        if event.key == K_RETURN:
+            start_str = self.html_text[:self.edit_position]
+            end_str = self.html_text[self.edit_position:]
+            self.html_text = start_str + '\n' + end_str
+            self.text_box_layout.insert_line_break(self.edit_position, self.parser)
+            self.edit_position += 1
+            self.redraw_from_text_block()
+            self.cursor_has_moved_recently = True
+            consumed_event = True
+        elif event.key == K_BACKSPACE:
             if abs(self.select_range[0] - self.select_range[1]) > 0:
                 self.text_box_layout.delete_selected_text()
                 low_end = min(self.select_range[0], self.select_range[1])
@@ -432,44 +445,40 @@ class UITextEntryBox(UITextBox):
         :return: True if consumed.
         """
         consumed_event = False
-        if event.key == K_RETURN:
-            start_str = self.html_text[:self.edit_position]
-            end_str = self.html_text[self.edit_position:]
-            self.html_text = start_str + '\n' + end_str
-            self.text_box_layout.insert_line_break(self.edit_position, self.parser)
-            self.edit_position += 1
-            self.redraw_from_text_block()
-            self.cursor_has_moved_recently = True
-            consumed_event = True
 
-        elif hasattr(event, 'unicode'):
+        if hasattr(event, 'unicode'):
             character = event.unicode
-            # here we really want to get the font metrics for the current active style where the edit cursor is
-            # instead we will make do for now
-            font = self.ui_theme.get_font(self.combined_element_ids)
-            char_metrics = font.get_metrics(character)
-            if len(char_metrics) > 0 and char_metrics[0] is not None:
-                valid_character = True
-                if valid_character:
-                    if abs(self.select_range[0] - self.select_range[1]) > 0:
-                        low_end = min(self.select_range[0], self.select_range[1])
-                        high_end = max(self.select_range[0], self.select_range[1])
-                        self.html_text = self.html_text[:low_end] + character + self.html_text[high_end:]
-                        self.text_box_layout.delete_selected_text()
-                        self.text_box_layout.insert_text(character, low_end, self.parser)
-                        self.edit_position = low_end + 1
-                        self.select_range = [0, 0]
-                    else:
-                        start_str = self.html_text[:self.edit_position]
-                        end_str = self.html_text[self.edit_position:]
-                        self.html_text = start_str + character + end_str
-                        self.text_box_layout.insert_text(character, self.edit_position, self.parser)
-                        self.edit_position += 1
-                    self.redraw_from_text_block()
-                    self.cursor_has_moved_recently = True
-                    consumed_event = True
+            consumed_event = self._process_entered_character(character)
         return consumed_event
 
+    def _process_entered_character(self, character: str) -> bool:
+        processed_char = False
+        # here we really want to get the font metrics for the current active style where the edit cursor is
+        # instead we will make do for now
+        font = self.ui_theme.get_font(self.combined_element_ids)
+        char_metrics = font.get_metrics(character)
+        if len(char_metrics) > 0 and char_metrics[0] is not None:
+            valid_character = True
+            if valid_character:
+                if abs(self.select_range[0] - self.select_range[1]) > 0:
+                    low_end = min(self.select_range[0], self.select_range[1])
+                    high_end = max(self.select_range[0], self.select_range[1])
+                    self.html_text = self.html_text[:low_end] + character + self.html_text[high_end:]
+                    self.text_box_layout.delete_selected_text()
+                    self.text_box_layout.insert_text(character, low_end, self.parser)
+                    self.edit_position = low_end + 1
+                    self.select_range = [0, 0]
+                else:
+                    start_str = self.html_text[:self.edit_position]
+                    end_str = self.html_text[self.edit_position:]
+                    self.html_text = start_str + character + end_str
+                    self.text_box_layout.insert_text(character, self.edit_position, self.parser)
+                    self.edit_position += 1
+                self.redraw_from_text_block()
+                self.cursor_has_moved_recently = True
+                processed_char = True
+        return processed_char
+
     def _process_keyboard_shortcut_event(self, event: Event) -> bool:
         """
         Check if event is one of the CTRL key keyboard shortcuts.
diff --git a/pygame_gui/elements/ui_text_entry_line.py b/pygame_gui/elements/ui_text_entry_line.py
index 77668a3..b67c9fb 100644
--- a/pygame_gui/elements/ui_text_entry_line.py
+++ b/pygame_gui/elements/ui_text_entry_line.py
@@ -406,8 +406,13 @@ class UITextEntryLine(UIElement):
                 consumed_event = True
             elif self._process_action_key_event(event):
                 consumed_event = True
-            elif self._process_text_entry_key(event):
-                consumed_event = True
+
+        if self.is_enabled and self.is_focused and event.type == pygame.TEXTINPUT:
+            processed_any_char = False
+            for char in event.text:
+                if self._process_entered_character(char):
+                    processed_any_char = True
+            consumed_event = processed_any_char
 
         if self.is_enabled and self.is_focused and event.type == pygame.KEYUP:
             if event.key == pygame.K_RETURN or event.key == pygame.K_KP_ENTER:
@@ -429,6 +434,12 @@ class UITextEntryLine(UIElement):
         return consumed_event
 
     def _process_text_entry_key(self, event: pygame.event.Event) -> bool:
+        consumed_event = False
+        if hasattr(event, 'unicode'):
+            consumed_event = self._process_entered_character(event.unicode)
+        return consumed_event
+
+    def _process_entered_character(self, character: str) -> bool:
         """
         Process key input that can be added to the text entry text.
 
@@ -436,15 +447,14 @@ class UITextEntryLine(UIElement):
 
         :return: True if consumed.
         """
-        consumed_event = False
+        processed_character = False
         within_length_limit = True
         if (self.length_limit is not None
                 and (len(self.text) -
                      abs(self.select_range[0] -
                          self.select_range[1])) >= self.length_limit):
             within_length_limit = False
-        if within_length_limit and hasattr(event, 'unicode') and self.font is not None:
-            character = event.unicode
+        if within_length_limit and self.font is not None:
             char_metrics = self.font.get_metrics(character)
             if len(char_metrics) > 0 and char_metrics[0] is not None:
                 valid_character = True
@@ -476,8 +486,8 @@ class UITextEntryLine(UIElement):
 
                         self.edit_position += 1
                     self.cursor_has_moved_recently = True
-                    consumed_event = True
-        return consumed_event
+                    processed_character = True
+        return processed_character
 
     def _process_action_key_event(self, event: pygame.event.Event) -> bool:
         """


Test patch:
diff --git a/tests/test_elements/test_ui_text_entry_box.py b/tests/test_elements/test_ui_text_entry_box.py
index 42fc242..9b1fced 100644
--- a/tests/test_elements/test_ui_text_entry_box.py
+++ b/tests/test_elements/test_ui_text_entry_box.py
@@ -714,17 +714,12 @@ class TestUITextEntryBox:
         assert text_entry.is_enabled is True
         text_entry.focus()
         # process a mouse button down event
-        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
-                                                                          {'key': pygame.K_d,
-                                                                           'mod': 0,
-                                                                           'unicode': 'd'}))
+        processed_text_input_event = text_entry.process_event(pygame.event.Event(pygame.TEXTINPUT, {'text': 'd'}))
 
-        text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_a, 'mod': 0,
-                                                                     'unicode': 'a'}))
-        text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_n, 'mod': 0,
-                                                                     'unicode': 'n'}))
+        text_entry.process_event(pygame.event.Event(pygame.TEXTINPUT, {'text': 'a'}))
+        text_entry.process_event(pygame.event.Event(pygame.TEXTINPUT, {'text': 'n'}))
 
-        assert processed_key_event is True and text_entry.get_text() == 'dan'
+        assert processed_text_input_event is True and text_entry.get_text() == 'dan'
 
     def test_on_locale_changed(self, _init_pygame, default_ui_manager, _display_surface_return_none):
         text_box = UITextEntryBox(initial_text='la la LA LA LAL LAL ALALA'
@@ -862,17 +857,12 @@ class TestUITextEntryBox:
 
         text_entry.focus()
 
-        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
-                                                                          {'key': pygame.K_d,
-                                                                           'mod': 0,
-                                                                           'unicode': 'd'}))
+        processed_text_input_event = text_entry.process_event(pygame.event.Event(pygame.TEXTINPUT, {'text': 'd'}))
 
-        text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_a, 'mod': 0,
-                                                                     'unicode': 'a'}))
-        text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_n, 'mod': 0,
-                                                                     'unicode': 'n'}))
+        text_entry.process_event(pygame.event.Event(pygame.TEXTINPUT, {'text': 'a'}))
+        text_entry.process_event(pygame.event.Event(pygame.TEXTINPUT, {'text': 'n'}))
 
-        assert processed_key_event and text_entry.get_text() == 'dan'
+        assert processed_text_input_event and text_entry.get_text() == 'dan'
 
     def test_process_event_text_entered_with_select_range(self, _init_pygame: None,
                                                           default_ui_manager: UIManager,
@@ -884,13 +874,10 @@ class TestUITextEntryBox:
         text_entry.focus()
         text_entry.select_range = [1, 9]
 
-        # process a mouse button down event
-        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
-                                                                          {'key': pygame.K_o,
-                                                                           'mod': 0,
-                                                                           'unicode': 'o'}))
+        # process a text input event
+        processed_text_input_event = text_entry.process_event(pygame.event.Event(pygame.TEXTINPUT, {'text': 'o'}))
 
-        assert (processed_key_event is True and
+        assert (processed_text_input_event is True and
                 text_entry.get_text() == 'Ho hours of fun writing tests')
 
     def test_process_event_text_ctrl_c(self, _init_pygame: None,
diff --git a/tests/test_elements/test_ui_text_entry_line.py b/tests/test_elements/test_ui_text_entry_line.py
index a5cbfad..b7fd453 100644
--- a/tests/test_elements/test_ui_text_entry_line.py
+++ b/tests/test_elements/test_ui_text_entry_line.py
@@ -262,15 +262,10 @@ class TestUITextEntryLine:
 
         text_entry.focus()
 
-        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
-                                                                          {'key': pygame.K_d,
-                                                                           'mod': 0,
-                                                                           'unicode': 'd'}))
+        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.TEXTINPUT, {'text': 'd'}))
 
-        text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_a, 'mod': 0,
-                                                                     'unicode': 'a'}))
-        text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_n, 'mod': 0,
-                                                                     'unicode': 'n'}))
+        text_entry.process_event(pygame.event.Event(pygame.TEXTINPUT, {'text': 'a'}))
+        text_entry.process_event(pygame.event.Event(pygame.TEXTINPUT, {'text': 'n'}))
 
         assert processed_key_event and text_entry.get_text() == 'dan'
 
@@ -318,13 +313,10 @@ class TestUITextEntryLine:
         text_entry.focus()
         text_entry.select_range = [1, 9]
 
-        # process a mouse button down event
-        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
-                                                                          {'key': pygame.K_o,
-                                                                           'mod': 0,
-                                                                           'unicode': 'o'}))
+        # process a text input event
+        processed_text_input_event = text_entry.process_event(pygame.event.Event(pygame.TEXTINPUT, {'text': 'o'}))
 
-        assert (processed_key_event is True and
+        assert (processed_text_input_event is True and
                 text_entry.get_text() == 'Ho hours of fun writing tests')
 
     def test_process_event_text_entered_too_long(self, _init_pygame: None,
@@ -336,18 +328,13 @@ class TestUITextEntryLine:
         text_entry.set_text_length_limit(3)
         text_entry.focus()
 
-        text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_t, 'mod': 0,
-                                                                     'unicode': 't'}))
-        text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_e, 'mod': 0,
-                                                                     'unicode': 'e'}))
-        text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_s, 'mod': 0,
-                                                                     'unicode': 's'}))
-        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
-                                                                          {'key': pygame.K_s,
-                                                                           'mod': 0,
-                                                                           'unicode': 't'}))
+        text_entry.process_event(pygame.event.Event(pygame.TEXTINPUT, {'text': 't'}))
+        text_entry.process_event(pygame.event.Event(pygame.TEXTINPUT, {'text': 'e'}))
+        text_entry.process_event(pygame.event.Event(pygame.TEXTINPUT, {'text': 's'}))
 
-        assert processed_key_event is False and text_entry.get_text() == 'tes'
+        processed_text_input_event = text_entry.process_event(pygame.event.Event(pygame.TEXTINPUT, {'text': 't'}))
+
+        assert processed_text_input_event is False and text_entry.get_text() == 'tes'
 
     def test_process_event_text_ctrl_c(self, _init_pygame: None,
                                        _display_surface_return_none: None):
@@ -1105,17 +1092,12 @@ class TestUITextEntryLine:
         assert text_entry.is_enabled is True
         text_entry.focus()
         # process a mouse button down event
-        processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN,
-                                                                          {'key': pygame.K_d,
-                                                                           'mod': 0,
-                                                                           'unicode': 'd'}))
+        processed_text_input_event = text_entry.process_event(pygame.event.Event(pygame.TEXTINPUT, {'text': 'd'}))
 
-        text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_a, 'mod': 0,
-                                                                     'unicode': 'a'}))
-        text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_n, 'mod': 0,
-                                                                     'unicode': 'n'}))
+        text_entry.process_event(pygame.event.Event(pygame.TEXTINPUT, {'text': 'a'}))
+        text_entry.process_event(pygame.event.Event(pygame.TEXTINPUT, {'text': 'n'}))
 
-        assert processed_key_event is True and text_entry.get_text() == 'dan'
+        assert processed_text_input_event is True and text_entry.get_text() == 'dan'
 
     def test_show(self, _init_pygame, default_ui_manager, _display_surface_return_none):
         text_entry = UITextEntryLine(relative_rect=pygame.Rect(100, 100, 200, 30),